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


对于IO工作线程池的优化,可以先采用系统默认值(即CPU内核数×2)进行性能测试,在性能测试过程中采集IO线程的CPU占用大小,看是否存在瓶颈对于O工作线程池的优化,可以先采用系统默认值(即CPU内核数×2)进行性能
测试,在性能测试过程中采集IO线程的CPU占用大小,看是否存在瓶颈, 具体可以观察线程堆栈,如果连续采集几次进行对比,发现线程堆栈都停留在 Selectorlmpl. lock AnDDoSelect,则说明IO线程比较空闲,无须对工作线程数做调整 。
如果发现IO线程的热点停留在读或者写操作,或者停留在 Channelhandler的执行处,则可以通过适当调大 Nio EventLoop线程的个数来提升网络的读写性能 。
2、心跳优化针对海量设备接入的服务端,心跳优化策略如下 。

  1. 要能够及时检测失效的连接,并将其剔除,防止无效的连接句柄积压,导致OOM等问题
  2. 设置合理的心跳周期,防止心跳定时任务积压,造成频繁的老年代GC(新生代和老年代都有导致STW的GC,不过耗时差异较大),导致应用暂停
  3. 使用Nety提供的链路空闲检测机制,不要自己创建定时任务线程池,加重系统的负担,以及增加潜在的并发安全问题 。
当设备突然掉电、连接被防火墙挡住、长时间GC或者通信线程发生非预期异常时,会导致链路不可用且不易被及时发现 。特别是如果异常发生在凌晨业务低谷期间,当早晨业务高峰期到来时,由于链路不可用会导致瞬间大批量业务失败或者超时,这将对系统的可靠性产生重大的威胁 。
从技术层面看,要解决链路的可靠性问题,必须周期性地对链路进行有效性检测 。目前最流行和通用的做法就是心跳检测 。心跳检测机制分为三个层面
  1. TCP层的心跳检测,即TCP的 Keep-Alive机制,它的作用域是整个TCP协议栈 。
  2. 协议层的心跳检测,主要存在于长连接协议中,例如MQTT 。
  3. 应用层的心跳检测,它主要由各业务产品通过约定方式定时给对方发送心跳消息实现 。
心跳检测的目的就是确认当前链路是否可用,对方是否活着并且能够正常接收和发送消息 。作为高可靠的NIO框架,Nety也提供了心跳检测机制 。
一般的心跳检测策略如下 。
  1. 连续N次心跳检测都没有收到对方的Pong应答消息或者Ping请求消息,则认为链路已经发生逻辑失效,这被称为心跳超时 。
  2. 在读取和发送心跳消息的时候如果直接发生了IO异常,说明链路已经失效,这被称为心跳失败 。无论发生心跳超时还是心跳失败,都需要关闭链路,由客户端发起重连操作,保证链路能够恢复正常 。
Nety提供了三种链路空闲检测机制,利用该机制可以轻松地实现心跳检测
  1. 读空闲,链路持续时间T没有读取到任何消息 。
  2. 写空闲,链路持续时间T没有发送任何消息
  3. 读写空闲,链路持续时间T没有接收或者发送任何消息
对于百万级的服务器,一般不建议很长的心跳周期和超时时长
3、接收和发送缓冲区调优在一些场景下,端侧设备会周期性地上报数据和发送心跳,单个链路的消息收发量并不大,针对此类场景,可以通过调小TCP的接收和发送缓冲区来降低单个TCP连接的资源占用率
当然对于不同的应用场景,收发缓冲区的最优值可能不同,用户需要根据实际场景,结合性能测试数据进行针对性的调优
4、合理使用内存池随着JVM虚拟机和JT即时编译技术的发展,对象的分配和回收是一个非常轻量级的工作 。但是对于缓冲区 Buffer,情况却稍有不同,特别是堆外直接内存的分配和回收,是一个耗时的操作 。
为了尽量重用缓冲区,Nety提供了基于内存池的缓冲区重用机制 。
在百万级的情况下,需要为每个接入的端侧设备至少分配一个接收和发送缓冲区对象,采用传统的非池模式,每次消息读写都需要创建和释放 ByteBuf对象,如果有100万个连接,每秒上报一次数据或者心跳,就会有100万次/秒的 ByteBuf对象申请和释放,即便服务端的内存可以满足要求,GC的压力也会非常大 。
以上问题最有效的解决方法就是使用内存池,每个 NioEventLoop线程处理N个链路,在线程内部,链路的处理是串行的 。假如A链路首先被处理,它会创建接收缓冲区等对象,待解码完成,构造的POJO对象被封装成任务后投递到后台的线程池中执行,然后接收缓冲区会被释放,每条消息的接收和处理都会重复接收缓冲区的创建和释放 。如果使用内存池,则当A链路接收到新的数据报时,从 NioEventLoop的内存池中申请空闲的 ByteBuf,解码后调用 release将 ByteBuf释放到内存池中,供后续的B链路使用 。


推荐阅读