- 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
- 修改/etc/security/limits.conf 文件
- 如果修改失败:
- 操作系统:
- 设置合理的线程数
- 如果发现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 堆大小
- 选择合适的垃圾回收器和回收策略
- GC:吞吐量、延迟和内存占用不能兼得
- 水平触发 (LT)
- 当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写
- 如果这次没有把数据一次性全部读写完,那么下次调用 epoll_wait() 时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你
- 边缘触发 (ET):比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符
- 当被监控的文件描述符上有可读写事件发生时,epoll_wait() 会通知处理程序去读写
- 如果这次没有把数据全部读写完,那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你
- 触发模式
- select(),poll()模型都是水平触发模式
- 信号驱动IO 是边缘触发模式
- epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发
- JDK 中的select 实现是水平触发
- Netty提供的Epoll 的实现中是边缘触发
上一篇
JVM-常量池
2024-09-09
下一篇
Netty-常用组件
2024-09-06