当系统负载压力比较大时,尔凯加果继续全量处理业务,可能会导致长时间的也可能是应用进程内部中负亦田进积宁机,最终将压力转移到集群中的其他节点,引起级Full GC、消息严重积压或者应用进程宕机,最终将压力转移到集群中的其他节点,引起级联故障。通过动态流控,拒绝一定比例新接入的请求消息,可以保障系统不被压垮。除了动态流控,有时候还需要对消息的读取和发送速度做控制,以便消息能以较恒定的速度发送到下游网元,保护下游各系统不受突发的流量冲击 ,通过Netty提供的流量整形功能,就可以达到控制消息读取和发送速度的目标。
流量整形(Traffic Shaping)是一种主动调整流量输出速度的措施。一个典型的应用是基于下游网络节点的TPS指标控制本地流量的输出。流量整形与流量控制的主要区别在于,流量整形是对流量控制中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列。当令牌桶有足够多的令牌时,再均匀地向外发送这些被缓存的报文。流量整形与流量控制的另一区别是,整形可能会增加延迟,而流控几乎不引入额外的延迟。
Netty内置了三种流量整形功能。
流量整形应用相对比较简单, 只需要将流量整形ChannelHandler添加到业务解码器之前,即可对消息的读取和发送速度进行均匀控制,而且不会丢弃消息。下面以服务端对单个Channel的读取速度进行整形为例进行说明。
(1)单个链路的流量整形: ChannelTrafficShapingHandler,可以对某个链路的消息发送和读取速度进行控制。
(2)全局流量整形: GlobalTrafficShapingHandler,针对某个进程所有链路的消息发送和读取速度的总和进行控制。
(3)全局和单个链路综合型流量整形: GlobalChannelTrafficShapingHandler,同时对全局和单个链路的消息发送和读取速度进行控制。
【测试1:ChannelTrafficShapingHandler】
在服务端添加ChannelTrafficShapingHandler对消息读取速度进行整形,代码示例如下( TrafficShappingServer)实现服务端以1MB/s的速率读取消息:
业务处理端中定时打印接收到的消息大小:
流量整形ChannelTrafficShapingHandler原理分析:
(1)在解码之前拦截channelRead方法,计算读取的 ByteBuf大小,源码如下(AbstractTrafficShapingHandler类):
(2)计算需要暂停读取消息的等待时间,代码如下(TrafficCounter类):
(3)满足整形条件,则修改Channel的状态为非自动读取,并将READ_SUSPENDED的属性修改为True,Channel进入整形状态,不再从TCP缓冲区读取请求消息,相关代码如下(AbstractTrafficShapingHandler类channelRead方法):
(4)创建恢复Channel为可读的定时任务,由Channel对应的NioEventLoop执行,代码如下(AbstractTrafficShapingHandler类channelRead方法):
(5)到达暂停读取时间之后,触发定时任务,重新修改Channel的READ_SUSPENDED属性为False,同时将autoRead设置为True,代码如下(ReopenReadTimerTask类):
同样消息发送的流量整形如下:
【流量整形使用注意】
1.因为需要计算请求和发送消息的大小,消息类型必须是ByteBuf或者ByteBufHolder,所以流量整形ChannelHandler需要添加到业务编码之后、解码之前,代码示例如下( TrafficShappingServer类):
2.全局流量整形实例只需要创建一次,全局流量整形GlobalChannelTrafficShapingHandler和GlobalTrafficShapingHandler是全局共享的,因此实例只需要创建一次,添加到不同的ChannelPipeline即可不要创建多个实例,否则流量整形将失效。GlobalTrafficShapingHandler类定义如图16-7所示。
3.流量整形参数调整不要过于频繁,虽然通过AbstractTrafficShapingHandler 的configure接口可以动态调整流量整形的读写速度和检测周期,但是由于调整之后需要对一些统计数据进行重新设置和重新计算,而且在下一个周期才能生效,过于频繁的参数调整会导致流量整形不精确,甚至失效。动态调整流量整形参数的相关接口如图16-8所示。
4.资源释放问题,在Channel关闭或者流量整形ChannelHandler被移除时,由于ChannelTrafficShaping-Handler持有消息发送队列,如果不对消息队列进行清空处理,则会导致待发送消息丢失,或者消息队列积压,引起内存泄漏(频繁地断连和重连,会创建N个ChannelTrafficShaping-Handler实例,对应N个消息发送队列)。Netty框架已经考虑了上述场景,当连接关闭时,会调用handlerRemoved方法,将待发送的消息全部释放,防止内存泄漏,代码如下(ChannelTrafficShapingHandler类):
如果连接正常,用户主动调用handlerRemoved删除流量整形ChannelHandler,则将积压的消息全部发送完成,清空消息发送队列。由于消息发送成功后由Netty负责释放ByteBuf,因此避免了内存泄漏,相关代码如下:
5.消息发送保护机制,通过流量整形可以控制发送速度,但是它的控制原理是将待发送的消息封装成Task放入消息队列,等待执行时间到达后继续发送,所以如果业务发送线程不判断Channel的可写状态,就可能会导致OOM 等问题。将16.2中的代码进行改造,注释掉TrafficShappingClientHandler的Channel可写状态判断,代码如下:
【netty内置流控整形的并发编程技巧】
1.volatile的使用
以maxWriteSize为例,它被声明为volatile型的变量,因为它提供了public 的 set方法,用户线程或者其他Channel对应的NioEventLoop线程可以调用它修改的值,设置max WriteSize属性的接口定义如图16-6所示。
2.减小锁的范围
由于涉及多线程调用,需要对消息发送队列ArrayDeque加锁,但是不需要对后续的其他操作加锁,例如通过NioEventLoop的schedule方法执行定时任务,因为它本身就是并发安全的,所以不需要额外加锁,相关代码如下(ChannelTrafficShapingHandler类):
3.原子类
由于支持对所有的Channel做流量整形,不同的Channel会绑定不同的NioEventLoop线程,所以消息发送和读取的计算都是并发操作的,如果不做同步保护,统计数据将不准确。
如果使用同步关键字,最主要的问题就是进行线程阻塞和唤醒带来的性能的额外损耗,因此这种同步被称为阻塞同步,它属于一种悲观的并发策略,被称为悲观锁。 随着硬件和操作系统指令集的发展,产生了非阻塞同步,被称为乐观锁。简单地说,就是先进行操作,操作完成再判断操作是否成功,是否有并发问题,如果有则进行失败补偿,如果没有就算操作成功,这样就从根本上避免了同步锁的弊端。 在流量整形中大量使用了原子类提升并发操作的安全性和性能。