參考:
https://www.jianshu.com/p/61df929aa98b
SO_REUSEPORT學習筆記:http://www.blogjava.net/yongboy/archive/2015/02/12/422893.html
代碼示例:https://www.programcreek.com/java-api-examples/index.php?api=io.netty.channel.epoll.EpollDatagramChannel
Linux下UDP丟包問題分析思路:https://www.jianshu.com/p/22b0f89937ef
美團的一篇文章:Redis 高負載下的中斷優化
當前Linux網絡應用程序問題
運行在Linux系統上網絡應用程序,為了利用多核的優勢,一般使用以下比較典型的多進程/多線程服務器模型:
- 單線程listen/accept,多個工作線程接收任務分發,雖CPU的工作負載不再是問題,但會存在:
- 單線程listener,在處理高速率海量連接時,一樣會成為瓶頸
- CPU緩存行丟失套接字結構(socket structure)現象嚴重
- 所有工作線程都accept()在同一個服務器套接字上呢,一樣存在問題:
- 多線程訪問server socket鎖競爭嚴重
- 高負載下,線程之間處理不均衡,有時高達3:1不均衡比例
- 導致CPU緩存行跳躍(cache line bouncing)
- 在繁忙CPU上存在較大延遲
上面模型雖然可以做到線程和CPU核綁定,但都會存在:
- 單一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()); }