作者:張文博

Kubernetes(K8s)是一個開源容器編排系統,可自動執行應用程序部署、擴展和管理。它是雲原生世界的操作系統。 K8s 或操作系統中的任何缺陷都可能使用戶進程存在風險。作為 PingCAP EE(效率工程)團隊,我們在 K8s 中測試 TiDB Operator(一個創建和管理 TiDB 集羣的工具)時,發現了兩個 Linux 內核錯誤。這些錯誤已經困擾我們很長一段時間,並沒有在整個 K8s 社區中徹底修復。

經過廣泛的調查和診斷,我們已經確定了處理這些問題的方法。在這篇文章中,我們將與大家分享這些解決方法。不過,儘管這些方法很有用,但我們認為這只是權宜之策,相信未來會有更優雅的解決方案,也期望 K8s 社區、RHEL 和 CentOS 可以在不久的將來徹底修復這些問題。

Bug #1: 診斷修復不穩定的 Kmem Accounting

關鍵詞:SLUB: Unable to allocate memory on node -1

社區相關 Issue:

  • github.com/kubernetes/k
  • github.com/opencontaine
  • support.mesosphere.com/

問題起源

薛定諤平臺是我司開發的基於 K8s 建立的一套自動化測試框架,提供各種 Chaos 能力,同時也提供自動化的 Bench 測試,各類異常監控、告警以及自動輸出測試報告等功能。我們發現 TiKV 在薛定諤平臺上做 OLTP 測試時偶爾會發生 I/O 性能抖動,但從下面幾項來看未發現異常:

  • TiKV 和 RocksDB 的日誌
  • CPU 使用率
  • 內存和磁碟等負載信息

只能偶爾看到 dmesg 命令執行的結果中包含一些 「SLUB: Unable to allocate memory on node -1」 信息。

問題分析

我們使用 perf-tools 中的 funcslower trace 來執行較慢的內核函數並調整內核參數 hung_task_timeout_secs 閾值,抓取到了一些 TiKV 執行寫操作時的內核路徑信息:

從上圖的信息中可以看到 I/O 抖動和文件系統執行 writepage 有關。同時捕獲到性能抖動的前後,在 node 內存資源充足的情況下,dmesg 返回的結果也會出現大量 「SLUB: Unable to allocate memory on node -1」 的信息。

hung_task 輸出的 call stack 信息結合內核代碼發現,內核在執行 bvec_alloc 函數分配 bio_vec 對象時,會先嘗試通過 kmem_cache_alloc 進行分配,kmem_cache_alloc 失敗後,再進行 fallback 嘗試從 mempool 中進行分配,而在 mempool 內部會先嘗試執行 pool->alloc 回調進行分配,pool->alloc 分配失敗後,內核會將進程設置為不可中斷狀態並放入等待隊列中進行等待,當其他進程向 mempool 歸還內存或定時器超時(5s) 後,進程調度器會喚醒該進程進行重試 ,這個等待時間和我們業務監控的抖動延遲相符。

但是我們在創建 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 分配失敗的問題也消失了。

操作步驟

  1. 我們需要在 kubelet 和 docker 上都將 kmem account 功能關閉。kubelet 需要重新編譯,不同的版本有不同的方式。如果 kubelet 版本是 v1.14 及以上,則可以通過在編譯 kubelet 的時候加上 Build Tags 來關閉 kmem account:

$ 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,步驟如下。

首先下載 Kubernetes 代碼:

$ 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 中。

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

Bug #2:診斷修復網路設備引用計數泄漏問題

關鍵詞:kernel:unregister_netdevice: waiting for eth0 to become free. Usage count = 1

社區相關 Issue:

  • github.com/kubernetes/k
  • github.com/projectcalic
  • github.com/moby/moby/is

問題起源

我們的薛定諤分散式測試集羣運行一段時間後,經常會持續出現「kernel:unregister_netdevice: waiting for eth0 to become free. Usage count = 1」 問題,並會導致多個進程進入不可中斷狀態,只能通過重啟伺服器來解決。

問題分析

通過使用 crash 工具對 vmcore 進行分析,我們發現內核線程阻塞在 netdev_wait_allrefs 函數,無限循環等待 dev->refcnt 降為 0。由於 pod 已經釋放了,因此懷疑是引用計數泄漏問題。我們查找 K8s issue 後發現問題出在內核上,但這個問題沒有簡單的穩定可靠復現方法,且在社區高版本內核上依然會出現這個問題。

為避免每次出現問題都需要重啟伺服器,我們開發一個內核模塊,當發現 net_device 引用計數已泄漏時,將引用計數清 0 後移除此內核模塊(避免誤刪除其他非引用計數泄漏的網卡)。為了避免每次手動清理,我們寫了一個監控腳本,週期性自動執行這個操作。但此方案仍然存在缺陷:

  • 引用計數的泄漏和監控發現之間存在一定的延遲,在這段延遲中 K8s 系統可能會出現其他問題;
  • 在內核模塊中很難判斷是否是引用計數泄漏,netdev_wait_allrefs 會通過 Notification Chains 向所有的消息訂閱者不斷重試發布 NETDEV_UNREGISTERNETDEV_UNREGISTER_FINAL 消息,而經過 trace 發現消息的訂閱者多達 22 個,而去弄清這 22 個訂閱者註冊的每個回調函數的處理邏輯來判斷是否有辦法避免誤判也不是一件簡單的事。

解決方案

在我們準備深入到每個訂閱者註冊的回調函數邏輯的同時,我們也在持續關注 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 升級到新版後,不必再擔心這個問題。

原文:pingcap.com/blog-cn/fix

推薦閱讀:

相關文章