Stories

Detail Return Return

記一次內存泄露排查 - Stories Detail

概述

在值班過程中,排查了一個erlang otp24的bug.
erlang 編譯模塊在 otp/erts/emulator/beam/jit/x86/beam_asm.cpp:update_gdb_jit_info 中有內存泄露。只分配不釋放。
每次泄露 1K 左右,泄露和編譯模塊描述信息大小相關(模塊名,函數名,數量)。
因為otp24已經超過維護期了,所以提交的 issue 沒有被處理:
https://github.com/erlang/otp/issues/10062
所有的內存泄露,都可以用頻率分析的方式來快速定位。不管什麼語言的應用,只要是linux ELF, windows PE, 都可以用 coredump + 頻率分析定位問題。

排查過程

確認應用有內存泄露

排查步驟

收到上個值班同學的事項,開始排查:

麻煩看下xxx週末出現OOM了,是不是實例規格配置的太低了。
  1. 通過k8s監控,主要的內存是system。運行了很多天。2025-xx-xx xx:xx 重啓。監控可能有誤。容器監控內存較 erlang vm 監控小。
  2. 通過k8s監控,發現 RSS 有長期上漲趨勢。
  3. 通過k8s監控,其它應用沒有長期上漲趨勢。
  4. 通過k8s監控,其它環境的同一個應用有長期上漲趨勢,確認是應用問題。
  5. 通過進入容器top,確認是erlang應用佔用的內存。
  6. 對應的內存泄露用 :erlang.memory() 無法觀測。

    階段性結論

  7. xx 應用有bug。
  8. erlang memory看不出來,高度懷疑是c malloc的泄露。

    確認泄露點

    我將排查過程整理成文檔,交給了業務負責同學。業務負責同學快速定位到了泄露點並修復:

strings core.28 | sort | uniq -c | sort -nr | head -n 200
157933 'Elixir.xxx_module':xxx_method/0-CodeInfoPrologue
157854 elixir_compiler_1
157783 elixir_compiler_1:module_info/1-CodeInfoPrologue
157783 elixir_compiler_1:module_info/1
157783 elixir_compiler_1:module_info/0-CodeInfoPrologue
157783 elixir_compiler_1:module_info/0
157783 elixir_compiler_1::codeHeader
157783 elixir_compiler_1::codeFooter
157783 elixir_compiler_1:'__RELATIVE__'/0-CodeInfoPrologue
157783 elixir_compiler_1:'__RELATIVE__'/0
157783 elixir_compiler_1:'__MODULE__'/1-CodeInfoPrologue
157783 elixir_compiler_1:'__MODULE__'/1

泄露點在於重複的模塊編譯。我們的代碼類似如下,不應該有泄露:

  def build_test_mod(mod \\ TestModule) do
    Code.compiler_options(ignore_module_conflict: true)
    ast = {:__block__, [], [build_block()]}
    Module.create(mod, ast, file: "test_module.ex", line: 1)
  end

定位otp24編譯時的內存泄露

  1. 閲讀代碼,看Module.create 做了什麼,刪除模塊做了什麼?模塊名重複有什麼影響,閲讀 elixir, erlang 的文檔和代碼,沒有發現參數誤用情況。
  2. 構建最小重現用例:

      test "global const" do
     total_time = 17317
     emu_flavor = :erlang.system_info(:emu_flavor)
     Logger.warn("emu_flavor: #{emu_flavor} total_compile_time:#{total_time}")
    
     for idx <- 1..total_time do
       GlobalConst.delete(GlobalMap)
       GlobalConst.new(GlobalMap, %{qwerty_key: "qwerty_value", qwerty_key2: "qwerty_value2"})
       "qwerty_value" = GlobalMap.get(:qwerty_key)
    
       # Logger.warn("idx: #{idx}")
       if rem(idx, 1000) == 0 do
         Logger.warn("idx: #{idx}")
       end
     end
    
     Logger.warn("complete:#{total_time}")
     :timer.sleep(600_000)
  3. 和 emu 模式做對比:發現只有 jit 模式下有問題。因為同樣接口調用下,emu模式無問題,開始懷疑 erlang jit 可能有bug。
  4. 看 jitallocator 代碼,因為 jit 需要將內存中修改為可執行,我不清楚可執行段是否會動態變化。用 objdump 等工具觀察 coredump,沒有段信息。從 CodeInfoPrologue 等 magic string 出發,調試了一番後,未發現泄露點。
  5. 開始換用源碼編譯的otp24,從 magic string 附近的代碼找到泄露點,修改代碼確認問題。
    驗證:
    -註釋掉 update_gdb_jit_info 後內存泄露消失。
    -gdb 調試,記錄 symfile 的地址,跑一段時間之後,再打印內存,內容和開始分配的一樣,説明沒有釋放。
    一個有用的gdb函數:

    define xac
     dont-repeat
     set $addr = (char *)($arg0)
     set $endaddr = $addr + $arg1
     while $addr < $endaddr
         printf "%p: ", $addr
         set $lineendaddr = $addr + 8
         if $lineendaddr > $endaddr
             set $lineendaddr = $endaddr
         end
         set $a = $addr
         while $a < $lineendaddr
             printf "0x%02x ", *(unsigned char *)$a
             set $a++
         end
         printf "'"
         set $a = $addr
         while $a < $lineendaddr
             printf "%c", *(char *)$a
             set $a++
         end
         printf "'\n"
         set $addr = $addr + 8
     end
    end
    
    document xac
    usage: xac address count
    end

    輸出效果:

    (gdb) xac 0x7c59c8045d50 1000
    0x7c59c8045d50: 0x20 0xf2 0x05 0xd8 0x59 0x7c 0x00 0x00 ' ��Y|'
    0x7c59c8045d58: 0x50 0x55 0x05 0xc8 0x59 0x7c 0x00 0x00 'PU�Y|'
    0x7c59c8045d60: 0x20 0xc3 0x05 0xc8 0x59 0x7c 0x00 0x00 ' ��Y|'
    0x7c59c8045d68: 0x52 0x02 0x00 0x00 0x00 0x00 0x00 0x00 'R'
    0x7c59c8045d70: 0x2f 0x30 0x00 0x00 0x00 0x00 0x00 0x00 '/0'
    0x7c59c8045d78: 0xe5 0x03 0x00 0x00 0x00 0x00 0x00 0x00 '�'
    0x7c59c8045d80: 0x12 0x00 0x00 0x00 0x00 0x00 0x00 0x00 ''
    0x7c59c8045d88: 0x80 0x81 0x70 0xf6 0x58 0x7c 0x00 0x00 '��p�X|'
    0x7c59c8045d90: 0x60 0x08 0x00 0x00 0x00 0x00 0x00 0x00 ''
    0x7c59c8045d98: 0x45 0x6c 0x69 0x78 0x69 0x72 0x2e 0x47 'Elixir.G'
    0x7c59c8045da0: 0x6c 0x6f 0x62 0x61 0x6c 0x4d 0x61 0x70 'lobalMap'
    0x7c59c8045da8: 0x00 0x80 0x81 0x70 0xf6 0x58 0x7c 0x00 '��p�X|'
    0x7c59c8045db0: 0x00 0x90 0x82 0x70 0xf6 0x58 0x7c 0x00 '��p�X|'
    0x7c59c8045db8: 0x00 0x45 0x6c 0x69 0x78 0x69 0x72 0x2e 'Elixir.'
    0x7c59c8045dc0: 0x47 0x6c 0x6f 0x62 0x61 0x6c 0x4d 0x61 'GlobalMa'
    0x7c59c8045dc8: 0x70 0x3a 0x3a 0x63 0x6f 0x64 0x65 0x48 'p::codeH'
    0x7c59c8045dd0: 0x65 0x61 0x64 0x65 0x72 0x00 0x90 0x82 'eader��'
    0x7c59c8045dd8: 0x70 0xf6 0x58 0x7c 0x00 0x00 0xc0 0x82 'p�X|��'
    0x7c59c8045de0: 0x70 0xf6 0x58 0x7c 0x00 0x00 0x27 0x45 'p�X|'E'
    0x7c59c8045de8: 0x6c 0x69 0x78 0x69 0x72 0x2e 0x47 0x6c 'lixir.Gl'
    0x7c59c8045df0: 0x6f 0x62 0x61 0x6c 0x4d 0x61 0x70 0x27 'obalMap''
    0x7c59c8045df8: 0x3a 0x27 0x5f 0x5f 0x69 0x6e 0x66 0x6f ':'__info'
    0x7c59c8045e00: 0x5f 0x5f 0x27 0x2f 0x31 0x2d 0x43 0x6f '__'/1-Co'
    0x7c59c8045e08: 0x64 0x65 0x49 0x6e 0x66 0x6f 0x50 0x72 'deInfoPr'
    0x7c59c8045e10: 0x6f 0x6c 0x6f 0x67 0x75 0x65 0x00 0xc0 'ologue�'
    0x7c59c8045e18: 0x82 0x70 0xf6 0x58 0x7c 0x00 0x00 0xf0 '�p�X|�'
    0x7c59c8045e20: 0x83 0x70 0xf6 0x58 0x7c 0x00 0x00 0x27 '�p�X|''
    0x7c59c8045e28: 0x45 0x6c 0x69 0x78 0x69 0x72 0x2e 0x47 'Elixir.G'
    0x7c59c8045e30: 0x6c 0x6f 0x62 0x61 0x6c 0x4d 0x61 0x70 'lobalMap'
    0x7c59c8045e38: 0x27 0x3a 0x27 0x5f 0x5f 0x69 0x6e 0x66 '':'__inf'
    0x7c59c8045e40: 0x6f 0x5f 0x5f 0x27 0x2f 0x31 0x00 0xf0 'o__'/1�'
    0x7c59c8045e48: 0x83 0x70 0xf6 0x58 0x7c 0x00 0x00 0x20 '�p�X| '
    0x7c59c8045e50: 0x84 0x70 0xf6 0x58 0x7c 0x00 0x00 0x27 '�p�X|''
    0x7c59c8045e58: 0x45 0x6c 0x69 0x78 0x69 0x72 0x2e 0x47 'Elixir.G'
    0x7c59c8045e60: 0x6c 0x6f 0x62 0x61 0x6c 0x4d 0x61 0x70 'lobalMap'
    0x7c59c8045e68: 0x27 0x3a 0x63 0x6d 0x70 0x2f 0x31 0x2d '':cmp/1-'
    0x7c59c8045e70: 0x43 0x6f 0x64 0x65 0x49 0x6e 0x66 0x6f 'CodeInfo'
    0x7c59c8045e78: 0x50 0x72 0x6f 0x6c 0x6f 0x67 0x75 0x65 'Prologue'
    0x7c59c8045e80: 0x00 0x20 0x84 0x70 0xf6 0x58 0x7c 0x00 ' �p�X|'

總結

  1. 所有的內存泄露,不管什麼語言的應用,只要是linux ELF, windows PE, 都可以用 coredump + 頻率分析定位問題,這次的泄露是可讀字符串,對人類友好,頻率分析並不要求字節是可讀字符串。
  2. 開源組件出問題的可能性還是不小的,雖然一般先從自己代碼開始懷疑,在細心檢查後,可以大膽懷疑底層的問題。
  3. 從 elixir, erlang 一層層看下去,花了較多時間。在有代碼的情況下,完全不需要那麼多步驟,直接從 CodeInfoPrologue 開始找泄露點,修改代碼來驗證,會比較有效率。還是第一點,開始沒懷疑 erlang 會有bug,但是,如果是erlang層無法監測到的泄露,必然不可能是 elixir,在不瞭解全局的情況下,直接從 magic string 出發找泄露點,也是可行的,不一定非得知道整個過程中幹了什麼,分配了哪些內存,直接從 magic string 的地方 gdb 看調用棧。
  4. valgrind 的 memory leak 檢測對此次的泄露是有效的,valgrind 可以跟中分配的內存,懸垂的部分,並按調用棧聚合。erlang 啓用 valgrind 需要特別的編譯選項。
user avatar aaaak Avatar huazhu7k7k Avatar CHLL55 Avatar
Favorites 3 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.