Linux下Netty实现高性能UDP服务(SO_REUSEPORT)

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

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

运行在Linux系统上网络应用程序,为了利用多核的优势,一般使用以下比较典型的多进程/多线程服务器模型:在这里插入图片描述
首先需要单线程listen一个端口上,然后由多个工作进程/线程去accept()在同一个服务器套接字上。 但有以下两个瓶颈:

  • 单线程listener,在处理高速率海量连接时,一样会成为瓶颈
  • 多线程访问server socket锁竞争严重。

那么怎么解决? 这里先别扯什么分布式调度,集群xxx的 , 就拿单机来说问题。在Linux kernel 3.9带来了SO_REUSEPORT特性,她可以解决上面(单进程listen,多工作进程accept() )的问题.
在这里插入图片描述
如上,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核心的一部分,所以需要有额外依赖

  <build>
    <extensions>
      <extension>
        <groupId>kr.motd.maven</groupId>
        <artifactId>os-maven-plugin</artifactId>
        <version>1.5.0.Final</version>
      </extension>
    </extensions>
    ...
  </build>

  <dependencies>
    <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-transport-native-epoll</artifactId>
      <version>${project.version}</version>
      <classifier>${os.detected.name}-${os.detected.arch}</classifier>
    </dependency>
    ...
  </dependencies>

其中# os-maven-plugin 插件是为了自检检测当前系统的名称以及架构。然后自动填充到classifier中的两个变量 ${os.detected.name} 以及 ${os.detected.arch}。如果是在Linux 64系统,那么可能的结果就是os.detected.name=linux,os.detected.arch=x86_64 。

由于官网中没有提供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<Channel>() {
                    @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<Channel>() {
                    @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服务在这里插入图片描述
在这里插入图片描述

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

下面再来看一下运行期间的CPU分布。可以看到其中一个线程占用99%的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服务
在这里插入图片描述
在这里插入图片描述
可以看到没有丢包

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

在这里插入图片描述
在这里插入图片描述
可以看到同时有4个线程负责处理UDP连接。其中3个线程比较繁忙。

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

3.3测试结论

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

作者:藤伦柳揶
链接:https://www.jianshu.com/p/61df929aa98b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值