当UDP丢包的时候,我们正常情况下是增加各种缓冲区的大小,有调整内核缓冲区的,也有调整应用缓冲区的。但是还有另外一种方式,就是加速UDP数据包的处理速度。
1.当前Linux网络应用程序问题
运行在Linux系统上网络应用程序,为了利用多核的优势,一般使用以下比较典型的多进程/多线程服务器模型:
多进程/多线程服务器模型
首先需要单线程listen一个端口上,然后由多个工作进程/线程去accept()在同一个服务器套接字上。 但有以下两个瓶颈:
单线程listener,在处理高速率海量连接时,一样会成为瓶颈
多线程访问server socket锁竞争严重。
那么怎么解决? 这里先别扯什么分布式调度,集群xxx的 , 就拿单机来说问题。在Linux kernel 3.9带来了SO_REUSEPORT特性,她可以解决上面(单进程listen,多工作进程accept() )的问题.
Socket 分片
如上,SO_REUSEPORT是支持多个进程或者线程绑定到同一端口,提高服务器程序的吞吐性能,具体来说解决了下面的几个问题:
允许多个套接字 bind()/listen() 同一个TCP/UDP端口
每一个线程拥有自己的服务器套接字
在服务器套接字上没有了锁的竞争,因为每个进程一个服务器套接字
内核层面实现负载均衡
安全层面,监听同一个端口的套接字只能位于同一个用户下面
关于SO_REUSEPORT可以参考这篇文章SO_REUSEPORT学习笔记
2.Netty使用SO_REUSEPORT
要想在Netty中使用SO_REUSEPORT特性,需要满足以下两个前提条件
linux内核版本 >= 3.9
Netty版本 >= 4.0.16
然后只需要两步就可以使用SO_REUSEPORT特性了。第一步:添加Netty本地库依赖。第二步:替换Netty中的Nio组件为原生组件。第三步:多线程绑定同一个端口
2.1.添加Netty本地库依赖
Netty官方提供了使用本地库的说明 Native transports
Netty是通过JNI本地库的方式来提供的。而且这种本地库的方式不是Netty核心的一部分,所以需要有额外依赖
kr.motd.maven
os-maven-plugin
1.5.0.Final
...
io.netty
netty-transport-native-epoll
${project.version}
${os.detected.name}-${os.detected.arch}
...
其中#
由于官网中没有提供gradle的配置,所以这边总结一下gradle的配置
// gradle构建配置
buildscript {
// buildscript 加上osdetector的依赖
dependencies {
classpath 'com.google.gradle:osdetector-gradle-plugin:1.6.0'
}
}
// 添加原生依赖
dependencies{
compile group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.22.Final', classifier: osdetector.classifier
}
以上的gradle配置虽然没什么问题,但是实际上大多数开发者实在Windows上开发的,所以osdetector.classifier=windows.x86_64,而实际上Netty并没有这样的组件,所以会编译报错。
所以我的建议是直接写死osdetector.classifier=linux-x86_64
2.2.替换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() {
@Override
protected void initChannel(Channel channel)
throws Exception {
ChannelPipeline pipeline = channel.pipeline();
// ....
}
});
不过要注意这些代码只能在Linux上运行,如果实在windows或者mac上开发,那最好还是要换成普通Nio方式的,Netty提供了方法Epoll.isAvailable()来判断是否可用epoll
所以实际上优化的时候需要加上是否支持epoll特性的判断
group = Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(Epoll.isAvailable() ? EpollDatagramChannel.class : NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.option(ChannelOption.SO_RCVBUF, 1024 * 1024)
.handler( new ChannelInitializer() {
@Override
protected void initChannel(Channel channel)
throws Exception {
ChannelPipeline pipeline = channel.pipeline();
}
});
// linux平台下支持SO_REUSEPORT特性以提高性能
if (Epoll.isAvailable()) {
bootstrap.option(EpollChannelOption.SO_REUSEPORT, true);
}
2.3. 多线程绑定同一个端口
使用原生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);
}
}
}
3.测试
3.1优化前
我们使用大概17万的QPS来压测我们的UDP服务
Jmeter
指标数据
可以发现最终丢弃了一部分UDP。
下面再来看一下运行期间的CPU分布。可以看到其中一个线程占用99%的CPU。
线程所占CPU
我们来看一下是哪一个线程。
[root@localhost ~]# printf "%x\n" 1983
7bf
然后使用jstack命令dump出线程。可以看到是处理UDP的连接的线程比较繁忙,导致在高QPS的情况下处理不过来,从而丢包。
线程栈
3.2优化后
使用epoll优化后,在启动的时候有一些错误信息值得关注。
03:23:06.155 [main] DEBUG io.netty.util.internal.NativeLibraryLoader - Unable to load the library 'netty_transport_native_epoll_x86_64', trying other loading mechanism.
java.lang.UnsatisfiedLinkError: no netty_transport_native_epoll_x86_64 in java.library.path
...
03:23:06.155 [main] DEBUG io.netty.util.internal.NativeLibraryLoader - netty_transport_native_epoll_x86_64 cannot be loaded from java.libary.path, now trying export to -Dio.netty.native.workdir: /tmp
java.lang.UnsatisfiedLinkError: no netty_transport_native_epoll_x86_64 in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
... 18 common frames omitted
03:23:06.174 [main] DEBUG io.netty.util.internal.NativeLibraryLoader - Successfully loaded the library /tmp/libnetty_transport_native_epoll_x86_647320427488873314678.so
初看上去好像是启动出错了,但是再细看实际上没什么问题。因为其实上面的日志只是在说netty在加载本地库的时候有优先级。前两次加载失败了,最后一次加载成功了。所以这段时间可以忽略。关于这个问题github上也有人提出了issue。可以关注一下When netty_transport_native_epoll_x86_64 cannot be found, stacktrace is logged
我们同样适用大概17万的QPS来压测我们的UDP服务
Jmeter
指标
可以看到没有丢包
我们再来看一下接受连接的线程所占的CPU
线程所在CPU
线程栈
可以看到同时有4个线程负责处理UDP连接。其中3个线程比较繁忙。
可能是因为QPS还不够高,所以4个线程中只有3个比较繁忙,剩余一个几乎不占用CPU。但是由于单机Jmeter能轰出的UDP QPS有限(我本机大概在17万左右),所以暂时无法测试。后续我们可以使用分布式jmeter来测试,敬请期待。
3.3测试结论
使用SO_REUSEPORT优化后,不但性能提升了,而且CPU占用更加均衡,在一定程度上性能和CPU个数成正相关