1: Netty 4的线程模型转变

在Netty 3的时候,upstream是在IO线程里执行的,而downstream是在业务线程里执行的。比如netty从网络读取一个包传递给你的handler的时候,

你的handler部分的代码是执行在IO线程里,而你的业务线程调用write向网络写出一些东西的时候,你的handler是执行在业务线程里。

而Netty 4修改了这一模型。在Netty 4里inbound(upstream)和outbound(downstream)都是执行在EventLoop(IO线程)里。也就是你如果在业务线程里通过

channel.write向网络写出一些东西的时候,在某一点,netty 4会往这个channel的EventLoop里提交一个写出的任务。那也就是业务线程和IO线程是异步执行的。

这有什么问题呢?一般我们在网络通信里,业务层写出的都是对象。然后经过序列化等手段转换成字节流到网络,而Netty给我们提供了很好的编码解码的模型,

一般我们也会将序列化和反序列化放到一个handler里处理,而在Netty 4里这些handler都是在EventLoop里执行,那么就意味着在Netty 4里下面的代码可能会导致一些微妙的结果:

User user = new User();

user.setName("admin");

channel.write(user);

user.setName("guest");

因为序列化和业务线程异步执行,那么在write执行后并不表示user对象已经序列化了,如果这个时候修改了user对象那么传递到peer的对象可能就不再是你期望的那个user了。

所以在Netty 4里如果还是使用handler实现序列化就一定要小心了。你要么在调用channel.write写出之前将对象进行深度拷贝,要么就不在handler里进行序列化了,

直接将序列化好的东西传递给channel。

2. 在不同的线程里使用PooledByteBufAllocator分配和回收

这个问题其实是上面一个问题的续集。在碰到之前一个问题后,我们就决定不再在handler里做序列化了,而是直接在业务线程里做。但是为了减少内存的拷贝,我们就期望在序列化的时

候直接将字节流序列化到DirectByteBuf里,这样通过socket写出的时候就不进行拷贝了。而DirectByteBuf的分配成本比HeapByteBuf的成本要高,为此Netty 4借鉴jemalloc的思路实现了

一个PooledByteBufAllocator。顾名思义,就是将DirectByteBuf池化起来,回收的时候不真正回收,分配的时候从池里取一个空闲的。这对于大多数应用来说优化效果还是很明显的,

比如在一些RPC场景中,我们所传递的对象的大小往往是差不多的,这可以充分利用池化的效果。

但是我们在使用类似下面的伪代码的时候内存占用不断飙高,然后疯狂Full GC,并且有的时候还会出现OOM。这好像是内存泄漏的迹象:

//业务线程

PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;

ByteBuf buffer = allocator.buffer();

User user = new User();

//将对象直接序列化到ByteBuf

serialization.serialize(buffer, user);

//进入EventLoop

channel.writeAndFlush(buffer);

上面的代码表面看没什么问题。但实际上,PooledByteBufAllocator为了减少锁竞争,池是通过thread local来实现的。也就是分配的时候会从本线程(这里就是业务线程)的thread local里取。

而channel.writeAndFlush调用后,在将buffer写到socket后,这个buffer将被回收到池里。回收的时候也是通过thread local找到对应的池,回收掉。这样就有一个问题,分配的时候是在业务线程,

也就是说从业务线程的thread local对应的池里分配的,而回收的时候是在IO线程。这两个是不同的线程。池的作用完全丧失了,一个线程不断地去分配,不断地转移到另外一个池。

3. ByteBuf扩展引起的问题

其实这个问题和上面一个问题是一样的。但是比之前的问题更加隐晦,就在你弹冠相庆的时候给你致命一击。在碰到上面一个问题后我们就在想,既然分配和回收都得在同一个线程里执行,

那我们是不是可以启动一个专门的线程来负责分配和回收呢?于是就有了下面的代码:

package com.ys.scs.db;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelPromise;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.GenericFutureListener; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; public class Allocator { public static final ByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT; private static final BlockingQueue<ByteBuf> bufferQueue = new ArrayBlockingQueue<ByteBuf>(100); private static final BlockingQueue<ByteBuf> toCleanQueue = new LinkedBlockingQueue<ByteBuf>(); private static final int TO_CLEAN_SIZE = 50; private static final long CLEAN_PERIOD = 100; private static class AllocThread implements Runnable { @Override
public void run() { long lastCleanTime = System.currentTimeMillis();
while (!Thread.currentThread().isInterrupted()) {
try { ByteBuf buffer = allocator.buffer();
//确保是本线程释放
buffer.retain();
bufferQueue.put(buffer); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
} if (toCleanQueue.size() > TO_CLEAN_SIZE || System.currentTimeMillis() - lastCleanTime > CLEAN_PERIOD) {
final List<ByteBuf> toClean = new ArrayList<ByteBuf>(toCleanQueue.size());
toCleanQueue.drainTo(toClean);
for (ByteBuf buffer : toClean) {
ReferenceCountUtil.release(buffer);
}
lastCleanTime = System.currentTimeMillis(); } } } } static {
Thread thread = new Thread(new AllocThread(), "qclient-redis-allocator");
thread.setDaemon(true);
thread.start();
} public static ByteBuf alloc() {
try {
return bufferQueue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null; } public static void release(ByteBuf buf) { toCleanQueue.add(buf); } }

在业务线程里调用alloc,从queue里拿到专用的线程分配好的buffer。在将buffer写出到socket之后再调用release回收:

        //业务线程
ByteBuf buffer = Allocator.alloc();
//序列化
........
//写出
ChannelPromise promise = channel.newPromise();
promise.addListener(new GenericFutureListener<Future<Void>>() {
@Override
public void operationComplete(Future<Void> future) throws Exception {
//buffer已经输出,可以回收,交给专用线程回收
Allocator.release(buffer);
}
});
//进入EventLoop
channel.write(buffer, promise);

好像问题解决了。而且我们通过压测发现性能果然有提升,内存占用也很正常,通过写出各种不同大小的buffer进行了几番测试结果都很OK。

不过你如果再提高每次写出包的大小的时候,问题就出现了。在我这个版本的netty里,ByteBufAllocator.buffer()分配的buffer默认大小是256个字节,

当你将对象往这个buffer里序列化的时候,如果超过了256个字节ByteBuf就会自动扩展,而对于PooledByteBuf来说,自动扩展是会去池里取一个,

然后将旧的回收掉。而这一切都是在业务线程里进行的。意味着你使用专用的线程来做分配和回收功亏一篑。

上面三个问题就好像冥冥之中,有一双看不见的手将你一步一步带入深渊,最后让你绝望。一个问题引出一个必然的解决方案,而这个解决方案看起来将问题解决了,但却是将问题隐藏地更深。

如果说前面三个问题是因为你不熟悉Netty的新机制造成的,那么下面这个问题我觉得就是Netty本身的API设计不合理导致使用的人出现这个问题了。

4. 连接超时

在网络应用中,超时往往是最后一道防线,或是最后一根稻草。我们不怕干脆利索的宕机,怕就怕要死不活。当碰到要死不活的应用的时候往往就是依靠超时了。

在使用Netty编写客户端的时候,我们一般会有类似这样的代码:

bootstrap.connect(address).await(1000, TimeUnit.MILLISECONDS)

向对端发起一个连接,超时等待1秒钟。如果1秒钟没有连接上则重连或者做其他处理。而其实在bootstrap的选项里,还有这样的一项:

bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);

如果这两个值设置的不一致,在await的时候较短,而option里设置的较长就出问题了。这个时候你会发现connect里已经超时了,你以为连接失败了,

但实际上await超时Netty并不会帮你取消正在连接的链接。这个时候如果第2秒的时候连上了对端服务器,那么你刚才的判断就失误了。

如果你根据connect(address).await(1000, TimeUnit.MILLISECONDS)来决定是否重连,很有可能你就建立了两个连接,而且很有可能你的handler就在这两个

channel里共享起来了,这就有可能让你产生:哎呀,Netty的handler不是在单线程里执行的这样的假象。所以我的建议是,不要在await上设置超时,

而总是使用option上的选项来设置。这个更准确些,超时了就是真的表示没有连上。

5. 异步处理,流控先行

一般来讲我们的业务如果比较小的时候我们用同步处理,等业务到一定规模的时候,一个优化手段就是异步化。

异步化是提高吞吐量的一个很好的手段。但是,与异步相比,同步有天然的负反馈机制,也就是如果后端慢了,前面也会跟着慢起来,可以自动的调节。但是异步就不同了,

异步就像决堤的大坝一样,洪水是畅通无阻。如果这个时候没有进行有效的限流措施就很容易把后端冲垮。如果一下子把后端冲垮倒也不是最坏的情况,就怕把后端

冲的要死不活。这个时候,后端就会变得特别缓慢,如果这个时候前面的应用使用了一些无界的资源等,就有可能把自己弄死。

那么现在要介绍的这个坑就是关于Netty里的ChannelOutboundBuffer这个东西的。这个buffer是用在netty向channel write数据的时候,有个buffer缓冲,

这样可以提高网络的吞吐量(每个channel有一个这样的buffer)。初始大小是32(32个元素,不是指字节),但是如果超过32就会翻倍,一直增长。大部分时候是没有什

么问题的,但是在碰到对端非常慢(对端慢指的是对端处理TCP包的速度变慢,比如对端负载特别高的时候就有可能是这个情况)的时候就有问题了,这个时候如果还是不

断地写数据,这个buffer就会不断地增长,最后就有可能出问题了(我们的情况是开始吃swap,最后进程被linux killer干掉了)。

为什么说这个地方是坑呢,因为大部分时候我们往一个channel写数据会判断channel是否active,但是往往忽略了这种慢的情况。

那这个问题怎么解决呢?

其实ChannelOutboundBuffer虽然无界,但是可以给它配置一个高水位线和低水位线,

当buffer的大小超过高水位线的时候对应channel的isWritable就会变成false,

当buffer的大小低于低水位线的时候,isWritable就会变成true。所以应用应该判断isWritable,如果是false就不要再写数据了。

高水位线和低水位线是字节数,默认高水位是64K,低水位是32K,我们可以根据我们的应用需要支持多少连接数和系统资源进行合理规划。

.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 64 * 1024)
.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 32 * 1024)

 

出处:横天笑刀

Netty 使用经验总结(一)的更多相关文章

  1. springboot学习(三)————使用HttpMessageConverter进行http序列化和反序列化

    以下内容,如有问题,烦请指出,谢谢! 对象的序列化/反序列化大家应该都比较熟悉:序列化就是将object转化为可以传输的二进制,反序列化就是将二进制转化为程序内部的对象.序列化/反序列化主要体现在程序 ...

  2. springboot学习(三)——使用HttpMessageConverter进行http序列化和反序列化

    以下内容,如有问题,烦请指出,谢谢! 对象的序列化/反序列化大家应该都比较熟悉:序列化就是将object转化为可以传输的二进制,反序列化就是将二进制转化为程序内部的对象.序列化/反序列化主要体现在程序 ...

  3. Java 书单

    Java 基础 <Head First Java> 有人说这本书不适合编程新手阅读?其实本书还是很适合稍微有一点点经验的新手来阅读的,当然也适合我们用来温故 Java 知识点. ps:刚入 ...

  4. netty的调优-及-献上写过注释的源码工程

    Netty能干什么? Http服务器 使用Netty可以编写一个 Http服务器, 就像tomcat那样,能接受用户发送的http请求, , 只不过没有实现Servelt规范, 但是它也能解析携带的参 ...

  5. Netty源码解析一——线程池模型之线程池NioEventLoopGroup

    本文基础是需要有Netty的使用经验,如果没有编码经验,可以参考官网给的例子:https://netty.io/wiki/user-guide-for-4.x.html.另外本文也是针对的是Netty ...

  6. 谈谈如何使用Netty开发实现高性能的RPC服务器

    RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络,从远程计算机程序上请求服务,而不必了解底层网络技术的协议.说的再直白一点,就是客户端在不必知道 ...

  7. 基于netty http协议栈的轻量级流程控制组件的实现

    今儿个是冬至,所谓“冬大过年”,公司也应景五点钟就放大伙儿回家吃饺子喝羊肉汤了,而我本着极高的职业素养依然坚持留在公司(实则因为没饺子吃没羊肉汤喝,只能呆公司吃食堂……).趁着这一个多小时的时间,想跟 ...

  8. 从netty-example分析Netty组件续

    上文我们从netty-example的Discard服务器端示例分析了netty的组件,今天我们从另一个简单的示例Echo客户端分析一下上个示例中没有出现的netty组件. 1. 服务端的连接处理,读 ...

  9. 源码分析netty服务器创建过程vs java nio服务器创建

    1.Java NIO服务端创建 首先,我们通过一个时序图来看下如何创建一个NIO服务端并启动监听,接收多个客户端的连接,进行消息的异步读写. 示例代码(参考文献[2]): import java.io ...

随机推荐

  1. Java的多线程

    Java使用Thread代表线程,所有的线程对象都必须是Thread类或其子类的实例.每个线程的作用就是执行一段程序流(完成一定的任务). Java使用线程执行体来代表这段程序流. 1. 继承Thre ...

  2. Java API方式调用Kafka各种协议

    众所周知,Kafka自己实现了一套二进制协议(binary protocol)用于各种功能的实现,比如发送消息,获取消息,提交位移以及创建topic等.具体协议规范参见:Kafka协议  这套协议的具 ...

  3. ip防刷脚本

    #!/bin/sh #防刷脚本 #env ACCESS_PATH=/home/wwwlogs ACCESS_LOG=y.log IPTABLES_TOP_LOG=iptables_top.log DR ...

  4. STL——序列式容器

    一.容器概述与分类 1. STL容器即是将运用最广的一些数据结构实现出来.常用的数据结构有array, list, tree, stack, queue, hash table, set, map…… ...

  5. vue - 父组件数据变化控制子组件类名切换

    先说当时的思路和实现核心是父子组件传值和v-bind指令动态绑定class实现 1. 父组件引用.注册.调用子组件script中引用 import child from '../components/ ...

  6. initializer element is not constant 问题

    在Ubuntu下,比葫芦画瓢,写了一个程序,居然报错!!!! #include <stdio.h> ; int j = *(int *)(&i) ; int main (int a ...

  7. css3整理--text-shadow

    text-shadow语法: text-shadow:[颜色(Color) x轴(X Offset) y轴(Y Offset) 模糊半径(Blur)],[颜色(color) x轴(X Offset) ...

  8. python tkinter学习——布局

    目录 一.pack() 二.grid() 三.place() 四.Frame() 正文 布局 一.pack() pack()有以下几个常用属性: side padx pady ipadx ipady ...

  9. Android O PackageInstaller 解析

    Android O 8.0 1.src\com\android\packageinstaller\permission\mode\PermissionGroups.java @Override pub ...

  10. Android Studio 3.1.2 版本包下载

    Android Studio 3.1.2 bug 修复版已发布,本次更新修复了一些错误,并改进了某些场景下 lint 审查的速度.详细的修复内容请查看 Android Studio 3.1.2 的发布 ...