- 超大流量分布式系統架構解決方案:人人都是架構師2.0
- 高翔龍
- 7601字
- 2020-06-08 17:57:00
1.2 服務治理需求
隨著業務復雜度的上升,服務化能夠有效幫助企業解決共享業務被重復建設、業務系統水平伸縮,以及大規模業務開發團隊協作等問題,那么接下來筆者就會重點為大家講解大規模服務化場景下企業應該如何實施服務治理。
1.2.1 服務化與RPC協議
在本章的前面幾個小節中,筆者為大家詳細介紹了究竟什么是服務化,以及企業為什么需要落地服務化架構。當然,在筆者正式開始為大家演示具體的實施細節之前,不得不提及的就是與服務化息息相關的RPC(Remote Procedure Call,遠程過程調用)協議。從嚴格意義上來說,服務化其實只是一個抽象概念,而RPC協議才是用于實現服務調用的關鍵。RPC由客戶端(服務調用方)和服務端(服務提供方)兩部分構成,和在同一個進程空間內執行本地方法調用相比,RPC的實現細節會相對復雜不少。簡單來說,服務提供方所提供的方法需要由服務調用方以網絡的形式進行遠程調用,因此這個過程也稱為RPC請求,服務提供方根據服務調用方提供的參數執行指定的服務方法,執行完成后再將執行結果響應給服務調用方,這樣一次RPC調用就完成了,如圖1-13所示。

圖1-13 RPC的請求調用過程
筆者簡單闡述下Dubbo消費端Proxy的創建流程。當我們試圖從Spring的IoC容器中獲取出目標對象實例時,就會觸發Dubbo的ReferenceBean類(繼承自Spring的BeanFactory)的getObject()方法,由該方法負責調用父類ReferenceConfig的get()方法觸發init()調用。在init()方法中,最終會調用createProxy()方法來生成服務接口的Proxy對象。當Proxy成功創建后,一旦發起RPC調用,那么在服務接口的Proxy中將會使用Netty請求調用目標Provider。
服務化框架的核心就是RPC,目前市面上成熟的RPC實現方案有很多,比如Java RMI、Web Service、Hessian及Finagle等。在此需要注意,不同的RPC實現對序列化和反序列化的處理也不盡相同,比如將對象序列化成XML/JSON等文本格式盡管具備良好的可讀性、擴展性和通用性,但卻過于笨重,不僅報文體積大,解析過程也較為緩慢,因此在一些特別注重性能的場景下,采用二進制協議更合適。看到這里或許有些人已經產生了疑問,實現服務調用無非就是跨進程通信而已,那是否可以使用Socket技術自行實現呢?帶著疑問,筆者來為大家梳理一下完成一次RPC調用主要需要經歷的三個步驟:
● 底層的網絡通信協議處理;
● 解決尋址問題;
● 請求/響應過程中參數的序列化和反序列化工作。
其實,RPC的本質就是屏蔽上述復雜的底層處理細節,讓服務提供方和服務調用方都能夠以一種極其簡單的方式(甚至簡單到就像是在實現一個本地方法和調用一個本地方法一樣)來實現服務的發布和調用,使開發人員只需要關注自身的業務邏輯即可。
1.2.2 基于服務治理框架Dubbo實現服務化
由于RPC協議屏蔽了底層復雜的細節處理,并且隨著后續服務規模的不斷擴大需要考慮服務治理等因素,因此筆者在生產環境使用的服務治理框架基于的就是Apache開源的Dubbo。之所以選擇Dubbo,主要因為其設計精良、使用簡單、性能高效,以及技術文檔豐富等特點。并且Dubbo還預留了足夠多的接口,以便讓開發人員能夠以一種非常簡單的方式對其進行功能擴展,更好地滿足和適配自身業務,所以Dubbo在開源社區擁有眾多的技術擁護者和推進者。早期因為一些特殊原因曾導致Dubbo一度停止維護和更新,但好在從2017年的9月份開始,Dubbo又重新回歸了開源社區的懷抱,陸陸續續發布了一系列版本,而且目前已經正式成為Apache的頂級項目。
如圖1-14所示,Provider作為服務提供方負責對外提供服務,當JVM啟動時Provider會被自動加載和啟動,由于Provider并不需要依賴任何的Web容器,因此它可以運行在任何一個普通的Java程序中。在Provider成功啟動后,會向注冊中心注冊指定的服務,這樣作為服務調用方的Consumer在啟動后便可以向注冊中心訂閱目標服務(服務提供者的地址列表),然后在本地根據負載均衡算法從地址列表中選擇其中的某一個可用服務節點進行RPC調用,如果調用失敗則自動Failover到其他服務節點上。

圖1-14 Dubbo整體架構
在此需要注意,在服務的調用過程中還存在一個不容忽視的問題,即監控系統。試想一下,如果在生產環境中業務系統和外圍系統沒有部署相對應的監控系統來幫助開發人員分析和定位問題,那么這與脫了韁的野馬無異,當問題出現時必然會導致開發人員手足無措,相互之間推卸責任,因此監控系統的重要性不言而喻。值得慶幸的是,Dubbo為開發人員提供了一套完善的監控中心,使我們能夠非常清楚地知道指定服務的狀態信息(如服務調用成功次數、服務調用失敗次數、平均響應時間等),如圖1-15所示。這些數據由Provider和Consumer負責統計,再實時提交到監控中心。

圖1-15 Dubbo服務狀態監控
接下來筆者就為大家演示Dubbo的一些基本使用方式。截至本書成稿之時,Dubbo的最新版本為2.7.0,但由于筆者所在企業使用的版本為2.5.3,因此本書也使用此版本來進行演示。下載Dubbo構件,如下所示:

Dubbo項目的pom.xml文件中依賴有一些其他的第三方構件,如果版本過低或者與當前項目中依賴的構件產生沖突時,那么可以在項目的pom.xml文件中使用Maven提供的<;exclusions/>;標簽來排除依賴,自行下載指定版本的相關構件即可。除此之外,大家還需要注意一點,Dubbo的源碼是在Java 5版本上進行編譯的,因此在項目中使用Dubbo時,JDK版本應該高于或等于Java 5。
成功下載好運行Dubbo所需的相關構件后,首先要做的事情就是定義服務接口和服務實現,如下所示:

在上述程序示例中,服務接口UserService中提供了一個用于模擬用戶登錄的login()方法,它的服務實現為UserServiceImpl類。如果用戶正確輸入賬號和密碼,那么結果將返回“true”,否則返回“false”。服務接口需要同時包含在服務提供方和服務調用方兩端,而服務實現對于服務調用方來說是隱藏的,因此它僅僅需要包含在服務提供方即可。
從理論上來說,盡管Dubbo可以不依賴任何的第三方構件,只需有JDK的支撐就可以運行,但是在實際的開發過程中,為了避免Dubbo對業務代碼造成侵入,筆者推薦大家將Dubbo集成到Spring中來實現遠程服務的發布和調用。在Provider的Spring配置信息中發布服務,如下所示:

成功啟動Provider后,便可以在注冊中心看見由Provider發布的遠程服務。在Consumer的Spring配置信息中引用遠程服務,如下所示:

成功啟動Consumer后,便可以對目標遠程服務進行RPC調用(使用Dubbo進行服務的發布和調用,就像實現一個本地方法和調用一個本地方法一樣簡單,因此筆者省略了服務調用的相關代碼)。關于Dubbo的更多使用方式,本書不再一一進行講解,大家可以參考Dubbo的用戶指南。
1.2.3 警惕因超時和重試引起的系統雪崩
在上一個小節中,筆者為大家演示了Dubbo框架的一些基本使用方法,盡管使用Dubbo來實現RPC調用非常簡單,但是剛接觸Dubbo框架的開發人員極有可能因為對其不熟悉而跌進一些“陷阱”中。因此,開發人員需要重視看似微不足道的細節,這往往可以非常有效地避免系統在大流量場景下產生雪崩現象。在對數據庫、分布式緩存進行讀/寫訪問操作時,我們往往都會設置超時時間,一般筆者不建議在生產環境中將超時時間設置得太長,否則如果應用獲取不到會話,又長時間不肯返回,那么一定會對業務產生較大的影響。但將超時時間設置得太短又可能適得其反,因此超時時間究竟應該如何設置需要根據實際的業務場景而定,大家可以在日常的壓測過程中仔細評估。
為Dubbo設置超時時間應該是有針對性的,比較簡單的業務執行時間較短,可以將超時時間設置得短一點,但對于復雜業務而言,則需要將超時時間適當地設置得長一點。筆者之前曾經提過,Dubbo的Consumer會在本地根據負載均衡算法從地址列表中選擇某一個服務節點進行RPC調用,如果調用失敗則自動Failover到其他服務節點上,默認重試次數為兩次,服務調用超時就意味著調用失敗,需要進行重試,如圖1-16所示。在此需要注意,如果一些復雜業務本身就需要耗費較長的時間來執行,但超時時間卻被不合理地設置為小于服務執行的實際時間,那么在大流量場景下,系統的負載壓力將被逐步放大,最終產生蝴蝶效應。假設有1000個并發請求同時對服務A進行RPC調用,但都因超時導致服務調用失敗,由于Dubbo默認的Failover機制,共將產生3000次并發請求對服務A進行調用,這是系統正常壓力的3倍,若處于峰值流量時情況可能還會更糟糕,大量的并發重試請求很可能直接將Dubbo的容量撐爆,甚至影響到后端存儲系統,導致資源連接被耗盡,從而引發系統出現雪崩。

圖1-16 Dubbo的Failover機制
還有一點很重要,并不是任何類型的服務都適合Failover的,比如寫服務,由于需要考慮冪等性,因此筆者建議調用失敗后不應該進行重試,否則將導致數據被重復寫入。只有讀服務開啟Failover才會顯得有意義,既然不需要考慮冪等性,就可以通過Failover來提升服務質量。
除了Failover,Dubbo也提供了其他容錯方案供開發人員參考,如下所示:

上述相關參數除了可以在服務調用方配置,也適用于服務提供方,如果服務調用方和服務提供方配置有相同的參數,默認以服務調用方的配置信息為主。關信息大家可以參考Dubbo的配置參考手冊。
1.2.4 為什么需要實施服務治理
當企業的系統架構逐步演變到服務化階段時,架構師重點需要考慮的問題是服務如何拆分、粒度如何把控,以及服務之間的RPC調用應該如何實現。這些問題都迎刃而解了,也并不意味著我們就能夠一勞永逸,隨著服務規模的逐漸擴大,一些棘手的問題終會暴露出來,因此架構師必須具備一定的前瞻性,提前規劃和準備充足的預案去應對將來可能發生的種種變故,否則這就等于給自己挖坑,與其花大把時間去填坑,不如花更多的精力去思考企業在大規模服務化前應該如何實施服務治理。
那么究竟什么是服務治理呢?這個問題至今也沒有在社區達成一個統一的共識,幾乎每個人對服務治理都有一套自己的認知。服務治理所涉及的范圍較廣,包括但不限于:服務注冊/發現、服務限流、服務熔斷、負載均衡、服務路由、配置管理、服務監控等。參考Dubbo對服務治理的定義來看,服務治理的主要作用是改變運行時服務的行為和選址邏輯,達到限流,權重配置等目的,當然,采取什么樣的治理策略還需要依賴監控數據來進行全方位的分析和指導。
以服務的動態注冊/發現為例,當服務變得越來越多時,如果把服務的調用地址(URL)配置在服務調用方,那么URL的配置管理將變得非常麻煩,如圖1-17所示。因此引入注冊中心的目的就是實現服務的動態注冊和發現,讓服務的位置更加透明,這樣服務調用方將得到解脫,并且在客戶端實現負載均衡和Failover將會大大降低對硬件負載均衡器的依賴,從而減少企業的支出成本。

圖1-17 不依賴于注冊/發現
既然談到了服務注冊/發現機制,那么筆者就順便為大家講解下服務發現的2種模式,有助于幫助大家更好地在不同的業務場景下做出合適技術選型。
首先從服務端服務發現模式(Client-side Service Discovery Pattern)開始講起。在此模式下,除注冊中心外,還需引入代理中心(路由器)。當Provider成功啟動后,會向注冊中心注冊指定的服務,由代理中心來處理服務發現;Consumer的職責很簡單,只需要連接代理中心并向其發送請求即可,代理中心會根據負載均衡算法從地址列表中選擇一個可用的服務節點進行RPC調用,其實,這和部署在接入層的反向代理服務器作用類似。在此模式下,由于Consumer無須處理服務發現等相關邏輯,因此職責相對更加簡單和清晰,如圖1-18所示。

圖1-18 服務端服務發現模式
客戶端服務發現模式(Client-side Service Discovery Pattern)相信大家都非常熟悉了,Dubbo整體架構其實也基于的是此模式。基礎的服務注冊步驟和服務端模式是一致的,只是將服務發現交給了Consumer來負責實現,無須再引入代理中心,由Consumer根據指定的負載均衡算法從地址列表中選擇一個可用的服務節點進行RPC調用,如圖1-19所示。

圖1-19 客戶端服務發現模式
在分布式場景下,依賴的外圍系統越多,系統存在宕機的風險就越大。在服務端服務發現模式下,我們需要額外引入代理中心來負責服務發現和請求的負載均衡等任務,這就意味著代理中心必須具備容錯性和伸縮性;而且,從性能表現來看,客戶端模式整體的吞吐量也會優于服務端模式,畢竟在服務端模式下需多增加一層網絡開銷,不如客戶端模式來得直接。因此,在實際的開發過程中,筆者建議應該優先考慮使用基于客戶端的服務發現模式。
部署監控中心是為了更好地掌握服務當前的狀態信息,因為有了這些指標數據后我們才能夠清楚目標服務的負載壓力,以便迅速做出調整,采用合理的治理策略。比如在大促場景下,為了讓系統的負載處于比較均衡的水位,不會因為峰值流量過大,導致系統產生雪崩而宕機,我們往往都會選擇“斷臂求生”,采用流控、熔斷,或服務降級等治理策略對外提供有損服務,盡可能保證交易系統的穩定和大多數用戶能用。關于其他的服務治理問題,大家可以參考Dubbo的用戶指南。而關于服務調用跟蹤的問題,大家可以直接閱讀1.3節。
1.2.5 關于服務化后的分布式事務問題
從單機系統演變到分布式系統并不像書本中描述得那樣簡單,不同的業務之間必然會存在較大的差異,因此企業在實施服務化改造時肯定會困難重重,即使最終服務成功被拆分出來,架構師還需要提前思考和規劃后續的服務治理等問題。當然這一切都還不是終點,我們能夠看見的問題其實只是冰山一角,大型網站架構演變過程中等待我們解決的技術難題還有很多。大家思考一下,實施服務化改造后事務的問題應該如何解決?或許很多同學都會毫不猶豫地指出,分布式事務簡直讓人感到“痛心疾首”。的確,就算是銀行業務系統也并不一定都采用強一致性,那么我們是否還有必要去追求強一致性呢?
其實分布式事務一直就是業界沒有徹底解決的一個技術難題,沒有通用的解決方案,沒有高效的實現手段,但是這并不能成為我們不去解決的借口。既然分布式事務實施起來非常困難,那么我們為什么不換個思路,使用其他更優秀的替代方案呢?只要能夠保證最終一致性,哪怕數據會出現短暫不一致窗口期又有什么關系?在架構的演變過程中,哪個是主要矛盾就優先解決哪一個,就像我們對JVM進行性能調優一樣,吞吐量和低延遲這兩個目標本身就是相互矛盾的,如果吞吐量優先,那么GC就必然需要花費更長的暫停時間來執行內存回收;反之,頻繁地執行內存回收,又會導致程序吞吐量的下降,因此大家要學會權衡和折中。關于最終一致性的實現方案,大家可以直接閱讀5.2.8節和5.4.2節。
1.2.6 注冊中心性能瓶頸方案
隨著服務拆分的粒度越來越細,服務及服務實例越來越多,整個服務治理體系中注冊中心的性能瓶頸會逐漸開始暴露出來,這幾乎是任何一家大中型規模的電商企業都會經歷和面對的問題。筆者所在企業目前線上環境常態下的服務數量都保持在1W+左右,更不用說大促擴容后的服務數量,由于我們之前使用的注冊中心是ZooKeeper,所以面臨著如下2個棘手的問題:
● 服務擴容時,應用啟動異常緩慢;
● 冗余的服務配置項會增加存儲壓力和擴大網絡開銷。
筆者一直認為,ZooKeeper真的不太適合作為大規模服務化場景下的注冊中心,因為它是一個典型的CP系統,是基于ZAB(Zookeeper Atomic Broadcast,原子廣播)協議的強一致性中間件,它的寫操作存在單點問題,無法通過水平擴容來解決。當客戶端發送寫請求時,集群中的其他節點會優先轉發給Leader節點,由Leader節點來負責具體的寫入操作,只有當集群中>;=N/2+1個節點都同步成功后,一次寫操作才算完成。當服務擴容時,TPS越高,服務注冊時的寫入效率就越低,這會導致上游產生大量的請求排隊,表象就是服務啟動變得異常緩慢。
Dubbo服務啟動時向注冊中心寫入的配置項有近30多個,但是其中大部分配置項都不需要傳遞給消費者,或者說消費者并不需要這些配置項(比如:interface、method、owner等),消費者完全可以通過接口API在生成Proxy時獲取到必要的關鍵信息,那么在服務大規模擴容時,冗余的配置項會導致數據量的急劇膨脹,增加ZooKeeper集群的存儲壓力,甚至直接導致內存溢出。筆者生產環境中確實發生過一次這樣的生產事故,最初我們也沒有深究,只是緊急擴大了內存應急,但如果后續服務規模更大,繼續盲目擴大內存是無法有效支撐企業未來發展的,最重要的是,數據量的膨脹會直接擴大注冊中心的網絡開銷。
網絡開銷被擴大會帶來什么后果?不急,聽筆者徐徐道來。大促結束后,運維同學需要釋放掉過剩的服務資源,減少不必要的資源開銷,在不考慮服務優雅下線的情況下,通常采用的做法是簡單粗暴的“kill-9”操作,但這會導致消費者無法及時更新本地的服務地址列表,在某個單位時間內仍然路由到失效節點上,導致大量的調用超時異常,因此我們的架構團隊擴展了Dubbo的優雅下線功能(Dubbo2.5.3版本的優雅停機存在Bug)。簡單來說,對那些需要下線的服務,會優先從注冊中心內摘除它們注冊時寫入的相關信息,然后等服務處理完當前任務后再結束其進程。盡管設計上是合理的,但需要釋放的服務數量之多,服務節點的任何變化,都會導致消費者每次都全量拉取一個服務接口下的所有服務地址列表信息,筆者線上注冊中心的內網帶寬是1.5Gbit/s,消費者拉取帶來的瞬時流量瞬間就會將ZooKeeper集群的網卡打滿,導致大量的消費者沒有及時拉取到變化后的服務地址列表而繼續路由到失效節點上。
接下來筆者為大家分享下注冊中心的改造方案,如圖1-20所示。

圖1-20 注冊中心的3個改造階段
階段1的改造成本非常小,僅僅只需要運維同學增加ZooKeeper的ObServer節點即可。由于ObServer節點并不參與投票,只負責同步Leader狀態,因此我們可以通過擴展ObServer節點來提升ZooKeeper集群的QPS和分擔帶寬壓力,并且擴展ObServer節點并不會降低集群的寫入性能。盡管階段1可以在短時間內解決我們的燃眉之急,但治標不治本,所以我們引入了階段2。
階段2的改造相對復雜。由于我們使用的Dubbo版本是2.5.3,所以修改了Provider的注冊邏輯,向注冊中心注冊時只寫入了部分關鍵信息(比如:ip、port、version,以及timeout等),其他配置項則寫入元數據中心,實現了數據分離,所帶來的好處就是,注冊中心再也不會存在過多的冗余配置項而引起數據膨脹。由于數據量的減少,消費者在拉取時,極大程度地降低了網絡開銷。在此大家需要注意,目前Dubbo的2.7.0版本已經提供了精簡注冊配置項和元數據服務等功能,且支持向下兼容,所以筆者建議大家可以直接使用新版本的Dubbo,不必再去重復造輪子。
階段3是最終目標,同時也是3個階段中改造成本最大的。寫操作對于服務注冊/發現場景來說完全沒必要保持強一致性,所以筆者選擇了自研一個具備最終一致性、增量數據返回,且去中心化架構的分布式Registry中間件corgi-proxy,當注冊中心負載較高時,完全可以通過水平擴容來提升其整體的讀/寫性能。除此之外,大家還可以考慮使用Dubbo生態中的Nacos中間件,截至本書成稿之時,Nacos的當前版本(0.0.9)已經具備了小規模運用于線上環境的條件。最后,關于數據中心的遷移方案,大家可以采用業界通用的雙注冊中心方案,盡可能降低對原有架構的影響。
1.2.7 分布式多活架構下的服務就近調用方案
在1.1.9小節中,筆者對分布式多活架構的演變進行了詳細的介紹,那么接下來請大家仔細思考下,當企業在實施多活建設后,服務之間是否應該繼續保持安常習故的調用方式?理論上,盡管多數據中心之間通過內網專線通道可以將網絡傳輸的延遲率控制在幾十ms內,但在一些特殊情況下,比如大促帶來的峰值流量極有可能瞬間就將延遲率放大數倍,導致大量的服務調用產生超時,因此企業在實施分布式多活架構后,服務調用應該優先在同機房內進行。筆者之前曾經提過,并非所有業務都能夠實現分布式多活,只能夠通過多種有效手段來保證絕大多數的核心業務實現多活架構,因此部分未實現多活的服務才需要進行跨機房調用。
實際上,這是一個優先級路由的問題,目前筆者所在企業采用的方案是擴展Dubbo的路由功能,加入路由分級策略實現服務的就近調用。簡單來說,當Provider注冊時,會將當前所在機房的標識信息一同寫進注冊中心;這樣一來,當Consumer進行調用時,便有跡可循,通過檢測機房的標識信息來判斷路由走向。
- Istio入門與實戰
- 電腦軟硬件維修大全(實例精華版)
- SDL Game Development
- Getting Started with Qt 5
- R Deep Learning Essentials
- 固態存儲:原理、架構與數據安全
- SiFive 經典RISC-V FE310微控制器原理與實踐
- 計算機組裝維修與外設配置(高等職業院校教改示范教材·計算機系列)
- 電腦高級維修及故障排除實戰
- 深入理解序列化與反序列化
- 基于Proteus仿真的51單片機應用
- Java Deep Learning Cookbook
- 計算機電路基礎(第2版)
- 計算機組裝、維護與維修項目教程
- The Machine Learning Workshop