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 等问题
相关推荐
程序员爱钓鱼6 分钟前
Go语言实战案例 — 工具开发篇:实现一个图片批量压缩工具
后端·google·go
程序员的世界你不懂1 小时前
【Flask】测试平台开发,新增说明书编写和展示功能 第二十三篇
java·前端·数据库
星空寻流年1 小时前
设计模式第一章(建造者模式)
java·设计模式·建造者模式
程序员不迷路1 小时前
湖仓一体学习-数据架构演进路线
架构
gb42152872 小时前
java中将租户ID包装为JSQLParser的StringValue表达式对象,JSQLParser指的是?
java·开发语言·python
曾经的三心草2 小时前
Python2-工具安装使用-anaconda-jupyter-PyCharm-Matplotlib
android·java·服务器
Metaphor6922 小时前
Java 高效处理 Word 文档:查找并替换文本的全面指南
java·经验分享·word
ChinaRainbowSea2 小时前
7. LangChain4j + 记忆缓存详细说明
java·数据库·redis·后端·缓存·langchain·ai编程
舒一笑2 小时前
同步框架与底层消费机制解决方案梳理
后端·程序员
stormsha2 小时前
飞算JavaAI炫技赛电商系统商品管理模块的架构设计与实现
java·架构·鸿蒙系统