蘇寧的RPC遠程服務調用框架RSF

蘇寧的RPC遠程服務調用框架RSF

 

  蘇寧的系統間交互最初使用中心化 ESB 架構,但隨着系統拆分工作的展開及業務量的迅速攀升,系統間調用規模越來越大,ESB 中心化架構帶來的諸如中心資源隔離、中心容量動態評估、問題排查難度、中心化擴展能力瓶頸等問題迅速顯現。並且,隨着自研系統逐步替換商用系統,需要進行協議轉換等工作逐步弱化,因此蘇寧亟待一個更輕量化的去中心化的跨系統服務調用方案。

蘇寧遠程服務框架(RSF)致力於解決系統間的服務調用問題,提供一種透明的、高性能的 RPC 服務調用方案。目前應用於蘇寧 1000+ 系統,每天的服務調用次數在 200 億左右,是蘇寧使用最廣泛的技術組件。

開源世界裏成熟的 RPC 比較多,簡單的如 spring remoting,應用廣泛,短短几行代碼及配置就可以實現跨系統方法調用,但是都只是止步於調通服務。對於一個由上千個系統協同交互構成的複雜電商交易平臺來說,只是達到跨系統能調通是遠遠不夠的,需要考慮的問題有很多,比如服務節點的動態註冊和發現,生產問題的快速干預,服務治理等等。而在不同的環境、背景下,也會有各自的需求和挑戰,這也正是我們選擇構建自己的 RPC 框架的核心原因。

本文將重點介紹 RSF 的重點特性及一些我們面臨的挑戰和相應的解決方案。

重點特性

1. 同步、異步 Future、異步 Callback 三種調用模型

同步調用:是最普遍使用的形式,使用同步調用,當前調用線程會阻塞等待服務調用返回結果或拋出異常。

異步 future:調用立刻返回,當前線程不會阻塞,不會等待服務提供方執行完成。服務提供方的返回結果可以後續通過 Future get 來獲得,這種調用形式在某些場景下會特別有用,能實現並行調用服務。Future get 會阻塞當前線程,直到服務方返回結果或有異常拋出。

異步 Callback:調用立刻返回,當前線程不會阻塞,不會等待服務提供方執行完成。當服務方返回結果或拋出異常時,異步執行 callback 中相應的方法。

使用 Future 及 Callback 異步調用在某些場景下非常有用,它能做到調用方的線程不會阻塞等待服務調用結果。結合一些其他的異步技術可以使得整個調用鏈條異步化。

2. 服務方異步返回調用結果

服務方異步返回調用結果的機制類似 Async Servlet,當前處理調用請求的線程不返回最終結果,而是在其他線程中異步返回結果給消費方,比如服務實現方在實現服務 A 時,需要調用依賴的其他外部服務 B,如果外部服務 B 通過 Http 協議開放服務,則可以通過支持異步的 Http Client 來調用外部服務 B,然後在異步回調中返回 A 的最終調用結果。如果 B 也通過 RSF 開放服務,可以通過異步 Callback 來調用 B,在 callback 中返回 A 的最終調用結果。

處理請求 A 的服務方線程自身不會阻塞等待 B 的調用結果,只是發起了一次異步 Http 或 RSF 調用就結束了。

通過這些異步手段,可以做到整個調用鏈條異步化,不會有線程阻塞(浪費)在等待服務調用結果上,從而能極大提高整體資源利用率。在線程技術還在主宰着 java 的今天,如何讓線程不阻塞、少阻塞是一件很重要的事。

3. 所有服務調用相關配置統一管理,修改後實時生效

比如服務調用的超時時間、重試次數、授權、負載均衡方式、流控、熔斷、成員權重、服務路由策略等等服務調用相關的所有配置,在 RSF 都不是寫死在應用側代碼或配置文件中的,都是在 RSF 服務管理平臺上統一管理的,並且支持修改立刻生效,這一點針對線上問題干預非常重要,可以想象一下,當一個服務出現服務質量等問題時,想修改一個調用相關的配置,還需要發佈應用是完全無法接受的。同時,這個能力還可以和監控體系有機結合起來,實現自動調控。

4. 重試及防重

當進行一次服務調用時,如果調用過程出現可重試的異常(如網絡異常,調用方資源不足),並且配置是允許重試的話,那麼將發起重試。

RSF 的重試和大部分的重試設計相比,稍微複雜。大部分的重試設計都是包含重試的幾次請求之間是不交叉的,比如第一次請求已經超時引發了第二次重試請求,在第二次請求過程中,第一次請求結果回來了。大部分的重試設計是忽略第一次請求結果的,因爲認爲第一次請求的生命週期已經結束了。在 RSF 中則是認爲第一次請求返回的結果是有效的,這個設計的目的是儘可能的促使調用成功,但是也引發了一些複雜的併發相關的問題需要處理,太過細節不再展開。

如果服務調用是冪等的,那麼不管調用多少次都不會影響系統的狀態,重試是安全的。但是,如果服務調用不是冪等的,那麼重試就需要考慮防重的問題,RSF 中包含一些擴展點可以由用戶來定義自己的防重邏輯,並且也自帶了一個基於 redis 的默認防重實現。

5. 服務節點的自動註冊和發現

Service discovery 是服務框架中最核心的部件,這個部件的目標很明確,就是服務節點上下線 (包括擴縮容、應用發佈、節點宕機等等場景) 引起服務方節點列表變更時,服務消費方能實時、準確的知道。怎麼達到則有各種設計,有基於中心化的如 Netflix/eureka,或者基於 ZooKeeper、etcd 的簡單一點的方案,也有去中心化的方案,這個部件對數據一致性要求並不高,並不追求數據強一致性,但是如何做到可靠非常關鍵,試想如果這個部件出問題,導致消費方錯誤的認爲服務方節點全部或者大部分都下線了,會引起什麼樣的後果,如果是中心化的設計,則會引發全局性的災難。

RSF 的服務節點發現採用的是中心化的設計,但是我們認爲去中心化的思路更優,因爲不存在中心化架構下的中心瓶頸,出問題也不會是全局性的災難,我們也曾基於 gossip 完整設計了一個方案,但是評估後認爲實現較爲複雜,重點要規避的風險是任何情況下都不會引發 gossip 風暴。

RSF 的服務發現會在本文後半部分稍微深入的展開探討。

6. 負載均衡

當消費方發起一次服務調用時,RSF 會基於隨機策略優先選擇當前負載低(Least Pending Requests)的服務提供方節點,選擇過程同時也會加入提供方節點權重因子。這種負載均衡方式能基於服務方節點的實時處理能力進行動態調整,能較好的規避短板效應。並且,負載均衡還會優先選擇當前和提供方的連接已就緒的(關於 RSF 的連接管理,本文後半部分會稍微深入展開探討),並且沒有被熔斷的服務方節點,這些策略目的都是爲了爲每次請求選擇最優的服務方節點。

7. 熔斷

RSF 的熔斷有兩種,一種是服務方法級的熔斷,當調用某服務方法出現較高異常比例時,會禁止訪問該服務方法一段時間,這段時間過去後,允許少量的請求,如果依然出現較高異常比例時,則繼續禁止訪問一段時間,否則放開訪問限制。合適的設置消費方服務方法熔斷,既可以保護服務提供方,避免其已經處於不健康狀態下時繼續給壓。也可以避免消費方應用大量線程因等待服務方結果返回被阻塞(在同步調用下),有效的保護服務消費方自身。從而避免事故級聯蔓延。但同時,一旦觸發服務方法熔斷,後果是嚴重的,會引起服務方法在一段時間內完全被禁止訪問,所以應根據服務自身情況合理的設置觸發條件閥值,不應該因爲瞬間的服務質量毛刺導致輕易被觸發。

RSF 另外一種熔斷是針對服務方節點的,當某個服務方節點出現超時、資源繁忙等等異常時,會快速被熔斷,負載均衡在選擇提供方節點時,會優先選擇沒有被熔斷的服務方節點,以提高調用成功率。

8. 併發流控

RSF 的併發流控有兩種,一種是服務提供方側的流控,一種是服務消費方側的流控。

服務提供方側的流控,是爲了避免服務請求的併發量超出其設計的承受能力,從而引起各種蔓延以至整個服務方最終被沖垮,應該合理的設置服務方法的併發閥值,超過併發閥值的請求會被快速拒絕,從而有效的保護服務方。 在服務提供方,RSF 維護一個線程池,該線程池負責服務調用的業務代碼執行。線程池有一個任務排隊隊列,一旦排隊隊列滿了,請求將被拒絕。線程池的線程數和排隊隊列長度都可以在服務配置平臺中設置。

爲服務設置併發閥值,是將有限寶貴的線程池線程及排隊數資源分配給服務。

併發閥值可以分組來設置,也可以爲某一個服務方法單獨設置。如果使用分組的方式進行設置,那麼分組下的所有接口方法將共享一個計數器和閥值。

服務消費方側的流控,RSF 可以針對某一個服務方法設置某一個服務消費方應用的併發流控閥值。當調用開始時併發計數 +1,當調用結束(調用返回或拋出異常都認爲是結束)時併發計數 -1。當計數累計超過指定閥值,則拋出超出併發閥值相關的異常。

合適的設置消費方流控,既可以保護服務提供方,也可以避免消費方大量線程因等待服務方結果返回被阻塞(在同步調用下),有效的保護服務消費方自身。

9. 服務路由

RSF 服務路由是根據調用請求參數列表,調用方機房信息,服務方節點機房部署拓撲等信息,將請求路由到正確的目標服務方節點的過程,比如會員系統可能在多個機房部署相同的服務,並基於會員編號特定的規則將會員數據分佈到不同的機房,那麼在調用獲取會員信息服務時,RSF 需要根據調用參數中的會員編號以及會員服務的機房部署拓撲信息,將請求路由到正確的機房中的服務方節點。

RSF 的服務路由在蘇寧多機房多活項目中發揮至關重要的作用,當前支持優先同機房、會員編號分片、主機房、自定義腳本等多種路由策略。

10. 服務治理及監控

RSF 提供全量服務調用統計信息,以幫助服務提供方進行服務質量的持續優化,服務調用的統計信息包括調用次數、失敗率、響應時間(平均 /TP90/TP99/TP999)等核心指標,服務相關方可以針對這些核心指標設置安全閥值進行告警。

RSF 還提供了端到端的完整 trace 能力,可以清晰的看到某一次調用的各個時間點明細,如調用方什麼時候發起的請求,請求什麼時候寫到 socket,請求什麼時間點到達服務方節點,在服務方線程池中排隊等待了多長時間,什麼時候開始執行業務代碼,業務代碼執行了多長時間等等。 這些能力對迅速感知及定位線上問題至關重要。

11. 與非 JAVA 系統的交互

蘇寧大部分的系統基於 JAVA,但也存在一些老的系統如 SAP 或一些異構系統如使用 PHP/NODEJS 等,這些系統目前和 RSF 的交互使用 RSF 提供的 SAP Adapter 和 HTTP Adapter 來達到。

一些挑戰

下面稍微深入的展開探討下我們經歷過的一些挑戰和解決方案。

1. 服務節點註冊和發現中心的擴展能力及穩定性

RSF 早期版本的服務節點註冊和發現模塊基於 ZooKeeper,思路也很簡單,就是服務方節點上線的時候向 ZK 寫入一個臨時節點,當服務方節點主動下線時刪除這個臨時節點,當服務方節點宕機或其他異常狀況時,依賴 ZK 的 session timeout 機制由 ZK server 自動剔除這個臨時節點,當發生 session expire 時恢復這個臨時節點。當服務方節點列表發生變更時,通過 ZK 的 watch 機制,將最新的服務節點列表下發給訂閱的服務消費方節點。這種方案實現簡單,但是當 ZK 集羣出現故障時,大量服務方節點發生 session timeout,引起大量服務方臨時節點下線。如果消費方完全信任 ZK 下發的服務方節點列表就會引發服務不可用的災難。

另外,ZK 集羣因爲數據一致性設計的考量,所有的寫操作都要經過 leader,包括 session create,session expire,臨時節點寫入等等都要經過 leader,因此 ZK 集羣的寫能力是存在單機瓶頸的,就是不管 ZK 集羣怎麼擴容,寫能力就那麼多,並且隨着加入的 follower 節點越多,寫能力越差(其實加入 observer 越多,對寫能力也會有影響,畢竟 leader 也要把數據同步給 observer)。

在服務節點註冊和發現這個場景下,服務數量 * 每個服務的節點數,這是一個非常巨大的數字,試想當 ZK 集羣在最壞情況下要集中處理這麼多臨時節點數據的寫入,還有大量的 session 恢復涉及的數據寫入,會造成 leader 處理嚴重延遲,由此導致的更壞的情況是一個 session 剛恢復了,又因爲後續寫操作或心跳處理超時導致又 expire,然後又去恢復這種局面,恢復的時間難以估計。

圖 1

RSF 目前採用的方案如圖 1,服務方節點註冊和續約是通過 Redis 來達到的,當服務方節點啓動時向 Redis 寫入該節點提供的服務列表信息,然後定時發 expire 指令來續約這份信息,當服務方節點主動下線時,從 Redis 刪除該數據。當服務方節點宕機或其他異常狀況時,依賴 Redis 的 expire 機制來自動刪除這份數據。

pump 訂閱所有 redis 的 key space,當 redis 的 key space 發生變化時都會通知到 pump,pump 聚合所有的 Redis 數據,將提供方節點 - 服務列表信息的數據轉化爲服務 - 提供方節點列表的數據結構,寫進 ZK。消費方獲取及更新服務的服務方節點列表還是通過 ZK 來達到。

在這種設計下,以 Redis 的處理能力,少量的幾臺 Redis 就可以處理幾十萬的服務節點註冊和續約。ZK 方面,只有 pump 節點向 ZK 寫入數據,寫入的頻率是 pump 側控制的(不需要每次 redis 數據變動都會寫一次 ZK,可以做秒級延遲合併處理),並且數據經過壓縮,因此,這部分數據的寫入對 ZK 基本沒有寫壓力。

實際測試下來,這種設計經過橫向擴展後,可以輕鬆的處理幾萬的服務 * 幾十萬的服務節點的規模,能滿足我們未來一段時間的需求。

另外,還有一個值得一提的問題就是如何解決大量消費方的 session 相關操作對 ZK 的壓力(雖然沒有了臨時節點,但是 session create,expire 等還是會都經過 leader),在 ZK 3.5.X 版本中有新的 local session 的概念,session 的生命週期都在 follower 或 subscriber 各自節點本地處理,不會再跟 leader 進行交互。具體細節這裏不再展開。

另外,因爲存在中心化的設計,所以還是要考慮災難應對的問題,在 RSF 組件側我們也提供了災難應對的能力,即使註冊中心出現問題,也能快速在組件側進行自動修正。

2. 連接數控制的問題

大部分基於 TCP 的 RPC 框架,都是消費方和所有的服務方節點建立長連接,並且通過心跳包機制來保活或檢查連接健康情況,針對某些會消費很多服務的應用,或者某些有很多消費方的服務來說,連接數會是一個問題,即使當期沒問題,至少存在擴展性方面的問題。而且,這些同時建立的長連接,大部分時間裏都是空閒的,存在資源的浪費。

RSF 採用的是和部分服務方建連的方案,就是消費方選擇和提供方列表中一部分節點建立連接,在這個思路下,還需要進一步考慮的問題是:

採用部分建連的策略,需要考慮連接不均衡的問題。當服務方節點列表發生少量變化時(比如服務方節點宕機),不應導致大面積的連接轉移。需要考慮服務之間的連接儘量重用等問題。在有業務路由的場景下,無法預先部分建連,也就是請求的時候,才知道目標提供方,所以沒辦法預建連。

RSF 最終使用的策略是:

和部分服務方節點按需建連,並且當連接超過設定的業務空閒期,就關閉連接。動態調整連接,以儘量做到連接均衡。算法和邏輯保證當消費方或服務方節點部分增減時不引起大面積連接轉移。當不同服務的服務方列表有重合時,保證連接重用。

結語

服務調用框架作爲最廣泛使用的基礎技術組件,除了一些基本的共通的能力,每個企業在不同發展階段,都會或多或少的有自己的特定問題,你遇到的問題換個上下文背景可能問題根本不存在,對應的解決方案也是如此,只有在特定的上下文背景下最合適的方案。

蘇寧公共平臺組件研發中心所轄產品有:服務調用框架,蘇寧 ESB,蘇寧消息中間件 WindQ,任務調度平臺,配置管理平臺、日誌採集平臺、短信平臺、驗證碼平臺、分佈式事務等核心基礎平臺和組件。我們會陸續進行系列純技術乾貨分享,開放、探討、共進,共同尋求突破~ 歡迎大家多多關注、共同交流!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章