第 12 章 慢业务、线程池与背压
12.1 把慢操作移出 EventLoop
假设收到一行后要同步访问数据库。可以给业务处理器指定单独的执行器组,例如:
DefaultEventExecutorGroup business = new DefaultEventExecutorGroup(4);
// 初始化每条连接时:
pipeline.addLast(business, "business", new MyBusinessHandler());这段是线程模型示意,MyBusinessHandler 需要你实现,并导入 io.netty.util.concurrent.DefaultEventExecutorGroup。执行器组应在服务级创建并复用,不能每条连接创建一组,停机时也要关闭。
把整个处理器交给这种执行器组,与在回调里临时向普通线程池提交每个请求,顺序语义不同。若自己把请求并行提交,后发请求可能先完成;需要按协议决定是否允许乱序,以及怎样关联应答。
12.2 换线程不等于容量无限
业务线程池、待处理任务、数据库连接池都有限。若接收速度长期大于处理速度,队列会增长,内存和延迟都会恶化。
应明确容量和拒绝策略:队列满时返回繁忙、限制读入、拒绝新请求或关闭连接。对于会修改数据的请求,还要明确“拒绝”发生在执行前还是执行后,避免客户端误判。
12.3 关注发送队列
当客户端读取很慢时,服务端出站数据可能积压。channel.isWritable() 是基于写缓冲水位等状态的信号,不是对方能否处理业务的证明,也不是硬性的内存上限。
可以监听 channelWritabilityChanged,在不可写时暂停产生更多消息,恢复可写后继续。仅仅忽略 isWritable 并持续发送,仍可能造成内存压力。设置水位并不会自动替业务拒绝所有后续写入。
12.4 自动读取开关
autoRead 可以影响是否自动继续读入。更复杂的服务会结合高低水位暂停与恢复读取。但暂停读取时必须有可靠的恢复路径,否则连接可能一直卡住。初学阶段先把帧长限制、业务队列容量与超时做好,再逐步增加流控。
小练习: 为什么把数据库查询放进线程池后,仍然可能出现内存不断增长?