Upgrade to Pro — share decks privately, control downloads, hide ads and more …

카카오가 K8s CNI Cilium의 메모리 누수 버그를 해결하는 방법

kakao
December 09, 2022

카카오가 K8s CNI Cilium의 메모리 누수 버그를 해결하는 방법

#Kubernetes #CNI #Cilium #Golang

CNI는 Kubernetes의 필수 컴포넌트입니다. 그러므로 Kubernetes를 개발/운영하는 엔지니어라면 누구나 CNI 이슈를 경험했을 텐데요. CNI Plugin인 Cilium의 메모리 누수 버그를 분석/해결하는 과정에서 얻은 경험과 지식을 공유합니다. 특히 누수 디버깅에 가장 중요한 프로그램의 Stack과 Heap 구조에 대해 알아보고 이를 어떻게 활용하는지 이야기합니다.

발표자 : gyu.gyu
카카오에서 클라우드 플랫폼을 개발하고 있는 규입니다.

kakao

December 09, 2022
Tweet

More Decks by kakao

Other Decks in Programming

Transcript

  1. 카카오가 K8s CNI Cilium의 메모리 누수 버그를 해결하는 방법 배규태

    gyu.gyu 카카오 Copyright 2022. Kakao Corp. All rights reserved. Redistribution or public display is not permitted without written permission from Kakao. if(kakao)2022
  2. - Kubernetes를 개발/운영 중인 분 - Cilium을 사용 중인 분

    - Go 언어 개발자 - 프로그램의 메모리 할당과 해제, 그리고 누수 디버깅에 관심이 있는 분 - Go의 Stack, Heap, Assembly 발표 대상
  3. Kubernetes 네트워크 모델 - 요구사항 - 클러스터의 모든 파드는 고유한

    IP를 가진다. - 파드는 NAT 없이 노드 상의 모든 파드와 통신할 수 있다. - 노드 상의 에이전트(예: kubelet)는 해당 노드의 모든 파드와 통신할 수 있다. - Kubernetes 네트워크 모델의 요구사항을 구현한 것이 CNI Plugin. - 카카오 클라우드는 Cilium 사용 중.
  4. Cilium의 구조 Pod User Kernel 가용성 유지가 중요 -> OOM

    분석 시작 Pod Bytecode Injection Event Node Kubernetes
  5. 프로세스 메모리 사용량 패턴 프로세스 A (No GC) 프로세스 B

    150 300 01:00 05:00 10:00 메모리 사용량(MB)
  6. 프로세스 메모리 사용량 패턴 프로세스 A 프로세스 B (GC) 150

    300 01:00 05:00 10:00 메모리 사용량(MB) 프로세스 A (No GC)
  7. 프로세스 메모리 사용량 패턴 Limit OOM 발생 150 300 01:00

    05:00 10:00 메모리 사용량(MB) 프로세스 B (GC) 프로세스 A (No GC)
  8. 프로세스 메모리 사용량 패턴 Limit 150 300 01:00 05:00 10:00

    메모리 사용량(MB) 프로세스 B (GC) 프로세스 A (No GC)
  9. 프로세스 메모리 사용량 패턴 150 300 01:00 05:00 10:00 Cilium

    메모리 사용량(MB) 프로세스 B (GC) 프로세스 A (No GC)
  10. 메모리 누수 디버깅 방법 1. 코드를 바로 분석한다. 
 -

    문제의 커밋을 특정할 수 있을 때 
 - 코드 수정량이 적을 때 2. 메모리 관련 시스템 콜을 추적한다. 
 - 시스템 콜의 Caller를 역추적 
 - User space에 메모리 allocator가 있다면 추적이 어려움 3. 프로파일러를 사용한다. 
 - 가장 추천하는 방법
  11. 프로파일러 - 바이너리의 CPU, MEM, I/O 등을 추적하여 데이터로 제공하는

    도구 - 각 언어마다 프로파일러가 존재 - Java: JPro fi ler - C: gProf - Python: cPro fi le - Cilium은 Go로 구현 - Go: runtime/pprof
  12. 메모리 프로파일링 특이사항 - Cilium 외 다른 프로그램에서도 사용하는 


    범용 라이브러리 
 - NetlinkSocket 통신에 
 300MB 이상 사용 ??? 

  13. 왜 해석을 못하는가? Go의 메모리 구조에 대해 깊게 생각해 본

    적이 없다. 😅😅😅😅😅😅 -어느 경우에 Stack과 Heap을 사용하는지 모른다... -그냥 GC가 알아서 해주겠지...
  14. package main func useMem(buf []byte) { return } func memAlloc()

    []byte { sliceA := make([]byte, 100) useMem(sliceA) sliceB := make([]byte, 100) useMem(sliceB) return sliceB } func main() { _ = memAlloc() }
  15. package main func useMem(buf []byte) { return } func memAlloc()

    []byte { sliceA := make([]byte, 100) useMem(sliceA) sliceB := make([]byte, 100) useMem(sliceB) return sliceB } func main() { _ = memAlloc() }
  16. package main func useMem(buf []byte) { return } func memAlloc()

    []byte { sliceA := make([]byte, 100) useMem(sliceA) sliceB := make([]byte, 100) useMem(sliceB) return sliceB } func main() { _ = memAlloc() } 1. 하위 함수로만 전달하는 메모리는 Stack을 사용한다. 2. 상위 함수(caller)로 전달하는 메모리는 Heap을 사용한다.
  17. package main func useMem(buf []byte) { return } func memAlloc()

    []byte { sliceA := make([]byte, 100) useMem(sliceA) sliceB := make([]byte, 100) useMem(sliceB) return sliceB } func main() { _ = memAlloc() } 1. 하위 함수로만 전달하는 메모리는 Stack을 사용한다. 2. 상위 함수(caller)로 전달하는 메모리는 Heap을 사용한다.
  18. 0000000000457c20 <main.memAlloc>: 457c20: 4c 8d 64 24 b0 lea -0x50(%rsp),%r12

    457c25: 4d 3b 66 10 cmp 0x10(%r14),%r12 457c29: 0f 86 05 01 00 00 jbe 457d34 <main.memAlloc+0x114> 457c2f: 48 81 ec d0 00 00 00 sub $0xd0,%rsp ... ... ... 457cc2: e8 99 a8 fe ff callq 442560 <runtime.makeslice>
  19. 0000000000457c20 <main.memAlloc>: 457c20: 4c 8d 64 24 b0 lea -0x50(%rsp),%r12

    457c25: 4d 3b 66 10 cmp 0x10(%r14),%r12 457c29: 0f 86 05 01 00 00 jbe 457d34 <main.memAlloc+0x114> 457c2f: 48 81 ec d0 00 00 00 sub $0xd0,%rsp ... ... ... 457cc2: e8 99 a8 fe ff callq 442560 <runtime.makeslice> 1. sliceA는 SP를 감소시켜 Stack에 공간을 확보한다. 2. sliceB는 Heap 할당을 위해, 별도의 함수를 호출한다.
  20. 0000000000457c20 <main.memAlloc>: 457c20: 4c 8d 64 24 b0 lea -0x50(%rsp),%r12

    457c25: 4d 3b 66 10 cmp 0x10(%r14),%r12 457c29: 0f 86 05 01 00 00 jbe 457d34 <main.memAlloc+0x114> 457c2f: 48 81 ec d0 00 00 00 sub $0xd0,%rsp ... ... ... 457cc2: e8 99 a8 fe ff callq 442560 <runtime.makeslice> 1. sliceA는 SP를 감소시켜 Stack에 공간을 확보한다. 2. sliceB는 Heap 할당을 위해, 별도의 함수를 호출한다.
  21. 0000000000457c20 <main.memAlloc>: 457c20: 4c 8d 64 24 b0 lea -0x50(%rsp),%r12

    457c25: 4d 3b 66 10 cmp 0x10(%r14),%r12 457c29: 0f 86 05 01 00 00 jbe 457d34 <main.memAlloc+0x114> 457c2f: 48 81 ec d0 00 00 00 sub $0xd0,%rsp ... ... ... 457cc2: e8 99 a8 fe ff callq 442560 <runtime.makeslice> 1. sliceA는 SP를 감소시켜 Stack에 공간을 확보한다. 2. sliceB는 Heap 할당을 위해, 별도의 함수를 호출한다.
  22. buf bp sp 0x100 func main() { buf := make([]byte,

    100) A(buf) } func A(buf []byte) { B(buf) } func B(buf []byte) { C(buf) } func C(buf []byte) { } main()
  23. func main() { buf := make([]byte, 100) A(buf) } func

    A(buf []byte) { B(buf) } func B(buf []byte) { C(buf) } func C(buf []byte) { } buf bp sp 0x100 main() buf = 0x100 A()
  24. func main() { buf := make([]byte, 100) A(buf) } func

    A(buf []byte) { B(buf) } func B(buf []byte) { C(buf) } func C(buf []byte) { } buf bp sp 0x100 main() A() buf = 0x100 buf = 0x100 B()
  25. func main() { buf := make([]byte, 100) A(buf) } func

    A(buf []byte) { B(buf) } func B(buf []byte) { C(buf) } func C(buf []byte) { } buf bp sp 0x100 main() A() buf = 0x100 buf = 0x100 B() Go의 컨셉 - 하위 함수로의 메모리 전달은 Stack을 이용해 메모리 할당 비용을 줄인다. 
 - 상위 함수로의 메모리 전달은 Stack Frame을 보존할 수 없으므로 Heap에 할당한다.
  26. 프로파일링 결과의 재해석 - Stack은 누수가 발생하지 않으니 
 -

    코드에서 Caller로 전달하는 
 Heap만 뽑아내자!
  27. Pseudo Code func NetlinkAPI() ([]byte, error) { fd := Load()

    ... ... // ׮ܲ ௏٘ח न҃ॶ ೙ਃ হ׮. ... buffer := make([]byte, 1024) // Heapী ೡ׼ !!!!!!!!!!!!!!! RecvMsg(fd, buffer) msg, err := ParseMessage(buffer) // ݫद૑ ౵य if err != nil { return nil, err } return msg, nil // Caller ઺ Netlink ݫद૑ܳ ҅ࣘ // ଵઑೞҊ ੓ח ё୓о ߧੋ੉׮. }
  28. 사람이 코드를 분석한다. -할당받은 Heap 메모리 주소를 어디서 참조하는지 확인

    -프로파일러가 제공하는 Stack Trace를 따라 흐름을 파악 -이 구간은 온전히 개발자의 몫 -프로파일러는 어디서 이 주소를 가지고 있는지 알 수 없음 -하지만 메모리 할당 코드가 어딘지 아는 것만으로 디버깅의 80%는 완료
  29. Cilium 메모리 누수 버그 발견 -IP, Routing 등 네트워크 정보

    캐싱 기능의 버그 -캐시를 비우지 않고 계속 쌓는 것이 원인 -Cilium의 버그를 고쳐 배포할 수도 있지만 빠른 대응을 위해 -기능 on/off가 가능하여 캐싱을 안하도록 먼저 배포 -버그는 수정하여 cilium에 PR 작성 -이후 DKOS Cilium OOM 0건
  30. 결론 -모든 메모리 누수 디버깅의 첫발은 메모리 할당 코드를 찾는

    것입니다. -이때 프로파일러를 사용하면 시간을 굉장히 단축할 수 있습니다. -그 다음 코드 분석을 통해, 메모리 참조 코드를 찾으세요. -가능하면 프로파일러를 자주써서 익숙해지세요.