Linux下Netty实现高性能UDP服务

news2024/10/7 10:16:57

前言

近期笔者基于Netty接收UDP报文进行业务数据统计的功能,因为Netty默认情况下处理UDP收包只能由一个线程负责,无法像TCP协议那种基于主从reactor模型实现多线程监听端口,所以笔者查阅网上资料查看是否有什么方式可以接收UDP收包的性能瓶颈,遂以此文来记录一下笔者的解决过程。

简介Linux内核3.9的新特性对Netty的影响

常规的Netty处理UDP包我们只能用按个NIOEventLoop线程接收传输的数据包,从底层来看即只使用一个socket线程监听网络端口,通过这一个线程将数据传输到应用层上,这一切使得我们唯一能够调优的方式就是在Socket监听传输时尽可能快速将发送给应用程序,让应用程序及时处理完以便NIOEventLoop线程能够及时处理下一个UDP数据包。亦或者,我们也可以直接通过增加服务器的数量通过集群的方式提升系统整体的吞吐量。

在这里插入图片描述

然而事实真是如此吗?在Linux内核3.9版本新增了一个SO_REUSEPORT的特性,它使得单台Linux的端口可以被多个Socket线程监听,这一特性使得Netty在高并发场景下的UDP数据包能够及时被多个线程及时处理,尽可能的避免了丢包线程且最大化的利用了CPU核心,实现内核层面的负载均衡。

在这里插入图片描述

Netty实现Linux下UDP端口复用步骤

引入Netty依赖

为了使用Netty我们必须先引入对应的maven依赖,这里笔者选择了4.1.58的最终版,读者可以按需选择自己的版本。

 <!--netty-->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.58.Final</version>
        </dependency>

编写启动类和启动逻辑

然后我我们需要编写Netty的启动类,代码模板如下,因为Netty默认使用的是Java NIO,而在Linux支持epoll模型,相比与常规的Java NIO这种通过来回在用户态和内核态来回拷贝事件数组fd的方式,epoll内部自己维护了事件的数组并可以将自行去询问连接状态并将结果返回到用户态显得更加高效。
所以笔者在启动类的编写时会判断当前服务器是否支持epoll的逻辑,并通过该判断顺手解决了是否基于SO_REUSEPORT开启多线程监听的功能(注:这段代码读者必须自行查阅一下服务器内核版本是否大于等于3.9)。

/**
 * netty服务
 */
@Component
public class NettyUdpServer {

    private static final Logger LOG = LoggerFactory.getLogger(NettyUdpServer.class);

    private EventLoopGroup bossLoopGroup;

    private Channel serverChannel;


    /**
     * netty初始化
     */
    public void init(int port) {

        LOG.info("Epoll.isAvailable():{}", Epoll.isAvailable());


        //表示服务器连接监听线程组,专门接受 accept 新的客户端client 连接
        bossLoopGroup = Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup();

        try {
            //1、创建netty bootstrap 启动类
            Bootstrap serverBootstrap = new Bootstrap();
            //2、设置boostrap 的eventLoopGroup线程组
            serverBootstrap.group(bossLoopGroup)
                    //3、设置NIO UDP连接通道
                    .channel(Epoll.isAvailable() ? EpollDatagramChannel.class : NioDatagramChannel.class)
                    //4、设置通道参数 SO_BROADCAST广播形式
                    .option(ChannelOption.SO_BROADCAST, true)
                    .option(ChannelOption.SO_RCVBUF, 1024 * 1024)
                    //5、设置处理类 装配流水线
                    .handler(new NettyUdpHandler());

            // linux平台下支持SO_REUSEPORT特性以提高性能
            if (Epoll.isAvailable()) {
                LOG.info("SO_REUSEPORT");
                serverBootstrap.option(EpollChannelOption.SO_REUSEPORT, true);
            }

            // 如果支持epoll则说明是Linux版本,则利用SO_REUSEPORT创建多个线程
            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++) {
                    LOG.info("worker-{} bind", i);
                    //6、绑定server,通过调用sync()方法异步阻塞,直到绑定成功
                    ChannelFuture future = serverBootstrap.bind(port).sync();
                    if (!future.isSuccess()) {
                        LOG.error("bootstrap bind fail port is " + port);
                        throw new Exception(String.format("Fail to bind on [host = %s , port = %d].", "192.168.2.128", port), future.cause());
                    } else {
                        LOG.info("bootstrap bind success ");
                    }
                }
            } else {
                ChannelFuture future = serverBootstrap.bind(port).sync();
                if (!future.isSuccess()) {
                    LOG.error("bootstrap bind fail port is " + port);
                    throw new Exception(String.format("Fail to bind on [host = %s , port = %d].", "127.0.0.1", port), future.cause());
                } else {
                    LOG.info("bootstrap bind success ");
                }
            }

        } catch (Exception e) {
            LOG.error("报错了,错误原因:{}", e.getMessage(), e);
        }


    }


}

因为该代码是编写在spring boot项目中,所以我们还需要添加一下启动的逻辑。

@Component
public class InitTask implements CommandLineRunner {

    private static final Logger LOG = LoggerFactory.getLogger(InitTask.class);



    @Autowired
    private NettyUdpServer nettyUdpServer;






    @Override
    public void run(String... args) {

        LOG.info("netty服务器初始化成功,端口号:{}", 7000);
        nettyUdpServer.init(7000);

    }

}

封装业务处理类

处理类的逻辑比较简单了,收到内容后打印后,原子类自增一下,该原子类是用于后续压测统计是否丢包用的。

/**
 * 报文处理器
 */
@Component
@ChannelHandler.Sharable
public class NettyUdpHandler extends SimpleChannelInboundHandler<DatagramPacket> {

    private static final Logger LOG = LoggerFactory.getLogger(NettyUdpHandler.class);

    private static AtomicInteger atomicInteger=new AtomicInteger(0);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket dp) {
        try {

            int length = dp.content().readableBytes();
            //分配一个新的数组来保存具有该长度的字节数据
            byte[] array = new byte[length];
            //将字节复制到该数组
            dp.content().getBytes(dp.content().readerIndex(), array);
            LOG.info("收到UDP报文,报文内容:{} 包处理个数:{}", new String(array),atomicInteger.incrementAndGet());


        } catch (Exception e) {
            LOG.error("报文处理失败,失败原因:{}", e.getMessage(), e);
        }
    }
}

基于jmeter完成压测统计丢包率

自此我们项目都编写完成了,我们不妨使用jmeter进行一次压测,可以看到笔者会一次性发送100w个数据包查看最终的收包数。

在这里插入图片描述

而UDP包的格式以及目的地址和内容如下

在这里插入图片描述

最终压测结果如下,可以看到服务器都及时的收到了数据包,并不存在丢包的现象。

在这里插入图片描述

为了可以看到性能的提升,笔者将代码还原回单线程监听的老代码段:

/**
     * netty初始化
     */
    public void init(int port) {

        LOG.info("Epoll.isAvailable():{}", Epoll.isAvailable());


        //表示服务器连接监听线程组,专门接受 accept 新的客户端client 连接
        bossLoopGroup = Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup();

        try {
            //1、创建netty bootstrap 启动类
            Bootstrap serverBootstrap = new Bootstrap();
            //2、设置boostrap 的eventLoopGroup线程组
            serverBootstrap.group(bossLoopGroup)
                    //3、设置NIO UDP连接通道
                    .channel(Epoll.isAvailable() ? EpollDatagramChannel.class : NioDatagramChannel.class)
                    //4、设置通道参数 SO_BROADCAST广播形式
                    .option(ChannelOption.SO_BROADCAST, true)
                    .option(ChannelOption.SO_RCVBUF, 1024 * 1024)
                    //5、设置处理类 装配流水线
                    .handler(new NettyUdpHandler());

           


            ChannelFuture future = serverBootstrap.bind(port).sync();
            if (!future.isSuccess()) {
                LOG.error("bootstrap bind fail port is " + port);
                throw new Exception(String.format("Fail to bind on [host = %s , port = %d].", "127.0.0.1", port), future.cause());
            } else {
                LOG.info("bootstrap bind success ");
            }


        } catch (Exception e) {
            LOG.error("报错了,错误原因:{}", e.getMessage(), e);
        }


    }

根据老的压测结果来看,单线程监听的情况下,确实会存在一定的丢包,所以如果在高并发场景下使用Netty接收UDP数据包的小伙伴,建立利用好Linux内核3.9的特性提升程序的吞吐量哦。

在这里插入图片描述

参考文献

Linux下Netty实现高性能UDP服务(SO_REUSEPORT): https://blog.csdn.net/monokai/article/details/108453746

Netty网络传输简记: https://www.sharkchili.com/pages/710071/#前言

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1319789.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

虹科干货 | 克服端口顺序影响,使用 PCAN实现固定设备ID/通道分配

导读&#xff1a;多设备协同工作是常见的需求。然而&#xff0c;适配器的插入顺序可能会影响到设备的识别和访问&#xff0c;给系统管理带来不便。虹科PCAN能够进行固定设备ID/通道分配&#xff0c;确保设备不受适配器插入顺序的影响&#xff0c;提高系统的稳定性和可靠性。本文…

台积电大幅上调产能,12英寸晶圆产能提至每月5.5万片 | 百能云芯

台积电熊本新厂势如破竹&#xff0c;产能将迎来大幅提升&#xff0c;计划逐步达到每月5.5万片的12英寸晶圆产能。据了解&#xff0c;新厂的扩产计划将从2024年第4季开始实施。此次的战略举措不仅是对海外市场布局的重大突破&#xff0c;更是对日本半导体产业生态系统的积极推动…

TikTok矩阵玩法分享,如何建立TikTok矩阵?

矩阵是在 TikTok 上非常常见的营销方式&#xff0c;很多卖家想要通过矩阵化运营快速涨粉。但要想做好TikTok矩阵&#xff0c;需要有明确的方向和计划。下面东哥我将分享一些做TikTok矩阵的玩法&#xff0c;帮助大家更好地搭建自己的TikTok矩阵。 了解TikTok矩阵 TikTok矩阵是一…

深度学习笔记_6经典预训练网络LeNet-18解决FashionMNIST数据集

1、 调用模型库&#xff0c;定义参数&#xff0c;做数据预处理 import numpy as np import torch from torchvision.datasets import FashionMNIST import torchvision.transforms as transforms from torch.utils.data import DataLoader import torch.nn.functional as F im…

眼镜正确清洗方式有哪些?超声波眼镜清洗机推荐

随着人们对健康的重视&#xff0c;眼镜已经成为了日常生活中的必需品。然而&#xff0c;眼镜的清洗却常常被忽视。正确的清洗方式不仅可以保护眼睛健康&#xff0c;还可以延长眼镜的使用寿命。那么&#xff0c;眼镜的正确清洗方式有哪些呢&#xff1f;经常去眼镜店清洗眼镜的朋…

【PHP入门】1.2-常量与变量

-常量与变量- PHP是一种动态网站开发的脚本语言&#xff0c;动态语言特点是交互性&#xff0c;会有数据的传递&#xff0c;而PHP作为“中间人”&#xff0c;需要进行数据的传递&#xff0c;传递的前提就是PHP能自己存储数据&#xff08;临时存储&#xff09; 1.2.1变量基本概…

未来LED全彩显示屏的发展趋势研究

随着LED产品性能的不断提升&#xff0c;全彩 LED 显示屏在亮度、颜色改善和白平衡方面已经达到了比较理想的效果&#xff0c;完全可以满足户外全天候的环境条件。由于全彩 LED 显示屏在价格性能比上的优势&#xff0c;未来数年内有望逐渐取代传统的灯箱、霓虹灯、磁翻板等产品&…

如何在Eclipse中安装WindowBuilder插件,详解过程

第一步&#xff1a;找到自己安装eclipse的版本&#xff0c;在Help-关于eclipse里面&#xff0c;即Version 第二步&#xff1a;去下面这个网站找到对应的 link&#xff08;Update Site&#xff09;&#xff0c;这一步很重要&#xff0c;不然版本下载错了之后还得删除WindowBuil…

2023大湾区汽车创新大会在深圳坪山开幕

12月15日&#xff0c;2023大湾区汽车创新大会在深圳坪山开幕。 本次大会是由广东省科学技术厅、深圳市发展和改革委员会、深圳市工业和信息化局、中共深圳市新能源和智能网联汽车产业链委员会、坪山区人民政府指导&#xff0c;北京理工大学深圳汽车研究院、广东省大湾区新能源汽…

关于“Python”的核心知识点整理大全27

目录 10.5 小结 第&#xff11;1 章 测试代码 11.1 测试函数 name_function.py 函数get_formatted_name()将名和姓合并成姓名&#xff0c;在名和姓之间加上一个空格&#xff0c;并将它们的 首字母都大写&#xff0c;再返回结果。为核实get_formatted_name()像期望的那样工…

SwitchHosts - 管理、切换多个 hosts 方案的工具

一、hosts文件 简单的说&#xff0c;hosts文件是用于本地dns服务的&#xff0c;采用ip 域名的格式写在一个文本文件当中&#xff0c;Hosts是一个没有扩展名的系统文件&#xff0c;可以用记事本等工具打开&#xff0c;其作用就是将一些常用的网址域名与其对应的IP地址建立一个关…

机器学习——数据划分

【说明】文章内容来自《机器学习入门——基于sklearn》&#xff0c;用于学习记录。若有争议联系删除。 1、数据划分 在机器学习中&#xff0c;通常将数据集划分为训练集和测试集。训练集用于训练数据&#xff0c;生成机器学习模型&#xff1b;测试集用于评估学习模型的泛化性能…

如何让.NET应用使用更大的内存

我一直在思考为何Redis这种应用就能独占那么大的内存空间而我开发的应用为何只有4GB大小左右&#xff0c;在此基础上也问了一些大佬&#xff0c;最终还是验证下自己的猜测。 操作系统限制 主要为32位操作系统和64位操作系统。 每个进程自身还分为了用户进程空间和内核进程空…

安全算法(二):共享密钥加密、公开密钥加密、混合加密和迪菲-赫尔曼密钥交换

安全算法&#xff08;二&#xff09;&#xff1a;共享密钥加密、公开密钥加密、混合加密和迪菲-赫尔曼密钥交换 本章介绍了共享密钥加密、公开密钥加密&#xff0c;和两种加密方法混合使用的混合加密方法&#xff1b;最后介绍了迪菲-赫尔曼密钥交换。 加密数据的方法可以分为…

人工智能工程师

据悉&#xff1a;为进一步贯彻落实中共中央印发《关于深化人才发展体制机制改革的意见》和国务院印发《关于“十四五”数字经济发展规划》等有关工作的部署求&#xff0c;深入实施人才强国战略和创新驱动发展战略&#xff0c;加强全国数字化人才队伍建设&#xff0c;持续推进人…

Axure之交互与情节与一些实例

目录 一.交互与情节简介 二.ERP登录页到主页的跳转 三.ERP的菜单跳转到各个页面的跳转 四.省市联动 五.手机下拉加载 今天就到这里了&#xff0c;希望帮到你哦&#xff01;&#xff01;&#xff01; 一.交互与情节简介 "交互"通常指的是人与人、人与计算机或物体…

lseek()函数的原型及使用方法,超详细

对于所有打开的文件都有一个当前文件偏移量(current file offset)&#xff0c;文件偏移量通常是一个非负整数&#xff0c;用于表明文件开始处到文件当前位置的字节数。 读写操作通常开始于当前文件偏移量的位置&#xff0c;并且使其增大&#xff0c;增量为读写的字节数。文件被…

苹果M系列芯片安装Notepad-- 详细教程(亲测14以上系统也可用)

目录 1. 介绍2. 前言说明3. 安装使用教程3.1 下载3.2 安装3.3 打开3.4 最终效果 4. 主体功能一览5. 其他信息 1. 介绍 鉴于某些Notepad竞品作者的不当言论&#xff0c;Notepad–的意义在于&#xff1a;减少一点错误言论&#xff0c;减少一点自以为是。 Notepad–的目标&#xf…

Xpath注入

这里学习一下xpath注入 xpath其实是前端匹配树的内容 爬虫用的挺多的 XPATH注入学习 - 先知社区 查询简单xpath注入 index.php <?php if(file_exists(t3stt3st.xml)) { $xml simplexml_load_file(t3stt3st.xml); $user$_GET[user]; $query"user/username[name&q…

【网络安全技术】传输层安全——SSL/TLS

一、TLS位置及架构 TLS建立在传输层TCP/UDP之上&#xff0c;应用层之下。 所以这可以解决一个问题&#xff0c;那就是为什么抓不到HTTP和SMTP包&#xff0c;因为这两个在TLS之上&#xff0c;消息封上应用层的头&#xff0c;下到TLS层&#xff0c;TLS层对上层消息整个做了加密&…