다음으로는 kubernetes 네트워크 속 CNI를 알고, Cilium의 메모리 누수 버그를 해결한 kakao 사례에 대해 설명하겠다. DKOS 를 개발하면서 생겼던 오류와 해결방안에 대해 말씀해주셨는데 이 기회로 kubernetes의 세부적인 네트워킹 구조에 대해 공부해봐야겠다는 생각을 하게 되었다. 처음 들으면서 몰랐던 단어들이 많아서 좀 어지러웠는데 모르는 부분은 다른 블로그들을 참고하며 내용을 이해하기 위해 노력했다. 또한 좋았던 점은 실무에서 문제가 일어났을 때 문제 해결의 전체적인 흐름도 함께 말씀해주셔서 트러블슈팅 과정을 들어볼 수 있었다는 것이다.
kubernetes 네트워크 모델
요구사항
- 클러스터의 모든 파드는 고유한 IP를 가진다.
- 파드는 NAT 없이 노드 상의 모든 파드와 통신할 수 있다.
- 노드 상의 에이전트(예: kubelet)는 해당 노드의 모든 파드와 통신할 수 있다.
=> kubernetes 네트워크 모델의 요구사항을 구현한 것이 CNI Plugin 이다.
다양한 CNI Plugin 이 있지만 카카오는 Cilium을 사용중이다.
CNI 란?
CNCF(Cloud Native Computing Foundation) 의 프로젝트 중 하나로 컨테이너 간의 네트워킹을 제어할 수 있는 플러그인을 만들기 위한 표준이다. 다양한 형태의 컨테이너 런타임과 오케스트레이터 사이의 네트워크 계층을 구현하는 방식이 다양하게 분리되어 각자만의 방식으로 발전하게 되는 것을 방지하고 공통된 인터페이스를 제공하기 위해 만들어졌다. 쿠버네티스에서는 Pod 간의 통신을 위해서 CNI를 사용한다.
쿠버네티스는 기본적으로 ‘kubelet’ 이라는 자체적인 CNI 플러그인을 제공하지만 네트워크 기능이 매우 제한적인 단점이 있다. 그 단점을 보완하기 위해 3rd- party 플러그인을 사용하는데 그 종류에는 Flannel, Calico, Weavenet, NSX 등 다양한 종류의 3rd- party CNI 플러그인들이 존재한다.
💡 pod 끼리의 통신(클러스터 내에서 다른 pod 또는 외부 서비스와 통신) : CNI 플러그인 사용
pod 안의 컨테이너들의 통신 : 컨테이너 런타임이 제공하는 가상 네트워크 인터페이스 사용
CNI 플러그인의 필요성
예를 들어 위의 그림처럼 컨테이너 기반으로 동작하는 애플리케이션에 세 개의 컨테이너들이 동작하며 이렇게 멀티 호스트로 구성되어 있다. 컨테이너들은 서로 간의 당연히 통신이 되어야한다. 그런데 사진에서 보는 것과 같이 UI Containe와 Login Container의 IP 주소가 같아 한 쪽에서 통신을 시도하면 자신의 컨테이너 로컬로 통신을 시도할 것이다.
위 그림에서는 weavenet 이라고 하는 CNI 가 브릿지 인터페이스를 만들고 컨테이너 대역대를 나눠주며 라우팅 테이블까지 생성하여 각각의 컨테이너들이 통신 가능하도록 지원한다.
CNI 역할
- IP 주소할당: 각 pod 에 IP 주소를 동적으로 할당한다. pod는 독립적인 네트워크 엔티티로서 고유한 IP 주소를 가져야한다. CNI 플러그인은 이러한 IP 주소 할당을 관리하고 pod간의 통신을 위한 IP 주소를 제공한다.
- 네트워크 설정: pod의 네트워크 설정을 구성한다. pod의 라우팅, 서브넷, DNS 설정 등을 CNI 플러그인을 통해 구성할 수 있다. 이를 통해 pod는 올바른 네트워크 구성을 가지고 다른 pod 또는 외부 서비스와 통신할 수 있다.
- 통신 경로 설정: pod는 다른 pod 또는 외부 서비스와 통신해야하는데 CNI 플러그인은 이러한 통신을 가능하게 하는 네트워크 경로를 설정한다.
- 네트워크 정책 및 보안: CNI 플러그인은 네트워크 정책과 보안을 구현하는데 사용될 수 있다. pod 간의 통신을 제어하고 트래픽을 필터링하거나 암호화하는 등의 보안 기능을 구현할 수 있다.
- 다양한 네트워크 솔루션 통합: CNI 플러그인은 다양한 네트워크 솔루션과 통합될 수 있다. Calico, Flannel, Weave 등의 CNI 플러그인은 각각 다른 네트워크 기술을 구현하며 CNI 플러그인을 통해 쿠버네티스 클러스터에 통합된다.
CNI 에서 사용되는 네트워크 모델
CNI provider은 VXLAN(Virtual Extensible Lan), IP-in-IP 과 같은 캡슐화된 네트워크 모델 또는 BGP(Border Gateway Protocol) 와 같은 캡슐화 되지 않은 네트워크 모델을 사용하여 네트워크 패브릭을 구현한다.
CNI 3rd-party 플러그인 종류 및 지원하는 기능
Cilium 이란?
BPF(Berkeley Packet Filter)를 기반으로 pod network를 구축하는 CNI 플러그인
eBPF : 커널의 성능을 향상시킬 수 있음. 코드 최적화 할 수 있음. 커널에 injection 주입함. 상황에 맞춰 계속 관찰하면서 업데이트 해줌.
각 노드마다 Cilium Agent가 동작하며 이는 네트워크 설정을 담당한다. 쿠버네티스로 부터 파드의 시작/중지와 같은 이벤트를 수신하고 이를 eBPF btye code로 컴파일하여 커널에 적재하는 역할까지 수행한다. 그래서 Cilium Agent가 정상 동작해야 해당 노드 안에서 동작하는 파드의 정상적인 네트워크 통신을 보장할 수 있다.
하지만 DKOS 를 운영하면서 이 Agent에 OOM(Out of Memory: 메모리 부족)이 발생하여 죽는 문제가 발생했고 한다. Cilium Agent도 데몬셋의 파드로 동작하기 때문에 OOM이 발생하면 자동으로 재시작 된다. 하지만 재시작되는 동안 해당 노드의 파드에 health check 변화가 있거나 새로운 파드가 뜬다면 그 파드들에는 일시적인 네트워크 단절 문제가 생길 수 있다.
그래서 Agent의 가용성 유지의 중요성을 알게 되었고 이 Agent가 죽지 않도록 하는 것을 목표로 OOM 분석을 시작하였다고 한다. 먼저 memory limit을 잘못 설정인지 메모리 누수인지 판단하는 것이었다. 그것을 알기 위해서는 프로세스 메모리 사용량 패턴을 보아야한다.
하지만 Cilium 이슈인 이번 경우는 메모리는 충분하였으며, 클러스터의 별다른 이상 없어도 꾸준히 메모리 사용량이 증가한 것을 보고 전형적인 메모리 누수 패턴이라고 하셨다.
메모리 누수 디버깅 방법
User space에 일어나는 모든 메모리 할당과 해제를 추적할 수 있는 프로파일러를 사용하여 메모리 누수를 분석하였다.
프로파일러
바이너리의 CPU, MEM, I/O 등을 추적하여 데이터로 제공하는 도구
각 언어마다 프로파일러가 존재
Java : JProfiler
C : gProf
Python : cProfile
Go : runtime/pprof (Cilium은 go로 구현되어 있음.)
runtime/pprof
일종의 인터페이스이다. 어플리케이션에서 pprof API를 호출하면 해당 시점의 runtime으로부터 해당 정보를 받아온다. 문제는 프로파일링을 위해 pprof API 호출 코드 삽입과 바이너리 다시 빌드하는 것이 필요하다. 또한 정적으로 코드를 삽입하기에 API 호출 시점을 외부에서 동적으로 컨트롤할 수 없다는 것이다. 그래서 미리 어플리케이션에 pprof를 http로 서빙하는 server agent를 심어놓으면 동적으로 원하는 시간의 프로파일링 결과를 얻을 수 있다. Cilium은 gops라는 패키지를 사용하기 때문에 gops 명령을 이용해 프로파일링을 진행하였다.
gops로 프로파일링 가능한 프로세스 목록 확인 후, pid 1 번인 cilium-agent의 메모리 프로파일링 실행.생성된 덤프를 로컬로 가져온 후 pprof로 pdf로 뽑아낸다. pdf를 보면 stack trace 형식으로 보여준다. 박스의 크기가 클수록 박스의 색이 붉을수록 더 많은 메모리를 사용하고 있다는 뜻이다. 이를 통해 메모리 누수가 의심되는 함수를 찾을 수 있음.
하지만 코드를 봐도 문제를 찾지 못하고 메모리 사용도 빈번했기 때문에 해석을 못하는 상태가 되었다. 어느 경우에 stack과 heap을 사용하는지 모르기 때문. 그래서 문제를 해결하기 위해 go의 메모리 구조를 확인하고 stack과 heap을 어떻게 사용하는지 알아보는 것부터 시작했다고 한다.
해결 과정 1 ) Go의 메모리 구조 이해
예시를 통해 알아보자.
두 개의 100바이트 슬라이스를 만들고 슬라이스 A,B를 각각 다른 함수에 전달. 슬라이스 B의 경우만 caller로 return 하는 경우의 예제 코드. Go는 build 타임에 stack과 heap을 결정. gcflags의 -m 옵션을 통해 컴파일러가 메모리를 어떻게 결정하는지 볼 수 있다. 어느 라인에서 스택을 사용하고 힙을 사용하는지 알 수 있다.
결과 : 하위 함수로만 전달하는 메모리는 Stack을 사용, 상위 함수(caller)로 전할하는 메모리는 heap을 사용하는 것을 볼 수 있다.
뿐만 아니라 Assembly를 통해서도 확인 가능하다.
main함수의 stack에서 할당된 buf 라는 메모리를 하위 함수로 전달하는 예시
main의 stack 프레임에서 buf는 0x100에 할당되었다. 함수 a에 buf의 주소를 전달하지만 a함수 내에서 이 주소(0x100)는 여전히 유효하다. 메인함수의 stack 프레임이 보존되고 있기 때문이다. 마찬가지로 함수 b에서도 0x100 주소는 유효하다.
해결 과정 2) 프로파일링 결과 재해석 후 코드 확인
해결 과정 3) 개발자가 코드 다시 분석
카카오 cilium 메모리 누수 버그 원인
이렇게 쿠버테니스 환경 운영 시 발생한 메모리 누수 문제에 대한 원인과 해결책으로 전체 과정을 살펴보는 것을 끝으로 강의를 마무리하셨다. 미래 내가 운영하게 될 쿠버네티스 환경에서 OOM 뿐만 아니라 다양한 문제들을 마주하게 될텐데 이렇게 문제를 접근하는 방식부터 해결해가는 방식을 떠올리는 과정까지 알려주셔서 너무 뜻깊었던 시간이었다.
'Kubernetes' 카테고리의 다른 글
[ DEVOCEAN OpenLab ] Kubernetes API 와 kubebuilder (0) | 2024.06.29 |
---|---|
[ DEVOCEAN OpenLab ] Kubernetes Architecture 구성요소 (k8s VS k3s) (0) | 2024.05.03 |
[ DEVOCEAN OpenLab ] k3s 설치 with Multipass (mac M2) (0) | 2024.05.02 |
[ DEVOCEAN OpenLab ] CNCF 란? CNCF Projects (1) | 2024.05.01 |
[ DEVOCEAN OpenLab ] 쿠버네티스 등장배경 (0) | 2024.05.01 |