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 等问题
相关推荐
开心猴爷4 分钟前
移动端网页调试实战 Cookie 丢失问题的排查与优化
后端
kaika15 分钟前
告别复杂配置!使用 1Panel 运行环境功能轻松搭建 Java 应用
java·1panel·建站·halo
用户5724056145 分钟前
解析Json
后端
舒一笑6 分钟前
Mac 上安装并使用 frpc(FRP 内网穿透客户端)指南
后端·网络协议·程序员
每天学习一丢丢11 分钟前
Spring Boot + Vue 项目用宝塔面板部署指南
vue.js·spring boot·后端
邹小邹12 分钟前
Go 1.25 强势来袭:GC 速度飙升、并发测试神器上线,内存检测更精准!
后端·go
有梦想的攻城狮12 分钟前
Java 11中的Collections类详解
java·windows·python·java11·collections
颜颜yan_13 分钟前
企业级时序数据库选型指南:从传统架构向智能时序数据管理的转型之路
数据库·架构·时序数据库
lichenyang45315 分钟前
管理项目服务器连接数据库
数据库·后端
生无谓17 分钟前
在Windows系统上安装多个JDK版本并切换
后端