一切都始于我向我們的高級(jí)軟件工程師提出的一個(gè)問題: “忘掉通信速度。你真的覺得在gRPC中開發(fā)通信比REST更好嗎?” 我不想聽到的答案立刻就來了:“絕對(duì)是的。”
在我提出這個(gè)問題之前,我一直在監(jiān)控我們的服務(wù)在滾動(dòng)更新和擴(kuò)展Pod時(shí)出現(xiàn)的奇怪行為。我們的大多數(shù)微服務(wù)以往都通過REST調(diào)用進(jìn)行通信,沒有任何問題。我們已經(jīng)將一些這些集成遷移到了gRPC,主要是因?yàn)槲覀兿霐[脫REST的開銷。最近,我們觀察到了一些問題,都指向了同一個(gè)方向——我們的gRPC通信。當(dāng)然,我們遵循了在Kube.NETes中運(yùn)行g(shù)RPC而不使用服務(wù)網(wǎng)格的建議實(shí)踐,我們?cè)诜?wù)器上使用了一個(gè)無頭服務(wù)對(duì)象,并在gRPC中使用了客戶端的“輪詢”負(fù)載平衡與DNS發(fā)現(xiàn)等。
擴(kuò)展Pod數(shù)量
Kubernetes內(nèi)部負(fù)載均衡器不是用于負(fù)載均衡RPC,而是用于負(fù)載均衡TCP連接。第四層負(fù)載均衡器由于其簡單性而很常見,因?yàn)樗鼈兣c協(xié)議無關(guān)。但是,gRPC破壞了Kubernetes提供的連接級(jí)負(fù)載均衡。這是因?yàn)間RPC是基于HTTP/2構(gòu)建的,而HTTP/2被設(shè)計(jì)為維護(hù)一個(gè)長期存在的TCP連接,該連接中的所有請(qǐng)求都可以在任何時(shí)間點(diǎn)同時(shí)處于活動(dòng)狀態(tài)。這減少了連接管理的開銷。然而,在這種情況下,連接級(jí)別的負(fù)載平衡并不是非常有用,因?yàn)橐坏┙⒘诉B接,就不再需要進(jìn)行負(fù)載平衡。所有的請(qǐng)求都會(huì)固定到原始目標(biāo)Pod,直到發(fā)生新的DNS發(fā)現(xiàn)(使用無頭服務(wù))。這不會(huì)發(fā)生,直到至少有一個(gè)現(xiàn)有連接斷開。
問題示例:
- 2個(gè)客戶端(A)調(diào)用2個(gè)服務(wù)器(B)。
- 自動(dòng)縮放器介入并擴(kuò)展了客戶端。
- 服務(wù)器Pod負(fù)載過重,因此自動(dòng)縮放器介入并增加了服務(wù)器Pod的數(shù)量,但沒有進(jìn)行負(fù)載平衡。甚至可以看到新Pod上沒有傳入的流量。
- 客戶端被縮減。
- 客戶端再次擴(kuò)展,但負(fù)載仍然不平衡。
- 一個(gè)服務(wù)器Pod因過載而崩潰,發(fā)生了重新發(fā)現(xiàn)。
- 在圖片中沒有顯示,但是當(dāng)Pod恢復(fù)時(shí),情況看起來與圖3類似,即新Pod不會(huì)接收流量。
gRPC負(fù)載均衡的示例
兩個(gè)配置解決這個(gè)問題,技術(shù)上說是一行
正如我之前提到的,我們使用“客戶端負(fù)載均衡”,并使用無頭服務(wù)對(duì)象進(jìn)行DNS發(fā)現(xiàn)。其他選項(xiàng)可能包括使用代理負(fù)載均衡或?qū)崿F(xiàn)另一種發(fā)現(xiàn)方法,該方法將詢問Kubernetes API而不是DNS。
除此之外,gRPC文檔提供了服務(wù)器端連接管理提案,我們也嘗試過它。
以下是我為設(shè)置以下服務(wù)器參數(shù)提供的建議,以及gRPC初始化的Go代碼片段示例:
- 將MAX_CONNECTION_AGE設(shè)置為30秒。這個(gè)時(shí)間段足夠長,可以在沒有昂貴且頻繁的連接建立過程的情況下進(jìn)行低延遲通信。此外,它允許服務(wù)相對(duì)快速地響應(yīng)新Pod的存在,因此流量分布將保持平衡。
- 將MAX_CONNECTION_AGE_GRACE設(shè)置為10秒。定義了連接保持活動(dòng)狀態(tài)以完成未完成的RPC的最大時(shí)間。
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionAge: time.Second * 30, // THIS one does the trick
MaxConnectionAgeGrace: time.Second * 10,
})
- 1.
- 2.
- 3.
- 4.
在現(xiàn)實(shí)世界中的行為:
gRPC配置更改應(yīng)用前后的Pod數(shù)量
在gRPC配置更改后觀察到的新Pod中的網(wǎng)絡(luò)I/O活動(dòng)
接下來是第三行
擴(kuò)展問題已經(jīng)解決,但另一個(gè)問題變得更加明顯。焦點(diǎn)轉(zhuǎn)向了客戶端在滾動(dòng)更新期間出現(xiàn)的gRPC code=UNAVAILABLE 錯(cuò)誤。奇怪的是,這只在滾動(dòng)更新期間觀察到,而在單個(gè)Pod擴(kuò)展事件中卻沒有觀察到。
滾動(dòng)更新期間的gRPC錯(cuò)誤數(shù)量
部署滾動(dòng)的過程很簡單:創(chuàng)建一個(gè)新的副本集,創(chuàng)建一個(gè)新的Pod,當(dāng)Pod準(zhǔn)備就緒時(shí),舊的Pod將從舊的副本集中終止,以此類推。每個(gè)Pod之間的啟動(dòng)時(shí)間間隔為15秒。關(guān)于gRPC DNS重新發(fā)現(xiàn),我們知道它僅在舊連接中斷或以GOAWAY信號(hào)結(jié)束時(shí)才會(huì)啟動(dòng)。因此,客戶端每15秒開始一次新的重新發(fā)現(xiàn),但獲取到了過時(shí)的DNS記錄。然后,它們不斷進(jìn)行重新發(fā)現(xiàn),直到成功為止。
除非不是DNS問題...
幾乎每個(gè)地方都有DNS TTL緩存。基礎(chǔ)設(shè)施DNS具有其自己的緩存。JAVA客戶端遭受了它們默認(rèn)的30秒TTL緩存,而Go客戶端通常沒有實(shí)現(xiàn)DNS緩存。與此相反,Java客戶端報(bào)告了數(shù)百或數(shù)千次此問題的發(fā)生。當(dāng)然,我們可以縮短TTL緩存的時(shí)間,但為什么要在滾動(dòng)更新期間只影響gRPC呢?
幸運(yùn)的是,有一個(gè)易于實(shí)現(xiàn)的解決方法。或者更好地說,解決方案:讓新Pod啟動(dòng)時(shí)設(shè)置30秒的延遲。
.spec.minReadySeconds = 30
- 1.
Kubernetes部署規(guī)范允許我們?cè)O(shè)置新Pod必須處于就緒狀態(tài)的最短時(shí)間,然后才會(huì)開始終止舊Pod。在此時(shí)間之后,連接被終止,gRPC客戶端收到GOAWAY信號(hào)并開始重新發(fā)現(xiàn)。TTL已經(jīng)過期,因此客戶端獲取到了新的、最新的記錄。
結(jié)論
從配置的角度來看,gRPC就像一把瑞士軍刀,可能不會(huì)默認(rèn)適合您的基礎(chǔ)架構(gòu)或應(yīng)用程序。查看文檔,進(jìn)行調(diào)整,進(jìn)行實(shí)驗(yàn),并充分利用您已經(jīng)擁有的資源。我相信可靠和彈性的通信應(yīng)該是您的最終目標(biāo)。
我還建議查看以下內(nèi)容:
- Keepalives。對(duì)于短暫的內(nèi)部集群連接來說可能沒有意義,但在某些其他情況下可能會(huì)有用。
- 重試。有時(shí),值得首先進(jìn)行一些退避重試,而不是通過嘗試創(chuàng)建新連接來過載基礎(chǔ)設(shè)施。
- 代碼映射。將您的gRPC響應(yīng)代碼映射到眾所周知的HTTP代碼,以更好地了解發(fā)生了什么情況。
- 負(fù)載均衡。平衡是關(guān)鍵。不要忘記設(shè)置回退并進(jìn)行徹底的測(cè)試。
- 服務(wù)器訪問日志(gRPC code=OK)可能會(huì)因默認(rèn)設(shè)置為信息級(jí)別而太冗長。考慮將它們降低到調(diào)試級(jí)別并進(jìn)行篩選。