SOFA
javaScalable Open Financial Architecturegit
是螞蟻金服自主研發的金融級分佈式中間件,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。github
本文爲《螞蟻金服通訊框架 SOFABolt 解析》系列第五篇,做者胡蘿蔔、丞一。網絡
《螞蟻金服通訊框架 SOFABolt 解析》系列由 SOFA 團隊和源碼愛好者們出品。數據結構
請 star 咱們吧:架構
SOFARPC: https://github.com/alipay/sofa-rpc併發
SOFABolt: https://github.com/alipay/sofa-bolt框架
SOFABolt是一個基於 Netty 最佳實踐的輕量、易用、高性能、易擴展的通訊框架。目前已經運用在了螞蟻中間件的微服務,消息中心,分佈式事務,分佈式開關,配置中心等衆多產品上。異步
本文將分析SOFABolt的超時控制和心跳機制。tcp
在程序中,超時通常指的是程序在特定的等待時間內沒有獲得響應,網絡通訊問題、程序BUG等等都會引發超時。系統引入超時機制每每是爲了解決資源的問題,好比一個同步RPC請求,在網絡不穩定的狀況下可能一直沒法獲得響應,那麼請求線程將一直等待結果而沒法執行其它任務,最終致使全部線程資源耗盡。超時機制正是爲了解決這樣的問題,在特定的等待時間以後觸發一個「超時事件」來釋放資源。
在一個網絡通訊框架中,超時問題無處不在,鏈接的創建、數據的讀寫均可能遇到超時問題。而且網絡通訊框架做爲分佈式系統的底層組件,須要管理大量的鏈接,如何創建一個高效的超時處理機制就成爲了一個問題。
在網絡通訊框架中動輒管理上萬的鏈接,每一個鏈接上都有不少的超時任務,若是每一個超時任務都啓動一個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中支持四中調用方式:
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 管理全部的超時任務便可,這樣能夠節省系統的開銷。
以上關於 SOFABolt 的超時機制介紹都是關於 SOFABolt 客戶端如何完成高效的超時任務管理的,其實在 SOFABolt 的服務端一樣針對超時的場景作了優化。
客戶端爲了應對沒有響應的狀況,增長了超時機制,那麼就可能存在服務端返回一個響應可是客戶端在收到這個響應以前已經認爲請求超時了,移除了相關的請求上下文,那麼這個響應對客戶端來講就沒有意義了。既然這個響應對客戶端來講是沒有意義的,那麼服務端其實能夠進一步優化:在確認請求已經超時的狀況下,服務端能夠直接丟棄請求來減輕服務端的處理負擔,SOFABolt 把這個機制稱爲 Fail-Fast。
如上圖所示,請求可能在服務端積壓了一段時間,此時這些請求在客戶端看來已經超時了,若是服務端繼續處理這些超時的請求,第一請求的響應最終會被客戶端丟棄;第二可能加重服務端的壓力致使後續更多請求超時。經過 Fail-Fast 機制直接丟棄掉這批請求能減輕服務端的負擔使服務端儘快恢復並提供正常的服務能力。
Fail-Fast 機制是一個明顯的優化手段,惟一面臨的問題是如何肯定一個請求已經超時。注意,必定不要依賴跨系統的時鐘,由於時鐘可能不一致,從而致使未超時的請求被誤認爲超時而被服務端丟棄。
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 的高性能打下了堅實的基礎。
長按關注,獲取分佈式架構乾貨
歡迎你們共同打造 SOFAStack https://github.com/alipay