共計(jì) 5138 個(gè)字符,預(yù)計(jì)需要花費(fèi) 13 分鐘才能閱讀完成。
Consul 故障分析與優(yōu)化是怎么樣的,針對(duì)這個(gè)問(wèn)題,這篇文章詳細(xì)介紹了相對(duì)應(yīng)的分析和解答,希望可以幫助更多想解決這個(gè)問(wèn)題的小伙伴找到更簡(jiǎn)單易行的方法。
注冊(cè)中心背景及 Consul 的使用
從微服務(wù)平臺(tái)的角度出發(fā)希望提供統(tǒng)一的服務(wù)注冊(cè)中心,讓任何的業(yè)務(wù)和團(tuán)隊(duì)只要使用這套基礎(chǔ)設(shè)施,相互發(fā)現(xiàn)只需要協(xié)商好服務(wù)名即可; 還需要支持業(yè)務(wù)做多 DC 部署和故障切換。由于在擴(kuò)展性和多 DC 支持上的良好設(shè)計(jì),我們選擇了 Consul,并采用了 Consul 推薦的架構(gòu),單個(gè) DC 內(nèi)有 Consul Server 和 Consul Agent,DC 之間是 WAN 模式并且相互對(duì)等,結(jié)構(gòu)如下圖所示:
注:圖中只畫(huà)了四個(gè) DC,實(shí)際生產(chǎn)環(huán)境根據(jù)公司機(jī)房建設(shè)以及第三方云的接入情況,共有十幾個(gè) DC。
與 QAE 容器應(yīng)用平臺(tái)集成
愛(ài)奇藝內(nèi)部的容器應(yīng)用平臺(tái) QAE 與 Consul 進(jìn)行了集成。由于早期是基于 Mesos/Marathon 體系開(kāi)發(fā),沒(méi)有 Pod 容器組概念,無(wú)法友好的注入 sidecar 的容器,因此我們選擇了微服務(wù)模式中的第三方注冊(cè)模式,即由 QAE 系統(tǒng)實(shí)時(shí)向 Consul 同步注冊(cè)信息,如下圖所示; 并且使用了 Consul 的 external service 模式,這樣可以避免兩個(gè)系統(tǒng)狀態(tài)不一致時(shí)引起故障,例如 Consul 已經(jīng)將節(jié)點(diǎn)或服務(wù)實(shí)例判定為不健康,但是 QAE 沒(méi)有感知到,也就不會(huì)重啟或重新調(diào)度,導(dǎo)致沒(méi)有健康實(shí)例可用。
其中 QAE 應(yīng)用與服務(wù)的關(guān)系表示例如下:
每個(gè) QAE 應(yīng)用代表一組容器,應(yīng)用與服務(wù)的映射關(guān)系是松耦合的,根據(jù)應(yīng)用實(shí)際所在的 DC 將其關(guān)聯(lián)到對(duì)應(yīng) Consul DC 即可,后續(xù)應(yīng)用容器的更新、擴(kuò)縮容、失敗重啟等狀態(tài)變化都會(huì)實(shí)時(shí)體現(xiàn)在 Consul 的注冊(cè)數(shù)據(jù)中。
與 API 網(wǎng)關(guān)集成
微服務(wù)平臺(tái) API 網(wǎng)關(guān)是服務(wù)注冊(cè)中心最重要的使用方之一。網(wǎng)關(guān)會(huì)根據(jù)地區(qū)、運(yùn)營(yíng)商等因素部署多個(gè)集群,每個(gè)網(wǎng)關(guān)集群會(huì)根據(jù)內(nèi)網(wǎng)位置對(duì)應(yīng)到一個(gè) Consul 集群,并且從 Consul 查詢(xún)最近的服務(wù)實(shí)例,如下圖所示:
這里我們使用了 Consul 的 PreparedQuery 功能,對(duì)所有服務(wù)優(yōu)先返回本 DC 服務(wù)實(shí)例,如果本 DC 沒(méi)有則根據(jù) DC 間 RTT 由近到遠(yuǎn)查詢(xún)其它 DC 數(shù)據(jù)。
故障與分析優(yōu)化
Consul 故障
Consul 從 2016 年底上線開(kāi)始,已經(jīng)穩(wěn)定運(yùn)行超過(guò)三年時(shí)間,但是最近我們卻遇到了故障,收到了某個(gè) DC 多臺(tái) Consul Server 不響應(yīng)請(qǐng)求、大量 Consul Agent 連不上 Server 的告警,并且沒(méi)有自動(dòng)恢復(fù)。Server 端觀察到的現(xiàn)象主要有:
raft 協(xié)議不停選舉失敗,無(wú)法獲得 leader;
HTTP DNS 查詢(xún)接口大量超時(shí),觀察到有些超過(guò)幾十秒才返回(正常應(yīng)當(dāng)是毫秒級(jí)別返回);
goroutine 快速線性上升,內(nèi)存同步上升,最終觸發(fā)系統(tǒng) OOM; 在日志中沒(méi)能找到明確的問(wèn)題,從監(jiān)控 metrics 則觀察到 PreparedQuery 的執(zhí)行耗時(shí)異常增大,如下圖所示:
此時(shí) API 網(wǎng)關(guān)查詢(xún)服務(wù)信息也超時(shí)失敗,我們將對(duì)應(yīng)的網(wǎng)關(guān)集群切到了其它 DC,之后重啟 Consul 進(jìn)程,恢復(fù)正常。
故障分析
經(jīng)過(guò)日志排查,發(fā)現(xiàn)故障前發(fā)生過(guò) DC 間的網(wǎng)絡(luò)抖動(dòng)(RTT 增加,伴隨丟包),持續(xù)時(shí)間大約 1 分鐘,我們初步分析是 DC 間網(wǎng)絡(luò)抖動(dòng)導(dǎo)致正常收到的 PreparedQuery 請(qǐng)求積壓在 Server 中無(wú)法快速返回,隨著時(shí)間積累越來(lái)越多,占用的 goroutine 和內(nèi)存也越來(lái)越多,最終導(dǎo)致 Server 異常。
跟隨這個(gè)想法,嘗試在測(cè)試環(huán)境復(fù)現(xiàn),共有 4 個(gè) DC,單臺(tái) Server 的 PreparedQuery QPS 為 1.5K,每個(gè) PreparedQuery 查詢(xún)都會(huì)觸發(fā) 3 次跨 DC 查詢(xún),然后使用 tc-netem 工具模擬 DC 間的 RTT 增加的情況,得到了以下結(jié)果:
當(dāng) DC 間 RTT 由正常的 2ms 變?yōu)?800ms 之后,Consul Server 的 goroutine、內(nèi)存確實(shí)會(huì)線性增長(zhǎng),PreparedQuery 執(zhí)行耗時(shí)也線性增長(zhǎng),如下圖所示:
雖然 goroutine、內(nèi)存在增長(zhǎng),但是在 OOM 之前,Consul Server 的其它功能未受影響,Raft 協(xié)議工作正常,本 DC 的數(shù)據(jù)查詢(xún)請(qǐng)求也能正常響應(yīng);
在 DC 間 RTT 恢復(fù)到 2ms 的一瞬間,Consul Server 丟失 leader,接著 Raft 不停選舉失敗,無(wú)法恢復(fù)。
以上操作能夠穩(wěn)定的復(fù)現(xiàn)故障,使分析工作有了方向。首先基本證實(shí)了 goroutine 和內(nèi)存的增長(zhǎng)是由于 PreparedQuery 請(qǐng)求積壓導(dǎo)致的,而積壓的原因在初期是網(wǎng)絡(luò)請(qǐng)求阻塞,在網(wǎng)絡(luò)恢復(fù)后仍然積壓原因暫時(shí)未知,這時(shí)整個(gè)進(jìn)程應(yīng)當(dāng)是處于異常狀態(tài); 那么,為什么網(wǎng)絡(luò)恢復(fù)之后 Consul 反而故障了呢?Raft 只有 DC 內(nèi)網(wǎng)絡(luò)通信,為什么也異常了呢? 是最讓我們困惑的問(wèn)題。
最開(kāi)始的時(shí)候?qū)⒅攸c(diǎn)放在了 Raft 問(wèn)題上,通過(guò)跟蹤社區(qū) issue,找到了 hashicorp/raft#6852,其中描述到我們的版本在高負(fù)載、網(wǎng)絡(luò)抖動(dòng)情況下可能出現(xiàn) raft 死鎖,現(xiàn)象與我們十分相似。但是按照 issue 更新 Raft 庫(kù)以及 Consul 相關(guān)代碼之后,測(cè)試環(huán)境復(fù)現(xiàn)時(shí)故障依然存在。
之后嘗試給 Raft 庫(kù)添加日志,以便看清楚 Raft 工作的細(xì)節(jié),這次我們發(fā)現(xiàn) Raft 成員從進(jìn)入 Candidate 狀態(tài),到請(qǐng)求 peer 節(jié)點(diǎn)為自己投票,日志間隔了 10s,而代碼中僅僅是執(zhí)行了一行 metrics 更新,如下圖所示:
因此懷疑 metrics 調(diào)用出現(xiàn)了阻塞,導(dǎo)致整個(gè)系統(tǒng)運(yùn)行異常,之后我們?cè)诎l(fā)布?xì)v史中找到了相關(guān)優(yōu)化,低版本的 armon/go-metrics 在 Prometheus 實(shí)現(xiàn)中采用了全局鎖 sync.Mutex,所有 metrics 更新都需要先獲取這個(gè)鎖,而 v0.3.3 版本改用了 sync.Map,每個(gè) metric 作為字典的一個(gè)鍵,只在鍵初始化的時(shí)候需要獲取全局鎖,之后不同 metric 更新值的時(shí)候就不存在鎖競(jìng)爭(zhēng),相同 metric 更新時(shí)使用 sync.Atomic 保證原子操作,整體上效率更高。更新對(duì)應(yīng)的依賴(lài)庫(kù)之后,復(fù)現(xiàn)網(wǎng)絡(luò)抖動(dòng)之后,Consul Server 可以自行恢復(fù)正常。
這樣看來(lái)的確是由于 metrics 代碼阻塞,導(dǎo)致了系統(tǒng)整體異常。但我們依然有疑問(wèn),復(fù)現(xiàn)環(huán)境下單臺(tái) Server 的 PreparedQuery QPS 為 1.5K,而穩(wěn)定的網(wǎng)絡(luò)環(huán)境下單臺(tái) Server 壓測(cè) QPS 到 2.8K 時(shí)依然工作正常。也就是說(shuō)正常情況下原有代碼是滿足性能需求的,只有在故障時(shí)出現(xiàn)了性能問(wèn)題。
接下來(lái)的排查陷入了困境,經(jīng)過(guò)反復(fù)試驗(yàn),我們發(fā)現(xiàn)了一個(gè)有趣的現(xiàn)象:使用 go 1.9 編譯的版本 (也是生產(chǎn)環(huán)境使用的版本) 能復(fù)現(xiàn)出故障; 同樣的代碼使用 go 1.14 編譯就無(wú)法復(fù)現(xiàn)出故障。經(jīng)過(guò)仔細(xì)查看,我們?cè)?go 的發(fā)布?xì)v史中找到了以下兩條記錄:
根據(jù)代碼我們找到了用戶(hù)反饋在 go1.9~1.13 版本,在大量 goroutine 同時(shí)競(jìng)爭(zhēng)一個(gè) sync.Mutex 時(shí),會(huì)出現(xiàn)性能急劇下降的情況,這能很好的解釋我們的問(wèn)題。由于 Consul 代碼依賴(lài)了 go 1.9 新增的內(nèi)置庫(kù),我們無(wú)法用更低的版本編譯,因此我們將 go 1.14 中 sync.Mutex 相關(guān)的優(yōu)化去掉,如下圖所示,然后用這個(gè)版本的 go 編譯 Consul,果然又可以復(fù)現(xiàn)我們的故障了。
回顧語(yǔ)言的更新歷史,go 1.9 版本添加了公平鎖特性,在原有 normal 模式上添加了 starvation 模式,來(lái)避免鎖等待的長(zhǎng)尾效應(yīng)。但是 normal 模式下新的 goroutine 在運(yùn)行時(shí)有較高的幾率競(jìng)爭(zhēng)鎖成功,從而免去 goroutine 的切換,整體效率是較高的; 而在 starvation 模式下,新的 goroutine 不會(huì)直接競(jìng)爭(zhēng)鎖,而是會(huì)把自己排到等待隊(duì)列末端,然后休眠等待喚醒,鎖按照等待隊(duì)列 FIFO 分配,獲取到鎖的 goroutine 被調(diào)度執(zhí)行,這樣會(huì)增加 goroutine 調(diào)度、切換的成本。在 go 1.14 中針對(duì)性能問(wèn)題進(jìn)行了改善,在 starvation 模式下,當(dāng) goroutine 執(zhí)行解鎖操作時(shí),會(huì)直接將 CPU 時(shí)間讓給下一個(gè)等待鎖的 goroutine 執(zhí)行,整體上會(huì)使得被鎖保護(hù)部分的代碼得到加速執(zhí)行。
到此故障的原因就清楚了,首先網(wǎng)絡(luò)抖動(dòng),導(dǎo)致大量 PreparedQuery 請(qǐng)求積壓在 Server 中,同時(shí)也造成了大量的 goroutine 和內(nèi)存使用; 在網(wǎng)絡(luò)恢復(fù)之后,積壓的 PreparedQuery 繼續(xù)執(zhí)行,在我們的復(fù)現(xiàn)場(chǎng)景下,積壓的 goroutine 量會(huì)超過(guò) 150K,這些 goroutine 在執(zhí)行時(shí)都會(huì)更新 metrics 從而去獲取全局的 sync.Mutex,此時(shí)切換到 starvation 模式并且性能下降,大量時(shí)間都在等待 sync.Mutex,請(qǐng)求阻塞超時(shí); 除了積壓的 goroutine,新的 PreparedQuery 還在不停接收,獲取鎖時(shí)同樣被阻塞,結(jié)果是 sync.Mutex 保持在 starvation 模式無(wú)法自動(dòng)恢復(fù); 另一方面 raft 代碼運(yùn)行會(huì)依賴(lài)定時(shí)器、超時(shí)、節(jié)點(diǎn)間消息的及時(shí)傳遞與處理,并且這些超時(shí)通常是秒、毫秒級(jí)別的,但 metrics 代碼阻塞過(guò)久,直接導(dǎo)致時(shí)序相關(guān)的邏輯無(wú)法正常運(yùn)行。
接著生產(chǎn)環(huán)境中我們將發(fā)現(xiàn)的問(wèn)題都進(jìn)行了更新,升級(jí)到 go 1.14,armon/go-metrics v0.3.3,以及 hashicorp/raft v1.1.2 版本,使 Consul 達(dá)到一個(gè)穩(wěn)定狀態(tài)。此外還整理完善了監(jiān)控指標(biāo),核心監(jiān)控包括以下維度:
進(jìn)程:CPU、內(nèi)存、goroutine、連接數(shù)
Raft:成員狀態(tài)變動(dòng)、提交速率、提交耗時(shí)、同步心跳、同步延時(shí)
RPC:連接數(shù)、跨 DC 請(qǐng)求數(shù)
寫(xiě)負(fù)載:注冊(cè) 解注冊(cè)速率
讀負(fù)載:Catalog/Health/PreparedQuery 請(qǐng)求量,執(zhí)行耗時(shí)
冗余注冊(cè)
根據(jù) Consul 的故障期間的故障現(xiàn)象,我們對(duì)服務(wù)注冊(cè)中心的架構(gòu)進(jìn)行了重新審視。
在 Consul 的架構(gòu)中,某個(gè) DC Consul Server 全部故障了就代表這個(gè) DC 故障,要靠其它 DC 來(lái)做災(zāi)備。但是實(shí)際情況中,很多不在關(guān)鍵路徑上的服務(wù)、SLA 要求不是特別高的服務(wù)并沒(méi)有多 DC 部署,這時(shí)如果所在 DC 的 Consul 故障,那么整個(gè)服務(wù)就會(huì)故障。
針對(duì)本身并沒(méi)有做多 DC 部署的服務(wù),如果可以在冗余 DC 注冊(cè),那么單個(gè) DC Consul 故障時(shí),其它 DC 還可以正常發(fā)現(xiàn)。因此我們修改了 QAE 注冊(cè)關(guān)系表,對(duì)于本身只有單 DC 部署的服務(wù),系統(tǒng)自動(dòng)在其它 DC 也注冊(cè)一份,如下圖所示:
QAE 這種冗余注冊(cè)相當(dāng)于在上層做了數(shù)據(jù)多寫(xiě)操作。Consul 本身不會(huì)在各 DC 間同步服務(wù)注冊(cè)數(shù)據(jù),因此直接通過(guò) Consul Agent 方式注冊(cè)的服務(wù)還沒(méi)有較好的冗余注冊(cè)方法,還是依賴(lài)服務(wù)本身做好多 DC 部署。
保障 API 網(wǎng)關(guān)
目前 API 網(wǎng)關(guān)的正常工作依賴(lài)于 Consul PreparedQuery 查詢(xún)結(jié)果在本地的緩存,目前的交互方式有兩方面問(wèn)題:
網(wǎng)關(guān)緩存是 lazy 的,網(wǎng)關(guān)第一次用到時(shí)才會(huì)從 Consul 查詢(xún)加載,Consul 故障時(shí)查詢(xún)失敗會(huì)導(dǎo)致請(qǐng)求轉(zhuǎn)發(fā)失敗;
PreparedQuery 內(nèi)部可能會(huì)涉及多次跨 DC 查詢(xún),耗時(shí)較多,屬于復(fù)雜查詢(xún),由于每個(gè)網(wǎng)關(guān)節(jié)點(diǎn)需要單獨(dú)構(gòu)建緩存,并且緩存有 TTL,會(huì)導(dǎo)致相同的 PreparedQuery 查詢(xún)執(zhí)行很多次,查詢(xún) QPS 會(huì)隨著網(wǎng)關(guān)集群規(guī)模線性增長(zhǎng)。
為了提高網(wǎng)關(guān)查詢(xún) Consul 的穩(wěn)定性和效率,我們選擇為每個(gè)網(wǎng)關(guān)集群部署一個(gè)單獨(dú)的 Consul 集群,如下圖所示:
圖中紅色的是原有的 Consul 集群,綠色的是為網(wǎng)關(guān)單獨(dú)部署的 Consul 集群,它只在單 DC 內(nèi)部工作。我們開(kāi)發(fā)了 Gateway-Consul-Sync 組件,它會(huì)周期性的從公共 Consul 集群讀取服務(wù)的 PreparedQuery 查詢(xún)結(jié)果,然后寫(xiě)入到綠色的 Consul 集群,網(wǎng)關(guān)則直接訪問(wèn)綠色的 Consul 進(jìn)行數(shù)據(jù)查詢(xún)。這樣改造之后有以下幾方面好處:
從支持網(wǎng)關(guān)的角度看,公共集群的負(fù)載原來(lái)是隨網(wǎng)關(guān)節(jié)點(diǎn)數(shù)線性增長(zhǎng),改造后變成隨服務(wù)個(gè)數(shù)線性增長(zhǎng),并且單個(gè)服務(wù)在同步周期內(nèi)只會(huì)執(zhí)行一次 PreparedQuery 查詢(xún),整體負(fù)載會(huì)降低;
圖中綠色 Consul 只供網(wǎng)關(guān)使用,其 PreparedQuery 執(zhí)行時(shí)所有數(shù)據(jù)都在本地,不涉及跨 DC 查詢(xún),因此復(fù)雜度降低,不受跨 DC 網(wǎng)絡(luò)影響,并且集群整體的讀寫(xiě)負(fù)載更可控,穩(wěn)定性更好;
當(dāng)公共集群故障時(shí),Gateway-Consul-Sync 無(wú)法正常工作,但綠色的 Consul 仍然可以返回之前同步好的數(shù)據(jù),網(wǎng)關(guān)還可以繼續(xù)工作;
由于網(wǎng)關(guān)在改造前后查詢(xún) Consul 的接口和數(shù)據(jù)格式是完全一致的,當(dāng)圖中綠色 Consul 集群故障時(shí),可以切回到公共 Consul 集群,作為一個(gè)備用方案。
關(guān)于 Consul 故障分析與優(yōu)化是怎么樣的問(wèn)題的解答就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,如果你還有很多疑惑沒(méi)有解開(kāi),可以關(guān)注丸趣 TV 行業(yè)資訊頻道了解更多相關(guān)知識(shí)。