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

当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核心的一部分,所以需要有额外依赖

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

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个数成正相关

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,646评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,979评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,391评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,356评论 0 215
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,740评论 3 293
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,836评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,022评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,764评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,487评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,728评论 2 252
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,200评论 1 263
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,548评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,217评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,134评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,921评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,919评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,766评论 2 274

推荐阅读更多精彩内容

  • Netty的简单介绍 Netty 是一个 NIO client-server(客户端服务器)框架,使用 Netty...
    AI乔治阅读 8,341评论 1 101
  • 1、Netty基础入门 Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应...
    我是嘻哈大哥阅读 4,667评论 0 31
  • 前奏 https://tech.meituan.com/2016/11/04/nio.html 综述 netty通...
    jiangmo阅读 5,783评论 0 13
  • 不知道什么时候身边的姑娘会变成这样,在不合适的饭点,在老板不知道哪里去了的街边餐车旁,穿着拖鞋,裹得厚厚的,没...
    白新生阅读 87评论 0 0
  • 7点起床,3小时高速回冷,随即带队加班到下午四点多,走出单位,整个人有点恍惚,洗了头,做了肩颈,直接...
    T伊恩阅读 123评论 0 0