上一篇:流量整形工作机制
1. 并发编程在流量整形中的使用
1.1 volatile的使用
1.2 减小锁的范围
1.3 原子类
2. 相关注意事项
2.1 channelHandler的添加位置
2.2 全局流量整形实例只需要创建一次
2.3 流量整形参数调整不要过于频繁
2.4 资源释放问题
2.5 消息发送保护机制
3. 总结
volatile的使用
关键字volatile是Java提供的最轻量级的同步机制,Java内存模型对volatile专门定义了一些特殊的访问规则,当一个变量被volatile修饰后,具有以下特性:
- 线程可见性:一个线程修改了被volatile修饰的变量后,无论是否加锁,其他线程都可以立即看到最新的修改。
- 禁止指令重排序优化:普通的变量仅保证在方法的执行过程中所有依赖赋值结果的地方都能获取正确的结果,而不能保证变量赋值操作的顺序和程序代码执行顺序一致。
以maxWriteSize为例,它被声明为volatile变量,因为它提供了public的set方法,用户线程或者其他Channel对应的NioEventLoop线程可以调用它修改maxWriteSize的值。在Channel绑定的NioEventLoop线程中需要读取maxWriteSize,如果它不是volatile型变量,则业务线程对maxWriteSize修改后NioEventLoop可能会读取到脏数据。
减小锁的范围
由于涉及多线程调用,需要对消息发送队列ArrayDeque<ToSend>加锁,但是不需要对后续的其他操作加锁,例如通过NioEventLoop的schdule方法执行定时任务,因为它本身就是并发安全的,所以不需要额外加锁。
原子类
由于支持所有的Channel做流量整形,不同的channel会绑定不同的NioEventLoop线程,所以消息的读取和发送的计算都是并发操作的,如果不做同步保护,统计数据将不准确。
如果使用同步关键字,最主要的问题就是进行线程阻塞和唤醒带来的性能的额外损耗,因此这种同步被称为阻塞同步,它属于一种悲观的阻塞策略,被称为悲观锁。
所以Netty在流量整形使用了大量原子类提升并发操作的安全性和性能。
channelHandler的添加位置
因为需要计算请求和发送消息的大小,消息类型必须是ByteBuf或者ByteBufHolder,所以流量整形ChannelHandler需要添加到业务编码之后、解码之前。如下:
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast("Channel Traffic Shaping",new ChannelTrafficShapingHandler(1024 * 1024,1024 * 1024, 1000));
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(2048 * 1024, delimiter));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TrafficShapingServerHandler());
}
});
全局流量整形实例只需要创建一次
全局流量整形GlobalChannelTrafficShapingHandler和GlobalTrafficShapingHandler是全局共享的,因此实例只需要创建一次,添加到不同的ChannelPipeline即可,创建多个实例添加功能会失效。GlobalTrafficShapingHandler类定义如下:
@Sharable
public class GlobalTrafficShapingHandler extends AbstractTrafficShapingHandler
流量整形参数调整不要过于频繁
通过AbstractTrafficShapingHandler的configure接口可以动态调整流量整形的读写速度和检测周期,但是由于调整之后需要对一些统计数据进行重新设置和重新计算,而且在下一个周期才能生效,过于频繁的参数调整会导致流量整形不清却,甚至失效。
资源释放问题
在Channel关闭或者流量整形ChannelHandler被移除时,由于ChannelTrafficShapingHandler持有消息发送队列,如果不对消息队列进行清空处理,会导致发送消息丢失或者消息队列积压,引起内存泄漏(频繁地断连和重连,会创建N个ChannelTrafficShapingHandler实例,对应N个消息发送队列)。
Netty框架针对于这种情况,当连接关闭时,会调用handlerRemoved方法,将待发送消息全部释放,方式内存泄漏。如果连接正常,用户主动调用handlerRemoved删除流量整形ChannelHandler,则将积压的消息全部发送完成,清空消息发送队列。由于消息发送成功后由Netty负责释放ByteBuf,因此避免内存泄漏。
消息发送保护机制
通过流量整形可以控制发送速度,但是它的控制原理是将待发送的消息封装成Task放入消息队列,等待执行时间到达后继续发送,所以如果业务发送线程不判断Chanel的可写状态,就可能导致OOM等问题。将之前写的代码进行改造,注释掉ctx.channel().isWritable(),运行一段时间,客户端就会抛出异常,发生OOM异常。
总结
流量整形与流控最大区别在于,流控会拒绝消息,流量整形不拒绝和丢弃消息,无论接受量多大,它总能以近似恒定的速度下发消息。