SOFA
Scalable Open Financial Architecture
是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘鍊出來的最佳實踐。
本文為《螞蟻金服通訊框架 SOFABolt 解析》系列第五篇,作者胡蘿蔔、丞一。
《螞蟻金服通訊框架 SOFABolt 解析》系列由 SOFA 團隊和原始碼愛好者們出品。
請 star 我們吧:
SOFARPC: https://github.com/alipay/sofa-rpc
SOFABolt: https://github.com/alipay/sofa-bolt
前言
SOFABolt是一個基於 Netty 最佳實踐的輕量、易用、高效能、易擴充套件的通訊框架。目前已經運用在了螞蟻中介軟體的微服務,訊息中心,分散式事務,分散式開關,配置中心等眾多產品上。
本文將分析SOFABolt的超時控制和心跳機制。
超時
在程式中,超時一般指的是程式在特定的等待時間內沒有得到響應,網路通訊問題、程式BUG等等都會引起超時。系統引入超時機制往往是為了解決資源的問題,比如一個同步RPC請求,在網路不穩定的情況下可能一直無法得到響應,那麼請求執行緒將一直等待結果而無法執行其它任務,最終導致所有執行緒資源耗盡。超時機制正是為了解決這樣的問題,在特定的等待時間之後觸發一個“超時事件”來釋放資源。
在一個網路通訊框架中,超時問題無處不在,連線的建立、資料的讀寫都可能遇到超時問題。並且網路通訊框架作為分散式系統的底層元件,需要管理大量的連線,如何建立一個高效的超時處理機制就成為了一個問題。
時間輪(TimeWheel)
在網路通訊框架中動輒管理上萬的連線,每個連線上都有很多的超時任務,如果每個超時任務都啟動一個java.util.Timer,不僅低效而且會佔用大量的資源。George Varghese 和 Tony Lauck在1996年發表了一篇論文:《Hashed and Hierarchical Timing Wheels: EfficientData Structures for Implementing a Timer Facility》來高效的管理和維護大量的定時任務。
時間輪其實就是一種環形的資料結構,可以理解為時鐘,每個格子代表一段時間,每次指標跳動一格就表示一段時間的流逝(就像時鐘分為60格,秒針沒跳動一格代表一秒鐘)。時間輪每一格上都是一個連結串列,表示對應時間對應的超時任務,每次指標跳動到對應的格子上則執行連結串列中的超時任務。時間輪只需要一個執行緒執行指標的“跳動”來觸發超時任務,且超時任務的插入和取消都是O(1)的操作,顯然比java.util.Timer的方式要高效的多。
SOFABolt的超時控制機制
如上圖所示,SOFABolt中支援四中呼叫方式:
oneway:不關心呼叫結果,所以不需要等待響應,那麼就沒有超時
sync:同步呼叫,在呼叫執行緒中等待響應
future:非同步呼叫,返回future,由使用者從future中獲取結果
callback:非同步呼叫,非同步執行使用者的callback
在oneway呼叫中,因為並不關心響應結果,所以沒有超時的概念。下面具體介紹SOFABolt中同步呼叫(sync)和非同步呼叫(future\callback)的超時機制實現。
同步呼叫的超時控制實現
同步呼叫中,每一次呼叫都會阻塞呼叫執行緒等待服務端的響應,這種場景下同一時刻產生最大的超時任務取決於呼叫執行緒的數量。執行緒資源是非常昂貴的,使用者的執行緒數是相對可控的,所以這種場景下,SOFABolt使用簡單的java.util.concurrent.CountDownLatch來實現超時任務的觸發。
SOFABolt同步呼叫的程式碼如上,核心邏輯是:
建立 InvokeFuture
在 Netty 的 ChannelFuture 中新增 Listener,在寫入操作失敗的情況下通過 future.putResponse方法修改Future狀態(正常服務端響應也是通過 future.putResponse來改變InvokeFuture的狀態的,這個流程不展開說明)
寫入出現異常的情況下也是通過future.putResponse方法修改Future狀態
通過future.waitResponse來執行等待響應
其中和超時相關的是future.waitResponse的呼叫,InvokeFuture內部通過java.util.concurrent.CountDownLatch來實現超時觸發。
java.util.concurrent.CountDownLatch#await(timeout, timeoutUnit) 方法實現了等待一段時間的邏輯,並且通過countDown方法來提前中斷等待,SOFABolt 中 InvokeFuture 通過構建 new CountDownLatch(1)的例項,並將 await 和 countDown 方法包裝為 awaitResponse 和 putResponse 來實現同步呼叫的超時控制。
非同步呼叫的超時控制實現
相對於同步呼叫,非同步呼叫並不會阻塞呼叫執行緒,那麼超時任務的數量並不受限於執行緒對的數量,使用者可能通過一個執行緒來觸發量大的請求,從而產生大量的定時任務。那麼我們需要一個機制來管理大量的定時任務,並且作為系統底層的通訊框架,需要保證這個機制儘量少的佔用資源。上文已經提到 TimeWheel 是一個非常適合於這種場景的資料結構。
Netty 中實現了 TimeWheel 資料結構:io.netty.util.HashedWheelTimer,SOFABolt 非同步呼叫的超時控制直接依賴於 Netty 的 io.netty.util.HashedWheelTimer 實現。
Future 模式和 Callback 模式在超時控制機制上一致的,下面以 Callback為 例分析非同步呼叫的超時控制機制。
SOFABolt 非同步呼叫的程式碼如上,核心邏輯是:
建立 InvokeFuture
建立 Timeout 例項,Timeout 例項的 run 方法中通過 future.putResponse 來修改 InvokeFuture 的狀態
在 Netty 的 ChannelFuture 中新增 Listener,在寫入操作失敗的情況下通過 future.cancelTimeout 來取消超時任務,通過 future.putResponse 來修改 InvokeFuture的狀態
在寫入異常的情況下同樣通過 future.cancelTimeout 來取消超時任務,通過 future.putResponse 來修改 InvokeFuture 的狀態
在非同步呼叫的實現中,通過 Timeout 來觸發超時任務,相當於同步呼叫中的java.util.concurrent.CountDownLatch#await(timeout, timeoutUnit)。Future#cancelTimeout()方法則是呼叫了 Timeout 的 cancel 來取消超時任務,相當於同步呼叫中通過java.util.concurrent.CountDownLatch#countDown()來提前結束超時任務。具體超時任務的管理則全部委託給了 Netty 的 Timer 實現。
另外值得注意的一點是 SOFABolt 在使用 Netty 的 Timer 時採用了單例的模式,因為一般情況下使用一個 Timer 管理所有的超時任務即可,這樣可以節省系統的開銷。
Fail-Fast機制
以上關於 SOFABolt 的超時機制介紹都是關於 SOFABolt 客戶端如何完成高效的超時任務管理的,其實在 SOFABolt 的服務端同樣針對超時的場景做了優化。
客戶端為了應對沒有響應的情況,增加了超時機制,那麼就可能存在服務端返回一個響應但是客戶端在收到這個響應之前已經認為請求超時了,移除了相關的請求上下文,那麼這個響應對客戶端來說就沒有意義了。既然這個響應對客戶端來說是沒有意義的,那麼服務端其實可以進一步優化:在確認請求已經超時的情況下,服務端可以直接丟棄請求來減輕服務端的處理負擔,SOFABolt 把這個機制稱為 Fail-Fast。
如上圖所示,請求可能在服務端積壓了一段時間,此時這些請求在客戶端看來已經超時了,如果服務端繼續處理這些超時的請求,第一請求的響應最終會被客戶端丟棄;第二可能加劇服務端的壓力導致後續更多請求超時。通過 Fail-Fast 機制直接丟棄掉這批請求能減輕服務端的負擔使服務端儘快恢復並提供正常的服務能力。
Fail-Fast 機制是一個明顯的優化手段,唯一面臨的問題是如何確定一個請求已經超時。注意,一定不要依賴跨系統的時鐘,因為時鐘可能不一致,從而導致未超時的請求被誤認為超時而被服務端丟棄。
SOFABolt 採用了請求被處理時的時間和請求到達服務端的時間來判定請求是否已經超時,如下圖所示:
這樣會有一小部分客戶端認為已經超時的請求服務端還會處理(因為網路傳輸是需要時間的),但是不會出現誤判的情況。
SOFABolt 的心跳機制
除了上文提供的超時機制外,在通訊框架中往往還有另一類超時,那就是連線的超時。
我們知道,一次 tcp 請求大致分為三個步驟:建立連線、通訊、關閉連線。每次建立新連線都會經歷三次握手,中間包含三次網路傳輸,對於高併發的系統,這是一筆不小的負擔。所以在通訊框架中我們都會維護一定數量的連線,其中一個手段就是通過心跳來維持連線,避免連線因為空閒而被回收。
Netty 提供了 IdleStateHandler,如果連線空閒時間過長,則會觸發 IdleStateEvent。SOFABolt 基於 IdleStateHandler 的 IdleStateEvent 來觸發心跳,一來這樣可以通過心跳維護連線,二來基於 IdleStateEvent 可以減少不必要的心跳。
SOFABolt 心跳相關的處理有兩部分:客戶端傳送心跳,服務端接收心跳處理並返回響應。
上面是客戶端觸發心跳後的程式碼,當客戶端接收到 IdleStateEvent 時會呼叫上面的heartbeatTriggered 方法。
在 Connection 物件上會維護心跳失敗的次數,當心跳失敗的次數超過系統的最大次時,主動關閉 Connection。如果心跳成功則清除心跳失敗的計數。同樣的,在心跳的超時處理中同樣使用 Netty 的 Timer 實現來管理超時任務(和請求的超時管理使用的是同一個 Timer 例項)。
RpcHeartbeatProcessor 是 SOFABolt 對心跳處理的實現,包含對心跳請求的處理和心跳響應的處理(服務端和客戶端複用這個類,通過請求的資料型別來判斷是心跳請求還是心跳響應)。
如果接收到的是一個心跳請求,則直接寫回一個 HeartbeatAckCommand(心跳響應)。如果接收到的是來自服務端的心跳響應,則從 Connection 取出 InvokeFuture物件並做對應的狀態變更和其他邏輯的處理:取消超時任務、執行Callback。如果無法從 Connection 獲取 InvokeFuture 物件,則說明客戶端已經判定心跳請求超時。
另外值得注意的一點是,SOFABolt 中心跳請求和心跳響應物件都只包含 RequestCommand 和 ResponseCommand 的必要欄位,沒有額外增加任何屬性,這也是為了減少不必要的網路頻寬的開銷。
總結
本文簡單的介紹了 TimeWheel 的原理,SOFABolt 的超時控制機制和心跳機制的實現。SOFABolt 基於高效的 TimeWheel 實現了自己的超時控制機制,同時增加 Fail-Fast 策略優化服務端對超時請求的處理。另外 SOFABolt 預設實現了連線的心跳機制,以保持系統空閒時連線的可用性,這些都為 SOFABolt 的高效能打下了堅實的基礎。
《螞蟻金服通訊框架SOFABolt解析》系列歷史文章
長按關注,獲取分散式架構乾貨
歡迎大家共同打造 SOFAStack https://github.com/alipay