容器云平臺(tái)API Server卡頓問題排查
58云計(jì)算平臺(tái)是58集團(tuán)架構(gòu)線基于Kubernetes + Docker技術(shù)為集團(tuán)內(nèi)部服務(wù)開發(fā)的一套業(yè)務(wù)實(shí)例管理平臺(tái),它具有簡(jiǎn)單,輕量的特點(diǎn)及高效利用物理資源,更快的部署和統(tǒng)一規(guī)范的標(biāo)準(zhǔn)化運(yùn)行環(huán)境,通過(guò)云平臺(tái),使得服務(wù)標(biāo)準(zhǔn)化,上線流程規(guī)范化,資源利用合理化。然而云平臺(tái)的建設(shè)過(guò)程不是一帆風(fēng)順,也不乏出現(xiàn)一些問題挑戰(zhàn),本文就針對(duì)云平臺(tái)現(xiàn)實(shí)中遇到的一個(gè)問題和大家分享。
1、關(guān)于問題
1.1 問題概述
近期,很多業(yè)務(wù)同事反饋使用云平臺(tái)上線存在容器部署慢,平臺(tái)反應(yīng)慢的問題。通過(guò)詳細(xì)的問題排查定位后,最終問題得以解決。
1.2 Kubernetes基本知識(shí)
私有云平臺(tái)通過(guò)Kubernetes對(duì)容器進(jìn)行編排。Kubernetes整體架構(gòu)如下圖所示:
其中幾個(gè)主要的模塊的功能簡(jiǎn)要描述如下:
- etcd:用于Kubernetes的后端存儲(chǔ)。
- Pod:Kubernetes最基本的操作單元,包含一個(gè)或多個(gè)緊密相關(guān)的容器。
- Replication Controller:副本控制器,用來(lái)保證Deployment或者RC中副本的數(shù)量。
- Scheduler:Kubernetes的調(diào)度器,Scheduler監(jiān)聽API Server,當(dāng)需要?jiǎng)?chuàng)建新的Pod時(shí)Scheduler負(fù)責(zé)選擇該P(yáng)od與哪個(gè)Node進(jìn)行綁定。
- Kubelet:每個(gè)Node節(jié)點(diǎn)上都會(huì)有一個(gè)Kubelet負(fù)責(zé)Master下發(fā)到該節(jié)點(diǎn)的具體任務(wù),管理該節(jié)點(diǎn)上的Pod和容器。
- API Server:對(duì)于整個(gè)Kubernetes集群而言,API Server是通過(guò)暴露Kubernetes API的方式提供給內(nèi)部組件或者外部程序調(diào)用去完成對(duì)Kubernetes的操作。各個(gè)組件之間也是通過(guò)API Server作為橋梁進(jìn)行間接通信,這種方式做到各個(gè)組件間充分解耦。
業(yè)務(wù)同事操作管理平臺(tái)發(fā)出創(chuàng)建集群請(qǐng)求到集群創(chuàng)建完成的整個(gè)流程如下:
- 業(yè)務(wù)同學(xué)操作管理平臺(tái)進(jìn)行升級(jí)操作,管理平臺(tái)通過(guò)http方式向API Server發(fā)出請(qǐng)求。
- API Server處理和解析請(qǐng)求參數(shù),將待創(chuàng)建的Pod信息通過(guò)API Server存儲(chǔ)到etcd。
- Scheduler通過(guò)API Server的watch機(jī)制,查看到新的Pod,嘗試為Pod綁定Node。
- 經(jīng)過(guò)預(yù)選篩除不合適節(jié)點(diǎn)及從待選節(jié)點(diǎn)中根據(jù)一定規(guī)則選出最適合的節(jié)點(diǎn)。
- 對(duì)選中的節(jié)點(diǎn)及Pod進(jìn)行binding操作,將相關(guān)的結(jié)果通過(guò)API Server存儲(chǔ)到etcd。
- 對(duì)應(yīng)Node的Kubelet進(jìn)程調(diào)用容器運(yùn)行時(shí)創(chuàng)建容器。
2. 定位問題
2.1 問題排查
從1.2可以看到,API Server在創(chuàng)建Pod過(guò)程中起到非常關(guān)鍵的中間橋梁作用,解析外部請(qǐng)求及讀寫etcd。因此決定首先從API Server進(jìn)程所在宿主機(jī)的各項(xiàng)性能指標(biāo)及日志方面進(jìn)行排查,看是否有所發(fā)現(xiàn)。
目前線上環(huán)境有3臺(tái)主機(jī)運(yùn)行API Server,以達(dá)到流量負(fù)載均衡的目的,異常時(shí)間段網(wǎng)卡eth2入流量如下圖所示:
由3臺(tái)API Server主機(jī)的監(jiān)控?cái)?shù)據(jù),發(fā)現(xiàn)服務(wù)器A的網(wǎng)卡入流量遠(yuǎn)高于另外兩臺(tái),說(shuō)明絕大部分請(qǐng)求發(fā)送到了服務(wù)器A。
通過(guò)對(duì)比三臺(tái)服務(wù)器API Server 的CPU利用率,發(fā)現(xiàn)服務(wù)器A的API Server進(jìn)程CPU使用率一直保持在2000%(20核)上下波動(dòng),而另外兩臺(tái)服務(wù)器的API Server的CPU利用率沒有超過(guò)100%(1核)。進(jìn)一步證實(shí)了A的API Server進(jìn)程處理了絕大多數(shù)的請(qǐng)求。
查看A服務(wù)器的API Server的相關(guān)log,發(fā)現(xiàn)正在大量輸出如下的日志:
這個(gè)日志顯示有大量請(qǐng)求通過(guò)API Server到etcd查詢Pod的狀態(tài)。
對(duì)于Kubernetes后端的存儲(chǔ)目前采用5個(gè)etcd節(jié)點(diǎn)組成etcd集群。登陸其中一個(gè)節(jié)點(diǎn)(E1),發(fā)現(xiàn)對(duì)E1節(jié)點(diǎn)執(zhí)行etcd操作命令,比如命令:“etcdctl ls /registry/pods/default”,命令執(zhí)行也會(huì)經(jīng)常超時(shí)。如果你想和更多Kubernetes技術(shù)專家交流,可以加我微信liyingjiese,備注『加群』。群里每周都有全球各大公司的***實(shí)踐以及行業(yè)***動(dòng)態(tài)。
同時(shí)對(duì)比5臺(tái)etcd節(jié)點(diǎn)的流量,發(fā)現(xiàn)有一個(gè)節(jié)點(diǎn)網(wǎng)卡入流量遠(yuǎn)高于其他四個(gè)節(jié)點(diǎn),該節(jié)點(diǎn)(E1)的etcd進(jìn)程的CPU利用率在100%左右,明顯高于剩余的4個(gè)節(jié)點(diǎn)CPU利用率。查看節(jié)點(diǎn)E1的etcd進(jìn)程日志,經(jīng)常看到如下報(bào)錯(cuò):
可以推斷節(jié)點(diǎn)E1的負(fù)載非常高,節(jié)點(diǎn)間同步心跳都已經(jīng)超時(shí),無(wú)法正常的響應(yīng)外部的請(qǐng)求了。
2.2 問題分析
經(jīng)過(guò)上述排查,主要集中在這兩個(gè)問題上:
2.2.1負(fù)載均衡策略失效
首先可以看到對(duì)Kubernetes集群的操作請(qǐng)求大部分都落在某個(gè)API Server上,導(dǎo)致其中一個(gè)API Server負(fù)載很高,那么有可能負(fù)載均衡策略有些問題。那就先看看當(dāng)前負(fù)載均衡策略是如何的。
當(dāng)前我們租賃的是騰訊的機(jī)房,負(fù)載均衡策略采用的是TGW(Tencent Gateway)系統(tǒng)所自帶支持的負(fù)載均衡策略。騰訊云上有關(guān)介紹如下:
TGW負(fù)載均衡策略保證請(qǐng)求的分?jǐn)傓D(zhuǎn)發(fā),也會(huì)自動(dòng)對(duì)resource server(RS)進(jìn)行存活檢測(cè),每分鐘會(huì)有心跳包去對(duì)接入TGW的IP Port進(jìn)行探測(cè)。
關(guān)于TGW相關(guān)配置具體如下:
- 做域名解析:我們對(duì)需要訪問到API Server的物理機(jī)都做了本地DNS,將一個(gè)固定域名(D)解析到一個(gè)特定的VIP(V),而該VIP就是TGW對(duì)外提供的虛擬IP。
- 配置TGW服務(wù)的RS列表:將三臺(tái)API Server節(jié)點(diǎn)對(duì)應(yīng)的物理IP加入到RS列表。
正常情況下,所有需要訪問API Server的請(qǐng)求都先本地域名解析到虛擬IP V,將請(qǐng)求的數(shù)據(jù)包都發(fā)送到V,V相當(dāng)于是TGW對(duì)外的接入點(diǎn),再通過(guò)TGW內(nèi)部負(fù)載均衡策略將請(qǐng)求數(shù)據(jù)包進(jìn)行目的網(wǎng)絡(luò)地址轉(zhuǎn)換(DNAT),分發(fā)到不同的RS上。
經(jīng)排查,TGW的監(jiān)控檢測(cè)模塊定期向所有的RS發(fā)送心跳包,但是TGW監(jiān)控檢測(cè)模塊只能收到A服務(wù)器的回包,因此TGW認(rèn)為只有A節(jié)點(diǎn)是存活狀態(tài),所有的請(qǐng)求數(shù)據(jù)包最終就由TGW轉(zhuǎn)發(fā)到A服務(wù)器上了,這就是負(fù)載均衡策略失效的根本原因。
這里還有一個(gè)現(xiàn)象是為什么etcd集群中只有一個(gè)節(jié)點(diǎn)的負(fù)載很高呢?
五個(gè)節(jié)點(diǎn)的etcd集群中只有一個(gè)節(jié)點(diǎn)負(fù)載很高,其他正常,通過(guò)查看A服務(wù)器的API Server的log,可以看到的大量的讀請(qǐng)求都固定發(fā)送到了同一個(gè)etcd節(jié)點(diǎn)。
對(duì)于這個(gè)現(xiàn)象,可以看下API Server訪問后端存儲(chǔ)的源碼,目前線上Kubernetes基于v1.7.12的源碼編譯運(yùn)行,API Server訪問etcd是在內(nèi)部初始化一個(gè)etcd client端,然后通過(guò)etcd client端發(fā)送請(qǐng)求到etcd server端。etcd client端有v2和v3兩個(gè)版本。線上API Server使用的是v2版本客戶端。主要代碼如下:
- //初始化etcd工作
- func New(cfg Config) (Client, error) {
- c := &httpClusterClient{//返回一個(gè)http類型的client
- clientFactory: newHTTPClientFactory(cfg.transport(), cfg.checkRedirect(), cfg.HeaderTimeoutPerRequest),
- rand: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),//傳入一個(gè)當(dāng)前時(shí)間的隨機(jī)種子
- selectionMode: cfg.SelectionMode,
- }
- if err := c.SetEndpoints(cfg.Endpoints); err != nil {
- return nil, err
- }
- return c, nil
- }
- //對(duì)etcd列表進(jìn)行打亂
- func (c *httpClusterClient) SetEndpoints(eps []string) error {
- ...
- neps, err := c.parseEndpoints(eps)
- c.Lock()
- defer c.Unlock()
- c.endpoints = shuffleEndpoints(c.rand, neps)//打亂etcd列表
- c.pinned = 0
- ...
- return nil
- }
- func shuffleEndpoints(r *rand.Rand, eps []url.URL) []url.URL {
- p := r.Perm(len(eps))//rank庫(kù)的Perm方法可以返回[0,n)之間的隨機(jī)亂序數(shù)組
- neps := make([]url.URL, len(eps))
- for i, k := range p {
- neps[i] = eps[k]
- }
- return neps
- }
可以看到在初始化etcd客戶端時(shí)候會(huì)傳入一個(gè)當(dāng)前時(shí)間的隨機(jī)種子去打亂所有Endpoints(etcd節(jié)點(diǎn))的順序。
對(duì)于etcd的操作都是通過(guò)API Server內(nèi)部的etcd客戶端發(fā)送http請(qǐng)求到etcd Server端,最主要是調(diào)用如下方法:
- func (c *httpClusterClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
- ...
- for i := pinned; i < leps+pinned; i++ {
- k := i % leps
- hc := c.clientFactory(eps[k])
- resp, body, err = hc.Do(ctx, action)
- ...
- if resp.StatusCode/100 == 5 {
- switch resp.StatusCode {
- case http.StatusInternalServerError, http.StatusServiceUnavailable:
- cerr.Errors = ...
- default:
- cerr.Errors = ...
- }
- ...
- continue
- }
- if k != pinned {
- c.Lock()
- c.pinned = k
- c.Unlock()
- }
- return resp, body, nil
- }
- return nil, nil, cerr
- }
該方法表明每次請(qǐng)求時(shí)候,會(huì)從pinned節(jié)點(diǎn)開始嘗試發(fā)送請(qǐng)求,如果發(fā)送請(qǐng)求異常,則按照初始化時(shí)候打亂順序的下一個(gè)節(jié)點(diǎn)(pinned++)開始嘗試發(fā)送數(shù)據(jù)。如此看來(lái),如果API Server使用了某個(gè)endpoint發(fā)送數(shù)據(jù),除非用壞了這個(gè)節(jié)點(diǎn),否則會(huì)一直使用該節(jié)點(diǎn)(pinned)發(fā)送數(shù)據(jù)。這就說(shuō)明了,沒有異常情況下,一個(gè)API Server就對(duì)應(yīng)往一個(gè)固定的etcd發(fā)送請(qǐng)求。
對(duì)于etcd集群,如果是寫請(qǐng)求的話,follower節(jié)點(diǎn)會(huì)把請(qǐng)求先轉(zhuǎn)發(fā)給leader節(jié)點(diǎn)處理,然后leader再轉(zhuǎn)發(fā)給follower同步。那么5個(gè)節(jié)點(diǎn)CPU負(fù)載不會(huì)這么不均衡,但是根據(jù)2.1排查API Server日志看到這里是大量的讀請(qǐng)求,相對(duì)于寫請(qǐng)求,讀請(qǐng)求是所有follower節(jié)點(diǎn)都能對(duì)外提供的。也就是大量請(qǐng)求由于負(fù)載均衡策略失效都轉(zhuǎn)發(fā)到A服務(wù)器,A再把查詢請(qǐng)求都打到其中一個(gè)固定的etcd,導(dǎo)致該節(jié)點(diǎn)忙于處理etcd查詢請(qǐng)求,負(fù)載就會(huì)飆高。
總的來(lái)說(shuō),TGW做負(fù)載均衡時(shí)候,由于心跳檢測(cè)模塊和其中兩個(gè)Resource Server間連接不通,導(dǎo)致誤將所有請(qǐng)求都轉(zhuǎn)發(fā)到其中一個(gè)API Server,而一個(gè)特定的API Server使用v2版本etcd客戶端就只會(huì)往一個(gè)固定的etcd服務(wù)端發(fā)請(qǐng)求,這樣整個(gè)負(fù)載均衡策略就失效了。
2.2.2 etcd存取數(shù)據(jù)緩慢
namespace未做劃分:
從2.1中查看API Server 的日志可以看出,很多get請(qǐng)求Pod對(duì)象信息,比如:“Get /api/v1/namespaces/default/pods?...” 這些都是從default namespace下獲取Pod信息,這就說(shuō)明線上并沒有對(duì)Pod的namespace做劃分。
Kubernetes是通過(guò)namespace對(duì)容器資源進(jìn)行隔離,默認(rèn)情況下,如果未指定namespace的話,創(chuàng)建的容器都被劃分到default namespace下,因?yàn)檫@個(gè)原因也給后面往etcd中存儲(chǔ)容器元數(shù)據(jù)信息也留下了坑。所有的Kuberentes的元數(shù)據(jù)都存儲(chǔ)在etcd的/registry目錄下,整體如下圖所示:
Kubernetes中Pod的信息存儲(chǔ)在/registry/pods/#{命名空間}/#{具體實(shí)例名}的目錄結(jié)構(gòu)中,正因?yàn)槿绻恢付╪amespace的話,就會(huì)存儲(chǔ)到default的namespace中,也就是/registry/pods/default目錄下保存了線上全部Pod對(duì)象信息。
也就是說(shuō)大量get請(qǐng)求Pod對(duì)象信息,由于未做namespace劃分,每次都會(huì)去訪問default子目錄,每次請(qǐng)求相當(dāng)于都要做全局搜索,隨著集群的增多,Pod不斷的存入到該子目錄中,搜索性能也會(huì)變得越來(lái)越差。
查詢結(jié)果未加入緩存:
從2.1中查看API Server 的日志看到很多Get/List操作,那么可以仔細(xì)看看相關(guān)方法的執(zhí)行流程,下面是List方法執(zhí)行過(guò)程中調(diào)用的中間函數(shù):
- f
- unc (c *Cacher) GetToList(ctx context.Context, key string, resourceVersion string, pred SelectionPredicate, listObj runtime.Object) error {
- if resourceVersion == "" {
- return c.storage.GetToList(ctx, key, resourceVersion, pred, listObj)//直接查詢etcd
- }
- listRV, err := ParseListResourceVersion(resourceVersion)
- ...
- obj, exists, readResourceVersion, err := c.watchCache.WaitUntilFreshAndGet(listRV, key, trace)//從緩存中獲取
- ...
- return nil
- }
可以看到,GetToList方法中傳入的有個(gè)resourceVersion 參數(shù),如果設(shè)置了就會(huì)從緩存中獲取,如果不設(shè)置就會(huì)去etcd中查詢。這個(gè)也是一個(gè)關(guān)鍵點(diǎn),有關(guān)resourceVersion 的相關(guān)使用如下:
- 不設(shè)置:通過(guò)API Server從etcd讀取。
- 設(shè)置成0:從API Server的cache讀取,減輕API Server和etcd壓力。例如Kubelet經(jīng)常通過(guò)此方法Get Node對(duì)象,Kubernetes Infomer***次啟動(dòng)時(shí)List也通過(guò)此方法獲得對(duì)象。
- 大于0:讀取對(duì)象指定版本。
線上管理平臺(tái)通過(guò)http接口去查詢Pod信息時(shí)候是沒有設(shè)置resourceVersion,所以每次通過(guò)Get/List方法獲取資源時(shí)候都會(huì)查詢etcd,如此一來(lái)經(jīng)常大量高頻率的查詢etcd會(huì)導(dǎo)致其壓力較大,開啟緩存策略不僅可以減輕訪問etcd壓力而且還可以加快查詢速度。
總結(jié)以上兩點(diǎn):所有的請(qǐng)求都發(fā)往一個(gè)固定的API Server,導(dǎo)致該API Server節(jié)點(diǎn)負(fù)載較高,同時(shí)該API Server又會(huì)將查詢請(qǐng)求固定的發(fā)給某個(gè)etcd節(jié)點(diǎn),然而請(qǐng)求結(jié)果并沒有在API Server端做緩存,每次都會(huì)直接查詢etcd,在從etcd中獲取Pod信息又是從default這個(gè)大的子目錄中全局搜索,每次請(qǐng)求都比較費(fèi)時(shí),這樣導(dǎo)致某一個(gè)固定的etcd一直處理大量的費(fèi)時(shí)的請(qǐng)求,最終將該etcd資源耗盡,負(fù)載過(guò)高,因而查詢結(jié)果不能及時(shí)返回給API Server,導(dǎo)致創(chuàng)建Pod時(shí)候拿不到相關(guān)的信息,Pod創(chuàng)建工作無(wú)法進(jìn)行,所以最終表象是集群部署長(zhǎng)時(shí)間卡頓。
3、解決方案
切換負(fù)載均衡方案:臨時(shí)切換為DNS輪詢方式,保證每個(gè)API Server節(jié)點(diǎn)的流量均衡。同時(shí)跟進(jìn)TGW對(duì)于某些網(wǎng)段的RS和TGW服務(wù)不能探測(cè)心跳及后續(xù)改進(jìn)。
將Kubernetes中Pod按多個(gè)namespace劃分,目前線上所有的Pod都劃分到默認(rèn)的default的namespace下,每次讀取Pod信息都是從etcd檢索整個(gè)namespace,比較損耗etcd性能,目前已經(jīng)將Pod的namespace進(jìn)行細(xì)分,加快了讀取Pod信息速度同時(shí)減少了etcd性能損耗。
etcd v3版本客戶端會(huì)對(duì)Endpoints定期打亂,后續(xù)我們會(huì)升級(jí)到v3版本,這樣同一個(gè)API Server的請(qǐng)求就不會(huì)一直落到某一個(gè)etcd上,這樣即使負(fù)載均衡策略失效也能做到對(duì)etcd請(qǐng)求的分?jǐn)偂?/p>
查詢Kubernetes資源信息時(shí)帶入resourceVersion開啟緩存機(jī)制,減輕對(duì)etcd的訪問壓力。
4、總結(jié)
從API Server卡頓問題排查過(guò)程來(lái)看,潛在的問題是長(zhǎng)期存在的,只是積累到一定量后,問題的影響才會(huì)凸顯。這就要求我們平時(shí)對(duì)Kubernetes相關(guān)組件的性能指標(biāo),日志等要保持時(shí)刻敏感,要對(duì)Kubernetes各種默認(rèn)策略及參數(shù)非常熟悉,同時(shí)對(duì)于重要功能模塊做到源碼層面了解,這樣才能規(guī)避潛在風(fēng)險(xiǎn)和出問題后能快速定位,保證生產(chǎn)環(huán)境穩(wěn)定健康的運(yùn)行。