在实际生活中我们可能会因为某些原因需要控制服务端的读写速率,根据业务需求对不同的用户提供不同的下载速度,Netty本身也提供了高低水位和流量整形,两种方式来控制流量。
1. 高低水位
netty中的高低水位机制会在发送缓冲区的数据超过高水位时触发channelWritabilityChanged事件同时将channel的写状态设置为false,但是这个写状态只是程序层面的状态,程序还是可以继续写入数据。所以使用这个机制时需要自行判断是否可写,并做相应的处理。
2. 流量整形
netty提供了ChannelTrafficShapingHandler、GlobalTrafficShapingHandler、GlobalChannelTrafficShapingHandler三个流量整形处理器,依次用于控制单个channel、所有channel、所有channel。这些处理器控制流量的原理相同的。控制读取速度是通过先从TCP缓存区读取数据,然后根据读取的数据大小和读取速率的限制计算出,需要暂停读取数据的时间。这样从平均来看读取速度就被降低了。控制写速度则是根据数据大小和写出速率计算出预期写出的时间,然后封装为一个待发送任务放到延迟消息队列中,同时提交一个延迟任务查看消息是否可发送了。这样从平均来看每秒钟的数据读写速度就被控制在读写限制下了。
众所周知netty通过一个ChannelPipeline维护了一个双向链表,当触发Inbound事件时事件会从head传播到tail,而Outboud事件则有两种传播方式,一是从当前handler传播到head、二是从tail传播到head。使用哪种传播方式取决于你是通过ChannelHandlerContext直接发送数据还是通过channel发送数据。
当我们在netty中使用write方法发送数据时,这个数据其实是写到了一个缓冲区中,并未直接发送给接收方,netty使用ChannelOutboundBuffer封装出站消息的发送,所有的消息都会存储在一个链表中,直到缓冲区被flush方法刷新,netty才会将数据真正发送出去。
netty默认设置的高水位为64KB,低水位为32KB.
以下代码为ChannelOutboundBuffer的部分源码
public void addMessage(Object msg, int size, ChannelPromise promise) {
ChannelOutboundBuffer.Entry entry = ChannelOutboundBuffer.Entry.newInstance(msg, size, total(msg), promise);
if(this.tailEntry == null) {
this.flushedEntry = null;
} else {
ChannelOutboundBuffer.Entry tail = this.tailEntry;
tail.next = entry;
}
this.tailEntry = entry;
if(this.unflushedEntry == null) {
this.unflushedEntry = entry;
}
this.incrementPendingOutboundBytes((long)entry.pendingSize, false);
}
该方法会在用户调用write()发送数据时被调用,它会将待发送的数据加入到数据缓冲区中
private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
if(size != 0L) {
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
if(newWriteBufferSize > (long)this.channel.config().getWriteBufferHighWaterMark()) {
this.setUnwritable(invokeLater);
}
}
}
加入缓冲区后,调用incrementPendingOutboundBytes方法增加待发送的字节数,同时判断字节数是否超过高水位。
private void setUnwritable(boolean invokeLater) {
int oldValue;
int newValue;
do {
oldValue = this.unwritable;
newValue = oldValue | 1;
} while(!UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue));
if(oldValue == 0 && newValue != 0) {
this.fireChannelWritabilityChanged(invokeLater);
}
}
超过高水位则将可写状态设置为false,并触发可写状态改变事件
public void addFlush() {
ChannelOutboundBuffer.Entry entry = this.unflushedEntry;
if(entry != null) {
if(this.flushedEntry == null) {
this.flushedEntry = entry;
}
do {
++this.flushed;
if(!entry.promise.setUncancellable()) {
int pending = entry.cancel();
this.decrementPendingOutboundBytes((long)pending, false, true);
}
entry = entry.next;
} while(entry != null);
this.unflushedEntry = null;
}
}
此方法将未刷新的数据刷到准备发送的已刷新队列中。
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//设置低水位
ctx.channel().config().setWriteBufferLowWaterMark(10);
//设置高水位
ctx.channel().config().setWriteBufferHighWaterMark(100);
}
if (channel.isWritable()) {
channel.writeAndFlush(msg);
}else {
System.out.println("服务端不可写");
}
通过ChannelHandlerContext即可设置高低水位,在激活事件中设置可以使配置在建立连接时就生效,高低水位如果设置在客户端就是控制发送给服务器的数据速度,设置在服务器就是控制发送给客户端的数据速度。
netty可以用于流量整形的处理器有ChannelTrafficShapingHandler、GlobalTrafficShapingHandler、GlobalChannelTrafficShapingHandler,这三个处理器都继承了AbstractTrafficShapingHandler,AbstractTrafficShapingHandler主要实现了控制读取速度、判断发送数据需要的时间、统计周期内读写的字节。本博客以ChannelTrafficShapingHandler为例,其他两个处理器的实现原理都是差不多的,下面贴一下AbstractTrafficShapingHandler的几段源码,看看netty是怎么控制的读写速率的
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//计算读取到的消息大小
long size = this.calculateSize(msg);
//获取当前时间
long now = TrafficCounter.milliSecondFromNano();
if(size > 0L) {
//根据读取速率计算已读取数据需要暂停多长时间
long wait = this.trafficCounter.readTimeToWait(size, this.readLimit, this.maxTime, now);
//此处直接返回wait参数的值,可以重写该方法控制暂停读取的时间
wait = this.checkWaitReadTime(ctx, wait, now);
//如果暂停时间小于10毫秒,则不暂停读取,允许读取速率存在一定的偏差
if(wait >= 10L) {
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中设置属性,标记读取事件被处理器暂停了
channel.attr(READ_SUSPENDED).set(Boolean.valueOf(true));
//从channel属性中获取重启读取任务
Attribute attr = channel.attr(REOPEN_TASK);
Runnable reopenTask = (Runnable)attr.get();
if(reopenTask == null) {
reopenTask = new AbstractTrafficShapingHandler.ReopenReadTimerTask(ctx);
attr.set(reopenTask);
}
//将读取事件加入到netty的线程池中作为一个定时任务
ctx.executor().schedule((Runnable)reopenTask, wait, TimeUnit.MILLISECONDS);
if(logger.isDebugEnabled()) {
logger.debug("Suspend final status => " + config.isAutoRead() + ':' + isHandlerActive(ctx) + " will reopened at: " + wait);
}
}
}
}
this.informReadOperation(ctx, now);
//继续向下传播读取事件
ctx.fireChannelRead(msg);
}
static final class ReopenReadTimerTask implements Runnable {
final ChannelHandlerContext ctx;
ReopenReadTimerTask(ChannelHandlerContext ctx) {
this.ctx = ctx;
}
public void run() {
Channel channel = this.ctx.channel();
ChannelConfig config = channel.config();
//如果channel的自动读取被关闭了且不是AbstractTrafficShapingHandler关闭的
if(!config.isAutoRead() && AbstractTrafficShapingHandler.isHandlerActive(this.ctx)) {
if(AbstractTrafficShapingHandler.logger.isDebugEnabled()) {
AbstractTrafficShapingHandler.logger.debug("Not unsuspend: " + config.isAutoRead() + ':' + AbstractTrafficShapingHandler.isHandlerActive(this.ctx));
}
//重置AbstractTrafficShapingHandler的状态
channel.attr(AbstractTrafficShapingHandler.READ_SUSPENDED).set(Boolean.valueOf(false));
} else {
if(AbstractTrafficShapingHandler.logger.isDebugEnabled()) {
if(config.isAutoRead() && !AbstractTrafficShapingHandler.isHandlerActive(this.ctx)) {
AbstractTrafficShapingHandler.logger.debug("Unsuspend: " + config.isAutoRead() + ':' + AbstractTrafficShapingHandler.isHandlerActive(this.ctx));
} else {
AbstractTrafficShapingHandler.logger.debug("Normal unsuspend: " + config.isAutoRead() + ':' + AbstractTrafficShapingHandler.isHandlerActive(this.ctx));
}
}
//重置AbstractTrafficShapingHandler的状态并开启自动读取
channel.attr(AbstractTrafficShapingHandler.READ_SUSPENDED).set(Boolean.valueOf(false));
config.setAutoRead(true);
channel.read();
}
if(AbstractTrafficShapingHandler.logger.isDebugEnabled()) {
AbstractTrafficShapingHandler.logger.debug("Unsuspend final status => " + config.isAutoRead() + ':' + AbstractTrafficShapingHandler.isHandlerActive(this.ctx));
}
}
}
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
//计算当前接收到的数据大小
long size = this.calculateSize(msg);
//获取当前时间
long now = TrafficCounter.milliSecondFromNano();
if(size > 0L) {
//计算要发送的数据以写速率需要发送多久
long wait = this.trafficCounter.writeTimeToWait(size, this.writeLimit, this.maxTime, now);
//如果等待时间小于10毫秒就可以不用等待,允许速率存在一定的偏差
if(wait >= 10L) {
if(logger.isDebugEnabled()) {
logger.debug("Write suspend: " + wait + ':' + ctx.channel().config().isAutoRead() + ':' + isHandlerActive(ctx));
}
//将写数据的功能留给子类实现
this.submitWrite(ctx, msg, size, wait, now, promise);
return;
}
}
this.submitWrite(ctx, msg, size, 0L, now, promise);
}
abstract void submitWrite(ChannelHandlerContext var1, Object var2, long var3, long var5, long var7, ChannelPromise var9);
接下来我们看一下ChannelTrafficShapingHandler的submitWrite方法的实现
void submitWrite(final ChannelHandlerContext ctx, Object msg, long size, long delay, long now, ChannelPromise promise) {
//内部类,用于存储待发送的数据
ChannelTrafficShapingHandler.ToSend newToSend;
synchronized(this) {
//如果不需要延迟发送且待发送消息队列为空
if(delay == 0L && this.messagesQueue.isEmpty()) {
//统计发送的字节
this.trafficCounter.bytesRealWriteFlowControl(size);
//发送数据给客户端
ctx.write(msg, promise);
return;
}
//封装待发送的数据
newToSend = new ChannelTrafficShapingHandler.ToSend(delay + now, msg, promise, null);
//将待发送的任务加入消息队列
this.messagesQueue.addLast(newToSend);
this.queueSize += size;
//检查待发送的消息队列大小是否超过了最大发送数据限制或延迟时间超过最大等待时间,超过则设置写状态为false,并触发写状态改变事件
this.checkWriteSuspend(ctx, delay, this.queueSize);
}
final long futureNow = newToSend.relativeTimeAction;
//将发送数据任务加入定时任务
ctx.executor().schedule(new Runnable() {
public void run() {
ChannelTrafficShapingHandler.this.sendAllValid(ctx, futureNow);
}
}, delay, TimeUnit.MILLISECONDS);
}
private void sendAllValid(ChannelHandlerContext ctx, long now) {
//由于是线程池可能存在多个定时任务同时执行,因此要加入同步锁
synchronized(this) {
//获取待发送消息队列的第一个元素
ChannelTrafficShapingHandler.ToSend newToSend = (ChannelTrafficShapingHandler.ToSend)this.messagesQueue.pollFirst();
while(true) {
if(newToSend != null) {
//如果任务的执行时间小于当前时间
if(newToSend.relativeTimeAction <= now) {
//计算数据大小
long size = this.calculateSize(newToSend.toSend);
//统计发送的字节数
this.trafficCounter.bytesRealWriteFlowControl(size);
//减少消息队列的数据大小
this.queueSize -= size;
//发送数据给客户端
ctx.write(newToSend.toSend, newToSend.promise);
//继续获取队列中第一个发送任务
newToSend = (ChannelTrafficShapingHandler.ToSend)this.messagesQueue.pollFirst();
continue;
}
//任务还不能执行,将发送任务重新加入到队列的队首
this.messagesQueue.addFirst(newToSend);
}
if(this.messagesQueue.isEmpty()) {
//将可写状态设置为true
this.releaseWriteSuspended(ctx);
}
break;
}
}
//刷新缓冲区,真正将数据发送给客户端
ctx.flush();
}
在上面的代码中,我们可以发现netty使用了TrafficCounter来计算读取和发送的等待时间以及监控读取发送的字节数,现在我们继续来看看这里是怎么做的吧。
public synchronized void start() {
//该监控器是否被启动了
if(!this.monitorActive) {
//记录本次启动监控器时间
this.lastTime.set(milliSecondFromNano());
//获取监控周期
long localCheckInterval = this.checkInterval.get();
//如果周期大于0且定时任务执行器不为空
if(localCheckInterval > 0L && this.executor != null) {
//设置监控状态
this.monitorActive = true;
//获取监控任务
this.monitor = new TrafficCounter.TrafficMonitoringTask();
//将监控任务加入定时任务中
this.scheduledFuture = this.executor.schedule(this.monitor, localCheckInterval, TimeUnit.MILLISECONDS);
}
}
}
public synchronized void stop() {
if(this.monitorActive) {
this.monitorActive = false;
//重置监控信息
this.resetAccounting(milliSecondFromNano());
if(this.trafficShapingHandler != null) {
this.trafficShapingHandler.doAccounting(this);
}
if(this.scheduledFuture != null) {
//取消定时任务
this.scheduledFuture.cancel(true);
}
}
}
void long recv) {
//累加当前周期内读取的字节数
this.currentReadBytes.addAndGet(recv);
//累加当前监控器记录的已读取字节数
this.cumulativeReadBytes.addAndGet(recv);
}
void bytesWriteFlowControl(long write) {
//累加当前周期内读取的字节数
this.currentWrittenBytes.addAndGet(write);
//累加当前监控器记录的已读取字节数
this.cumulativeWrittenBytes.addAndGet(write);
}
//该方法的作业时重置监控器状态,将本次周期内记录的数据转到上次
synchronized void resetAccounting(long newLastTime) {
//计算上次启动或重置监控器到本次重置监控器的时间
long interval = newLastTime - this.lastTime.getAndSet(newLastTime);
if(interval != 0L) {
if(logger.isDebugEnabled() && interval > this.checkInterval() << 1) {
logger.debug("Acct schedule not ok: " + interval + " > 2*" + this.checkInterval() + " from " + this.name);
}
//将本次监控周期内读取的字节数转到上次
this.lastReadBytes = this.currentReadBytes.getAndSet(0L);
//将本次监控周期内发送的字节数转到上次
this.lastWrittenBytes = this.currentWrittenBytes.getAndSet(0L);
this.lastReadThroughput = this.lastReadBytes * 1000L / interval;
this.lastWriteThroughput = this.lastWrittenBytes * 1000L / interval;
this.realWriteThroughput = this.realWrittenBytes.getAndSet(0L) * 1000L / interval;
this.lastWritingTime = Math.max(this.lastWritingTime, this.writingTime);
this.lastReadingTime = Math.max(this.lastReadingTime, this.readingTime);
}
}
private final class TrafficMonitoringTask implements Runnable {
private TrafficMonitoringTask() {
}
public void run() {
if(TrafficCounter.this.monitorActive) {
//重置监控器状态,转移监控信息到上次
TrafficCounter.this.resetAccounting(TrafficCounter.milliSecondFromNano());
if(TrafficCounter.this.trafficShapingHandler != null) {
//调用监控方法,用户可以重写该方法,以在监控重置时进行某些操作
TrafficCounter.this.trafficShapingHandler.doAccounting(TrafficCounter.this);
}
//重新将该监控任务加入定时任务中
TrafficCounter.this.scheduledFuture = TrafficCounter.this.executor.schedule(this, TrafficCounter.this.checkInterval.get(), TimeUnit.MILLISECONDS);
}
}
}
从上面的TrafficCounter的源码中,我们可以看出TrafficCounter的周期性统计也是通过定时任务来完成的,当AbstractTrafficShapingHandler读写数据时就会调用bytesWriteFlowControl和bytesRecvFlowControl方法记录读写的数据大小,而TrafficCounter每次执行任务时就会将当前周期记录的读写字节数,转移到上次状态中。在监控过程中,TrafficCounter会调用AbstractTrafficShapingHandler的doAccounting方法,该方法的实现是空的,什么也不做,我们想做些什么可以继承该类来重写doAccounting方法。
通过以上源码我们可以得出结论,netty的限流并非真正的控制每秒读取写出的字节数,而是先读取一部分字节(读取的这部分字节大小,可以通过设置netty的接收缓冲区控制,但是不建议更改,因为netty会根据读取的字节调整合适的大小,以减少内存开销),然后根据当前的读取速率限制计算出下一次读取需要等待多久。写出速率也是根据当前写出速率计算出发送时间,然后将发送数据封装到一个待发送任务队列中,再启动定时任务监听该消息队列将数据发送出去。虽然netty是通过暂停读取和发送来实现的,但平均来看还是实现了流量控制的目的。
public class FlowMonitoringHandler extends ChannelTrafficShapingHandler{
@Override
protected void doAccounting(TrafficCounter counter) {
System.out.println("上个监控周期读取了"+counter.lastReadBytes()+"个字节,发送了"+counter.lastWrittenBytes()+"个字节");
}
}
这里继承了ChannelTrafficShapingHandler重写doAccounting方法,方便我们查看流量整形的效果。
this.serverBootstrap.group(this.boss,this.worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast("FlowMonitoringHandler", new FlowMonitoringHandler(100,10,1000L,1000L*10));
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new HelloServeHandle());
pipeline.addLast(new TestInboundHandler());
pipeline.addLast(new TestOutBoundHandler()); }
});
将重写后的的流量整形处理器加入到netty的管道中,处理器的三个参数分别代表平均每秒读取10个字节,平均每秒发送100个字节,TrafficCounter每秒统计一次,最大读写等待时间为10秒。
通过日志可以看到netty的流量控制的确是我们分析的那样,先读取一段数据然后暂停读取,发送数据也是先将数据存放到消息队列,然后开启定时任务去发送数据。
最后推荐一个FQ软件(求Star):GitHub - zhining-lu/netty-quic-proxy: A forward proxy base netty uses quic protocol