从Netty发送消息流程聊聊Netty的高水位和低水位

416 阅读4分钟

大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术

Netty发送消息流程

我们先来了解一下Netty发送消息的流程,再谈Netty中的高水位和低水位

首先我们知道Netty写数据的时候都有两个流程

  1. write: 将数据写入到ChannelOutboundBuffer
  2. flush: 将ChannelOutboundBuffer中的数据写入到SocketChannel中,也就是真正的发送数据,即发送到TCP缓冲区,再通过网卡发送到对端

这里我们再来理解一下操作系统内核的两个缓冲区

  • SO_SEND_BUF: SO_SEND_BUF是操作系统内核的写缓冲区,所有应用程序需要发送到对端的信息,都会放到该缓冲区中,等待发往对端
  • SO_REC_BUFF: SO_REC_BUFF是操作系统内核的读缓冲区,所有从对端接收到的信息,都会放到该缓冲区中,等待应用程序读取

所以我们发送消息的一般流程是

业务 -> write -> ChannelOutboundBuffer -> flush -> SO_SEND_BUF -> 网卡

高低水位

我们想象这么一个场景

当网络出现阻塞或者Netty客户端负载很高的时候,客户端接收速度和处理速度越来越慢。 会出现什么情况

  1. TCP的滑动窗口不断缩小,以减少网络数据的发送,直到为0
  2. Netty服务端有大量频繁的写操作,不断写入到ChannelOutboundBuffer
  3. 但是ChannelOutboundBuffer中的数据flush不到SO_SEND_BUF中,导致ChannelOutboundBuffer中的数据不断增加,最终撑爆ChannelOutboundBuffer导致OOM

所以为了解决这个问题,Netty定义了高低水位,用来表示ChannelOutboundBuffer中的待发送数据的内存占用量的上限和下限

当待发送数据的内存占用总量(totalPendingSize)超过高水位线的时候,Netty 就会将 NioSocketChannel 的状态标记为不可写状态,并触发ChannelWritabilityChanged事件

当待发送数据的内存占用总量(totalPendingSize)低于低水位线的时候,Netty 会再次将 NioSocketChannel 的状态标记为可写状态,并触发ChannelWritabilityChanged事件

ChannelWritabilityChanged数据量有两个计数器,一个是pendingSize、一个是totalPendingSize

由于ChannelWritabilityChanged是由许多个Entry组成的,每个Entry都有一个pendingSizependingSize记录了堆外内存(待发送数据) + 堆内内存(Entry对象自身占用内存)

pendingSize = 堆外内存(待发送数据) + 堆内内存(Entry对象自身占用内存)

ChannelWritabilityChanged 自身还有一个totalPendingSize属性,用来记录所有的pendingSize之和

totalPendingSize = 所有的pendingSize之和

Netty的高低水位就是通过判断totalPendingSize大小来进行判断的

Netty高低水位设置及默认大小

Netty中的高低水位是通过ChannelOption.WRITE_BUFFER_WATER_MARK来设置的,WRITE_BUFFER_WATER_MARK是一个WriteBufferWaterMark对象,它包含两个属性lowhigh,分别表示低水位和高水位

设置高低水位的代码如下

bootstrap.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(32 * 1024, 64 * 1024));

netty中低水位默认是32KB,高水位默认是64KB,是每个Channel独享的

注意事项

如果仅仅只是设置了高低水位参数,但是在写代码中没有对channel.isWritable()进行判断,那么高低水位还是不会生效

所以一般会在ChannelWritabilityChanged事件中添加如下判断

        @Override
        public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
            Channel channel = ctx.channel();
            if (channel.isWritable()) {
                if (!channel.config().isAutoRead()) {
                    channel.config().setAutoRead(true);
                    log.info("Channel[{}] turns writable, bytes to buffer before changing channel to un-writable: {}",
                        RemotingHelper.parseChannelRemoteAddr(channel), channel.bytesBeforeUnwritable());
                }
            } else {
                channel.config().setAutoRead(false);
                log.warn("Channel[{}] auto-read is disabled, bytes to drain before it turns writable: {}",
                    RemotingHelper.parseChannelRemoteAddr(channel), channel.bytesBeforeWritable());
            }
            super.channelWritabilityChanged(ctx);
        }

来源于RocketMQNettyRemotingAbstract

  1. 当写缓冲区快满时,channel变为不可写,关闭自动读取,停止从socket读取数据
  2. 当写缓冲区有空间时,channel变为可写,打开自动读取,继续从socket读取数据

这样可以实现背压机制,当下游处理慢时,自动降低上游发送速度。同时防止出现OOM

我们查看开源框架会发现都有类似的做法

开源框架中的高低水位设置

  • RocketMQ

上面的演示代码就是

  • Pulsar
        @Override
        public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
            // handle backpressure
            // stop/resume reading input from connection between the client and the proxy
            // when the writability of the connection between the proxy and the broker changes
            inboundChannel.config().setAutoRead(ctx.channel().isWritable());
            super.channelWritabilityChanged(ctx);
        }

总结

  1. Netty中的高低水位是通过ChannelOption.WRITE_BUFFER_WATER_MARK来设置的,WRITE_BUFFER_WATER_MARK是一个WriteBufferWaterMark对象,它包含两个属性lowhigh,分别表示低水位和高水位
  2. 如果待发送数据的内存占用总量超过高水位线的时候,Netty 就会将 Channel 的状态标记为不可写状态,并触发ChannelWritabilityChanged事件
  3. 如果待发送数据的内存占用总量低于低水位线的时候,Netty 会再次将 Channel 的状态标记为可写状态,并触发ChannelWritabilityChanged事件
  4. 如果仅仅只是设置了高低水位参数,但是在写代码中没有对channel.isWritable()进行判断,那么高低水位还是不会生效
  5. 高低水位主要是为了防止ChannelOutboundBuffer中的数据不断增加,最终撑爆ChannelOutboundBuffer导致OOM