Netty-调优


  • Selector BUG 会导致 Selector 空轮询,最终导致CPU 100%(JDK1.8 的131 版本中依然存在)
    • JDK 认为linux 的epoll 告诉我事件来了,但是 JDK 没有拿到任何事件(READ、WRITE、CONNECT、ACCPET),但此时select()方法不再选择阻塞了,而是选择返回了 0,于是就会进入一种无限循环,导致 CPU 100%
    • 在部分Linux 的 2.6 的 kernel 中,poll 和epoll 对于突然中断的连接socket 会对返回的 eventSet 事件集合置为POLLHUP 或POLLERR,eventSet 事件集合发生了变化,这就可能导致Selector 会被唤醒
    • 但是这个时候selector 的select 方法返回numKeys是0,所以下面本应该对 key 值进行遍历的事件处理根本执行不了,又回到最上面的while(true)循环,循环往复,不断的轮询,直到 linux 系统出现100%的 CPU 情况
  • Netty 是如何解决JDK 中的Selector BUG 的
    • 对 Selector 的 select 操作周期进行统计,每完成一次空的 select 操作进行一次计数,若在某个周期内连续发生N 次空轮询,则触发了epoll 死循环 bug
    • 重建Selector,判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的Selector上去除注册,重新注册到新的 Selector 上,并将原来的 Selector 关闭
  • 如何让单机下Netty 支持百万长连接
    • 操作系统:ulimit –n 1000000(默认1024)
      • 如果修改失败:
        • 修改/etc/security/limits.conf 文件
          • soft nofile 1000000:软限制, 表示警告的限制
          • hard nofile 1000000:硬限制,表示真正限制
        • 修改/etc/pam.d/login 文件
          • session required /lib/security/pam_limits.so
            • 在用户完成系统登录后,应该调用pam_limits.so 模块来设置系统对该用户可使用的各种资源数量的最大限制(包括用户可打开的最大文件数限制),而pam_limits.so模块就会从/etc/security/limits.conf 文件中读取配置来设置这些限制值
        • 修改 sysctl.conf 文件:系统级硬限制
          • fs.file_max = 1000000
          • sysctl -p
  • 设置合理的线程数
    • 如果发现IO 线程的热点停留在读或者写操作,或者停留在 Channelhandler 的执行处,则可以通过适当调大 Nio EventLoop 线程的个数来提升网络的读写性能
    • 如果连续采集几次进行对比,发现线程堆栈都停留在 Selectorlmpl. lockAndDoSelect,则说明IO 线程比较空闲,无须对工作线程数做调整
  • 心跳优化
    • 要能够及时检测失效的连接,并将其剔除,防止无效的连接句柄积压,导致OOM 等问题
    • 设置合理的心跳周期,防止心跳定时任务积压,造成频繁的老年代GC
    • 使用Nety 提供的链路空闲检测机制,不要自己创建定时任务线程池,加重系统的负担
      • 读空闲, 链路持续时间T 没有读取到任何消息
      • 写空闲, 链路持续时间T 没有发送任何消息
      • 读写空闲, 链路持续时间T 没有接收或者发送任何消息
    • 对于百万级的服务器,一般不建议很长的心跳周期和超时时长
  • 接收和发送缓冲区调优
    • 在一些场景下, 端侧设备会周期性地上报数据和发送心跳, 单个链路的消息收发量并不大, 针对此类场景, 可以通过调小 TCP 的接收和发送缓冲区来降低单个TCP 连接的资源占用率
    • 当然对于不同的应用场景, 收发缓冲区的最优值可能不同, 用户需要根据实际场景, 结合性能测试数据进行针对性的调优
  • 合理使用内存池:堆外直接内存和堆内存
    • 由于 DirectByteBuf 的创建成本比较高, 因此如果使用 DirectByteBuf, 则需要配合内存池使用,否则性价比可能还不如 Heap Byte
    • Netty 默认的IO 读写操作采用的都是内存池的堆外直接内存模式, 如果用户需要额外使 用 ByteBuf, 建议也采用内存池方式; 如果不涉及网络IO 操作 (只是纯粹的内存操作), 可以使用堆内存池, 这样内存的创建效率会更高一些
  • IO 线程和业务线程分离
    • 如果服务端不做复杂的业务逻辑操作, 仅是简单的内存操作和消息转发, 则可以通过调大 NioEventLoop 工作线程池的方式,直接在IO 线程中执行业务 Channelhandler, 这样便减少了一次线程上下文切换,性能反而更高
    • 如果有复杂的业务逻辑操作, 则建议IO 线程和业务线程分离, 对于IO 线程, 由于互相之间不存在锁竞争, 可以创建一个大的 NioEvent Loop Group 线程组, 所有 Channel 都共享同一个线程池
    • 对于后端的业务线程池, 则建议创建多个小的业务线程池,线程池可以与 IO 线程绑定, 这样既减少了锁竞争, 又提升了后端的处理性能
  • 针对端侧并发连接数的流控:FlowControlchannelhandler
    • 加到 ChannelPipeline 靠前的位置
    • 覆盖 channelActive() 方法
    • 如果达到流控阈值, 则拒绝该连接, 调用 ChannelHandler Context 的 close 方法关闭连接
  • JVM 层面相关性能优化
    • GC:吞吐量、延迟和内存占用不能兼得
      • GC 数据的采集和研读
      • 设置合适的 JVM 堆大小
      • 选择合适的垃圾回收器和回收策略
  • 水平触发 (LT)
    • 当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写
    • 如果这次没有把数据一次性全部读写完,那么下次调用 epoll_wait() 时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你
  • 边缘触发 (ET):比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符
    • 当被监控的文件描述符上有可读写事件发生时,epoll_wait() 会通知处理程序去读写
    • 如果这次没有把数据全部读写完,那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你
  • 触发模式
    • select(),poll()模型都是水平触发模式
    • 信号驱动IO 是边缘触发模式
    • epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发
    • JDK 中的select 实现是水平触发
    • Netty提供的Epoll 的实现中是边缘触发

文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录