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

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

kakao
PRO
December 09, 2022

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

#Kubernetes #CNI #Cilium #Golang

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

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

kakao
PRO

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

    View Slide

  2. - Kubernetes를 개발/운영 중인 분


    - Cilium을 사용 중인 분


    - Go 언어 개발자


    - 프로그램의 메모리 할당과 해제, 그리고 누수 디버깅에 관심이 있는 분


    - Go의 Stack, Heap, Assembly
    발표 대상

    View Slide

  3. Cilium이란?


    Cilium의 메모리 누수 분석과 해결


    결론

    View Slide

  4. Cilium이란?


    Cilium의 메모리 누수 분석과 해결


    결론

    View Slide

  5. Kubernetes 네트워크 모델
    - 요구사항


    - 클러스터의 모든 파드는 고유한 IP를 가진다.


    - 파드는 NAT 없이 노드 상의 모든 파드와 통신할 수 있다.


    - 노드 상의 에이전트(예: kubelet)는 해당 노드의 모든 파드와 통신할 수 있다.


    - Kubernetes 네트워크 모델의 요구사항을 구현한 것이 CNI Plugin.


    - 카카오 클라우드는 Cilium 사용 중.

    View Slide

  6. Cilium의 구조
    Pod
    User
    Kernel
    Cilium Agent
    Pod
    Node
    Bytecode Injection
    Kubernetes

    View Slide

  7. Cilium의 구조
    Pod
    User
    Kernel
    Cilium Agent
    Pod
    Bytecode Injection
    Event
    Node
    Kubernetes

    View Slide

  8. Cilium의 구조
    Pod
    User
    Kernel
    Cilium Agent
    Pod
    Bytecode Injection
    Event
    Node
    Kubernetes

    View Slide

  9. Cilium의 구조
    Pod
    User
    Kernel
    Cilium Agent
    Pod
    Bytecode Injection
    Event
    Node
    Kubernetes

    View Slide

  10. Cilium의 구조
    Pod
    User
    Kernel
    OOM Killed
    Pod
    Bytecode Injection
    Event
    Node
    Kubernetes

    View Slide

  11. Cilium의 구조
    Pod
    User
    Kernel
    Restart...
    Pod
    Bytecode Injection
    Event
    Node
    Kubernetes

    View Slide

  12. Cilium의 구조
    Pod
    User
    Kernel
    가용성 유지가 중요
    ->
    OOM 분석 시작
    Pod
    Bytecode Injection
    Event
    Node
    Kubernetes

    View Slide

  13. Cilium이란?


    Cilium 메모리 누수 분석과 해결


    결론

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. 메모리 누수 코드를 어떻게 찾지?

    View Slide

  21. 메모리 누수 디버깅 방법
    1. 코드를 바로 분석한다.

    - 문제의 커밋을 특정할 수 있을 때

    - 코드 수정량이 적을 때


    2. 메모리 관련 시스템 콜을 추적한다.

    - 시스템 콜의 Caller를 역추적

    - User space에 메모리 allocator가 있다면 추적이 어려움


    3. 프로파일러를 사용한다.

    - 가장 추천하는 방법

    View Slide

  22. 프로파일러
    - 바이너리의 CPU, MEM, I/O 등을 추적하여 데이터로 제공하는 도구


    - 각 언어마다 프로파일러가 존재


    - Java: JPro
    fi
    ler


    - C: gProf


    - Python: cPro
    fi
    le


    - Cilium은 Go로 구현


    - Go: runtime/pprof

    View Slide

  23. runtime/pprof
    pprof
    app
    cpu
    mem
    i/o
    runtime
    pprof.WriteHeapPro
    fi
    le()

    View Slide

  24. runtime/pprof
    pprof
    app
    runtime
    cpu
    mem
    i/o
    pprof.WriteHeapPro
    fi
    le()
    server
    agent
    http

    View Slide

  25. runtime/pprof
    pprof
    Cilium
    runtime
    cpu
    mem
    i/o
    pprof.WriteHeapPro
    fi
    le()
    gops
    http

    View Slide

  26. 프로파일러 실행

    View Slide

  27. 프로파일러 실행

    View Slide

  28. 프로파일러 실행

    View Slide

  29. 메모리 프로파일링 결과
    1) 정상동작할 때의 cilium
    -
    agent 프로파일링 결과 중 일부입니다.
    1)

    View Slide

  30. 메모리 프로파일링 결과
    1) 정상동작할 때의 cilium
    -
    agent 프로파일링 결과 중 일부입니다.
    1)

    View Slide

  31. 메모리 프로파일링 특이사항
    - Cilium 외 다른 프로그램에서도 사용하는

    범용 라이브러리

    - NetlinkSocket 통신에

    300MB 이상 사용 ???

    View Slide

  32. 메모리 프로파일링 특이사항
    -코드를 봐도 문제를 못 찾겠다...


    -코드의 양도 많고


    -메모리 사용도 빈번하다

    View Slide

  33. 결과는 나왔는데 해석을 못하겠다...

    View Slide

  34. 왜 해석을 못하는가?
    Go의 메모리 구조에 대해 깊게 생각해 본 적이 없다. 😅😅😅😅😅😅


    -어느 경우에 Stack과 Heap을 사용하는지 모른다...


    -그냥 GC가 알아서 해주겠지...

    View Slide

  35. 프로그래밍 언어의 기본부터 시작하자!


    그것은 바로?

    View Slide

  36. Go는 Stack과 Heap을 어떻게 사용하는가?

    View Slide

  37. 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()


    }


    View Slide

  38. 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()


    }


    View Slide

  39. 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을 사용한다.

    View Slide

  40. 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을 사용한다.

    View Slide

  41. 0000000000457c20 :


    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


    457c2f: 48 81 ec d0 00 00 00 sub $0xd0,%rsp


    ...


    ...


    ...


    457cc2: e8 99 a8 fe ff callq 442560


    View Slide

  42. 0000000000457c20 :


    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


    457c2f: 48 81 ec d0 00 00 00 sub $0xd0,%rsp


    ...


    ...


    ...


    457cc2: e8 99 a8 fe ff callq 442560


    1. sliceA는 SP를 감소시켜 Stack에 공간을 확보한다.
    2. sliceB는 Heap 할당을 위해, 별도의 함수를 호출한다.

    View Slide

  43. 0000000000457c20 :


    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


    457c2f: 48 81 ec d0 00 00 00 sub $0xd0,%rsp


    ...


    ...


    ...


    457cc2: e8 99 a8 fe ff callq 442560


    1. sliceA는 SP를 감소시켜 Stack에 공간을 확보한다.
    2. sliceB는 Heap 할당을 위해, 별도의 함수를 호출한다.

    View Slide

  44. 0000000000457c20 :


    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


    457c2f: 48 81 ec d0 00 00 00 sub $0xd0,%rsp


    ...


    ...


    ...


    457cc2: e8 99 a8 fe ff callq 442560


    1. sliceA는 SP를 감소시켜 Stack에 공간을 확보한다.
    2. sliceB는 Heap 할당을 위해, 별도의 함수를 호출한다.

    View Slide

  45. 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()

    View Slide

  46. 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()

    View Slide

  47. 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()

    View Slide

  48. 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에 할당한다.

    View Slide

  49. 프로파일링 결과를 재해석하자!

    View Slide

  50. 프로파일링 결과의 재해석
    - Stack은 누수가 발생하지 않으니

    - 코드에서 Caller로 전달하는

    Heap만 뽑아내자!

    View Slide

  51. -Heap을 사용하는 코드만 분리
    프로파일링 결과의 재해석

    View Slide

  52. 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 ݫद૑ܳ ҅ࣘ


    // ଵઑೞҊ ੓ח ё୓о ߧੋ੉׮.




    }


    View Slide

  53. 프로파일러를 통해


    메모리 할당 코드 확인. 그 다음은?

    View Slide

  54. 사람이 코드를 분석한다.
    -할당받은 Heap 메모리 주소를 어디서 참조하는지 확인


    -프로파일러가 제공하는 Stack Trace를 따라 흐름을 파악


    -이 구간은 온전히 개발자의 몫


    -프로파일러는 어디서 이 주소를 가지고 있는지 알 수 없음


    -하지만 메모리 할당 코드가 어딘지 아는 것만으로 디버깅의 80%는 완료

    View Slide

  55. 그래서 카카오는 어떻게 되었나요?

    View Slide

  56. Cilium 메모리 누수 버그 발견
    -IP, Routing 등 네트워크 정보 캐싱 기능의 버그


    -캐시를 비우지 않고 계속 쌓는 것이 원인


    -Cilium의 버그를 고쳐 배포할 수도 있지만 빠른 대응을 위해


    -기능 on/off가 가능하여 캐싱을 안하도록 먼저 배포


    -버그는 수정하여 cilium에 PR 작성


    -이후 DKOS Cilium OOM 0건

    View Slide

  57. Cilium이란?


    Cilium 메모리 누수 분석과 해결


    결론

    View Slide

  58. 결론
    -모든 메모리 누수 디버깅의 첫발은 메모리 할당 코드를 찾는 것입니다.


    -이때 프로파일러를 사용하면 시간을 굉장히 단축할 수 있습니다.


    -그 다음 코드 분석을 통해, 메모리 참조 코드를 찾으세요.


    -가능하면 프로파일러를 자주써서 익숙해지세요.

    View Slide

  59. 감사합니다.

    View Slide