如何让单机下Netty支持百万长连接?

如果要支持百万长连接,需要有很多的工作要做

操作系统

首先就是要突破操作系统的限制。
在Linux平台上,无论编写客户端程序还是服务端程序,在进行高并发TCP连接处理时,最高的并发数量都要受到系统对用户单一进程同时可打开文件数量的限制(这是因为系统为每个TCP连接都要创建一个Socket句柄,每个socket句柄也是一个文件句柄)。可使用ulimit命令查看系统允许当前用户进程打开的句柄数限制:

 ulimit -n
 1024

这表示当前用户的每个进程最多允许同时打开1024个句柄,这1024个句柄中还得出去每个进程必然打开的标准输入,标准输出,标准错误,服务器监听socket,进程间通讯的unix域socket等文件,那么剩下的可用于客户端socket连接的文件数只有大概1024-10=1014个左右。也就是说缺省情况下,基于Linux的通讯程序最多允许同时1014个TCP并发连接对于想支持更高数量的TCP并发连接的通讯处理程序,就必须修改Linux对当前用户进程同时打开的文件数量,修改单个进程打开最大文件数限制的最简单的办法就是使用ulimit命令:

 ulimit -n 1000000

如果系统回显类似于"Operation not permitted"之类的话,说明上述限制修改失败,实际上是因为指定的数值超过了Linux系统对该用户打开文件数的软限制或硬限制,因此,就需要修改Linux系统对用户的关于打开文件数的软限制和硬限制.
软限制(soft limit):是指Linux在当前系统能够承受的范围内进一步限制一个进程同时打开的文件数;
硬限制(hard limit):是根据系统硬件资源状况(主要是系统内存)计算出来的系统最多可同时打开的文件数量.
第一步:修改/etc/security/limits.conf文件,在文件中添加如下行

* soft nofile 1000000
* hard nofile 1000000

'*'号表示修改所有用户的限制;
soft 和hard为两种限制方式,其中soft表示警告的限制,hard表示真正限制,nofile表示打开的最大文件数。1000000则指定了想要修改的新的限制值,即最大打开文件数(请注意软限制值要小于或等于硬限制),修改完后保存文件.
第二步:修改/etc/pam.d/login文件,在文件中添加如下行:

session required /lib/security/pan_limits.so

这是告诉Linux在用户完成系统登录后,应该调用pam_limits.so模块来设置系统对该用户可使用的各种资源数量的最大限制(包括用户可打开的最大文件数限制),而pam_limits.so模块就会从/etc/security/limits.conf文件中读取配置来设置这些限制值,修改完后保存此文件
第三步,查看Linux系统级的最大打开文件数限制,使用如下命令:

cat /proc/sys/fs/file-max
12158

这表明这台Linux系统最多允许同时打开(即包含所有用户打开文件数总和)12158个文件,是Linux系统级硬限制,所有用户级打开文件数限制都不应超过这个数值。如果没有特殊需要,不应该修改此限制,除非想为用户级打开文件数限制设置超过此限制的值。如果想要修改系统最大文件描述符的限制,需要修改sysctl.conf

vim /etc/sysctl.conf

在末尾添加,fs.file_max=1000000
立即生效 sysctl -p

Netty调优

设置合理的线程数

对于线程池的调优,主要集中在用于接收海量设备TCP连接、TLS握手的Acceptor线程池(Netty通常叫boss NioEventLoopGroup)上,以及用于处理网络数据读写、心跳发送的IO工作线程池(Netty通常叫work NioEventLoopGroup)上。对于Netty服务端,通常只需要启动一个监听端口用于端侧设备接入即可,但是如果服务端集群实例比较少,甚至是单机(或者双机冷备)部署,在端侧设备在短时间内大量接入时,需要对服务端的监听方式和线程模型做优化,以满足短时间内(例如30s)百万级的端侧设备
接入的需要。服务端可以监听多个端口,利用主从Reactor线程模型做接入优化,前端通过SLB做4层门7层负载均衡。主从Reactor线程模型特点如下:服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池,Acceptor接收到客户端TCP连接请求并处理后(可能包含接入认证等),将新创建的SocketChannel注册到I/O线程池(subReactor线程池)的某个IO线程由它负责SocketChannel的读写和编解码工作;Acceptor线程池仅用于客户端的登录、握手和安全认证等,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程,由IO线程负责后续的IO操作。
对于IO工作线程池的优化,可以先采用系统默认值(即CPU内核数x2)进行性能测试,在性能测试过程中采集IO线程的CPU占用大小,看是否存在瓶颈,具体可以观察线程堆栈,如果连续采集几次进行对比,发现线程堆栈都停留在SelectorIml.lockAndDoSelect,则说明IO线程比较控线,无需对工作线程数做调整。如果发现IO线程的热点停留在读或者写操作,或者停留在ChannelHandler的执行处,则可以通过适当调大NioEventLoop线程的个数来提升网络的读写性能

心跳优化

针对海量设备接入的服务端,心跳优化策略如下:

  • 1.要能够即使检测失效的连接,并将其剔除,防止无效的连接句柄积压,导致OOM等问题
  • 2.设置核里的心跳周期,防止心跳定时任务积压,造成频繁的老年代GC(新生代和老年代都有STW的GC,不过耗时差异较大),导致应用暂停
  • 3.使用Netty提供的链路空闲检测机制,不要自己创建定时任务线程池,加重系统的负担,以及增加潜在的并发安全问题。

当设备突然断电、连接被防火墙挡住、长时间GC或者通信线程发生非预期异常时,会导致链路不可用且不易被即时发现。特别是如果异常发生在凌晨业务低谷期间,当早晨业务高峰期来时,由于链路不可用会导致瞬间大批量业务失败或者超时,这将对系统的可靠性产生重大的威胁。从技术层面看,要解决链路的可靠性问题,必须周期性地对链路进行有效性检测,
目前最流行和通用地做法就是心跳检测。心跳检测机制分为三个层面:

  • 1.TCP层的心跳检测,即TCP得Keep-Alive机制,它得作用域是整个TCP协议栈
  • 2.协议层的心跳检测,主要存在长连接协议中,例如MQTT
  • 3.应用层的心跳检测,它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。
    心跳检测的目的就是确认当前链路是否可用,对方是否或者并且能够正常接收和发送消息,作为高可靠的NIO框架,NIO也提供了心跳检测机制.
    一般的心跳检测策略如下:
  • 1.连续N次心跳检测都没有受到对方的Pong应答消息或者Ping请求消息,则认为链路已经发生逻辑失效,这被称为心跳超时。
  • 2.在读取和发送心跳消息的时候如果直接发生了IO异常,说明链路已经失效,这被称为心跳失败,无论发生心跳超时还是心跳失败,都需要关闭链路,由客户端发起重连操作,保证链路能够恢复正常

Netty提供了三种链路空闲检测机制,利用该机制可以轻松地实现心跳机制

  • 1.读空闲,链路持续时间T没有读取到任何消息
  • 2.写空闲,链路持续时间T没有发送任何消息
  • 3.读写空闲,链路持续时间T没有接收或者发送任何消息,对于百万级的服务器,一般不建议很长的心跳周期和超时时长

接收和发送缓冲区调优接收和发送缓冲区调优

在一些场景下,端侧设备会周期性地上报数据和发送心跳,单个链路的消息收发量并不大,针对此类场景,可以通过调小TCP的接收和发送缓冲区来降低单个TCP连接的资源占用率。当然对于不同的应用场景,收发缓冲区的最优值可能不同,用户需要根据实际场景,结合性能测试数据进行针对性的调优

合理使用内存池

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是一个非常轻量级的工作。但是对于缓冲区Buffer,情况却稍有不同,特别是对外直接内存的分配和回收,是一个耗时的操作。为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。
在百万级的情况下,需要为每个接入的端侧设备至少分配一个接收和发送ByteBuf缓冲区对象,采用传统的非池模式,每次消息读写都需要创建和释放ByteBuf对象,如果有100万个连接,每秒上报一次数据或者心跳,就会有100万次/秒的ByteBuf对象申请和释放,即便服务端的内存可以满足要求,GC的压力也会非常大。
以上问题最有效的解决办法就是使用内存池,每个NioEventLoop线程处理N个链路,在线程内部,链路的处理是串行的,假如A链路首先被处理,它会创建接收缓冲区等对象,待解码完成,构造的POJO对象被封装成任务后投递到后台的线程池中执行,然后接收缓冲区会被释放,每条消息的接收和处理都会重复接收缓冲区的创建和释放。如果使用内存池,则当A链路接收到新的数据报时,从NioEventLoop的内存池中申请空闲的ByteBuf,解码后调用release将ByteBuf释放到内存池中,
供后续的B链路使用Netty内存池从实现上可以分为两类:堆外直接内存和堆内存。由于ByteBuf主要用于网络IO读写,因此采用堆外直接内存会减少一次从用户堆内存到内核态字节数组拷贝,所以性能更高。由于DirectByteBuf的创建成本比较高,因此如果使用DirectByteBuf,则需要配合内存池的使用,否则性价比可能还不如HeapByte,Netty默认的IO读写操作采用的都是内存池的堆外直接内存模式,如果用户需要额外使用ByteBuf,建议也采用内存池方式;如果不涉及网络IO操作(只是纯粹的内存操作),可以使用堆内存池,这样内存的创建效率会更高一些

IO线程和业务线程分离

如果服务端不做复杂的业务逻辑操作,仅仅时简单的内存操作和消息转发,则可以通过调大NioEventLoop工作线程池的方式,直接在IO线程中执行业务ChannelHandler,这样便减少了一次线程上下文切换,性能反而更高。如果有复杂的业务逻辑操作,则将以IO线程和业务线程分离,对于IO线程,由于互相之间不存在锁竞争,可以创建一个大的NioEventLoopGroup线程组,所有Channel都共享同一个线程池。对于后端的业务线程池,则加你创建多个小的业务线程池,线程池可以与IO线程绑定,这样既减少了锁竞争,又提升了后端的处理性能

针对端侧并发连接数的流控

无论服务端的性能优化到多少,都需要考虑流控功能。当资源称为瓶颈,或者遇到端侧设备的大量接入,需要通过流控对系统做保护。流控的策略有很多种,比如针对端侧连接数的流控;
在Netty中,可以非常方便地实现流控功能:新增一个FlowCOntrolChannelHandler,然后添加到ChannelPipeline靠前的位置,覆盖channelActive()方法,创建TCP链路后,执行流控逻辑,如果达到流控阈值,则拒绝该连接,调用ChannelHandlerContext的close()方法关闭连接

JVM层面相关性能优化

当客户端的并发连接数达到数十万或者数百万时,系统一个较小的抖动就会导致很严重的后果,例如服务端的GC,导致应用暂停(STW)的GC持续几秒,就会导致海量的客户端设备掉线或者消息积压,一旦系统恢复,就会有海量的设备接入或者海量的数据发送很可能瞬间就把服务端冲垮。
JVM层面的调优主要涉及GC参数优化,GC参数设置不当会导致频繁GC,甚至OOM异常,对服务端的稳定运行产生重大影响

  • 1.确定GC优化目标
    GC(垃圾收集)有三个主要指标
    1.1 吞吐量:是评价GC能力的重要指标,在不考虑GC引起的停顿时间或内存消耗时,吞吐量是GC能支撑应用程序达到的最高性能指标。
    1.2 延迟:GC能力的最重要指标之一,是由于GC引起的停顿时间,优化目标是缩短延迟时间或完全消除停顿(STW),避免应用程序在运行
    过程中发生抖动
    1.3 内存占用:GC正常时占用的内存量

JVM调优的三个基本原则如下:
1.Minor gc回收原则:每次新生代GC回收尽可能多的内存,减少应用程序发生Full gc的频率
2.GC内存最大化原则:垃圾收集器能够使用的内存越大,垃圾收集器效率越高,应用程序运行也越流畅,但是过大的内存一次Fullgc
耗时可能较长,如果能够有效避免FullGC,就需要做精细化调优
3. 3选2原则:吞吐量、延迟和内存占用不能兼得,无法同时做到吞吐量和暂停时间都最优,需要根据业务场景做选择。对于大多数
应用,吞吐量有限,其次是延迟。当然对于时延敏感型的业务,需要调整次序

  • 2.确定服务端内存占用
    在优化GC之前,需要确定应用程序的内存占用大小,以便为应用程序设置合适的内存,提升GC效率。内存占用与活跃数据有关,
    活跃数据指的是应用程序稳定运行时长时间存活的Java对象。活跃数据的计算方式:通过GC日志采集GC数据,获取应用程序稳定
    时老年代占用的Java堆大小,以及永久代(元数据区)占用的Java堆大小,两者之和就是活跃数据的内存占用大小

  • 3.GC优化过程
    3.1 GC数据的采集和研读
    3.2 设置何实的JVM堆大小
    3.3 选择合适的垃圾回收期和回收策略

GC调优会是一个需要多次调整的过程,期间不仅有参数的变化,更重要的是需要调整业务代码

  • 30
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coffee_babe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值