本篇用于记录学习SO_REUSEPORT的笔记和心得,末尾还会提供一个bindp小工具也能为已有的程序享受这个新的特性。html
运行在Linux系统上网络应用程序,为了利用多核的优点,通常使用如下比较典型的多进程/多线程服务器模型:java
单线程listen/accept,多个工做线程接收任务分发,虽CPU的工做负载再也不是问题,但会存在:python
单线程listener,在处理高速率海量链接时,同样会成为瓶颈linux
CPU缓存行丢失套接字结构(socket structure)现象严重git
全部工做线程都accept()在同一个服务器套接字上呢,同样存在问题:程序员
多线程访问server socket锁竞争严重github
高负载下,线程之间处理不均衡,有时高达3:1不均衡比例小程序
致使CPU缓存行跳跃(cache line bouncing)缓存
在繁忙CPU上存在较大延迟安全
上面模型虽然能够作到线程和CPU核绑定,但都会存在:
单一listener工做线程在高速的链接接入处理时会成为瓶颈
缓存行跳跃
很难作到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支持多个进程或者线程绑定到同一端口,提升服务器程序的性能,解决的问题:
容许多个套接字 bind()/listen() 同一个TCP/UDP端口
每个线程拥有本身的服务器套接字
在服务器套接字上没有了锁的竞争
内核层面实现负载均衡
安全层面,监听同一个端口的套接字只能位于同一个用户下面
其核心的实现主要有三点:
扩展 socket option,增长 SO_REUSEPORT 选项,用来设置 reuseport。
修改 bind 系统调用实现,以便支持能够绑定到相同的 IP 和端口
修改处理新建链接的实现,查找 listener 的时候,可以支持在监听相同 IP 和端口的多个 sock 之间均衡选择。
代码分析,能够参考引用资料 [多个进程绑定相同端口的实现分析[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}和当前绑定同一个端口的服务器套接字数量进行数据包分发。若服务器套接字数量产生变化,内核会把本该上一个服务器套接字所处理的客户端链接所发送的数据包(好比三次握手期间的半链接,以及已经完成握手但在队列中排队的链接)分发到其它的服务器套接字上面,可能会致使客户端请求失败,通常可使用:
使用固定的服务器套接字数量,不要在负载繁忙期间轻易变化
容许多个服务器套接字共享TCP请求表(Tcp request table)
不使用四元组做为Hash值进行选择本地套接字处理,挑选隶属于同一个CPU的套接字
与RFS/RPS/XPS-mq协做,能够得到进一步的性能:
服务器线程绑定到CPUs
RPS分发TCP SYN包到对应CPU核上
TCP链接被已绑定到CPU上的线程accept()
XPS-mq(Transmit Packet Steering for multiqueue),传输队列和CPU绑定,发送数据
RFS/RPS保证同一个链接后续数据包都会被分发到同一个CPU上
网卡接收队列已经绑定到CPU,则RFS/RPS则无须设置
须要注意硬件支持与否
目的嘛,数据包的软硬中断、接收、处理等在一个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<SocketChannel>() { @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带来的好处,不妨尝试一下bindp,更为经济,这一部分下面会讲到。
之前所写bindp小程序,能够为已有程序绑定指定的IP地址和端口,一方面能够省去硬编码,另外一方面也为测试提供了一些方便。
另外,为了让之前没有硬编码SO_REUSEPORT
的应用程序能够在Linux内核3.9以及以后Linux系统上也可以获得内核加强支持,稍作修改,添加支持。
但要求以下:
Linux内核(>= 3.9)支持SO_REUSEPORT特性
须要配置REUSE_PORT=1
不知足以上条件,此特性将没法生效。
使用示范:
REUSE_PORT=1 BIND_PORT=9999 LD_PRELOAD=./libbindp.so java -server -jar pingpongserver.jar &
固然,你能够根据须要运行命令屡次,多个进程监听同一个端口,单机进程水平扩展。
使用python脚本快速构建一个小的示范原型,两个进程,都监听同一个端口10000,客户端请求返回不一样内容,仅供娱乐。
server_v1.py,简单PING-PONG:
# -*- coding:UTF-8 -*- import socket import os PORT = 10000 BUFSIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', PORT)) s.listen(1) while True: conn, addr = s.accept() data = conn.recv(PORT) conn.send('Connected to server[%s] from client[%s]\n' % (os.getpid(), addr)) conn.close() s.close()
server_v2.py,输出当前时间:
# -*- coding:UTF-8 -*- import socket import time import os PORT = 10000 BUFSIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', PORT)) s.listen(1) while True: conn, addr = s.accept() data = conn.recv(PORT) conn.send('server[%s] time %s\n' % (os.getpid(), time.ctime())) conn.close() s.close()
借助于bindp运行两个版本的程序:
REUSE_PORT=1 LD_PRELOAD=/opt/bindp/libindp.so python server_v1.py & REUSE_PORT=1 LD_PRELOAD=/opt/bindp/libindp.so python server_v2.py &
模拟客户端请求10次:
for i in {1..10};do echo "hello" | nc 127.0.0.1 10000;done
看看结果吧:
Connected to server[3139] from client[('127.0.0.1', 48858)] server[3140] time Thu Feb 12 16:39:12 2015 server[3140] time Thu Feb 12 16:39:12 2015 server[3140] time Thu Feb 12 16:39:12 2015 Connected to server[3139] from client[('127.0.0.1', 48862)] server[3140] time Thu Feb 12 16:39:12 2015 Connected to server[3139] from client[('127.0.0.1', 48864)] server[3140] time Thu Feb 12 16:39:12 2015 Connected to server[3139] from client[('127.0.0.1', 48866)] Connected to server[3139] from client[('127.0.0.1', 48867)]
能够看出来,CPU分配很均衡,各自分配50%的请求量。
嗯,虽是小玩具,有些意思 :))
由于能力有限,仍是有不少东西(SO_REUSEADDR和SO_REUSEPORT的区别等)没有可以在一篇文字中表达清楚,做为补遗,也方便之后本身回过头来复习。
二者不是一码事,没有可比性。有时也会被其搞晕,本身总结的很差,推荐StackOverflow的Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ?资料,总结的很全面。
简单来讲:
设置了SO_REUSADDR的应用能够避免TCP 的 TIME_WAIT 状态 时间过长没法复用端口,尤为表如今应用程序关闭-重启交替的瞬间
SO_REUSEPORT更强大,隶属于同一个用户(防止端口劫持)的多个进程/线程共享一个端口,同时在内核层面替上层应用作数据包进程/线程的处理均衡
如有困惑,推荐二者都设置,不会有冲突。
上一篇讲到SO_REUSEPORT,多个程绑定同一个端口,能够根据须要控制进程的数量。这里讲讲基于Netty 4.0.25+Epoll navtie transport
在单个进程内多个线程绑定同一个端口的状况,也是比较实用的。
这是一个PING-PONG示范应用:
public void run() throws Exception { final EventLoopGroup bossGroup = new EpollEventLoopGroup(); final EventLoopGroup workerGroup = new EpollEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(EpollServerSocketChannel. class) .childHandler( new ChannelInitializer<SocketChannel>() { @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); int workerThreads = Runtime.getRuntime().availableProcessors(); ChannelFuture future;
//new thread for ( int i = 0; i < workerThreads; ++i) { future = b.bind( port).await(); if (!future.isSuccess()) throw new Exception(String. format("fail to bind on port = %d.", port), future.cause()); } Runtime. getRuntime().addShutdownHook (new Thread(){ @Override public void run(){ workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } }); }
打成jar包,在CentOS 7下面运行,检查同一个端口所打开的文件句柄。
# lsof -i:8000 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME java 3515 root 42u IPv6 29040 0t0 TCP *:irdmi (LISTEN) java 3515 root 43u IPv6 29087 0t0 TCP *:irdmi (LISTEN) java 3515 root 44u IPv6 29088 0t0 TCP *:irdmi (LISTEN) java 3515 root 45u IPv6 29089 0t0 TCP *:irdmi (LISTEN)
同一进程,但打开的文件句柄是不同的。
/** * UDP谚语服务器,单进程多线程绑定同一端口示范 */ public final class QuoteOfTheMomentServer { private static final int PORT = Integer.parseInt(System. getProperty("port" , "9000" )); public static void main(String[] args) throws Exception { final EventLoopGroup group = new EpollEventLoopGroup(); Bootstrap b = new Bootstrap(); b.group(group).channel(EpollDatagramChannel. class) .option(EpollChannelOption. SO_REUSEPORT, true ) .handler( new QuoteOfTheMomentServerHandler()); int workerThreads = Runtime.getRuntime().availableProcessors(); for (int i = 0; i < workerThreads; ++i) { ChannelFuture future = b.bind( PORT).await(); if (!future.isSuccess()) throw new Exception(String.format ("Fail to bind on port = %d.", PORT), future.cause()); } Runtime. getRuntime().addShutdownHook(new Thread() { @Override public void run() { group.shutdownGracefully(); } }); } } } @Sharable class QuoteOfTheMomentServerHandler extends SimpleChannelInboundHandler<DatagramPacket> { private static final String[] quotes = { "Where there is love there is life." , "First they ignore you, then they laugh at you, then they fight you, then you win.", "Be the change you want to see in the world." , "The weak can never forgive. Forgiveness is the attribute of the strong.", }; private static String nextQuote() { int quoteId = ThreadLocalRandom.current().nextInt( quotes .length ); return quotes [quoteId]; } @Override public void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { if ("QOTM?" .equals(packet.content().toString(CharsetUtil. UTF_8))) { ctx.write( new DatagramPacket(Unpooled.copiedBuffer( "QOTM: " + nextQuote(), CharsetUtil. UTF_8), packet.sender())); } } @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); } }
一样也要检测一下端口文件句柄打开状况:
# lsof -i:9000 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME java 3181 root 26u IPv6 27188 0t0 UDP *:cslistener java 3181 root 27u IPv6 27217 0t0 UDP *:cslistener java 3181 root 28u IPv6 27218 0t0 UDP *:cslistener java 3181 root 29u IPv6 27219 0t0 UDP *:cslistener
以上为Netty+SO_REUSEPORT多线程绑定同一端口的一些状况,是为记载。