Linux下Netty實現高性能UDP服務(SO_REUSEPORT)

參考:php

https://www.jianshu.com/p/61df929aa98bhtml

SO_REUSEPORT學習筆記:http://www.blogjava.net/yongboy/archive/2015/02/12/422893.htmljava

代碼示例:https://www.programcreek.com/java-api-examples/index.php?api=io.netty.channel.epoll.EpollDatagramChannellinux

Linux下UDP丟包問題分析思路:https://www.jianshu.com/p/22b0f89937efgit

美團的一篇文章:Redis 高負載下的中斷優化github

 

當前Linux網絡應用程序問題

運行在Linux系統上網絡應用程序,爲了利用多核的優點,通常使用如下比較典型的多進程/多線程服務器模型:bootstrap

  1. 單線程listen/accept,多個工做線程接收任務分發,雖CPU的工做負載再也不是問題,但會存在:
    • 單線程listener,在處理高速率海量鏈接時,同樣會成爲瓶頸
    • CPU緩存行丟失套接字結構(socket structure)現象嚴重
  2. 全部工做線程都accept()在同一個服務器套接字上呢,同樣存在問題:
    • 多線程訪問server socket鎖競爭嚴重
    • 高負載下,線程之間處理不均衡,有時高達3:1不均衡比例
    • 致使CPU緩存行跳躍(cache line bouncing)
    • 在繁忙CPU上存在較大延遲

上面模型雖然能夠作到線程和CPU核綁定,但都會存在:api

  • 單一listener工做線程在高速的鏈接接入處理時會成爲瓶頸
  • 緩存行跳躍
  • 很難作到CPU之間的負載均衡
  • 隨着核數的擴展,性能並無隨着提高

SO_REUSEPORT解決了什麼問題

linux man文檔中一段文字描述其做用:緩存

The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve the performance of multithreaded network server applications running on top of multicore systems.安全

SO_REUSEPORT支持多個進程或者線程綁定到同一端口,提升服務器程序的性能,解決的問題:

  • 容許多個套接字 bind()/listen() 同一個TCP/UDP端口
    • 每個線程擁有本身的服務器套接字
    • 在服務器套接字上沒有了鎖的競爭
  • 內核層面實現負載均衡
  • 安全層面,監聽同一個端口的套接字只能位於同一個用戶下面

其核心的實現主要有三點:

  • 擴展 socket option,增長 SO_REUSEPORT 選項,用來設置 reuseport。
  • 修改 bind 系統調用實現,以便支持能夠綁定到相同的 IP 和端口
  • 修改處理新建鏈接的實現,查找 listener 的時候,可以支持在監聽相同 IP 和端口的多個 sock 之間均衡選擇。

 

Netty使用SO_REUSEPORT

要想在Netty中使用SO_REUSEPORT特性,須要知足如下兩個前提條件

  • linux內核版本 >= 3.9
  • Netty版本 >= 4.0.16

替換Netty中的Nio組件爲原生組件

直接在Netty啓動類中替換爲在Linux系統下的epoll組件

  • NioEventLoopGroup → EpollEventLoopGroup
  • NioEventLoop → EpollEventLoop
  • NioServerSocketChannel → EpollServerSocketChannel
  • NioSocketChannel → EpollSocketChannel
  • 以下所示:
        group = new EpollEventLoopGroup();//NioEventLoopGroup ->EpollEventLoopGroup
        bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(EpollDatagramChannel.class) // NioServerSocketChannel -> EpollDatagramChannel
                .option(ChannelOption.SO_BROADCAST, true)
                .option(EpollChannelOption.SO_REUSEPORT, true) // 配置EpollChannelOption.SO_REUSEPORT
                .option(ChannelOption.SO_RCVBUF, 1024 * 1024 * bufferSize)
                .handler( new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel channel)
                            throws Exception {
                        ChannelPipeline pipeline = channel.pipeline();
                        // ....
                    }
                });

netty提供了方法Epoll.isAvailable()來判斷是否可用epoll

多線程綁定同一個端口

使用原生epoll組件替換nio原來的組件後,須要屢次綁定同一個端口。

        if (Epoll.isAvailable()) {
            // linux系統下使用SO_REUSEPORT特性,使得多個線程綁定同一個端口
            int cpuNum = Runtime.getRuntime().availableProcessors();
            log.info("using epoll reuseport and cpu:" + cpuNum);
            for (int i = 0; i < cpuNum; i++) {
                ChannelFuture future = bootstrap.bind(UDP_PORT).await();
                if (!future.isSuccess()) {
                    throw new Exception("bootstrap bind fail port is " + UDP_PORT);
                }
            }
        }

 

 

更多例子:https://www.programcreek.com/java-api-examples/index.php?api=io.netty.channel.epoll.EpollDatagramChannel

 

也能夠參考:https://github.com/netty/netty/issues/1706 

Bootstrap bootstrap = new Bootstrap()
    .group(new EpollEventLoopGroup(5))
    .channel(EpollDatagramChannel.class)
    .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
    .option(EpollChannelOption.SO_REUSEPORT, true)
    .handler(channelInitializer);

ChannelFuture future;
for(int i = 0; i < 5; ++i) {
future = bootstrap.bind(host, port).await();
if(!future.isSuccess())
    throw new Exception(String.format("Fail to bind on [host = %s , port = %d].", host, port), future.cause());
}
相關文章
相關標籤/搜索