網易考拉海購Dubbok框架優化詳解
摘要:微服務化是當前電商產品演化的必然趨勢,網易考拉海購通過微服務化打破了業務爆發增長的架構瓶頸。本文結合網易考拉海購引用的開源Dubbo框架,分享支援考拉微服務工作的基本原理。文章分析了使用Dubbo過程中遇到的問題,講解了團隊所做的一些問題修復和功能整合工作,在此基礎上最終形成了考拉內部持續維護升級的Dubbok框架。
本文背景還要從網易考拉海購(下文簡稱“考拉”)微服務化說起,現在任何大型的網際網路應用,尤其是電商應用從Monolithic單體應用走向微服務化已經是必然趨勢。微服務化是一個比較寬泛的概念,涉及到一個產品生命週期的多個方面,首先它作為一個指導原則指引業務劃分、架構解耦等;技術層面實施微服務需要開發測試階段、執行階段、釋出階段、部署階段等一系列基礎框架的支撐。我們在享受服務化易擴充套件易部署等便利性的同時,也面臨新的問題,如資料一致性、分散式呼叫鏈路追蹤、異常定位、日誌採集等。
本文將集中在支撐微服務互動、執行的基礎框架講解上,即考拉當前使用的Dubbok框架,Dubbok由阿里開源Dubbo框架的優化和功能改進而來。當前開源上可選用的微服務框架主要有Dubbo、Spring Cloud等,鑑於Dubbo完備的功能和文件且在國內被眾多大型網際網路公司選用,考拉自然也選擇了Dubbo作為服務化的基礎框架。其實相比於Dubbo,Spring Cloud可以說是一個更完備的微服務解決方案,它從功能性上是Dubbo的一個超集,個人認為從選型上對於一些中小型企業Spring Cloud可能是一個更好的選擇。提起Spring Cloud,一些開發的第一印象是http+JSON的rest通訊,效能上難堪重用,其實這也是一種誤讀。微服務選型要評估以下幾點:內部是否存在異構系統整合的問題;備選框架功能特性是否滿足需求;http協議的通訊對於應用的負載量會否真正成為瓶頸點(Spring Cloud也並不是和http+JSON強制繫結的,如有必要Thrift、protobuf等高效的RPC、序列化協議同樣可以作為替代方案);社群活躍度、團隊技術儲備等。作為已經沒有團隊持續維護的開源專案,選擇Dubbo框架內部就必須要組建一個維護團隊,先不論你要準備要整合多少功能做多少改造,作為一個支撐所有工程正常運轉的基礎元件,問題的及時響應與解答、重大缺陷的及時修復能力就已足夠重要。
下文將選取Dubbo高效能RPC通訊原理、服務註冊發現特性、依賴隔離、啟動與停機等幾個方面闡述Dubbok的工作原理和相關改進工作。
一、高效能RPC
Dubbo作為一個分散式通訊框架,最基本的職責就是完成跨程序的遠端呼叫(RPC)。以下是RPC基本流程圖:
RPC基本原理非常簡單,那麼Dubbo是如何實現高效的RPC通訊的呢,和其他分散式通訊元件關注點一樣,主要集中在以下幾點的優化:
1.協議棧:
- Dubbo支援自定義RPC協議,冗餘欄位少、通訊效能高;
- 序列化協議支援hessian2、Dubbo自定義序列化等高效能協議;
- Dubbo支援序列化協議解碼在業務執行緒(Netty3編碼自動在業務執行緒執行);
2.執行緒模型:
依賴Netty3的非阻塞執行緒模型,支援I/O、業務邏輯執行緒分離,通過Handler鏈處理請求。
這裡特別強調Netty3,是因為Netty4線上程模型、buffer緩衝區等方面做了重大的設計和效能改進,包括Inbound、Outbound事件強制在I/O執行緒發起、buffer通過緩衝池減少分配釋放、DirectBuffer實現緩衝區零複製等。Netty這塊升級相對是一個高風險的點,明面上的API相容性改造是小,如對Netty4工作原理認識不足,新的執行緒模型、buffer緩衝池等帶來的非預期效能下降、記憶體洩露等問題相對更難定位與跟蹤。
講到執行緒模型,實現上密切相關的Dubbo網路連線模型必須要提一下。Dubbo預設是所有服務共享單一的TCP長連線的(這也是為什麼服務介面不適合傳輸大負載值,即容易阻塞其他服務的呼叫)。為響應慢或重要的服務介面考慮,Dubbo支援設定多TCP連線,此時連線數和執行緒池數預設是繫結的,即每連線對應一個線池,consumer、provider都執行這個策略,從執行緒隔離的角度講是合理的,但不注意也容易造成執行緒佔用資源過多,尤其是對於消費端基本無執行緒阻塞的情況下可能是一個設計缺陷。
3.緩衝區:
Dubbo預設使用的全部是heap緩衝區,因此Socket通訊不可避免會存在核心緩衝區和堆緩衝區複製消耗;除此之外在RPC協議解析(包括粘包/半包處理)、序列化協議解析等處理上也存在heap區內的複製,因此效能上是存在優化點的(當然要確有必要)。
二、自動註冊/發現、負載均衡等服務化特性
高效能通訊是Dubbo作為RPC框架的基本功能,但使其區別於Thrift、hessian、gRPC等框架的關鍵在於其新增的服務間自動協調、服務治理等特性。
1. 服務自動註冊自動發現、負載均衡
服務自動註冊發現依賴於註冊中心的支援,consumer與provider通過註冊中心獲取各自地址後直接通訊。目前考拉使用Zookeeper作為註冊中心,Dubbo原生支援Redis作為註冊中心,使用pub/sub機制協調服務的上下線事件通知,但Redis方案要求伺服器時間同步且存在效能消耗過大的缺點。
- 使用Zookeeper作為註冊中心,建議選用curator作為客戶端框架;
- Zookeeper伺服器異常宕機並重新啟動的場景下,Dubbo服務的recover恢復機制存在不能重新註冊的問題,導致老zk session失效後服務被錯誤清除。
服務框架常見負載均衡實現方案包括:集中式、分散式,分散式又可分程序內、分程序兩種。Dubbo採用的是服務發現和負載均衡共同整合在consumer端的分散式程序內解決方案
負載均衡策略上Dubbo原生提供的有基於權重隨機負載、最少活躍數優先、Roundrobin、一致性Hash等幾個方案。
- 在實際應用中,為了能對個別錯誤率較高的異常provider做到及時發現、及時引流,Dubbok增加了新的負載均衡策略,在支援權重的基礎上自動發現異常provider,異常期自動減流、正常後自動恢復流量。
2.路由、叢集容錯、限流
和負載均衡策略一樣,Dubbo的路由方案是整合在消費端的,加上叢集容錯功能客戶端相對是一個重量的功能封裝。可選方案是將路由工作移到註冊中心完成(這要求註冊中心具有較強的可定製性,不僅路由像許可權控制、服務過濾、環境隔離等都可由註冊中心整合)
限流目前支援consumer、provider端併發限流,實際上是基於訊號量限制的,以介面粒度分配訊號量,當訊號量用完新的呼叫將被拒絕,當業務返回後訊號量被釋放。
- 消費端限流應該是為整個提供端叢集分配訊號量,而Dubbo錯誤的將訊號量分配給單個機器。這個問題目前可以通過下文提到的隔離框架的流控功能來實現。
- 限流並非精確限制,不應當依賴其實現嚴格的併發數控制。
- 後端backend服務限流需要業務方合理評估每個介面的流控值,要求對業務量有足夠經驗值(可能要在多次線上調優後才能最終得出合理的流控值)。考拉內部流控實踐證明,對於保證服務穩定性、優先保證重要消費方、實現服務隔離等有著重要的作用。
3.服務動態治理
動態治理本質上是依賴Dubbo執行期引數的動態調整,再通用一點其實就是應用的引數動態調整,開源常用的disconf、diamond、archaius等集中配置管理工具都是設計來解決這個問題。Dubbo內部在url引數傳遞模型基礎上實現了一套引數動態配置邏輯,個人認為相比於Dubbo的實現,整合disconf等更專業的框架應該是更好的解決方案,或許Dubbo為了一些其他設計目標解除了對一些外部框架的強制依賴。動態治理可以實現從基本引數如timeout、mock到一些高階特性如路由、限流等幾乎所有的執行期引數調整。
- Dubbo原生在動態配置上存在很多bug,配置不生效或配置規則誤讀等問題都遇到過,如果你再使用原生Dubbo過程中也遇到任何配置問題,Dubbok應該都已經解決掉了。
三、依賴隔離(服務降級)
當應用被設計依賴外部服務時,要始終保持警惕狀態:外部依賴是不穩定的,為此對接外部依賴做好解耦是關鍵,避免外部介面發生異常拖垮自身系統。Dubbo提供了超時timeout機制作為最基本的解耦措施,同時在介面報錯時支援提供降級的容錯邏輯;除了容錯降級,Dubbo進一步支援強制的短路降級。
然而在容錯降級與短路降級之間,Dubbo缺乏一種在容錯與短路間切換的機制,即自動熔斷。自動熔斷要達到的效果是:當介面偶然報錯時執行容錯返回備用資料,而當介面持續大量報錯時能自動在消費端對介面呼叫短路直接返回備用資料,之後持續監測介面可用性,介面恢復後自動恢復呼叫。這樣能最大限度減少介面異常對消費方的影響,同時也減輕本就處於異常狀態的提供端負載。
Dubbok通過標準SPI的的形式,實現了熔斷功能。目前支援兩套方案:一套是自己實現的熔斷邏輯;一套是通過整合hystrix框架實現。目前支援錯誤率、最低請求量、熔斷時間窗等基本配置,支援將業務異常納入統計範疇;以上引數均可通過SOA治理平臺執行期動態調整;支援外部Dubbo依賴呼叫的準實時監控。
Hystrix是Netflix在微服務實踐中為實現外部依賴解耦而設計的框架,它假設所有的外部依賴(http、MySQL、Redis等等)可能在任何時間出現問題(你甚至可以想像不經意間就使用了一個沒有提供超時設定的http客戶端)。於任何可能的外部延時造成的阻塞或其他異常,hystix提供了基於執行緒池隔離的超時機制,新版本在RxJava基礎上訊號量隔離也同樣支援超時。此外框架還支援定製容錯邏輯、請求結果快取、請求合併、消費端執行緒池隔離等,由於某些功能當前無明確需求或與Dubbo功能設計重合而沒有進行整合。
Hystrix更多特性及實現原理請參見Netflix官方文件。
四、啟動與停機
這裡主要關注Dubbo工程啟動初始化階段和停機銷燬階段的一些特性和改進點:
1.延遲暴露。
預設Dubbo服務會隨著Spring框架的載入逐一完成服務到註冊中心的註冊(暴露),如果某些服務需要等待資源就位才能暴露,那就需要延時註冊。
增加Spring context初始化完成後繼續延時等待的配置項
在無特殊配置的情況下,所有的Dubbo服務預設是註冊在同一個tcp埠的。而延遲暴露是通過開啟新的延時執行緒實現的,當延時結束時由於多執行緒併發執行導致多服務隨機註冊在多個埠。
2.啟動預熱
一些應用在執行期會通過本地快取中間結果提升效能,而當此類應用重啟時本地快取資料丟失,如果重啟後的機器立即有大量請求導流過來,由於沒有快取加速會導致請求阻塞響應效能降低。通過對重啟後的機器設定預熱期可有效緩解重啟快取失效問題:具體做法是降低預熱期內的機器權重,引導少部分流量到此機器,此機器可以在預熱期內逐步建立快取,待預熱期過後恢復正常權重與其他機器平均分攤流量。
3.優雅停機
在叢集部署的情況下,單個消費者或提供者機器上下線對整個產品的運轉應該是近乎無感知的,Dubbo提供了優雅停機功機制保障在程序關閉前請求都得到妥善處理。
消費方優雅停機:控制不再有新的請求發出;等待已經發出的請求正確返回;釋放連線等資源。
提供方優雅停機:通知消費端停止傳送請求到當前機器;通知註冊中心服務下線;等待已經接收的請求處理完成並返回;釋放連線等資源。
- 考拉在每次服務上下線過程中,每個工程總是收到大量的消費方/提供方報出的服務呼叫異常,經排查是Dubbo優雅停機實現的問題,修復問題後工程上線階段異常數明顯減少。
- 另外停機階段總是莫名的收到zk連線為空的異常資訊。是由於在通知註冊中心服務下線的過程中,Spring銷燬執行緒和jvm hook執行緒併發執行,導致zk客戶端被提前銷燬導致丟擲異常。
4.Provider重啟
註冊中心傳送大量服務銷燬與註冊通知導致consumer工程Full GC。
歷史原因,考拉內部仍存在一個提供近200個Dubbo服務的單體工程,而每次當這個工程上線時,消費它的consumer工程就會出現頻繁Full GC(3-5次,非記憶體洩露)。
- 是Dubbo為保證高可用而設計的註冊中心快取導致的問題:在每次收到註冊中心變更時consumer會在本地磁碟儲存一份服務資料副本,由於多註冊中心共享同一份快取檔案,為了避免相互覆蓋,每個註冊中心例項會在收到變更時重新從磁碟載入檔案到快取,和變更資料對比後重新寫回磁碟,在近100提供者機器不斷重啟的過程中,大量的變更通知導致的頻繁載入快取檔案佔用大量記憶體導致Full GC。
五、Dubbok近期優化目標:
- 提供端執行緒池隔離,解決提供端執行緒池阻塞等問題;優化消費端執行緒池分配方案
- 服務治理動態配置功能增加應用、機器粒度的配置
- 多註冊中心消費端負載均衡策略
- Dubbo內部資源JMX監控
- 結合SOA平臺優化監控統計資料:錯誤型別細分(超時、限流、網路異常等);執行時間細分如90%、99% RT等;統計佔 用執行緒數較多的服務、傳送資料量較大的服務,為分執行緒池或連線做參考
- 對Spring boot推行的Javaconfig配置方式提供更友好、全面的註解支援
- 一些框架升級,如Javassist、Netty等
- 替代Zookeeper的高效能、可擴充套件註冊中心
- 服務安全、授權問題調研
- Spring Cloud的一些優秀特性將作為Dubbok改進的一個持續關注點
……
六、總結
文中提到的一些改進點只是簡略描述,Dubbok的很多改進點也沒有一一提及,後續有時間再詳細道來。
Dubbok框架、SOA服務治理平臺、分散式呼叫鏈路跟蹤以及其他考拉內部維護的如分散式事務、訊息中介軟體等共同支撐了考拉微服務化的正常運轉。上文也提到微服務需要有開發、測試、執行、運維、部署、釋出等各階段的全鏈路支撐才能發揮最大價值,後續我們將繼續聯合其他兄弟部門,對微服務化實踐進一步優化。
作者簡介:劉軍,2013年碩士畢業於中國科學院,2015年入職網易先後在杭州研究院和考拉海購參與一些中介軟體開發,現主要負責考拉微服務化的基礎中介軟體開發工作,包括服務治理平臺、分散式呼叫框架、呼叫鏈追蹤等,近期專注於微服務化全鏈路基礎框架的研究。
如果您對本文的分享有疑問,或者想了解更多的優化工作,歡迎與作者展開更深入的討論。微信:Jianding_zhou,郵箱:[email protected]。