接收程序員的 8 點技術早餐
兩年來,我們一直在使用 Kubernetes 進行深度學習方面的研究。雖然我們大量的 workloads 可以直接運行在云虛擬機上,但是因為 Kubernetes 目前仍在快速迭代,而且擁有比較好的擴展性,也沒有過多的約束,因此 Kubernetes 成為了我們最理想的集群管理工具。目前,我們已經有好幾個正在運行的 Kubernetes 集群了(有些運行在云虛擬機上,有些則直接運行在物理機上)。其中最大的一個集群運行在 Azure 上,由 D15v2 和 NC24 兩種類型的虛擬機組成,目前已經超過 2500 個節(jié)點。
在達到 2500 節(jié)點規(guī)模的過程中,許多的系統(tǒng)組件都出現(xiàn)了問題,包括 etcd,Kube Master,Docker 拉取鏡像,網絡,Kube DNS,機器上的 ARP 緩存。接下來將分享我們所遇到的問題和解決方案,希望對大家有所幫助。
在集群中添加了超過 500 個節(jié)點的時候,我們的研究人員發(fā)現(xiàn)使用 kubectl 操作集群的時候會發(fā)生超時的情況。于是我們嘗試添加更多的 Kube Master 節(jié)點(運行 kube-apiserver 的節(jié)點)。這樣做臨時解決了這個問題,但當我們添加了 10 個 Master 節(jié)點后,我們發(fā)現(xiàn)這只是在表面上解決問題,并沒有找到這個問題的根本原因(作為對比,GKE 只使用了一臺 32 核的虛擬機作為 Master 就能管理 500 個節(jié)點。
etcd 存儲了 Kubernetes 集群的所有狀態(tài)數(shù)據(jù),因此我們強烈懷疑是 etcd 集群出現(xiàn)了問題。通過查看 Datadog,我們發(fā)現(xiàn)雖然我們的每臺機器都使用了支持 5000 IOPS 的 P30 SSD,但是運行了 etcd 的 DS15v2 虛擬機的寫延遲在某些時間段猛增至數(shù)百毫秒。
這么高的寫延遲阻塞了整個集群!
在使用 fio 進行性能測試之后,我們發(fā)現(xiàn) etcd 的 I/O 性能只能達到 I/O 上限的 10% 左右 。這主要是因為單次寫延遲為 2ms,而 etcd 又使用了串行 I/O,導致 I/O 之間的延遲疊加(latency-bound)。
然后,我們將每個節(jié)點上的 etcd 的目錄從網絡磁盤移到本地 SSD 磁盤上。移動之后,寫延遲降低到了 200 微秒,etcd 終于正常了。
我們的集群一直運行良好,直到我們增加到了 1000 個節(jié)點。這個時候我們再次發(fā)現(xiàn) etcd 出現(xiàn)了很高的延遲。這一次,我們注意到 kube-apiservers 從 etcd 讀取數(shù)據(jù)的速度超過了 500MB/s。我們搭建了 Prometheus 用于監(jiān)控 kube-apiservers,還設置了 --audit-log-path 和 --audit-log-maxbackup 選項來保存更多的日志數(shù)據(jù)。通過日志,我們發(fā)現(xiàn)了許多慢查詢請求和大量的對 Events 的 List API 的請求。
我們找到了問題的根本原因,我們發(fā)現(xiàn)部分程序會頻繁去 kube-apiservers 查詢各種信息(比如 Fluentd 和 Datdog 會去 kube-apiservers 查詢每個節(jié)點信息)。我們修改了這些程序的設置,降低它們對 kube-apiservers 的查詢頻率,然后 kube-apiservers 的負載就降下來了:
etcd 的出口速度從 500MB/s 左右降低到了接近 0MB/s(上圖中負值表示出口速度)
另一個有效的改動是把 Kubernetes Events 存儲在一個單獨的 etcd 集群里,這樣對 Events 的操作就不會影響到主 etcd 集群的正常工作。要想能做到這一點,我們只需要將這 --etcd-servers-overrides 設置為
另一個超過 1000 節(jié)點后的故障是觸發(fā)了 etcd 的硬盤存儲上限(默認是 2GB), 于是 etcd 不再接受寫請求。這直接導致了一連串的問題:所有 kube node 的健康檢查都失敗了,我們的 autoscaler 決定刪除所有的 workers。于是我們通過 etcd 的 --quota-backend-bytes 增大了存儲大小。另外還讓 autoscaler 支持基礎(sanity)檢查,在發(fā)現(xiàn)要終止集群里超過 50% 的 workers 的時候,放棄這個操作。
我們在同一臺機器上安裝了 kube-apiserver,kube-controller-manager 和 kube-scheduler processes。為了高可用,我們一直有至少兩個 masters,并將 --apiserver-count 設置為我們正在運行的 apiservers 數(shù)量(否則 Prometheus 可能會混淆這些實例)。
我們主要使用 Kubernetes 作為批量調度系統(tǒng),并依靠 autoscaler 動態(tài)地伸縮我們的集群。這使我們可以顯著地降低空閑節(jié)點的成本,在快速迭代的同時提供低延遲。默認的 kube-scheduler 策略是將負載均勻分散在節(jié)點之間,但這與我們的期望相悖,我們希望可以終止未使用的節(jié)點,同時也可以快速調度大的 pod。所以我們切換到下面的策略:
我們使用 KubeDNS 實現(xiàn)服務發(fā)現(xiàn),但是在我們使用新的調度策略后,很快它就出現(xiàn)了一些可靠性問題。我們發(fā)現(xiàn)故障只在 KubeDNS 的 Pods 中出現(xiàn)。在新的調度規(guī)則影響下,有些機器上運行了十多個 KubeDNS 實例,這就導致了這些機器成為了熱點,它們收到的 DNS 查詢超過了 200 QPS(這是 Azure 虛擬機對 DNS 查詢的上限值)。
我們的 Dota 項目剛開始運行在 Kubernetes 上的時候,一旦它開始擴容,我們就注意到一些新的節(jié)點上有很多 Pods 處于 Pending 狀態(tài),而且持續(xù)很長時間。Dota 的鏡像大小為 17GB,把這個鏡像拉取到一個新的節(jié)點上需要大約 30 分鐘的時間。因此我們知道了 Dota 的 Pod 處于 Pending 的原因,但是同一個節(jié)點上的其他比較小的 Pod 也出現(xiàn)了相同的情況。
隨著調查的深入,我們發(fā)現(xiàn) kubelet 的 --serialize-image-pulls 默認為 true,這意味著拉取 Dota 鏡像的時候,其他鏡像的拉取都會被阻塞住。把這個選項修改為 false 需要讓 Docker 使用 overlay2 文件系統(tǒng)而不是 AUFS。為了加快拉取速度,我們把 Docker 的根存儲目錄遷移到了機器的本地 SSD 上,就像 etcd 那樣。
在優(yōu)化了拉取速度之后,我們仍然發(fā)現(xiàn) Pods 出現(xiàn)了奇怪的錯誤:error message: rpc error: code = 2 desc = net/http: request canceled。由于進度不足,Kubelet 和 Docker 的日志里也指出了鏡像拉取被取消了。我們究其根源,發(fā)現(xiàn)當有很多積壓的鏡像拉取任務,或者有些大鏡像花費很長的時間都沒有完成拉取和解壓工作的時候,就會出現(xiàn)這種錯誤。為了解決這個問題,我們將 Kubelet 的 --image-pull-progress-deadline 設置為 30 分鐘, 并且設置了 Docker 的 max-concurrent-downloads 為 10。(第二個選項不會加速大鏡像的解壓工作,但是可以讓拉取鏡像的工作并行執(zhí)行 。)
我們最后的 Docker 鏡像拉取問題源自 Google Container Registry。默認情況下, Kubelet 會自動從 gcr.io 拉取一個特殊的鏡像(可由 --pod-infra-container-image 控制),這個鏡像用于啟動新的容器。如果拉取失敗,這個節(jié)點就不能啟動任何 Pod 。由于我們的節(jié)點沒有公網 IP,只能通過 NAT 訪問 gcr.io,一旦超過了單個 IP 的配額上限,就會拉取失敗。為了解決這個問題,我們通過預加載鏡像直接將鏡像部署到機器上。通過 docker image save -o /opt/preloaded_docker_images.tar 和 docker image load -i /opt/preloaded_docker_images.tar 完成這個工作。為了提升性能,我們對其他公共鏡像比如 OpenAI-internal 和 Dota 鏡像也做了相同的事情。
隨著我們的實驗越來越大,我們的系統(tǒng)也逐漸變成了重度依賴網絡的復雜的分布式系統(tǒng)。當我們第一次開始分布式實驗的時候,立即就發(fā)現(xiàn)了我們的網絡不是很好。機器之間的吞吐量大約在 10-15Gbit/s, 但是我們基于 Flannel 的 Pods 之間的最大吞吐量僅僅只有 2Gbit/s。
Machine Zone 的公開性能測試也顯示了類似的結果,這意味著這個問題未必是由配置不好導致的,很有可能是我們的環(huán)境中隱藏了一些問題(機器上的 Flannel 應該不會增加這么大的開銷)。
為了解決這個問題,用戶可以使用兩個不同的設置來讓 Pods 繞過 Flannel 的網絡:hostNetwork 設置為 true,dnsPolicy 設置為 ClusterFirstWithHostNet(在做這件事情之前請先閱讀 Kubernetes 中的警告)。
雖然我們優(yōu)化了 DNS 的 Pods,但是 DNS 解析仍然時不時地出點問題。有一天,一位工程師報告說,他用 nc -v 連接 Redis 服務器時,花費了超過 30 秒才成功建立連接。我們深入這個問題一直到內核的 ARP 棧。初步調查顯示 Redis Pod 所在節(jié)點的網絡出現(xiàn)了嚴重錯誤:連接任意端口都會掛起好幾秒,而且本地的 dnsmasq 沒有任何 DNS 記錄,dig 只是打印了一個奇怪的錯誤信息:socket.c:1915: internal_send: 127.0.0.1#53: Invalid argument。dmesg 的日志反而有用的多:neighbor table overflow!這表示 ARP 已經用完了緩存空間。ARP 是用來映射 IPv4 地址到一個物理地址的(比如 MAC 地址)。幸運的是,只需要在 /etc/sysctl.conf 中設置幾個選項就能輕松解決這個問題:
這個修改在 HPC 集群中非常常見,而此時在 Kubernetes 中這個選項也非常重要,因為每個 Pod 都有自己的 IP,而每個 IP 都需要占用 ARP 緩存空間。
https://blog.openai.com/scaling-kubernetes-to-2500-nodes/?utm_source=digg
本文由【K8sMeetup 中國社區(qū)】授權轉載,微信號【kuberneteschina】。