WRITE_BUFFER_WATER_MARK

为了防止高并发场景下,由于对方处理慢导致自身消息积压,除了服务端做流控之外,客户端也需要做并发保护,防止自身发生消息积压。当发送队列待发送的字节数组达到高水位上限时,对应的 Channel 就变为不可写状态。由于高水位并不影响业务线程调用 write 方法并把消息加入到待发送队列中,因此,必须要在消息发送时对 Channel 的状态进行判断:当到达高水位时,Channel 的状态被设置为不可写,通过对 Channel 的可写状态进行判断来决定是否发送消息。

image.png

内存池

推送服务器承载了海量的长链接,每个长链接实际就是一个会话。如果每个会话都持有心跳数据、接收缓冲区、指令集等数据结构,而且这些实例随着消息的处理朝生夕灭,这就会给服务器带来沉重的 GC 压力,同时消耗大量的内存。
最有效的解决策略就是使用内存池,每个 NioEventLoop 线程处理 N 个链路,在线程内部,链路的处理时串行的。假如 A 链路首先被处理,它会创建接收缓冲区等对象,待解码完成之后,构造的 POJO 对象被封装成 Task 后投递到后台的线程池中执行,然后接收缓冲区会被释放,每条消息的接收和处理都会重复接收缓冲区的创建和释放。如果使用内存池,则当 A 链路接收到新的数据报之后,从 NioEventLoop 的内存池中申请空闲的 ByteBuf,解码完成之后,调用 release 将 ByteBuf 释放到内存池中,供后续 B 链路继续使用。

使用内存池优化之后,单个 NioEventLoop 的 ByteBuf 申请和 GC 次数从原来的 N = 1000000/64 = 15625 次减少为最少 0 次(假设每次申请都有可用的内存)。

Netty 默认不使用内存池,需要在创建客户端或者服务端的时候进行指定,代码如下:

  1. Bootstrap b = new Bootstrap();
  2. b.group(group)
  3. .channel(NioSocketChannel.class)
  4. .option(ChannelOption.TCP_NODELAY, true)
  5. .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT

如果使用内存池,完成 ByteBuf 的解码工作之后必须显式的调用 ReferenceCountUtil.release(msg) 对接收缓冲区 ByteBuf 进行内存释放,否则它会被认为仍然在使用中,这样会导致内存泄露。