java so reuseport_Linux下Netty实现高性能UDP服务(SO_REUSEPORT)

本文介绍了如何使用Linux内核的SO_REUSEPORT特性来优化Netty的UDP服务,解决多线程访问server socket锁竞争的问题。通过在Netty中引入本地库依赖,替换Nio组件为Epoll组件,并实现多线程绑定同一端口,实现在Linux环境下提升UDP服务的性能和吞吐量,同时避免丢包现象。
摘要由CSDN通过智能技术生成

当UDP丢包的时候,我们正常情况下是增加各种缓冲区的大小,有调整内核缓冲区的,也有调整应用缓冲区的。但是还有另外一种方式,就是加速UDP数据包的处理速度。

1.当前Linux网络应用程序问题

运行在Linux系统上网络应用程序,为了利用多核的优势,一般使用以下比较典型的多进程/多线程服务器模型:

61df929aa98b

多进程/多线程服务器模型

首先需要单线程listen一个端口上,然后由多个工作进程/线程去accept()在同一个服务器套接字上。 但有以下两个瓶颈:

单线程listener,在处理高速率海量连接时,一样会成为瓶颈

多线程访问server socket锁竞争严重。

那么怎么解决? 这里先别扯什么分布式调度,集群xxx的 , 就拿单机来说问题。在Linux kernel 3.9带来了SO_REUSEPORT特性,她可以解决上面(单进程listen,多工作进程accept() )的问题.

61df929aa98b

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服务

61df929aa98b

Jmeter

61df929aa98b

指标数据

可以发现最终丢弃了一部分UDP。

下面再来看一下运行期间的CPU分布。可以看到其中一个线程占用99%的CPU。

61df929aa98b

线程所占CPU

我们来看一下是哪一个线程。

[root@localhost ~]# printf "%x\n" 1983

7bf

然后使用jstack命令dump出线程。可以看到是处理UDP的连接的线程比较繁忙,导致在高QPS的情况下处理不过来,从而丢包。

61df929aa98b

线程栈

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服务

61df929aa98b

Jmeter

61df929aa98b

指标

可以看到没有丢包

我们再来看一下接受连接的线程所占的CPU

61df929aa98b

线程所在CPU

61df929aa98b

线程栈

可以看到同时有4个线程负责处理UDP连接。其中3个线程比较繁忙。

可能是因为QPS还不够高,所以4个线程中只有3个比较繁忙,剩余一个几乎不占用CPU。但是由于单机Jmeter能轰出的UDP QPS有限(我本机大概在17万左右),所以暂时无法测试。后续我们可以使用分布式jmeter来测试,敬请期待。

3.3测试结论

使用SO_REUSEPORT优化后,不但性能提升了,而且CPU占用更加均衡,在一定程度上性能和CPU个数成正相关

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值