趙 鑫 李銘軒
1 北京郵電大學(xué) 北京 100876
2 中國聯(lián)通研究院 北京 100048
在傳統(tǒng)代碼架構(gòu)中,隨著新功能的增加,代碼庫會越變越大。盡管代碼被分成各個模塊,但隨著時間推移,這些界限將變得模糊。代碼之間功能類似的模塊將越來越多,維護將變得越來越困難[1]。相對于傳統(tǒng)軟件,微服務(wù)是一種新型架構(gòu)方式。它提倡將單一應(yīng)用程序劃分成一組小的服務(wù),每個服務(wù)獨立完成一個很小的功能。服務(wù)之間相互協(xié)調(diào)、互相配合,為用戶提供最終價值。每個服務(wù)運行在其獨立的進程中,服務(wù)和服務(wù)之間采用輕量級的通信機制(通常是基于HTTP的Restful API)相互溝通[2]。但微服務(wù)架構(gòu)下軟件穩(wěn)定性不夠強,故障率相對較高,且故障原因復(fù)雜,微服務(wù)的故障治理一直是一個很大的挑戰(zhàn)。
容器是一種虛擬化技術(shù),相比于VM,具有更好的性能,更輕量和更好的擴展性[3]。每個容器上運行一個進程,容器內(nèi)打包這個進程所需要的所有依賴環(huán)境,擁有良好的移植性。容器之間彼此獨立運行,負(fù)責(zé)單一的功能。容器間也可以相互配合,相互調(diào)用,完成更大的功能。容器技術(shù)的特點使它與微服務(wù)架構(gòu)完美適配。這里也使用容器技術(shù)實現(xiàn)微服務(wù)架構(gòu)。
服務(wù)網(wǎng)格是用于處理服務(wù)與服務(wù)之間通信的專用基礎(chǔ)設(shè)施層。其主要思路是為每個微服務(wù)實例(往往以容器形式)都設(shè)置反向代理組件,即Sidecar。所有進出微服務(wù)的流量會被Sidecar劫持,通過該組件進行處理和轉(zhuǎn)發(fā)[4]。服務(wù)網(wǎng)格在不侵入業(yè)務(wù)代碼的情況下可以完成對微服務(wù)集群的監(jiān)控,為微服務(wù)故障治理提供了新的解決思路。本文基于服務(wù)網(wǎng)格技術(shù),對于微服務(wù)軟件進行管理,深入探討微服務(wù)的故障特點,提出一套完善的治理方案。
微服務(wù)帶來靈活性的同時,也使得架構(gòu)變得復(fù)雜,原本一個單體軟件被分解成上百個微服務(wù),微服務(wù)之間調(diào)用關(guān)系復(fù)雜[5],服務(wù)之間協(xié)作也容易受到網(wǎng)絡(luò)層影響,這對故障的治理提出了很高的要求。微服務(wù)故障治理主要體現(xiàn)在以下幾個方面。
一個大型的應(yīng)用程序被拆分成幾十甚至上百個微服務(wù),分布在多臺服務(wù)器上。各個微服務(wù)之間相互協(xié)作,一層層進行調(diào)用,調(diào)用拓?fù)洚惓?fù)雜。故障可能是某個微服務(wù)實例內(nèi)部發(fā)生故障(計算錯誤、返回數(shù)據(jù)錯誤),可能是多個實例之間交互錯誤(如實例運行順序不對),也有可能是環(huán)境因素(如網(wǎng)絡(luò)延時,使得請求無法及時響應(yīng);配置錯誤,JVM配置和容器配置不一致)。
故障還可以分成功能性故障和非功能性故障,功能性故障會導(dǎo)致運行結(jié)果的錯誤,非功能性故障會使服務(wù)性能、穩(wěn)定性下降[6]。相比于功能性故障,非功能性故障更為隱秘,更不容易被發(fā)現(xiàn)??傊?,在微服務(wù)架構(gòu)下,故障的種類繁多且原因復(fù)雜,定位也十分困難,尤其是在幾百臺服務(wù)器組成的大型集群中。
在單一架構(gòu)的軟件中,各個功能的協(xié)作在設(shè)計和開發(fā)之初就被設(shè)計好,大多數(shù)問題能夠在最初開發(fā)時被規(guī)避。當(dāng)軟件被拆分成一個個小的服務(wù)時,服務(wù)之間、服務(wù)組件之間層層依賴,一個服務(wù)組件出現(xiàn)故障或者因為網(wǎng)絡(luò)層發(fā)生故障無法通信,會影響到所有與它協(xié)作的服務(wù)組件,導(dǎo)致這些組件積累大量請求,最終不可用,形成雪崩式的崩潰。
例如,上游發(fā)來一次請求,為了完成這個任務(wù),組件中形成了一系列的調(diào)用。這些調(diào)用將服務(wù)組件連接在一起,稱之為調(diào)用鏈[7]。假設(shè)每次調(diào)用服務(wù)出故障概率為Fone,整條調(diào)用鏈上出現(xiàn)故障的概率Fall,則Fall=1-(1-Fone)n,其中n為調(diào)用鏈長度。
如圖1所示,當(dāng)調(diào)用鏈長度增加時,即使單個服務(wù)出現(xiàn)故障的概率極小,本次請求調(diào)用失敗的概率也會變得很大。
圖1 整體故障概率與調(diào)用鏈長度關(guān)系
考慮到每個類型的服務(wù)有m個備份均衡流量,如果某一個服務(wù)組件故障,請求會轉(zhuǎn)移到備份服務(wù)組件,每種類型的服務(wù)不可用的概率是:Ftype=Fonem,整條調(diào)用鏈Fall=1-(1-Ftype)n=1-(1-Fonem)n,圖2是每種服務(wù)分別只有1個、5個、10個備份實例的情況,當(dāng)每種服務(wù)有多個備份時(m變大)能夠有效降低請求失敗的概率。但是在微服務(wù)架構(gòu)中,上層服務(wù)需要通過網(wǎng)絡(luò)層調(diào)用下層服務(wù)。網(wǎng)絡(luò)層是脆弱的,很容易因為一些不可抗因素發(fā)生網(wǎng)絡(luò)連接失敗,或者服務(wù)沒有及時響應(yīng),使得上游一系列服務(wù)積壓了大量的請求,占用大量資源,F(xiàn)one數(shù)值激增,導(dǎo)致整個集群雪崩式地崩潰。因此在生產(chǎn)環(huán)境下,需要及時發(fā)現(xiàn)故障位置,防止整個集群發(fā)生崩潰。
圖2 服務(wù)有備份時整體故障概率與調(diào)用鏈長度關(guān)系
測試對于提前發(fā)現(xiàn)錯誤,驗證軟件功能有著極其重要的意義,但是在微服務(wù)架構(gòu)下,軟件被拆分成多個服務(wù),需要為每個服務(wù)搭建測試環(huán)境,對每個服務(wù)功能進行驗證,過程繁瑣,消耗人力巨大。其次,微服務(wù)故障多在實例之間交互中產(chǎn)生,而且交互結(jié)果容易受到網(wǎng)絡(luò)層狀態(tài)(延遲、帶寬)影響。微服務(wù)本身和實例之間沒有固定的對應(yīng)關(guān)系,服務(wù)實例在整個集群中動態(tài)地創(chuàng)建和銷毀。故障具有動態(tài)性,難以重現(xiàn),測試環(huán)境下很難提前發(fā)現(xiàn)故障。
容器技術(shù)有很強的可移植性,一次打包隨處部署,相比于VM更加輕量級,啟動和銷毀十分容易[8],最重要的是容器之間能相互協(xié)作,共同完成更加復(fù)雜的任務(wù)。這些特點與微服務(wù)完美適配,目前工業(yè)界普遍以容器的形式部署微服務(wù)。如圖3所示,服務(wù)網(wǎng)格是在每個容器上增加一個反向代理Sidecar,所有進出容器的流量完全經(jīng)過Sidecar,被Sidecar監(jiān)控和控制。Sidecar構(gòu)成了服務(wù)網(wǎng)格的Data Plane。比較新的服務(wù)網(wǎng)格在Data Plane的基礎(chǔ)之上增加Control Plane。Control Plane直接與Sidecar通信,將用戶策略轉(zhuǎn)發(fā)至Sidecar,實現(xiàn)對流量的更精準(zhǔn)地控制。
圖3 服務(wù)網(wǎng)格示意圖
服務(wù)網(wǎng)格在微服務(wù)實例上掛載代理,它與Spring Cloud、Dubbo這類侵入式框架不同,它與業(yè)務(wù)代碼耦合很小,業(yè)務(wù)代碼的技術(shù)選型、迭代升級都不會受到框架的制約[9]。服務(wù)網(wǎng)格有強大的監(jiān)控功能,能夠提供四個黃金指標(biāo)的監(jiān)控(延遲、流量、錯誤、飽和),同時提供完善的日志功能,對于故障定位、原因分析有很大幫助。
但長期以來服務(wù)網(wǎng)格的性能被人詬病,消耗過多的系統(tǒng)資源的同時,對進出流量也造成了比較大的延時。而且伴隨服務(wù)網(wǎng)格強大功能的是較高的復(fù)雜性,要熟練運用服務(wù)網(wǎng)格需要投入一定的學(xué)習(xí)時間。
目前服務(wù)網(wǎng)格產(chǎn)品有很多,文章挑選了比較主流的幾款,分別是Istio、linkerd 2.0、Consul,AWS App Mesh和ASM。綜合比對他們的架構(gòu)設(shè)計、支持的功能、安全性和操作復(fù)雜度四個方面的信息,如表1所示。
表1 服務(wù)網(wǎng)格軟件綜合對比
在各種服務(wù)網(wǎng)格產(chǎn)品中,Istio的功能最為強大,最為靈活。它提供流量管理、擴展性、安全和可觀察性四大方面功能,幾乎涵蓋微服務(wù)監(jiān)控所有需求。Istio從1.6版本開始支持虛擬機節(jié)點,不再完全依賴Kubernetes平臺,能更好地適應(yīng)異質(zhì)架構(gòu)的大型集群管理。同時Istio是一個開源項目,社區(qū)相比于其他的服務(wù)網(wǎng)格產(chǎn)品更加活躍,使用Istio作為微服務(wù)故障治理解決方案,更有代表意義。
整個故障預(yù)測流程如圖4所示。首先由手動點擊或者使用postman模擬發(fā)送http請求,得到正常情況下和故障注入情況下兩種數(shù)據(jù),包括可視化監(jiān)控圖、服務(wù)調(diào)用日志,各個容器運行指標(biāo)如表2所示。
表2 容器采集指標(biāo)
圖4 故障預(yù)測流程
最后將可視化監(jiān)控圖、日志和容器運行時參數(shù)、定位故障,輸入故障預(yù)測模型,確定故障原因。
使用Istio提供的HTTP abort功能,該功能可以攔截并丟棄所有到達某個容器的流量,使得該容器對其他容器處于不可達狀態(tài)。同時故障注入分為服務(wù)級別和容器級別故障注入。一種微服務(wù)往往有多個容器備份,服務(wù)級別故障注入,讓該服務(wù)所對應(yīng)的所有容器全部處于故障狀態(tài),用于服務(wù)級別的故障定位驗證;容器級別故障注入,讓該服務(wù)的某個容器處于故障狀態(tài),用于容器級別的故障定位驗證。
在微服務(wù)中,為了完成一個任務(wù),微服務(wù)之間往往形成一個很長的調(diào)用鏈,這個調(diào)用鏈上任意一個服務(wù)失敗都會導(dǎo)致本次請求失敗。當(dāng)故障出現(xiàn)時,表現(xiàn)的是請求結(jié)果無回應(yīng)或者返回錯誤結(jié)果。但具體錯誤是出現(xiàn)哪個微服務(wù)上,需要消耗大量時間排查。這里提出調(diào)用鏈交叉累計法(Invoke Chain Intersection Accumulation Method,ICIAM)。在圖5中,綠色方塊代表正常服務(wù),紅色方塊代表故障服務(wù),灰色方塊代表因上游服務(wù)故障而導(dǎo)致下游故障的服務(wù)。
圖5 ICIAM示意圖
正常情況下,有三個請求,當(dāng)某個服務(wù)出現(xiàn)故障時(紅色),會影響到兩條調(diào)用鏈。對每條出現(xiàn)故障的調(diào)用鏈上所有服務(wù)實例分?jǐn)?shù)加1。分?jǐn)?shù)最高的服務(wù),往往是故障源頭。
每種服務(wù)可能存在多個容器備份,需要確定具體故障是否出現(xiàn)在容器[10]。其次,調(diào)用鏈交叉部分服務(wù)可能不止一個,故障服務(wù)不一定會導(dǎo)致后續(xù)的服務(wù)不可用,使用ICIAM定位的故障源范圍較大,需要進一步縮小。Istio集成Prometheus,可以對每個容器數(shù)據(jù)進行監(jiān)控,收集容器CPU、內(nèi)存方面的數(shù)據(jù)。容器正常運行和故障時的各項metric特性會有很大差異,可以通過分析這些參數(shù)(如表2所示),得到故障容器位置。
使用4臺8GB、4VCPU、操作系統(tǒng)為ubuntu18.04x64 的虛擬機,搭建kubernetes-1.17.5 集群。在Kubernetes集群基礎(chǔ)上,搭建Istio 1.6.8,對Kubernetes上運行的所有微服務(wù)實例進行管理。
使用TrainTicket[11]軟件作為本次實驗測試用例,檢驗故障治理流程能否定位故障。這是由復(fù)旦大學(xué)實驗室基于微服務(wù)架構(gòu)開發(fā)的應(yīng)用。它是由41個微服務(wù)組成,使用了Java、Node.js、Python、Go、Mongo DB和MySQL多種語言和技術(shù)。雖然復(fù)雜程度不及工業(yè)界軟件,但是服務(wù)間技術(shù)獨立,服務(wù)互相配合,使用多種語言開發(fā),幾乎體現(xiàn)了微服務(wù)架構(gòu)下軟件所有特點,使用這款應(yīng)用進行實驗具有一定的代表性。
使用Postman模擬ts-preserve-service:preserve;ts-travel-plan-service:getByMinStation;ts-travel-service:queryInfo;Ts-preserv-otherservice:preserve;Ts-travel-plan-service:getByCheapest多種操作,這些請求都會調(diào)用ts-seatservice,用來獲得交叉調(diào)用。每次請求有50ms延時,一共發(fā)送1 200次。使用Postman ts-travel-planservice:getByCheapest和ts-preserve-service:preserve兩種請求各300次,測試當(dāng)服務(wù)在實際生產(chǎn)情況下,調(diào)用鏈種類不夠豐富的情況下能否定位故障。
使用Istio中集成的Jaeger,每次有請求發(fā)生時,將微服務(wù)之間調(diào)用關(guān)系,每個微服務(wù)所消耗時間以日志形式記錄。將服務(wù)的調(diào)用路徑、消耗時間等數(shù)據(jù)以json格式導(dǎo)出,同時Istio也支持微服務(wù)信息可視化,方便故障定位。
收集兩類數(shù)據(jù)集,一類是所有服務(wù)正常情況下的數(shù)據(jù)集,另外一類是故障注入下的數(shù)據(jù)集。使用Istio故障注入功能,調(diào)用ts-seat-service發(fā)生abort故障。請求到此類服務(wù)的HTTP請求,全部得到500的HTTP狀態(tài)碼。
4.4.1 ICIAM驗證錯誤服務(wù)環(huán)節(jié)
我們得到的正確數(shù)據(jù)集合一共是1 200次請求,錯誤數(shù)據(jù)集也是1 200次請求。每種數(shù)據(jù)集中包含以下請求,ts-preserve-service:preserve;Ts-travel-planservice:getByMinStation;Ts-travel-service:queryInfo;Ts-preserv-other-service:preserve;Ts-travel-planservice:getByCheapest。
無故障情況下每種請求一次請求所引起的總共調(diào)用次數(shù),以及調(diào)用了ts-seat-service的次數(shù)如表3、表4所示。
表3 無故障時各類服務(wù)調(diào)用次數(shù)
表4 ts-seat-service故障時服務(wù)調(diào)用次數(shù)
總的調(diào)用次數(shù)少了,因為當(dāng)請求到達ts-seatservice故障位置時,得到錯誤響應(yīng),后續(xù)服務(wù)全部終止。ts-seat-service被調(diào)用的次數(shù)為1,因為到這里請求發(fā)生錯誤,調(diào)用終止。
如圖6所示,同時發(fā)起5種請求,使用ICCA算法得分最高的前5名服務(wù)如表5所示。
圖6 5種請求故障調(diào)用鏈?zhǔn)疽鈭D
表5 5種請求時ICCA算法得分最高前5名服務(wù)
由于ts-seat-service是故障源頭,所有故障鏈的終點都會匯聚到這個服務(wù),增加ts-seat-service得分,最終故障源頭的得分最高。當(dāng)我們減少請求種類如圖7所示,只發(fā)起ts-travel-plan-service:getByCheapest和tspreserve-service:preserve兩種請求,使用ICIAM得分最高的前5名服務(wù)如表6所示。
表6 2種請求時ICCA算法得分最高前5名服務(wù)
圖7 2種請求故障調(diào)用鏈?zhǔn)疽鈭D
可以看到,當(dāng)故障服務(wù)上游服務(wù)一一對應(yīng)時,可能出現(xiàn)故障服務(wù)得分和唯一上游服務(wù)相同的情況,但是這種情況比較少見,很少出現(xiàn)一種服務(wù)只是被一種服務(wù)調(diào)用。其次,即使出現(xiàn)這種情況,故障服務(wù)的得分也是故障調(diào)用鏈上最高之一,也能大概確定故障服務(wù)位置。
綜合比較,使用ICIAM,服務(wù)被越多種服務(wù)調(diào)用,發(fā)生故障時,越容易被定位出來。總體來說,ICIAM可以通過服務(wù)網(wǎng)格提供的調(diào)用路徑,定位到故障服務(wù)位置。
4.4.2 定位故障容器位置
一個服務(wù)往往有多個備份容器分?jǐn)偭髁?,需要定位到具體哪個實例出現(xiàn)問題,采集容器CPU、內(nèi)存等方面指標(biāo),從這些指標(biāo)可以得到具體故障位置。
如表7、表8所示,使用Istio進行故障注入,sidecar將所有請求攔截,返回http狀態(tài)碼500,此刻容器的CPU負(fù)載明顯低于正常情況,內(nèi)存也有少量降低,但是影響很小,分析原因是容器在啟動時Request Memory設(shè)置在250MB,容器處于空閑狀態(tài)或處于較低負(fù)載水平時,內(nèi)存保持在250MB左右,當(dāng)容器處理大量求量之時,會額外申請內(nèi)存。通過容器各項指標(biāo)參數(shù),可以發(fā)現(xiàn)故障容器異常,從而判別故障。
表7 無故障情況下容器CPU、內(nèi)存情況
表8 故障注入情況下容器CPU,內(nèi)存情況
使用服務(wù)網(wǎng)格實現(xiàn)了對微服務(wù)全方位的監(jiān)控,包括服務(wù)調(diào)用路徑、在每個節(jié)點延時、容器運行各項指標(biāo)。完善的監(jiān)控體系對于故障定位有著很大意義。
使用ICIAM方法能有效確定故障服務(wù)種類,而且在服務(wù)請求種類越多的情況下效果越明顯。因為服務(wù)請求種類越多,故障服務(wù)越有幾率被多種請求調(diào)用,得分更高。如果服務(wù)請求種類少,而且故障服務(wù)上游僅有少量服務(wù)甚至一條調(diào)用路徑,定位的故障范圍可能會擴大,但基本能夠確定故障位置。通過容器各項運行指標(biāo),定位具體故障容器。
然而,微服務(wù)故障治理不應(yīng)該僅僅定位故障位置,還應(yīng)該找到故障原因。這篇文章研究故障注入類型比較單一,只是使用istio對ts-seat-service在網(wǎng)絡(luò)連接上進行故障注入,真實生產(chǎn)環(huán)境下故障種類十分復(fù)雜,有內(nèi)存負(fù)載過大、CPU負(fù)載過大、網(wǎng)絡(luò)環(huán)境故障、程序本身錯誤、配置錯誤等多種原因,需要進一步細分。這也是未來需要研究的方向。