前言
在 RPC 調用過程中,我們經常會和多個服務端進行遠程調用,如果在每次調用的時候,都進行 TCP連接,會對 RPC的性能有比較大的影響,因此,實際的場景中,我們經常要對連接進行管理和保持。
SOFARPC應用心跳包以及斷線重連實現,結合系統tcp-keepalive機制,來實現對RPC連接的管理和保持。
連接管理
首先我們將會介紹連接管理相關的一些背景知識。
長連接和短連接
短連接,一般是指客戶端向服務端發起連接請求。連接建立後,發送數據,接收服務端數據返回,然後觸發連接斷開,下次再重新重複以上過程。
長連接,則是在建立連接後,發送數據,接收數據,但是不主動斷開,並且主動通過心跳等機制來維持這個連接可用,當再次有數據發送請求時,不需要進行建立連接的過程。
一般的,長連接多用於數據發送頻繁,點對點的通訊,因爲每個TCP連接都需要進行握手,這是需要時間的,在一些跨城,或者長距離的情況下,如果每個操作都是先連接,再發送數據的話,那麼業務處理速度會降低很多,所以每個操作完後都不斷開,再次處理時直接發送數據包即可,節省再次建立連接的過程。
但是,客戶端不主動斷開,並不是說連接就不會斷。因爲系統設置原因,網絡原因,網絡設備防火牆,都可能導致連接斷開。因此我們需要實現對長連接的管理。
TCP層keep-alive
tcp的keep-alive是什麼
tcp-keepalive,顧名思義,它可以儘量讓 TCP 連接“活着”,或者讓一些對方無響應的 TCP 連接斷開,
使用場景主要是:
- 一些特定環境,比如兩個機器之間有防火牆,防火牆能維持的連接有限,可能會自動斷開長期無活動的 TCP 連接。
- 還有就是客戶端,斷電重啓,卡死等等,都會導致tcp連接無法釋放。
這會導致:
一旦有熱數據需要傳遞,若此時連接已經被中介設備斷開,應用程序沒有及時感知的話,那麼就會導致在一個無效的數據鏈路層面發送業務數據,結果就是發送失敗。
無論是因爲客戶端意外斷電、死機、崩潰、重啓,還是中間路由網絡無故斷開、NAT超時等,服務器端要做到快速感知失敗,減少無效鏈接操作。
而 tcp-keepalive 機制可以在連接無活動一段時間後,發送一個空 ack,使 TCP 連接不會被防火牆關閉。
默認值
tcp-keepalive,操作系統內核支持,但是不默認開啓,應用需要自行開啓,開啓之後有三個參數會生效,來決定一個keepalive的行爲。
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75
可以通過如下命令查看系統tcp-keepalive參數配置。
sysctl -a | grep keepalive
cat /proc/sys/net/ipv4/tcp_keepalive_time
sysctl net.ipv4.tcp_keepalive_time
系統默認值可以通過這個查看。
tcp_keepalive_time,在TCP保活打開的情況下,最後一次數據交換到TCP發送第一個保活探測包的間隔,即允許的持續空閒時長,或者說每次正常發送心跳的週期,默認值爲7200s(2h)。
tcp_keepalive_probes 在tcp_keepalive_time之後,沒有接收到對方確認,繼續發送保活探測包次數,默認值爲9(次)。
tcp_keepalive_intvl,在tcp_keepalive_time之後,沒有接收到對方確認,繼續發送保活探測包的發送頻率,默認值爲75s。
這個不夠直觀,直接看下面這個圖的說明
<div id="2y3gkp" data-type="image" data-display="block" data-align="" data-src="https://cdn.nlark.com/yuque/0/2018/png/156121/1535513238795-57f765f2-908b-4689-b0a7-b6aa3a3599f7.png" data-width="747">
<img src="https://cdn.nlark.com/yuque/0/2018/png/156121/1535513238795-57f765f2-908b-4689-b0a7-b6aa3a3599f7.png" width="747" />
</div>
如何使用
應用層,以Java的Netty爲例,服務端和客戶端設置即可。
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new EchoServerHandler());
}
});
// Start the server.
ChannelFuture f = b.bind(port).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
就是這裏面的ChannelOption.SO_KEEPALIVE, true
對應即可打開.
目前bolt中也是默認打開的.
.childOption(ChannelOption.SO_KEEPALIVE,
Boolean.parseBoolean(System.getProperty(Configs.TCP_SO_KEEPALIVE, "true")));
Java程序只能做到設置SO_KEEPALIVE選項,至於TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等參數配置,只能依賴於sysctl配置,系統進行讀取。
檢查
查看tcp連接tcp_keepalive狀態
我們可以用 `netstat -no|grep keepalive` 命令來查看當前哪些 tcp 連接開啓了 tcp keepalive.
<div id="wiwvxz" data-type="image" data-display="block" data-align="left" data-src="https://cdn.nlark.com/yuque/0/2018/png/156121/1534919046315-385e6336-a8c7-43af-af63-0aa7a29405c9.png" data-width="747">
<img src="https://cdn.nlark.com/yuque/0/2018/png/156121/1534919046315-385e6336-a8c7-43af-af63-0aa7a29405c9.png" width="747" />
</div>
應用層keep-alive
應用層keep-alive方案,一般叫做心跳包,跟tcp-keepalive類似,心跳包就是用來及時監測是否斷線的一種
機制,通過每間隔一定時間發送心跳數據,來檢測對方是否連接,是屬於應用程序協議的一部分。
心跳是什麼
心跳想要實現的和tcp keep-alive是一樣的。
由於連接丟失時,TCP不會立即通知應用程序。比如說,客戶端程序斷線了,服務端的TCP連接不會檢測到斷線,而是一直處於連接狀態。這就帶來了很大的麻煩,明明客戶端已經斷了,服務端還維護着客戶端的連接,比如遊戲的場景下,用戶客戶端都關機了,但是連接沒有正常關閉,服務端無法知曉,還照常執行着該玩家的遊戲邏輯。
聽上去和tcp-alive類似,那爲什麼要有應用層心跳?
原因主要是默認的tcp keep-alive超時時間太長默認是7200秒,也就是2個小時。並且是系統級別,一旦更改,影響所有服務器上開啓keep alive選項的應用行爲。另外,socks proxy會讓tcp keep-alive失效,
socks協議只管轉發TCP層具體的數據包,而不會轉發TCP協議內的實現細節的包(也做不到)。
所以,一個應用如果使用了socks代理,那麼tcp keep-alive機制就失效了,所以應用要自己有心跳包。
socks proxy只是一個例子,真實的網絡很複雜,可能會有各種原因讓tcp keep-alive失效。
如何使用
基於netty開發的話,還是很簡單的。這裏不多做介紹,因爲後面說到rpc中的連接管理的時候,會介紹。
應用層心跳還是Keep-Alive
默認情況下使用keepalive週期爲2個小時,
系統keep-alive優勢:
1.TCP協議層面保活探測機制,系統內核完全替上層應用自動給做好了。
2.內核層面計時器相比上層應用,更爲高效。
3.上層應用只需要處理數據收發、連接異常通知即可。
4.數據包將更爲緊湊。
應用keep-alive優勢:
關閉TCP的keepalive,完全使用業務層面心跳保活機制。完全應用掌管心跳,靈活和可控,比如每一個連接心跳週期的可根據需要減少或延長。
1.應用的心跳包,具有更大的靈活性,可以自己控制檢測的間隔,檢測的方式等等。
2.心跳包同時適用於TCP和UDP,在切換TCP和UDP時,上層的心跳包功能都適用。
3.有些情況下,心跳包可以附帶一些其他信息,定時在服務端和客戶端之間同步。(比如幀數同步)
所以大多數情況採用業務心跳+TCP keepalive一起使用,互相作爲補充。
SOFARPC如何實現
SOFABOLT基於系統tcp-keepalive機制實現
這個比較簡單,直接打開 KeepAlive選項即可。
客戶端
RpcConnectionFactory用於創建RPC連接,生成用戶觸發事件,init()方法初始化Bootstrap通過option()方法給每條連接設置TCP底層相關的屬性,ChannelOption.SO_KEEPALIVE表示是否開啓TCP底層心跳機制,默認打開SO_KEEPALIVE選項。
/**
* Rpc connection factory, create rpc connections. And generate user triggered event.
*/
public class RpcConnectionFactory implements ConnectionFactory {
public void init(final ConnectionEventHandler connectionEventHandler) {
bootstrap = new Bootstrap();
bootstrap.group(workerGroup).channel(NioSocketChannel.class)
...
.option(ChannelOption.SO_KEEPALIVE, SystemProperties.tcp_so_keepalive());
...
}
}
服務端
RpcServer服務端啓動類ServerBootstrap初始化通過option()方法給每條連接設置TCP底層相關的屬性,默認設置ChannelOption.SO_KEEPALIVE選項爲true,即表示RPC連接開啓TCP底層心跳機制。
/**
* Server for Rpc.
*/
public class RpcServer extends RemotingServer {
protected void doInit() {
this.bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
...
.childOption(ChannelOption.SO_KEEPALIVE, SystemProperties.tcp_so_keepalive());
...
}
}
SOFABOLT基於Netty IdleStateHandler心跳實現
SOFABOLT基於 Netty的事件來實現心跳,Netty的 IdleStateHandler當連接的空閒時間(讀或者寫)太長時,將會觸發一個IdleStateEvent事件,然後BOLT通過重寫userEventTrigged方法來處理該事件。如果連接超過指定時間沒有接收或者發送任何的數據即連接的空閒時間太長,IdleStateHandler使用IdleStateEvent事件調用fireUserEventTriggered()方法,當檢測到IdleStateEvent事件執行發送心跳消息等業務邏輯。
<div id="ggbngd" data-type="image" data-display="block" data-align="" data-src="https://cdn.nlark.com/yuque/0/2018/png/156121/1534923163448-5354810e-a2a6-40c5-ad57-8e1557517752.png" data-width="747">
<img src="https://cdn.nlark.com/yuque/0/2018/png/156121/1534923163448-5354810e-a2a6-40c5-ad57-8e1557517752.png" width="747" />
</div>
簡而言之,向 Netty中註冊一個處理 Idle事件的監聽器。同時註冊的時候,會傳入 idle產生的事件,比如讀IDLE還是寫 IDLE,還是都有,多久沒有讀寫則認爲是 IDLE等。
客戶端
final boolean idleSwitch = SystemProperties.tcp_idle_switch();
final int idleTime = SystemProperties.tcp_idle();
final RpcHandler rpcHandler = new RpcHandler(userProcessors);
final HeartbeatHandler heartbeatHandler = new HeartbeatHandler();
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
...
if (idleSwitch) {
pipeline.addLast("idleStateHandler", new IdleStateHandler(idleTime, idleTime,
0, TimeUnit.MILLISECONDS));
pipeline.addLast("heartbeatHandler", heartbeatHandler);
}
...
}
});
SOFABOLT心跳檢測客戶端默認基於IdleStateHandler(15000ms, 150000 ms, 0)即15秒沒有讀或者寫操作,註冊給了 Netty,之後調用HeartbeatHandler的userEventTriggered()方法觸發RpcHeartbeatTrigger發送心跳消息。RpcHeartbeatTrigger心跳檢測判斷成功標準爲是否接收到服務端回覆成功響應,如果心跳失敗次數超過最大心跳次數(默認爲3)則關閉連接。
/**
* Heart beat triggerd.
*/
@Sharable
public class HeartbeatHandler extends ChannelDuplexHandler {
@Override
public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
ProtocolCode protocolCode = ctx.channel().attr(Connection.PROTOCOL).get();
Protocol protocol = ProtocolManager.getProtocol(protocolCode);
protocol.getHeartbeatTrigger().heartbeatTriggered(ctx);
} else {
super.userEventTriggered(ctx, evt);
}
}
}
服務端
SOFABOLT心跳檢測服務端默認基於IdleStateHandler(0,0, 90000 ms)即90秒沒有讀或者寫操作爲空閒,調用ServerIdleHandler的userEventTriggered()方法觸發關閉連接。
SOFABOLT心跳檢測由客戶端在沒有對TCP有讀或者寫操作後觸發定時發送心跳消息,服務端接收到提供響應;如果客戶端持續沒有發送心跳無法滿足保活目的則服務端在90秒後觸發關閉連接操作。正常情況由於默認客戶端15秒/服務端90秒進行心跳檢測,因此一般場景服務端不會運行到90秒仍舊沒有任何讀寫操作的,並且只有當客戶端下線或者拋異常的時候等待90秒過後服務端主動關閉與客戶端的連接。如果是tcp-keepalive需要等到90秒之後,在此期間則爲讀寫異常。
this.bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel channel) throws Exception {
...
if (idleSwitch) {
pipeline.addLast("idleStateHandler", new IdleStateHandler(0, 0, idleTime,
TimeUnit.MILLISECONDS));
pipeline.addLast("serverIdleHandler", serverIdleHandler);
}
...
createConnection(channel);
}
...
});
服務端一旦產生 IDLE,那麼說明服務端已經6個15s沒有發送或者接收到數據了。這時候認爲客戶端已經不可用。直接斷開連接。
/**
* Server Idle handler.
*
* In the server side, the connection will be closed if it is idle for a certain period of time.
*/
@Sharable
public class ServerIdleHandler extends ChannelDuplexHandler {
private static final Logger logger = BoltLoggerFactory.getLogger("CommonDefault");
@Override
public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
try {
ctx.close();
} catch (Exception e) {
...
}
} else {
super.userEventTriggered(ctx, evt);
}
}
}
SOFARPC連接管理斷開重連實現
通常RPC調用過程是不需要斷鏈與重連的。因爲每次RPC調用過程都校驗是否有可用連接,如果沒有則新建連接。但有一些場景是需要斷鏈和保持長連接的:
- 自動斷連:比如通過LVS VIP或者F5建立多個連接的場景,因爲網絡設備的負載均衡機制,有可能某一些連接固定映射到了某幾臺後端的RS上面,此時需要自動斷連然後重連,靠建連過程的隨機性來實現最終負載均衡。注意開啓自動斷連的場景通常需要配合重連使用。
- 重連:比如客戶端發起建連後由服務端通過雙工通信發起請求到客戶端,此時如果沒有重連機制則無法實現。
連接管理是客戶端的邏輯,啓動好,連接管理開啓異步線程。
<div id="pph1mp" data-type="image" data-display="block" data-align="" data-src="https://cdn.nlark.com/yuque/0/2018/png/156121/1534926707654-5f4c6fc0-cd94-403a-8f99-bc6c164babfd.png" data-width="747">
<img src="https://cdn.nlark.com/yuque/0/2018/png/156121/1534926707654-5f4c6fc0-cd94-403a-8f99-bc6c164babfd.png" width="747" />
</div>
其中,SOFARPC連接管理ConnectionHolder維護存活的客戶端列表aliveConnections和失敗待重試的客戶端列表retryConnections,RPC啓動守護線程以默認10秒的間隔檢查存活和失敗待重試的客戶端列表的可用連接:
- 檢查存活的客戶端列表aliveConnections是否可用,如果存活列表裏連接已經不可用則需要放到待重試列表retryConnections裏面;
- 遍歷失敗待重試的客戶端列表retryConnections,如果連接命中重連週期則進行重連,重連成功放到存活列表aliveConnections裏面,如果待重試連接多次重連失敗則直接丟棄。
核心代碼在連接管理器的方法中:
com.alipay.sofa.rpc.client.AllConnectConnectionHolder#doReconnect
篇幅有限,我們不貼具體代碼,歡迎大家通過源碼來學習瞭解。
最後
本文介紹連接管理的策略和SOFARPC中連接管理與心跳機制的實現,希望通過這篇文章,大家對此有一個瞭解,如果對其中有疑問的,也歡迎留言與我們討論。
參考文檔
TCP-Keepalive-HOWTO
隨手記之TCP Keepalive筆記
爲什麼基於TCP的應用需要心跳包
Netty心跳簡單Demo
淺析 Netty 實現心跳機制與斷線重連