Netty流量整形:保障微服务通信稳定性的关键策略

流量整形前瞻

当系统负载压力比较大时,系统进入过负荷状态,可能是CPU、内存资源已经过载,也可能是应用进程内部的资源几乎耗尽,如果继续全量处理业务,可能会导致长时间的Full GC、消息严重积压或者应用进程宕机,最终将压力转移到集群中的其他节点,引起级联故障,通过动态流控技术,拒绝一定比例新接入的请求消息,可以保障系统不被压垮,但除了动态流控之外,有时候还需要对消息的读取和发送速度做控制,以便消息能以较恒定的速度发送到下游系统,保护下游各系统不受突发的流量冲击,通过 Netty提供的流量整形功能,就可以达到控制消息读取和发送速度的目标。

流量整形和流量控制的区别以及作用

流量整形是一种主动调整流量输出速度的措施,流量整形与流量控制的主要区别在于,流量整形是对流量控制中需要丢弃的报文进行缓存---通常是将它们放入缓冲区或队列,当令牌桶有足够多的令牌时,再均匀地向外发送这些被缓存的报文,流量整形与流量控制的另一区别是,流量整形可能会增加延迟,而流量控制几乎不引入额外的延迟。

流量整形的主要作用有两个,一个是防止由于上、下游应用系统性能不均衡导致下游应用服务被压垮,业务流程中断,另一个是防止由于通信模块接收消息过快,后端业务线程处理不及时,导致出现"撑死"问题。

Netty流量整形功能简介

Netty内置了三种流量整形功能,分别如下:

单个Channel链路的流量整形:ChannelTrafficShapingHandler,针对单个Channel链路的消息发送和读取速度进行控制。

全局流量整形:GlobalTrafficShapingHandler,针对Netty应用服务进程所有Channel链路的消息发送和读取速度的总和进行控制。

全局和单个链路综合型流量整形:GlobalChannelTrafficShapingHandler,既包含全局流量整形,又有单个channel链路的流量整形。

Netty流量整形应用

流量整形应用相对比较简单,只需要将流量整形ChannelHandler添加到业务解码器之前,即可对消息的读取和发送速度进行均匀控制,而且不会丢弃消息

less 复制代码
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 100)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline p = ch.pipeline();
                if (sslCtx != null) {
                    p.addLast(sslCtx.newHandler(ch.alloc()));
                }
                p.addLast(new ChannelTrafficShapingHandler(1024 * 1024, 1024 * 1024, 1000));
                p.addLast(new StringDecoder());
                p.addLast(new StringEncoder());
                p.addLast(serverHandler);
            }
        });

以上示例新增了ChannelTrafficShapingHandler针对单个Channel的流量整形控制器,要求读写速率不能超过1M,在1000毫秒这个时间窗口内。

Netty流量整形工作机制

本质上是拦截channelRead和write方法,计算当前需要发送的消息大小,对读取和发送阈值进行判断,如果达到了阈值,则暂停读取和发送消息,待下一个周期继续处理,以实现在某个周期内对消息读写速度进行控制。

我们以ChannelTrafficShapingHandle的读取消息为例来具体展开讲解:

以上的流程图总结的是消息读取流量整形的流程,具体重点的代码段参考AbstractTrafficShapingHandler的channelRead方法,AbstractTrafficShapingHandler是ChannelTrafficShapingHandler的父类,也是其余两个流量整形handler的父类。继承了ChannelDuplexHandler,同时具备入站和出站的处理能力。

AbstractTrafficShapingHandler的channelRead方法代码,wait就是计算出来的等待时间,核心在readTimeToWait方法中

ini 复制代码
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
    long size = calculateSize(msg);
    long now = TrafficCounter.milliSecondFromNano();
    if (size > 0) {
        long wait = trafficCounter.readTimeToWait(size, readLimit, maxTime, now);
        wait = checkWaitReadTime(ctx, wait, now);
        if (wait >= MINIMAL_WAIT) { // At least 10ms seems a minimal
            Channel channel = ctx.channel();
            ChannelConfig config = channel.config();
            if (logger.isDebugEnabled()) {
                logger.debug("Read suspend: " + wait + ':' + config.isAutoRead() + ':'
                        + isHandlerActive(ctx));
            }
            if (config.isAutoRead() && isHandlerActive(ctx)) {
                config.setAutoRead(false);
                channel.attr(READ_SUSPENDED).set(true);
                Attribute<Runnable> attr = channel.attr(REOPEN_TASK);
                Runnable reopenTask = attr.get();
                if (reopenTask == null) {
                    reopenTask = new ReopenReadTimerTask(ctx);
                    attr.set(reopenTask);
                }
                ctx.executor().schedule(reopenTask, wait, TimeUnit.MILLISECONDS);
                if (logger.isDebugEnabled()) {
                    logger.debug("Suspend final status => " + config.isAutoRead() + ':'
                            + isHandlerActive(ctx) + " will reopened at: " + wait);
                }
            }
        }
    }
    informReadOperation(ctx, now);
    ctx.fireChannelRead(msg);
}

在readTimeToWait方法中,核心的计算逻辑也就是long time = sum * 1000 / limitTraffic - interval + pastDelay拆解一下这个方法计算的逻辑:

sum * 1000 / limitTraffic : 总计需要多长时间才能够按照给定的速率处理完这些数据,sum也即是时间窗口内总共读取的字节数,limitTraffic也就是设置的读取字节的速度 也即是前面设置的 1024* 1024

interval = now - lastTimeCheck :时间窗口内处理之前的数据已经花费多长时间了

pastDelay :还没有到应该读取的时间但唤醒去读取,后续再延迟读取时需要增加的延迟时间

在获取到time之后,若大于10ms就返回这个time,让Channel去延时读取数据,保障消息的读取速率符合用户的设定

ini 复制代码
public long readTimeToWait(final long size, final long limitTraffic, final long maxTime, final long now) {
    bytesRecvFlowControl(size);
    if (size == 0 || limitTraffic == 0) {
        return 0;
    }
    final long lastTimeCheck = lastTime.get();
    long sum = currentReadBytes.get();
    long localReadingTime = readingTime;
    long lastRB = lastReadBytes;
    final long interval = now - lastTimeCheck;
    long pastDelay = Math.max(lastReadingTime - lastTimeCheck, 0);
    if (interval > AbstractTrafficShapingHandler.MINIMAL_WAIT) {
        long time = sum * 1000 / limitTraffic - interval + pastDelay;
        if (time > AbstractTrafficShapingHandler.MINIMAL_WAIT) {
            if (logger.isDebugEnabled()) {
                logger.debug("Time: " + time + ':' + sum + ':' + interval + ':' + pastDelay);
            }
            if (time > maxTime && now + time - localReadingTime > maxTime) {
                time = maxTime;
            }
            readingTime = Math.max(localReadingTime, now + time);
            return time;
        }
        readingTime = Math.max(localReadingTime, now);
        return 0;
    }

Netty流量整形使用的一些注意事项

  1. 流量整形ChannelHandler添加位置,因为需要计算请求和发送消息的大小,消息类型必须是ByteBuf或者ByteBufHolder,所以流量整形ChannelHandler 需要添加到业务编码之后、解码之前。
  2. 全局流量整形,GlobalChannelTrafficShapingHandler 和GlobalTrafficShapingHandler 是全局共享的,因此实例只需要创建一次,添加到不同的ChannelPipeline即可,不要创建多个实例,否则流量整形将失效。
  3. 消息发送保护机制,通过流量整形可以控制发送速度,但是它的控制原理是将待发送的消息封装成Task放入消息队列,等待执行时间到达后继续发送,所以如果业务发送线程不判断Channel的可写状态,就可能会导致 OOM 等问题
相关推荐
DevnullCoffe13 分钟前
基于 OpenClaw + Pangolinfo API 的 Amazon 价格监控系统:架构设计与最佳实践
人工智能·架构
用户2986985301418 分钟前
.NET 文档自动化:Spire.Doc 设置奇偶页页眉/页脚的最佳实践
后端·c#·.net
码路飞1 小时前
GPT-5.3 Instant 终于学会好好说话了,顺手对比了下同天发布的 Gemini 3.1 Flash-Lite
java·javascript
序安InToo1 小时前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy1231 小时前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记1 小时前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang051 小时前
VS Code 配置 Markdown 环境
后端
navms1 小时前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang051 小时前
离线数仓的优化及重构
后端
Nyarlathotep01131 小时前
gin01:初探gin的启动
后端·go