本篇用于记录学习SO_REUSEPORT的笔记和心得,末尾还会提供一个bindp小工具也能为已有的程序享受这个新的特性。
运行在Linux系统上网络应用程序,为了利用多核的优势,一般使用以下比较典型的多进程/多线程服务器模型:
上面模型虽然可以做到线程和CPU核绑定,但都会存在:
比如HTTP CPS(Connection Per Second)吞吐量并没有随着CPU核数增加呈现线性增长:
Linux kernel 3.9带来了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支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:
其核心的实现主要有三点:
代码分析,可以参考引用资料 [多个进程绑定相同端口的实现分析[Google Patch]]。
以前通过 fork
形式创建多个子进程,现在有了SO_REUSEPORT,可以不用通过 fork
的形式,让多进程监听同一个端口,各个进程中accept socket fd
不一样,有新连接建立时,内核只会唤醒一个进程来 accept
,并且保证唤醒的均衡性。
模型简单,维护方便了,进程的管理和应用逻辑解耦,进程的管理水平扩展权限下放给程序员/管理员,可以根据实际进行控制进程启动/关闭,增加了灵活性。
这带来了一个较为微观的水平扩展思路,线程多少是否合适,状态是否存在共享,降低单个进程的资源依赖,针对无状态的服务器架构最为适合了。
可以很方便的测试新特性,同一个程序,不同版本同时运行中,根据运行结果决定新老版本更迭与否。
针对对客户端而言,表面上感受不到其变动,因为这些工作完全在服务器端进行。
想法是,我们迭代了一版本,需要部署到线上,为之启动一个新的进程后,稍后关闭旧版本进程程序,服务一直在运行中不间断,需要平衡过度。这就像Erlang语言层面所提供的热更新一样。
想法不错,但是实际操作起来,就不是那么平滑了,还好有一个 hubtime 开源工具,原理为 SIGHUP信号处理器+SO_REUSEPORT+LD_RELOAD
,可以帮助我们轻松做到,有需要的同学可以检出试用一下。
SO_REUSEPORT根据数据包的四元组{src ip, src port, dst ip, dst port}和当前绑定同一个端口的服务器套接字数量进行数据包分发。若服务器套接字数量产生变化,内核会把本该上一个服务器套接字所处理的客户端连接所发送的数据包(比如三次握手期间的半连接,以及已经完成握手但在队列中排队的连接)分发到其它的服务器套接字上面,可能会导致客户端请求失败,一般可以使用:
与RFS/RPS/XPS-mq协作,可以获得进一步的性能:
目的嘛,数据包的软硬中断、接收、处理等在一个CPU核上,并行化处理,尽可能做到资源利用最大化。
虽然SO_REUSEPORT解决了多个进程共同绑定/监听同一端口的问题,但根据新浪林晓峰同学测试结果来看,在多核扩展层面也未能够做到理想的线性扩展:
可以参考Fastsocket在其基础之上的改进,链接地址。
淘宝的Tengine已经支持了SO_REUSEPORT特性,在其测试报告中,有一个简单测试,可以看出来相对比SO_REUSEPORT所带来的性能提升:
使用SO_REUSEPORT以后,最明显的效果是在压力下不容易出现丢请求的情况,CPU均衡性平稳。
JDK 1.6语言层面不支持,至于以后的版本,由于暂时没有使用到,不多说。
Netty 3/4版本默认都不支持SO_REUSEPORT特性,但Netty 4.0.19以及之后版本才真正提供了JNI方式单独包装的epoll native transport版本(在Linux系统下运行),可以配置类似于SO_REUSEPORT等(JAVA NIIO没有提供)选项,这部分是在io.netty.channel.epoll.EpollChannelOption
中定义( 在线代码部分 )。
在linux环境下使用epoll native transport,可以获得内核层面网络堆栈增强的红利,如何使用可参考 Native transports 文档。
使用epoll native transport倒也简单,类名稍作替换:
NioEventLoopGroup → EpollEventLoopGroup
NioEventLoop → EpollEventLoop
NioServerSocketChannel → EpollServerSocketChannel
NioSocketChannel → EpollSocketChannel
比如写一个PING-PONG应用服务器程序,类似代码:
public void run() throws Exception {
EventLoopGroup bossGroup = new EpollEventLoopGroup();
EventLoopGroup workerGroup = new EpollEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
ChannelFuture f = b
.group(bossGroup, workerGroup)
.channel(EpollServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(
new StringDecoder(CharsetUtil.UTF_8),
new StringEncoder(CharsetUtil.UTF_8),
new PingPongServerHandler());
}
}).option(ChannelOption.SO_REUSEADDR, true)
.option(EpollChannelOption.SO_REUSEPORT, true)
.childOption(ChannelOption.SO_KEEPALIVE, true).bind(port)
.sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
Java/Netty应用程序在Linux kernel >= 3.9下同样享受到SO_REUSEPORT带来的好处。