Bootstrap

netty案例,netty4.1中级拓展篇十二《Netty流量整形数据流速率控制分析与实战》

前言介绍

你是否使用过某盘,在前几年我们使用的时候速度飞快,上传下载嗖嗖嗖。但是近年来只要不办会员,速度慢的像蜗牛,当然人家也得赚钱我们能理解。那么这样的限速是怎么实现的呢,我们这个案例使用Netty的流量整形进行限速传输,测试过程中当你把流量整形功能去掉后你就是年费VIP。

流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。流量整形与流量监管的主要区别在于,流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。流量整形与流量监管的另一区别是,整形可能会增加延迟,而监管几乎不引入额外的延迟。

Netty中通过实现抽象类AbstractTrafficShapingHandler,提供了三个流量整形的类;GlobalTrafficShapingHandler、ChannelTrafficShapingHandler、GlobalChannelTrafficShapingHandler;

>AbstractTrafficShapingHandler.java | 功能介绍

/**
 * 

AbstractTrafficShapingHandler allows to limit the global bandwidth * (see {@link GlobalTrafficShapingHandler}) or per session * bandwidth (see {@link ChannelTrafficShapingHandler}), as traffic shaping. * It allows you to implement an almost real time monitoring of the bandwidth using * the monitors from {@link TrafficCounter} that will call back every checkInterval * the method doAccounting of this handler.

* *

If you want for any particular reasons to stop the monitoring (accounting) or to change * the read/write limit or the check interval, several methods allow that for you:

*
    *
  • configure allows you to change read or write limits, or the checkInterval
  • *
  • getTrafficCounter allows you to have access to the TrafficCounter and so to stop * or start the monitoring, to change the checkInterval directly, or to have access to its values.
  • *
*/ public abstract class AbstractTrafficShapingHandler extends ChannelDuplexHandler{...}

AbstractTrafficShapingHandler允许限制全局的带宽(见GlobalTrafficShapingHandler)或者每个session的带宽(见ChannelTrafficShapingHandler)作为流量整形。

它允许你使用TrafficCounter来实现几乎实时的带宽监控,TrafficCounter会在每个检测间期(checkInterval)调用这个处理器的doAccounting方法。

如果你有任何特别的原因想要停止监控(计数)或者改变读写的限制或者改变检测间期(checkInterval),可以使用如下方法:

1、configure:允许你改变读或写的限制,或者检测间期(checkInterval);

2、getTrafficCounter:允许你获得TrafficCounter,并可以停止或启动监控,直接改变检测间期(checkInterval),或去访问它的值。

TrafficCounter:对读和写的字节进行计数以用于限制流量。

它会根据给定的检测间期周期性的计算统计入站和出站的流量,并会回调AbstractTrafficShapingHandler的doAccounting方法。

如果检测间期(checkInterval)是0,将不会进行计数并且统计只会在每次读或写操作时进行计算。

>GlobalTrafficShapingHandler.java | 全局限制

/**
 * 

This implementation of the {@link AbstractTrafficShapingHandler} is for global * traffic shaping, that is to say a global limitation of the bandwidth, whatever * the number of opened channels.

*

Note the index used in {@code OutboundBuffer.setUserDefinedWritability(index, boolean)} is 2.

* *

The general use should be as follow:

*
    *
  • Create your unique GlobalTrafficShapingHandler like:

    *

    GlobalTrafficShapingHandler myHandler = new GlobalTrafficShapingHandler(executor);

    *

    The executor could be the underlying IO worker pool

    *

    pipeline.addLast(myHandler);

    * *

    Note that this handler has a Pipeline Coverage of "all" which means only one such handler must be created * and shared among all channels as the counter must be shared among all channels.

    * *

    Other arguments can be passed like write or read limitation (in bytes/s where 0 means no limitation) * or the check interval (in millisecond) that represents the delay between two computations of the * bandwidth and so the call back of the doAccounting method (0 means no accounting at all).

    * *

    A value of 0 means no accounting for checkInterval. If you need traffic shaping but no such accounting, * it is recommended to set a positive value, even if it is high since the precision of the * Traffic Shaping depends on the period where the traffic is computed. The highest the interval, * the less precise the traffic shaping will be. It is suggested as higher value something close * to 5 or 10 minutes.

    * *

    maxTimeToWait, by default set to 15s, allows to specify an upper bound of time shaping.

    *
  • *
  • In your handler, you should consider to use the {@code channel.isWritable()} and * {@code channelWritabilityChanged(ctx)} to handle writability, or through * {@code future.addListener(new GenericFutureListener())} on the future returned by * {@code ctx.write()}.
  • *
  • You shall also consider to have object size in read or write operations relatively adapted to * the bandwidth you required: for instance having 10 MB objects for 10KB/s will lead to burst effect, * while having 100 KB objects for 1 MB/s should be smoothly handle by this TrafficShaping handler.

  • *
  • Some configuration methods will be taken as best effort, meaning * that all already scheduled traffics will not be * changed, but only applied to new traffics.

    * So the expected usage of those methods are to be used not too often, * accordingly to the traffic shaping configuration.
  • *
* * Be sure to call {@link #release()} once this handler is not needed anymore to release all internal resources. * This will not shutdown the {@link EventExecutor} as it may be shared, so you need to do this by your own. */ @Sharable public class GlobalTrafficShapingHandler extends AbstractTrafficShapingHandler {}

这实现了AbstractTrafficShapingHandler的全局流量整形,也就是说它限制了全局的带宽,无论开启了几个channel。

注意『 OutboundBuffer.setUserDefinedWritability(index, boolean)』中索引使用’2’。

一般用途如下:

创建一个唯一的GlobalTrafficShapingHandler

GlobalTrafficShapingHandler myHandler = new GlobalTrafficShapingHandler(executor);
pipeline.addLast(myHandler);

executor可以是底层的IO工作池

注意,这个处理器是覆盖所有管道的,这意味着只有一个处理器对象会被创建并且作为所有channel间共享的计数器,它必须于所有的channel共享。

所有你可以见到,该类的定义上面有个@Sharable注解。

在你的处理器中,你需要考虑使用『channel.isWritable()』和『channelWritabilityChanged(ctx)』来处理可写性,或通过在ctx.write()返回的future上注册listener来实现。

你还需要考虑读或写操作对象的大小需要和你要求的带宽相对应:比如,你将一个10M大小的对象用于10KB/s的带宽将会导致爆发效果,若你将100KB大小的对象用于在1M/s带宽那么将会被流量整形处理器平滑处理。

一旦不在需要这个处理器时请确保调用『release()』以释放所有内部的资源。这不会关闭EventExecutor,因为它可能是共享的,所以这需要你自己做。

GlobalTrafficShapingHandler中持有一个Channel的哈希表,用于存储当前应用所有的Channel:

private final ConcurrentMap channelQueues = PlatformDependent.newConcurrentHashMap();

key为Channel的hashCode;value是一个PerChannel对象。

PerChannel对象中维护有该Channel的待发送数据的消息队列(ArrayDeque messagesQueue)。

>ChannelTrafficShapingHandler.java | 功能介绍

/**
 * 

This implementation of the {@link AbstractTrafficShapingHandler} is for channel * traffic shaping, that is to say a per channel limitation of the bandwidth.

*

Note the index used in {@code OutboundBuffer.setUserDefinedWritability(index, boolean)} is 1.

* *

The general use should be as follow:

*
    *
  • Add in your pipeline a new ChannelTrafficShapingHandler.

    *

    ChannelTrafficShapingHandler myHandler = new ChannelTrafficShapingHandler();

    *

    pipeline.addLast(myHandler);

    * *

    Note that this handler has a Pipeline Coverage of "one" which means a new handler must be created * for each new channel as the counter cannot be shared among all channels..

    * *

    Other arguments can be passed like write or read limitation (in bytes/s where 0 means no limitation) * or the check interval (in millisecond) that represents the delay between two computations of the * bandwidth and so the call back of the doAccounting method (0 means no accounting at all).

    * *

    A value of 0 means no accounting for checkInterval. If you need traffic shaping but no such accounting, * it is recommended to set a positive value, even if it is high since the precision of the * Traffic Shaping depends on the period where the traffic is computed. The highest the interval, * the less precise the traffic shaping will be. It is suggested as higher value something close * to 5 or 10 minutes.

    * *

    maxTimeToWait, by default set to 15s, allows to specify an upper bound of time shaping.

    *
  • *
  • In your handler, you should consider to use the {@code channel.isWritable()} and * {@code channelWritabilityChanged(ctx)} to handle writability, or through * {@code future.addListener(new GenericFutureListener())} on the future returned by * {@code ctx.write()}.
  • *
  • You shall also consider to have object size in read or write operations relatively adapted to * the bandwidth you required: for instance having 10 MB objects for 10KB/s will lead to burst effect, * while having 100 KB objects for 1 MB/s should be smoothly handle by this TrafficShaping handler.

  • *
  • Some configuration methods will be taken as best effort, meaning * that all already scheduled traffics will not be * changed, but only applied to new traffics.

    *

    So the expected usage of those methods are to be used not too often, * accordingly to the traffic shaping configuration.

  • *
*/ public class ChannelTrafficShapingHandler extends AbstractTrafficShapingHandler {...}

ChannelTrafficShapingHandler是针对单个Channel的流量整形,和GlobalTrafficShapingHandler的思想是一样的。只是实现中没有对全局概念的检测,仅检测了当前这个Channel的数据。

这里就不再赘述了。

>GlobalChannelTrafficShapingHandler.java | 功能介绍

/**
 * This implementation of the {@link AbstractTrafficShapingHandler} is for global
 * and per channel traffic shaping, that is to say a global limitation of the bandwidth, whatever
 * the number of opened channels and a per channel limitation of the bandwidth.

* This version shall not be in the same pipeline than other TrafficShapingHandler.

* * The general use should be as follow:
*
    *
  • Create your unique GlobalChannelTrafficShapingHandler like:

    * GlobalChannelTrafficShapingHandler myHandler = new GlobalChannelTrafficShapingHandler(executor);

    * The executor could be the underlying IO worker pool
    * pipeline.addLast(myHandler);

    * * Note that this handler has a Pipeline Coverage of "all" which means only one such handler must be created * and shared among all channels as the counter must be shared among all channels.

    * * Other arguments can be passed like write or read limitation (in bytes/s where 0 means no limitation) * or the check interval (in millisecond) that represents the delay between two computations of the * bandwidth and so the call back of the doAccounting method (0 means no accounting at all).
    * Note that as this is a fusion of both Global and Channel Traffic Shaping, limits are in 2 sets, * respectively Global and Channel.

    * * A value of 0 means no accounting for checkInterval. If you need traffic shaping but no such accounting, * it is recommended to set a positive value, even if it is high since the precision of the * Traffic Shaping depends on the period where the traffic is computed. The highest the interval, * the less precise the traffic shaping will be. It is suggested as higher value something close * to 5 or 10 minutes.

    * * maxTimeToWait, by default set to 15s, allows to specify an upper bound of time shaping.

    *
  • *
  • In your handler, you should consider to use the {@code channel.isWritable()} and * {@code channelWritabilityChanged(ctx)} to handle writability, or through * {@code future.addListener(new GenericFutureListener())} on the future returned by * {@code ctx.write()}.
  • *
  • You shall also consider to have object size in read or write operations relatively adapted to * the bandwidth you required: for instance having 10 MB objects for 10KB/s will lead to burst effect, * while having 100 KB objects for 1 MB/s should be smoothly handle by this TrafficShaping handler.

  • *
  • Some configuration methods will be taken as best effort, meaning * that all already scheduled traffics will not be * changed, but only applied to new traffics.
    * So the expected usage of those methods are to be used not too often, * accordingly to the traffic shaping configuration.
  • *

* * Be sure to call {@link #release()} once this handler is not needed anymore to release all internal resources. * This will not shutdown the {@link EventExecutor} as it may be shared, so you need to do this by your own. */ @Sharable public class GlobalChannelTrafficShapingHandler extends AbstractTrafficShapingHandler {...}

相比于GlobalTrafficShapingHandler增加了一个误差概念,以平衡各个Channel间的读/写操作。也就是说,使得各个Channel间的读/写操作尽量均衡。比如,尽量避免不同Channel的大数据包都延迟近乎一样的是时间再操作,以及如果小数据包在一个大数据包后才发送,则减少该小数据包的延迟发送时间等。。

开发环境

1、jdk1.8【jdk1.7以下只能部分支持netty】

2、Netty4.1.36.Final【netty3.x 4.x 5每次的变化较大,接口类名也随着变化】

代码示例

itstack-demo-netty-2-12
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.netty
    │           ├── client
    │           │	├── MyChannelInitializer.java
    │           │	├── MyClientHandler.java
    │           │	└── NettyClient.java
    │           └── server
    │           	├── common
    │           	│	└── MyServerCommonHandler.java	
    │           	├── MyChannelInitializer.java
    │           	├── MyServerHandler.java	
    │           	└── NettyServer.java
    │
    └── test
         └── java
             └── org.itstack.demo.test
                 └── ApiTest.java

部分重点代码块讲解,获取全部代码,关注公众号:bugstack虫洞栈 | 回复netty源码

>client/MyChannelInitializer.java | 增加Channel流量整形配置,速率设置为10bytes/s

/**
 * 虫洞栈:https://bugstack.cn
 * 公众号:bugstack虫洞栈  {获取学习源码}
 * Create by fuzhengwei on 2019
 */
public class MyChannelInitializer extends ChannelInitializer {

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        //流量整形
        channel.pipeline().addLast(new ChannelTrafficShapingHandler(10, 10));
        // 基于换行符号
        channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
        // 解码转String,注意调整自己的编码格式GBK、UTF-8
        channel.pipeline().addLast(new StringDecoder(Charset.forName("GBK")));
        // 解码转String,注意调整自己的编码格式GBK、UTF-8
        channel.pipeline().addLast(new StringEncoder(Charset.forName("GBK")));
        // 在管道中添加我们自己的接收数据实现方法
        channel.pipeline().addLast(new MyClientHandler());
    }

}

>server/common/MyServerCommonHandler.java | 提供抽象类,监控发送速率以及获取发送状态

/**
 * 虫洞栈:https://bugstack.cn
 * 公众号:bugstack虫洞栈 | 欢迎关注并获取专题&源码
 * Create by fuzhengwei on 2019
 */
public abstract class MyServerCommonHandler extends SimpleChannelInboundHandler {

    protected boolean sentFlag;
    private Runnable counterTask;
    private AtomicLong consumeMsgLength = new AtomicLong();
    private long priorProgress;

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        counterTask = () -> {
            while (true) {
                try {
                    Thread.sleep(500);
                    long length = consumeMsgLength.getAndSet(0);
                    if (0 == length) continue;
                    System.out.println("数据发送速率(KB/S):" + length);
                } catch (InterruptedException ignored) {
                }
            }
        };
        super.handlerAdded(ctx);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        sendData(ctx);
        //启动监控线程
        new Thread(counterTask).start();
    }

    protected abstract void sendData(ChannelHandlerContext ctx);

    protected ChannelProgressivePromise getChannelProgressivePromise(ChannelHandlerContext ctx, Consumer completedAction) {
        ChannelProgressivePromise channelProgressivePromise = ctx.newProgressivePromise();
        channelProgressivePromise.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
                consumeMsgLength.addAndGet(progress - priorProgress);
                priorProgress = progress;
            }

            @Override
            public void operationComplete(ChannelProgressiveFuture future) throws Exception {
                sentFlag = false;
                if (future.isSuccess()) {
                    System.out.println("微信公众号:bugstack虫洞栈 | 提醒,消息发送成功!");
                    priorProgress -= 10;
                    Optional.ofNullable(completedAction).ifPresent(action -> action.accept(future));
                } else {
                    System.out.println("微信公众号:bugstack虫洞栈 | 提醒,消息发送失败!");
                    future.cause().printStackTrace();
                }
            }
        });
        return channelProgressivePromise;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println("微信公众号:bugstack虫洞栈 | NettyServer接收到消息:" + msg);
    }

}

>server/MyChannelInitializer.java | 增加全局流量整形配置,速率设置为10bytes/s

/**
 * 虫洞栈:https://bugstack.cn
 * 公众号:bugstack虫洞栈 | 欢迎关注并获取专题&源码
 * Create by fuzhengwei on 2019
 */
public class MyChannelInitializer extends ChannelInitializer {

    @Override
    protected void initChannel(SocketChannel channel) {

        // 基于换行符号
        channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
        // 流量整形;writeLimit/readLimit{0 or a limit in bytes/s}
        channel.pipeline().addLast(new GlobalTrafficShapingHandler(channel.eventLoop().parent(), 10, 10));
        // 解码转String,注意调整自己的编码格式GBK、UTF-8
        channel.pipeline().addLast(new StringDecoder(Charset.forName("GBK")));
        // 解码转String,注意调整自己的编码格式GBK、UTF-8
        channel.pipeline().addLast(new StringEncoder(Charset.forName("GBK")));
        // 在管道中添加我们自己的接收数据实现方法
        channel.pipeline().addLast(new MyServerHandler());

    }

}

>server/MyServerHandler.java | 处理消息验证是否可以发送ctx.channel().isWritable()

/**
 * 虫洞栈:https://bugstack.cn
 * 公众号:bugstack虫洞栈 | 欢迎关注并获取专题&源码
 * Create by fuzhengwei on 2019
 */
public class MyServerHandler extends MyServerCommonHandler {

    @Override
    protected void sendData(ChannelHandlerContext ctx) {
        sentFlag = true;
        ctx.writeAndFlush( "111111111122222222223333333333\r\n", getChannelProgressivePromise(ctx, new Consumer() {
            @Override
            public void accept(ChannelProgressiveFuture channelProgressiveFuture) {
                if (ctx.channel().isWritable() && !sentFlag) {
                    sendData(ctx);
                }
            }
        }));
    }

}

测试结果

>启动服务端NettyServer | 可以看到速率已经被限制

itstack-demo-netty server start done. {关注公众号:bugstack虫洞栈 | 获取专题案例源码}
微信公众号:bugstack虫洞栈 | 提醒,消息发送成功!
数据发送速率(KB/S):32
微信公众号:bugstack虫洞栈 | 提醒,消息发送成功!
数据发送速率(KB/S):10
微信公众号:bugstack虫洞栈 | NettyServer接收到消息:876d251b-aba8-481a-81d0-e123a4c42214
微信公众号:bugstack虫洞栈 | 提醒,消息发送成功!
数据发送速率(KB/S):10
微信公众号:bugstack虫洞栈 | NettyServer接收到消息:250d53fb-acc3-4390-b5c5-a660577fff6f
微信公众号:bugstack虫洞栈 | 提醒,消息发送成功!
数据发送速率(KB/S):10
微信公众号:bugstack虫洞栈 | NettyServer接收到消息:89cad8a0-8e5b-44ef-812b-39c4b2d2e0fb
微信公众号:bugstack虫洞栈 | 提醒,消息发送成功!
数据发送速率(KB/S):10
微信公众号:bugstack虫洞栈 | NettyServer接收到消息:e951ca01-a583-4c20-b884-5c272b1cc7a4
微信公众号:bugstack虫洞栈 | 提醒,消息发送成功!
数据发送速率(KB/S):10
微信公众号:bugstack虫洞栈 | NettyServer接收到消息:4b13d77c-188f-4613-9cd9-94a2a7751932
微信公众号:bugstack虫洞栈 | 提醒,消息发送成功!
数据发送速率(KB/S):10
微信公众号:bugstack虫洞栈 | 提醒,消息发送成功!
微信公众号:bugstack虫洞栈 | NettyServer接收到消息:fdc5378c-a594-4be8-885d-4caa7ecccd82
数据发送速率(KB/S):10

Process finished with exit code -1

>启动客户端NettyClient | 可以看到速率已经被限制

itstack-demo-netty client start done. {关注公众号:bugstack虫洞栈 | 获取专题案例源码}
微信公众号:bugstack虫洞栈 | NettyClient接收到消息:111111111122222222223333333333 length:30
微信公众号:bugstack虫洞栈 | NettyClient接收到消息:111111111122222222223333333333 length:30
微信公众号:bugstack虫洞栈 | NettyClient接收到消息:111111111122222222223333333333 length:30
微信公众号:bugstack虫洞栈 | NettyClient接收到消息:111111111122222222223333333333 length:30
微信公众号:bugstack虫洞栈 | NettyClient接收到消息:111111111122222222223333333333 length:30
微信公众号:bugstack虫洞栈 | NettyClient接收到消息:111111111122222222223333333333 length:30
微信公众号:bugstack虫洞栈 | NettyClient接收到消息:111111111122222222223333333333 length:30
微信公众号:bugstack虫洞栈 | NettyClient接收到消息:111111111122222222223333333333 length:30

Process finished with exit code -1

------------