作者:張文博
Kubernetes(K8s)是一個開源容器編排系統,可自動執行應用程序部署、擴展和管理。它是雲原生世界的操作系統。 K8s 或操作系統中的任何缺陷都可能使用戶進程存在風險。作為 PingCAP EE(效率工程)團隊,我們在 K8s 中測試 TiDB Operator(一個創建和管理 TiDB 集羣的工具)時,發現了兩個 Linux 內核錯誤。這些錯誤已經困擾我們很長一段時間,並沒有在整個 K8s 社區中徹底修復。
經過廣泛的調查和診斷,我們已經確定了處理這些問題的方法。在這篇文章中,我們將與大家分享這些解決方法。不過,儘管這些方法很有用,但我們認為這只是權宜之策,相信未來會有更優雅的解決方案,也期望 K8s 社區、RHEL 和 CentOS 可以在不久的將來徹底修復這些問題。
關鍵詞:SLUB: Unable to allocate memory on node -1
社區相關 Issue:
薛定諤平臺是我司開發的基於 K8s 建立的一套自動化測試框架,提供各種 Chaos 能力,同時也提供自動化的 Bench 測試,各類異常監控、告警以及自動輸出測試報告等功能。我們發現 TiKV 在薛定諤平臺上做 OLTP 測試時偶爾會發生 I/O 性能抖動,但從下面幾項來看未發現異常:
只能偶爾看到 dmesg 命令執行的結果中包含一些 「SLUB: Unable to allocate memory on node -1」 信息。
我們使用 perf-tools 中的 funcslower trace 來執行較慢的內核函數並調整內核參數 hung_task_timeout_secs 閾值,抓取到了一些 TiKV 執行寫操作時的內核路徑信息:
hung_task_timeout_secs
從上圖的信息中可以看到 I/O 抖動和文件系統執行 writepage 有關。同時捕獲到性能抖動的前後,在 node 內存資源充足的情況下,dmesg 返回的結果也會出現大量 「SLUB: Unable to allocate memory on node -1」 的信息。
dmesg
從 hung_task 輸出的 call stack 信息結合內核代碼發現,內核在執行 bvec_alloc 函數分配 bio_vec 對象時,會先嘗試通過 kmem_cache_alloc 進行分配,kmem_cache_alloc 失敗後,再進行 fallback 嘗試從 mempool 中進行分配,而在 mempool 內部會先嘗試執行 pool->alloc 回調進行分配,pool->alloc 分配失敗後,內核會將進程設置為不可中斷狀態並放入等待隊列中進行等待,當其他進程向 mempool 歸還內存或定時器超時(5s) 後,進程調度器會喚醒該進程進行重試 ,這個等待時間和我們業務監控的抖動延遲相符。
hung_task
bvec_alloc
bio_vec
kmem_cache_alloc
pool->alloc
但是我們在創建 Docker 容器時,並沒有設置 kmem limit,為什麼還會有 kmem 不足的問題呢?為了確定 kmem limit 是否被設置,我們進入 cgroup memory controller 對容器的 kmem 信息進行查看,發現 kmem 的統計信息被開啟了, 但 limit 值設置的非常大。
我們已知 kmem accounting 在 RHEL 3.10 版本內核上是不穩定的,因此懷疑 SLUB 分配失敗是由內核 bug 引起的,搜索 kernel patch 信息我們發現確實是內核 bug, 在社區高版本內核中已修復:
slub: make dead caches discard free slabs immediately
同時還有一個 namespace 泄漏問題也和 kmem accounting 有關:
mm: memcontrol: fix cgroup creation failure after many small jobs
那麼是誰開啟了 kmem accounting 功能呢?我們使用 bcc 中的 opensnoop 工具對 kmem 配置文件進行監控,捕獲到修改者 runc 。從 K8s 代碼上可以確認是 K8s 依賴的 runc 項目默認開啟了 kmem accounting。
通過上述分析,我們要麼升級到高版本內核,要麼在啟動容器的時候禁用 kmem accounting 功能,目前 runc 已提供條件編譯選項,可以通過 Build Tags 來禁用 kmem accounting,關閉後我們測試發現抖動情況消失了,namespace 泄漏問題和 SLUB 分配失敗的問題也消失了。
$ git clone --branch v1.14.1 --single-branch --depth 1 [https://github.com/kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) $ cd kubernetes
$ KUBE_GIT_VERSION=v1.14.1 ./build/run.sh make kubelet GOFLAGS="-tags=nokmem"
但如果 kubelet 版本是 v1.13 及以下,則無法通過在編譯 kubelet 的時候加 Build Tags 來關閉,需要重新編譯 kubelet,步驟如下。
$ git clone --branch v1.12.8 --single-branch --depth 1 https://github.com/kubernetes/kubernetes $ cd kubernetes
然後手動將開啟 kmem account 功能的 兩個函數 替換成 下面這樣:
func EnableKernelMemoryAccounting(path string) error { return nil }
func setKernelMemory(path string, kernelMemoryLimit int64) error { return nil }
之後重新編譯 kubelet:
$ KUBE_GIT_VERSION=v1.12.8 ./build/run.sh make kubelet
編譯好的 kubelet 在 ./_output/dockerized/bin/$GOOS/$GOARCH/kubelet 中。
./_output/dockerized/bin/$GOOS/$GOARCH/kubelet
2. 同時需要升級 docker-ce 到 18.09.1 以上,此版本 docker 已經將 runc 的 kmem account 功能關閉。
3. 最後需要重啟機器。
驗證方法是查看新創建的 pod 的所有 container 已關閉 kmem,如果為下面結果則已關閉:
$ cat /sys/fs/cgroup/memory/kubepods/burstable/pod<pod-uid>/<container-id>/memory.kmem.slabinfo cat: memory.kmem.slabinfo: Input/output error
關鍵詞:kernel:unregister_netdevice: waiting for eth0 to become free. Usage count = 1
我們的薛定諤分散式測試集羣運行一段時間後,經常會持續出現「kernel:unregister_netdevice: waiting for eth0 to become free. Usage count = 1」 問題,並會導致多個進程進入不可中斷狀態,只能通過重啟伺服器來解決。
通過使用 crash 工具對 vmcore 進行分析,我們發現內核線程阻塞在 netdev_wait_allrefs 函數,無限循環等待 dev->refcnt 降為 0。由於 pod 已經釋放了,因此懷疑是引用計數泄漏問題。我們查找 K8s issue 後發現問題出在內核上,但這個問題沒有簡單的穩定可靠復現方法,且在社區高版本內核上依然會出現這個問題。
netdev_wait_allrefs
dev->refcnt
為避免每次出現問題都需要重啟伺服器,我們開發一個內核模塊,當發現 net_device 引用計數已泄漏時,將引用計數清 0 後移除此內核模塊(避免誤刪除其他非引用計數泄漏的網卡)。為了避免每次手動清理,我們寫了一個監控腳本,週期性自動執行這個操作。但此方案仍然存在缺陷:
net_device
NETDEV_UNREGISTER
NETDEV_UNREGISTER_FINAL
在我們準備深入到每個訂閱者註冊的回調函數邏輯的同時,我們也在持續關注 kernel patch 和 RHEL 的進展,發現 RHEL 的 solutions:3659011 有了一個更新,提到 upstream 提交的一個 patch:
route: set the deleted fnhe fnhe_daddr to 0 in ip_del_fnhe to fix a race
在嘗試以 hotfix 的方式為內核打上此補丁後,我們持續測試了 1 周,問題沒有再復現。我們向 RHEL 反饋測試信息,得知他們已經開始對此 patch 進行 backport。
推薦內核版本 Centos 7.6 kernel-3.10.0-957 及以上。
1.安裝 kpatch 及 kpatch-build 依賴:
UNAME=$(uname -r) sudo yum install gcc kernel-devel-${UNAME%.*} elfutils elfutils-devel sudo yum install pesign yum-utils zlib-devel binutils-devel newt-devel python-devel perl-ExtUtils-Embed audit-libs audit-libs-devel numactl-devel pciutils-devel bison
# enable CentOS 7 debug repo sudo yum-config-manager --enable debug
sudo yum-builddep kernel-${UNAME%.*} sudo debuginfo-install kernel-${UNAME%.*}
# optional, but highly recommended - enable EPEL 7 sudo yum install ccache ccache --max-size=5G
2.安裝 kpatch 及 kpatch-build:
git clone https://github.com/dynup/kpatch && cd kpatch make sudo make install systemctl enable kpatch
3.下載並構建熱補丁內核模塊:
curl -SOL https://raw.githubusercontent.com/pingcap/kdt/master/kpatchs/route.patch kpatch-build -t vmlinux route.patch (編譯生成內核模塊) mkdir -p /var/lib/kpatch/${UNAME} cp -a livepatch-route.ko /var/lib/kpatch/${UNAME} systemctl restart kpatch (Loads the kernel module) kpatch list (Checks the loaded module)
雖然我們修復了這些內核錯誤,但是未來應該會有更好的解決方案。對於 Bug#1,我們希望 K8s 社區可以為 kubelet 提供一個參數,以允許用戶禁用或啟用 kmem account 功能。對於 Bug#2,最佳解決方案是由 RHEL 和 CentOS 修復內核錯誤,希望 TiDB 用戶將 CentOS 升級到新版後,不必再擔心這個問題。
原文:https://pingcap.com/blog-cn/fix-two-linux-kernel-bugs-while-testing-tidb-operator-in-k8s/
推薦閱讀: