일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- elasticsearch
- Service
- ebs
- AWS
- IAM
- 자바스크립트
- terraform
- Kubernetes
- Django
- AZ-900
- docker
- AZURE
- Python
- cpu
- K8S
- EC2
- Role
- Deployment
- DevOps
- RBAC
- FastAPI
- kernel
- asyncio
- leetcode
- POD
- AZ-104
- Network
- IAC
- asgi
- ansible
- Today
- Total
궁금한게 많은 개발자 노트
[ K8S ] CNI Cilium 메모리 누수 버그 해결 방법 (feat. 카카오) 본문
쿠버네티스 네트워크 모델은 잘 알다시피 아래 3가지 요구사항을 충족합니다.
- 클러스터의 모든 파드는 고유한 IP를 가진다.
- 파드는 NAT없이 노드 상의 모든 파드와 통신할 수 있다.
- 노드 상의 에이전트(ex. kubelet)는 해당 노드의 모든 파드와 통신할 수 있다.
이러한 네트워크 모델의 요구사항을 구현한 것이 CNI Plugin이며, 그 중 Cilium에 대해 알아보고자 합니다.
cilium은 daemonset으로 각 노드마다 cilium agent가 동작하며, agent로 각 노드의 네트워크 설정을 담당하며 쿠버네티스로부터 파드의 시작/중지와 같은 이벤트를 수신하고, 이를 ebpf바이트코드로 컴파일하여 커널에 적재하는 역할을 수행합니다.
그러므로, cilium agent가 정상 동작해야 파드에 존재하는 파드의 정상적인 네트워크 통신을 보장할 수 있습니다.
이 때, agent가 OOM이 발생하면 자동 재시작이 되며, 재시작되는 수초동안은 일시적인 네트워크 단절을 통해 다양한 문제가 발생할 수 있습니다. 이에 agent가 죽지 않게 가용성을 보장할 수 있도록 agent OOM을 분석해봐야 합니다.
OOM발생 원인으로 메모리 limit을 잘못 설정한 것인지 진짜 메모리 누수인지를 확인해봐야 할 것입니다. 프로세스 메모리 사용량 패턴을 보고 판단할 수 있을 것 같은데, 급격이 증가하거나 급격히 감소하는 부분이 있는지 확인해보겠습니다.
GC를 사용하는 프로세스와 그렇지 않은 프로세스의 메모리 사용량은 다를 수 있을 것 같은데, 위 프로세스 둘 다 정상 동작하고 있지만 리밋을 낮게 잡는 경우 GC를 사용하지 않는 경우 등락을 예측하기 어렵고 잦으므로 OOM이 발생할 수 있습니다. (GC사용의 경우 일정하게 메모리 요청량이 늘었다고 줄어드는 구간 생김) 메모리 limit을 잘못 잡으면 아래 문제가 발생할 수 잇으므로, 메모리 limit을 한번 늘려보는 것도 OOM의 원인을 파악할 수 있을 것 같습니다.
하지만, cilium의 경우 쿠버네티스 클러스터의 별다른 이벤트가 발생하지 않더라도 메모리 사용량 감소가 일어나는 구간 없이 지속적으로 상승하는 것으로 보아 메모리 누수가 발생하고 있음을 확인할 수 있습니다.
그렇다면, 메모리 누수 코드는 어떻게 찾을 수 있을까요? 3가지 방법이 있을 수 있습니다.
- 코드를 바로 분석 - 문제 커밋을 특정할 수 있거나 코드 수정량이 적을 때 사용
- 메모리 관련 시스템 콜을 추적 - 시스템 콜의 caller를 역추적, user space에 메모리 allocator가 있다면 추적이 어려움
- 프로파일러를 사용 - 가장 추천하는 방법으로 바이너리의 CPU, MEM, I/O등을 추적하여 데이터로 제공하는 도구
프로파일러는 각 언어마다 존재하며 cilium은 Go로 구현되어 있으므로 go runtime에서 제공하는 pprof라는 프로파일러 사용
pprof는 일종의 인터페이스이며 application에서 pprof.WirteHeapProfile()과 같은 pprof API를 호출하면 해당 시점에 runtime으로부터 정보를 가져올 수 있습니다.
문제는 프로파일링을 위해 pprof API호출 코드를 삽입해야하며 바이너리를 다시 빌드해야 합니다. 또한 정적으로 코드를 삽입하기에 API호출 시점을 외부에서 동적으로 컨틀로 할 수 없습니다. 이에 application에 pprof API를 서빙하는 서버 에이전트를 심어놓으면 동적으로 원하는 시점에 프로파일링 결과를 얻을 수 있습니다. (cilium은 gops라는 패키지를 사용하고 있고, gops명령을 통해 프로파일링을 실행할 수 있습니다. 내부적으로 http를 사용하여 프로세스와 통신하고, 프로파일링 결과를 파일로 저장)
PID 1번 프로세스인 cilium agent의 메모리 프로파일링을 실행하여, 생성한 덤프를 로컬로 가져와 go tool을 사용하여 인간이 읽을 수 있는 포맷(PDF)으로 뽑아낼 수 있습니다.
스택 트레이스 형태로 보여주며 한눈에 메모리 누수가 의심되는 함수를 찾을 수 있으며, 해당 함수는 netlinkAPI로 cilium외 다른 프로그램에서도 사용하는 범용 라이브러리였습니다. netlinksocket에 300MB이상을 사용한다는 점이 이상한 부분일 수 있습니다.
Go의 메모리 구조에 대해 깊게 생각해보고 어느 경우에 stack, heap을 사용하는지에 대한 이해가 있거나 GC가 어떻게 처리해주는지에 대한 이해가 있었다면 해당 문제를 해결할 수 있을 것 같습니다.
우선 스택과 힙을 분석하기 위해 예제 코드를 통해 Go의 메모리 구조를 확인해보겠습니다.
go build -gcflags -m main.go를 통해 -m 옵션을 주어 컴파일러가 메모리를 어떻게 결정하는지 확인할 수 있어 어느 라인에서 스택을 사용하고 어느 라인에서 힙을 사용하는지 알 수 있었습니다.
주요 라인만 살펴보면 sliceA는 하위 함수에서만 사용하므로 스택을 사용(does not escape)하고, 상위 caller로 전달되는 sliceB에서는 힙을 사용하는 것을 확인할수 있었습니다.
여기서 go의 컨셉을 알 수 있는데, 하위 함수로의 메모리 전달은 스택을 이용해 메모리 할당 비용을 줄이고, 상위 함수로의 메모리 전달은 stack frame을 보존할 수 없으므로 heap에 할당하는 방식입니다.
이제 프로파일링 결과를 재해석해보면 netlink API함수에서 스택은 누수가 발생하지 않으니, 코드에서 caller로 전달하는 heap만 뽑아내면 메모리 누수가 발생하는 원인을 찾을 수 있을 것 같습니다.
buffer는 parsing된 후 msg형태로 caller에 전달하게 되므로, 이 때는 heap을 사용하며 caller중 netlink 메시지를 계속 참조하고 있는객체가 범인이며 이것이 cilium-agent였습니다. 이것은 또 어떻게 분석하냐면 할당받은 heap메모리 주소를 어디서 참조하는지 확인해야 하며, 프로파일러가 제공하는 stack trace의 흐름을 따라 확인합니다.
프로파일러는 어디서 이 주소를 가지고 있는지 알 수 없습니다. 이유는 메모리 할당 시점에는 해당 정보가 없기 때문입니다. 하지만, 메모리 할당 코드가 어딘지 아는 것만으로 디버깅의 80%는 완료했다고 볼 수 있습니다.
IP, Routing등 네트워크 정보 캐싱 기능의 버그였고, 캐시를 비우지 않고 계속 쌓이는 것이 원인이었습니다. 빠른 확인을 위해 기능 on/off가 가능했으므로 캐싱을 안하도록 설정하여 배포한 결과 cilium OOM이 발생하지 않음을 확인할 수 있었습니다.
2022년에 카카오가 버그를 수정하여 cilium에 PR을 작성하였습니다.
결론적으로 모든 메모리 누수 디버깅의 첫 걸음은 메모리 할당 코드를 사용하는 것이고 이 때 프로파일링을 사용하면 시간을 매우 단축시킬 수 있습니다. 그 다음 코드 분석을 통해 메모리 참조 코드를 찾는 것입니다. 가능하면 프로파일러를 자주써서 익숙해지면 좋을 것 같습니다.
해당 글은 if(kakao) dev2022영상을 보고 학습용으로 작성한 글입니다. 영상은 아래 링크에서 확인 부탁드립니다.
좋은 영상 공유해주신 카카오에 감사합니다 😎
https://www.youtube.com/watch?v=8DxePykmO-A
'DevOps' 카테고리의 다른 글
[ DDoS ] AWS에서 L7 DDoS 공격 패턴에 대응하는 WAF 규칙 작성 (0) | 2025.06.16 |
---|---|
[ Monitoring ] Opentelemetry (0) | 2025.05.27 |
[ DDoS ] ipset+iptabels vs XDP+eBPF 차단 방식 비교 (0) | 2025.05.10 |
[ K8S ] 쿠버네티스 네트워크 모델 (0) | 2025.04.24 |
[ Azure ] Service Principal, Role, Action (0) | 2025.02.19 |