聊聊TCP Keepalive、Netty和Docker


聊聊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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM