本文選自「字節跳動基礎架構實踐」系列文章。html
「字節跳動基礎架構實踐」系列文章是由字節跳動基礎架構部門各技術團隊及專家傾力打造的技術乾貨內容,和你們分享團隊在基礎架構發展和演進過程當中的實踐經驗與教訓,與各位技術同窗一塊兒交流成長。git
「延遲突刺」、「性能抖動」等問題一般會受到多方因素影響不便排查,本文以線上問題爲例,詳解 TLB shootdown,最終使得 CPU 的消耗下降 2% 左右,並消除了抖動突刺,變得更加穩定。github
在互聯網業務運行的過程當中,不免遇到「延遲突刺」、「性能抖動」等問題,而一般這類問題會受到多種軟件環境甚至硬件環境的影響,緣由較爲隱晦,解決起來相對棘手。數據庫
本文以一個線上問題爲例子,深刻 x86 體系結構,結合內核內存管理的知識,輔以多種 Linux 平臺上的 Debug 工具,詳解 TLB shootdown 問題,最終解決掉該問題,提高了業務性能。api
Kernel: 本文中特指 Linux-4.14。緩存
KVM: Kernel-based Virtual Machine。如今主流的虛擬機技術之一。bash
Host: 指虛擬化場景下的宿主機。服務器
Guest: 指虛擬化場景下的虛擬機。多線程
APIC: Advanced Programmable Interrupt Controller 。Intel CPU 使用的中斷控制器。架構
LAPIC: Local Advanced Programmable Interrupt Controller 。
IPI: Inter-Processor Interrupt。CPU 之間相互通知使用。
MMU: Memory Management Unit。Kernel 用來管理虛擬地址和物理地址映射的硬件。
TLB: Translation Lookaside Buffer。MMU 爲了加速查找頁表,使用的 cache。用來加速 MMU 的轉化速度。
PTE: Page Table Entry 。管理頁表使用的頁表項。
jemalloc: 一個用戶態內存管理的庫,在多線程併發的場景下,malloc/free 的性能好於 glibc 默認的實現。
如上圖所示,一個進程有 4 個 thread並行執行。因爲 4 個 thread 共享同一個進程的頁表,在執行的過程當中,經過把 pgd 加載到 cr3 的方式,每一個 CPU 的 TLB 中加載了相同的 page table。
若是 CPU0 上,想要修改 page table,尤爲是想要釋放一些內存,那麼須要修改 page table,同時修改本身的 TLB(或者從新加載 TLB)。
然而,這還不夠。例如,CPU0 上釋放了 page A,而且 page A 被 kernel 回收,頗有可能被其餘的進程使用。可是,CPU一、CPU2 以及 CPU3 的 TLB 中仍是緩存了對應的 PTE 表項,依然能夠訪問到 page A。
爲了防止這個事情發生,CPU0 須要通知 CPU一、CPU2 和 CPU3,也須要在 TLB 中禁用掉對應的 PTE。通知的方式就是使用IPI (Inter-Processor Interrupt)。
在虛擬化的場景下,IPI 的成本比較高。若是 Guest 中有大量的 IPI,就會看到 Guest 的 CPU sys 暴漲。同時,在 Host 上能夠發現虛擬機發生 vmexit 突增,其中主要是 wrmsr 的 ICR Request 產生。(熟悉 x86 的同窗知道,x2apic 模式下,x86 上 IPI 的實現即經過 wrmsr 指令請求 ICR)
#watch -d -n 1 "cat /proc/interrupts | grep TLB"
複製代碼
若是看到數據上漲比較厲害,那麼基本就能夠看到問題了。
#perf top
複製代碼
若是看到 smp_call_function_many,那麼很不幸,就是在批量發送 IPI。
好消息是這個場景並不常見,比較特定的狀況下才會發生。典型的就是用戶態進程中調用了系統調用:
int madvise(void *addr, size_t length, MADV_DONTNEED);
複製代碼
# ls /proc/*/maps | xargs grep jemalloc
複製代碼
# strace -f -p 1510 2>&1 | grep madvise
複製代碼
確認上述的 TLB shootdown 問題以後,咱們再來回顧一下,系統調用 madvise 到底起了什麼做用呢?
int madvise(void *addr, size_t length, MADV_DONTNEED);
複製代碼
若是使用了 DONTNEED,就會釋放對應的 page。若是下一次再訪問到,就會重複上述的 2 和 3。
效果就是短暫的 page 歸還 kernel 以後,下次訪問從新分配。
例如 state 0 所示,用戶態進程分配了 VMA0 和 VMA1 兩個虛擬機地址空間。有的地址上已經分配了物理頁面(例如 0x800000),有的尚未分配(例如 0x802000)。
如 state 1 所示,用戶態進程第一次訪問到了例如 0x802000 地址的時候,觸發了 page fault,內核爲用戶態進程的 0x802000 分配了物理頁面(地址是 0x202000)。
如 state 2 所示,執行了:
madvise(0x800000, 8192, MADV_DONTNEED)
複製代碼
以後,內核釋放了對應的物理頁面。那麼下一次訪問到 0x800000 ~ 0x801fff 的時候,就會觸發 page fault。處理過程相似 state 1。
如 state 3 所示,執行了:
munmap(0x800000, 16384);
複製代碼
就把對應的 VMA 釋放了。那麼下次訪問到 0x800000 ~ 0x803fff 的時候,就會觸發 segment fault。由於地址已經釋放,屬於非法地址,內核會給進程發送 signal 11。大部分狀況下,會殺掉進程。
問題產生自 jemalloc,因此嘗試從 jemalloc 自己入手解決問題。
嘗試去社區,問 jemalloc 的 maintainer,是否有辦法解決 TLB shootdown 引發的問題,maintainer 建議經過 jemalloc 環境變量(MALLOC_CONF)動態控制 jemalloc 是否啓動 madvise。問題和答覆見:
https://github.com/jemalloc/jemalloc/issues/1422
複製代碼
在本地寫測試代碼,實際測試 jemalloc(比較靠近 upstream 的 5.0版本)和 maintainer 給出來的建議,在進程啓動前導入環境變量:
MALLOC_CONF=dirty_decay_ms:-1,muzzy_decay_ms:-1
複製代碼
能夠驗證能夠成功避免問題。該環境變量能夠解決 tlb 問題,詳細參數做用請參看手冊:
http://jemalloc.net/jemalloc.3.html#opt.dirty_decay_ms
複製代碼
某業務一樣使用了 jemalloc,可是測試沒有效果。
對業務實際使用的 so 動態連接庫進行:
#strings libjemalloc.so.2 | grep -i version
複製代碼
能夠發現實際使用的版本是:
JEMALLOC_VERSION "4.2.0-0-gf70a254d44c8d30af2cd5d30531fb18fdabaae6d"
複製代碼
經過閱讀 jemalloc 的源代碼發現,在 4.2 版本的時候,還不支持 maintainer 給出來的變量參數。可是能夠經過以下變量來達到相似的效果:
MALLOC_CONF=purge:decay,decay_time:-1
複製代碼
設置了 jemalloc 的參數以後,業務的表現獲得了明顯的提高。以下圖所示,最後一個零點和前一個零點進行對比,CPU 的抖動狀況獲得了很大的改善,從以前的 6% 左右抖動到低於 4% 的穩定運行,且 CPU 的消耗曲線更加穩定平滑。
與此同時,業務上的延遲也更加穩定,PCT99 也下降了延遲突刺狀況。
在解決問題的過程當中,也並不是如文章所寫的通常有序進行。期間也屢次使用 perf 觀察熱點函數的變化;使用 atop 對比先後的業務表現和系統指標;也觀察虛擬化的監控數據(wrmsr 的數量)等等手段,一步一步排除干擾,鎖定問題。
隨着當代操做系統的複雜度的提升,問題的難度也在提升。在解決問題的過程當中,咱們也在進步!
最後,歡迎加入字節跳動基礎架構團隊,一塊兒探討、解決問題,一塊兒變強!
字節跳動基礎架構團隊是支撐字節跳動旗下包括抖音、今日頭條、西瓜視頻、火山小視頻在內的多款億級規模用戶產品平穩運行的重要團隊,爲字節跳動及旗下業務的快速穩定發展提供了保證和推進力。
公司內,基礎架構團隊主要負責字節跳動私有云建設,管理數以萬計服務器規模的集羣,負責數萬臺計算/存儲混合部署和在線/離線混合部署,支持若干 EB 海量數據的穩定存儲。
文化上,團隊積極擁抱開源和創新的軟硬件架構。咱們長期招聘基礎架構方向的同窗,具體可參見 job.bytedance.com,感興趣能夠聯繫郵箱 arch-graph@bytedance.com 。
歡迎關注字節跳動技術團隊