聊聊TCP Keepalive、Netty和Docker
本文主要闡述TCP Keepalive和對應的內核參數,及其在Netty,Docker中的實現。簡單總結了工作中遇到的問題,與大家共勉。
起因
之所以研究TCP Keepalive機制,主要是由於在項目中涉及TCP長連接。服務端接收客戶端請求后需要執行時間較長的任務,再將結果返回給客戶端。期間,客戶端和服務端沒有任何通訊,客服端持續等待服務端返回結果。
+-----------+ +-----------+
| | | |
| Client | | Server |
| | | |
| | Long Connection | |
| <---+--------------------+--> |
| | | |
+-----------+ +-----------+
那么,問題來了,實際情況往往不會這么簡單。在服務器和客戶端之間往往還有眾多的網絡設備,其中一些網絡設備,由於特殊的原因,會導致上述的長連接無法維持較長時間,客戶端因此也無法獲得正確的結果。
典型的例子就是NAT或者防火牆,這類網絡中介設備都應用了一種叫連接跟蹤(connection tracking,conntrack)的技術,用來維護輸入和輸出的TCP連接信息,使兩端設備發送的數據可達。但由於硬件上的瓶頸及基於性能的考慮,這類設備不會維持所有的連接信息,而是會將過期的不活躍的連接信息踢出去。如果這時其中一方還在執行任務,沒有返回數據,造成這條連接徹底斷開,另一方永遠無法獲得數據。為了解決這一問題,引入了TCP Keepalive的技術。
+-----------+ +-----------+ +-----------+
| | | NAT OR | | |
| Client | | Firewall | | Server |
| | | | | |
| | Long Connection | drop | Long Connection | |
| <---+--------------------+--x x--+-------------------+--> |
| | | | | |
+-----------+ +-----------+ +-----------+
TCP Keepalive是什么
其實理解起來非常簡單,就是在TCP層的心跳包。當客戶端與服務端之間的連接空閑了很長時間,期間沒有任何交互時,服務端或客戶端會發送一個空數據的ACK探測包給對方,如果連接沒有問題,對方再以同樣的方式響應一個ACK包,如果網絡有中斷ACK包會重復發多次直到上限。這樣TCP Keepalive就能解決兩個問題,其中之一是上述中使網絡中介設備保持該連接的活性,維持連接的狀態;另外,通過發包也可以探測雙方的程序存活狀態。Linux在內核中內建了對TCP Keepalive的支持,不過默認是關閉的,需要通過Socket選項SO_KEEPALIVE
打開這個功能,這里還涉及三個內核參數:
- tcp_keepalive_time:連接空閑的時長,默認7200秒。
- tcp_keepalive_probes:發送ACK探測包的次數上限,默認9次。
- tcp_keepalive_intvl:發送ACK探測包之間的間隔,默認75秒。
Client Server
| |
+----------------->|
| Last |
| Communicate |
|<-----------------+
| |
| |
| Long |
| |
| Idle Time |
| |
| |
|<-----------------+
| Keepalive ACK |
+----------------->|
| |
| |
Docker和內核參數
在應用層,當我們打開了Socket SO_KEEPALIVE
選項,那么Linux內核就會通過內置的定時器幫我們做好TCP Keepalive的相關工作。由於第一節描述的原因,現實中網絡中介設備NAT或防火牆往往都會把失活的判斷標准調低,也就是說判斷長連接活性的空閑時間會遠遠小於Linux內核鎖設置的7200秒,一般也就幾十分鍾甚至幾分鍾,這就需要我們調整將內核參數tcp_keepalive_time調低。最簡單的方式就是通過sysctl
接口,調整對應的參數:
sysctl -w net.ipv4.tcp_keepalive_time=300
但是這里要留意的是,如果你的服務運行在Docker容器中,調整內核參數的方式會有所不同。
這是由於Docker會通過命名空間(namespace)隔離不同的容器網絡,而對應的內核參數也是被隔離的。當Docker在啟動容器的時候,創建的network命名空間並不會從宿主機繼承大部分的內核網絡參數,而是將這些參數設置為Linux內核編譯時指定的默認值。
因此我們必須通過--sysctl
參數,在Docker啟動容器時,將對應的內核參數初始化。
並不是所有的內核參數都支持命名空間,我們從Docker的官方文檔中,可以了解已支持的內核參數以及使用的限制:
IPC Namespace:
kernel.msgmax, kernel.msgmnb, kernel.msgmni, kernel.sem, kernel.shmall, kernel.shmmax, kernel.shmmni, kernel.shm_rmid_forced.
Sysctls beginning with fs.mqueue.*
If you use the --ipc=host option these sysctls are not allowed.Network Namespace:
Sysctls beginning with net.*
If you use the --network=host option using these sysctls are not allowed.
Netty中的Keepalive
在了解完TCP Keepalive的機制及Linux內核對其相關支持后,我們回到應用層,看看具體如何實現,以及另外推薦的解決方案。下面我拿Java的Netty舉例。Netty中直接提供了ChannelOption.SO_KEEPALIVE
選項,將其傳給ServerBootstrap.childOption
方法,即可開啟TCP Keepalive功能,配置好相關內核參數后,剩下的交給內核搞定。那么,既然內核將TCP Keepalive參數暴露給用戶態,有沒有一種方法能在應用級別調整這些參數,而不用修改系統全局的參數呢?通過man pages了解到,可以通過setsockopt方法為當前TCP Socket配置不同的TCP Keepalive參數,這些參數將會覆蓋系統全局的。
通過調整每個Socket的Keepalive參數會更加靈活,不會因修改系統全局參數而影響到其他應用。接下來看看如何通過Java 的Netty庫來設置對應的參數,Netty中默認的NIO transport沒有直接提供對應的Socket Option,除非使用了netty-transport-native-epoll (https://github.com/netty/netty/pull/2406)。而在JDK 11中新增了對這些參數的支持:
若想在Netty中使用,還需要做一層封裝。下面是對應的示例代碼,僅供參考:
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DiscardServerHandler());
}
})
// 配置TCP Keepalive參數,將Keepalive空閑時間設為150秒
.option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPIDLE), 150)
.option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPINTERVAL), 75)
.option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT), 9)
// 打開SO_KEEPALIVE
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
接下來,我們如何知道設置的參數已經起作用了呢?由於涉及TCP Keepalive機制內建在Linux內核,因此無法在應用級別debug,但可以通過一些其他手段對連接進行監測。其一是通過iproute2
提供的ss
命令的-o
選項查看對應的Socket Options;其二,是通過tcpdump
抓包分析。
首先來看,默認不做任何改動時的情況:
接下來僅開啟SO_KEEPALIVE
:
可以看到Socket Options的keepalive定時器為119min,也就是反映出系統默認配置的空閑時間為7200秒。
最后,我們開啟SO_KEEPALIVE
,並且設置TCP_KEEPIDLE
參數為150秒:
可以看到上面tcpdump
抓包顯示出,兩次ACK包間隔為2分半,即150秒,包的length為0,這就是TCP Keepalive的ACK探測包。同時也可以看到下面ss
命令顯示Socket Options中keepalive timer定時器的倒計時狀態。
總結
通過這篇文章,我們了解到:
- TCP Keepalive的概念、原理及其兩個重要作用。
- TCP Keepalive的三個系統內核參數,及其在Docker容器環境中的特殊配置方式。
- 通過Java的Netty庫演示如何開啟TCP Keepalive,探索在應用層靈活配置三個內核參數。
ref:
TCP Keepalive HOWTO
SO: tcp_keepalive_time in docker container
docker run Docs