AICon日程100%就绪,9折倒计时最后一周 了解详情
写点什么

Netty 案例集锦之多线程篇(续)

  • 2015-11-24
  • 本文字数:5056 字

    阅读完需:约 17 分钟

1. Netty 构建推送服务问题

1.1. 问题描述

最近在使用 Netty 构建推送服务的过程中,遇到了一个问题,想再次请教您:如何正确的处理业务逻辑?问题主要来源于阅读您发表在 InfoQ 上的文章《Netty 系列之 Netty 线程模型》,文中提到 “2.4Netty 线程开发最佳实践中 2.4.2 复杂和时间不可控业务建议投递到后端业务线程池统一处理。对于此类业务,不建议直接在业务 ChannelHandler 中启动线程或者线程池处理,建议将不同的业务统一封装成 Task,统一投递到后端的业务线程池中进行处理。”

我不太理解“统一投递到后端的业务线程池中进行处理”具体如何操作?像下面这样做是否可行:

复制代码
private ExecutorService executorService =
Executors.newFixedThreadPool(4);
@Override
public void channelRead (final ChannelHandlerContext ctx, final Object msg)
throws Exception {
executorService.execute(new Runnable()
{@Override
public void run() {
doSomething();

其实我想了解的是真实生产环境中如何将业务逻辑与 Netty 网络处理部分很好的作隔离,有没有通用的做法?

重要通知:接下来 InfoQ 将会选择性地将部分优秀内容首发在微信公众号中,欢迎关注 InfoQ 微信公众号第一时间阅读精品内容。

1.2. 答疑解惑

Netty 的 ChannelHandler 链由 I/O 线程执行,如果在 I/O 线程做复杂的业务逻辑操作,可能会导致 I/O 线程无法及时进行 read() 或者 write() 操作。所以,比较通用的做法如下:

  • 在 ChannelHanlder 的 Codec 中进行编解码,由 I/O 线程做 CodeC;
  • 将数据报反序列化成业务 Object 对象之后,将业务消息封装到 Task 中,投递到业务线程池中进行处理,I/O 线程返回。

不建议的做法:

图 1-1 不推荐业务和 I/O 线程共用同一个线程

推荐做法:

图 1-2 建议业务线程和 I/O 线程隔离

1.3. 问题总结

事实上,并不是说业务 ChannelHandler 一定不能由 NioEventLoop 线程执行,如果业务 ChannelHandler 处理逻辑比较简单,执行时间是受控的,业务 I/O 线程的负载也不重,在这种应用场景下,业务 ChannelHandler 可以和 I/O 操作共享同一个线程。使用这种线程模型会带来两个优势:

  1. 开发简单:开发业务 ChannelHandler 的不需要关注 Netty 的线程模型,只负责 ChannelHandler 的业务逻辑开发和编排即可,对开发人员的技能要求会低一些;
  2. 性能更高:因为减少了一次线程上下文切换,所以性能会更高。

在实际项目开发中,一些开发人员往往喜欢照葫芦画瓢,并不会分析自己的 ChannelHandler 更适合在哪种线程模型下处理。如果在 ChannelHandler 中进行数据库等同步 I/O 操作,很有可能会导致通信模块被阻塞。所以,选择什么样的线程模型还需要根据项目的具体情况而定,一种比较好的做法是支持策略配置,例如阿里的 Dubbo,支持通过配置化的方式让用户选择业务在 I/O 线程池还是业务线程池中执行,比较灵活。

2. Netty 客户端连接问题

2.1. 问题描述

Netty 客户端想同时连接多个服务端,使用如下方式,是否可行,我简单测试了下,暂时没有发现问题。代码如下:

复制代码
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
...... 代码省略
// Start the client.
ChannelFuture f1 = b.connect(HOST, PORT);
ChannelFuture f2 = b.connect(HOST2, PORT2);
// Wait until the connection is closed.
f1.channel().closeFuture().sync();
f2.channel().closeFuture().sync();
...... 代码省略
}

2.2. 答疑解惑

上述代码没有问题,原因是尽管 Bootstrap 自身不是线程安全的,但是执行 Bootstrap 的连接操作是串行执行的,而且 connect(String inetHost, int inetPort) 方法本身是线程安全的,它会创建一个新的 NioSocketChannel,并从初始构造的 EventLoopGroup 中选择一个 NioEventLoop 线程执行真正的 Channel 连接操作,与执行 Bootstrap 的线程无关,所以通过一个 Bootstrap 连续发起多个连接操作是安全的,它的原理如下:

图 2-1 Netty BootStrap 工作原理

2.3. 问题总结

注意事项 - 资源释放问题: 在同一个 Bootstrap 中连续创建多个客户端连接,需要注意的是 EventLoopGroup 是共享的,也就是说这些连接共用一个 NIO 线程组 EventLoopGroup,当某个链路发生异常或者关闭时,只需要关闭并释放 Channel 本身即可,不能同时销毁 Channel 所使用的 NioEventLoop 和所在的线程组 EventLoopGroup,例如下面的代码片段就是错误的:

复制代码
ChannelFuture f1 = b.connect(HOST, PORT);
ChannelFuture f2 = b.connect(HOST2, PORT2);
f1.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}

线程安全问题: 需要指出的是 Bootstrap 不是线程安全的,因此在多个线程中并发操作 Bootstrap 是一件非常危险的事情,Bootstrap 是 I/O 操作工具类,它自身的逻辑处理非常简单,真正的 I/O 操作都是由 EventLoop 线程负责的,所以通常多线程操作同一个 Bootstrap 实例也是没有意义的,而且容易出错,错误代码如下:

复制代码
Bootstrap b = new Bootstrap();
{
// 多线程执行初始化、连接等操作
}

3. 性能数据统计不准确案例

3.1. 问题描述

某生产环境在业务高峰期,偶现服务调用时延突刺问题,时延突然增大的服务没有固定规律,比例虽然很低,但是对客户的体验影响很大,需要尽快定位出问题原因并解决。

3.2. 问题分析

服务调用时延增大,但并不是异常,因此运行日志并不会打印 ERROR 日志,单靠传统的日志无法进行有效问题定位。利用分布式消息跟踪系统魔镜,进行分布式环境的故障定界。

通过对服务调用时延进行排序和过滤,找出时延增大的服务调用链详细信息,发现业务服务端处理很快,但是消费者统计数据却显示服务端处理非常慢,调用链两端看到的数据不一致,怎么回事?

对调用链的埋点日志进行分析发现,服务端打印的时延是业务服务接口调用的时延,并没有包含:

  • 通信端读取数据报、消息解码和内部消息投递、队列排队的时间
  • 通信端编码业务消息、在通信线程队列排队时间、消息发送到 Socket 的时间

调用链的工作原理如下:

图 3-1 调用链工作原理

将调用链中的消息调度过程详细展开,以服务端读取请求消息为例进行说明,如下图所示:

图 3-2 性能统计日志埋点

优化调用链埋点日志,措施如下:

  • 包含客户端和服务端消息编码和解码的耗时
  • 包含请求和应答消息在业务线程池队列中的排队时间;
  • 包含请求和应答消息在通信线程发送队列(数组)中的排队时间

同时,为了方便问题定位,我们需要打印输出 Netty 的性能统计日志,主要包括:

  • 每条链路接收的总字节数、周期 T 接收的字节数、消息接收 CAPs
  • 每条链路发送的总字节数、周期 T 发送的字节数、消息发送 CAPs

优化之后,上线运行一天之后,我们通过分析比对 Netty 性能统计日志、调用链日志,发现双方的数据并不一致,Netty 性能统计日志统计到的数据与前端门户看到的也不一致,因为怀疑是新增的性能统计功能存在 BUG,继续问题定位。

首先对消息发送功能进行 CodeReview,发现代码调用完 writeAndFlush 之后直接对发送的请求消息字节数进行计数,代码如下:

实际上,调用 writeAndFlush 并不意味着消息已经发送到网络上,它的功能分解如下:

图 3-3 writeAndFlush 工作原理图

通过对 writeAndFlush 方法展开分析,我们发现性能统计代码存在如下几个问题:

  • 业务 ChannelHandler 的执行时间
  • ByteBuf 在 ChannelOutboundBuffer 数组中排队时间
  • NioEventLoop 线程调度时间,它不仅仅只处理消息发送,还负责数据报读取、定时任务执行以及业务定制的其它 I/O 任务
  • JDK NIO 类库将 ByteBuffer 写入到网络的时间,包括单条消息的多次写半包

由于性能统计遗漏了上述 4 个步骤的执行时间,因此统计出来的性能比实际值更高,这会干扰我们的问题定位。

3.3. 问题总结

其它常见性能统计误区汇总:

1. 调用 write 方法之后就开始统计发送速率,示例代码如下:

2. 消息编码时进行性能统计,示例代码如下:

编码之后,获取 out 可读的字节数,然后做累加。编码完成,ByteBuf 并没有被加入到发送队列(数组)中,因此在此时做性能统计仍然是不准的。

正确的做法:

  1. 调用 writeAndFlush 方法之后获取 ChannelFuture;
  2. 新增消息发送 ChannelFutureListener,监听消息发送结果,如果消息写入网络 Socket 成功,则 Netty 会回调 ChannelFutureListener 的 operationComplete 方法;
  3. 在消息发送 ChannelFutureListener 的 operationComplete 方法中进行性能统计。

示例代码如下:

问题定位出来之后,按照正确的做法对 Netty 性能统计代码进行了修正,上线之后,结合调用链日志,很快定位出了业务高峰期偶现的部分服务时延毛刺较大问题,优化业务线程池参数配置之后问题得到解决。

3.4. 举一反三

除了消息发送性能统计之外,Netty 数据报的读取、消息接收QPS 性能统计也非常容易出错,我们第一版性能统计代码消息接收CAPs 也不准确,大家知道为什么吗?这个留作问题,供大家自己思考。

4. Netty 线程数膨胀案例

4.1. 问题描述

分布式服务框架在进行现网问题定位时,Dump 线程堆栈之后发现 Netty 的 NIO 线程竟然有 3000 多个,大量的 NIO 线程占用了系统的句柄资源、内存资源、CPU 资源等,引发了一些其它问题,需要尽快查明原因并解决线程过多问题。

4.2. 问题分析

在研发环境中模拟现网组网和业务场景,使用 jmc 工具进行问题定位,

使用飞行记录器对系统运行状况做快照,模拟示例图如下所示:

图 4-1 使用 jmc 工具进行问题定位

获取到黑匣子数据之后,可以对系统的各种重要指标做分析,包括系统数据、内存、GC 数据、线程运行状态和数据等,如下图所示:

图 4-2 获取系统资源占用详细数据

通过对线程堆栈分析,我们发现 Netty 的 NioEventLoop 线程超过了 3000 个!

图 4-3 Netty 线程占用超过 3000 个

对服务框架协议栈的 Netty 客户端和服务端源码进行 CodeReview,发现了问题所在:

  • 客户端每连接 1 个服务端,就会创建 1 个新的 NioEventLoopGroup,并设置它的线程数为 1;
  • 现网有 300 个 + 节点,节点之间采用多链路(10 个链路),由于业务采用了随机路由,最终每个消费者需要跟其它 200 多个节点建立长连接,加上自己服务端也需要占用一些 NioEventLoop 线程,最终客户端单进程线程数膨胀到了 3000 多个。

业务的伪代码如下:

复制代码
for(Link linkE : links)
{
EventLoopGroup group = new NioEventLoopGroup(1);
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
// 此处省略.....
b.connect(linkE.localAddress, linkE.remoteAddress);
}

如果客户端对每个链路连接都创建一个新的 NioEventLoopGroup, 则每个链路就会占用 1 个独立的 NIO 线程,最终沦为 1 连接:1 线程 这种同步阻塞模式线程模型。随着集群组网规模的不断扩大,这会带来严重的线程膨胀问题,最终会发生句柄耗尽无法创建新的线程,或者栈内存溢出。

从另一个角度看,1 个 NIO 线程只处理一条链路也体现不出非阻塞 I/O 的优势。案例中的错误线程模型如下所示:

图 4-4 错误的客户端连接线程使用方式

4.3. 案例总结

无论是服务端监听多个端口,还是客户端连接多个服务端,都需要注意必须要重用 NIO 线程,否则就会导致线程资源浪费,在大规模组网时还会存在句柄耗尽或者栈溢出等问题。

Netty 官方 Demo 仅仅是个 Sample,对用户而言,必须理解 Netty 的线程模型,否则很容易按照官方 Demo 的做法,在外层套个 For 循环连接多个服务端,然后,悲剧就这样发生了。

修正案例中的问题非常简单,原理如下:

图 4-5 正确的客户端连接线程模型

5. 作者简介

李林锋,2007 年毕业于东北大学,2008 年进入华为公司从事高性能通信软件的设计和开发工作,有 7 年 NIO 设计和开发经验,精通 Netty、Mina 等 NIO 框架和平台中间件,现任华为软件平台架构部架构师,《Netty 权威指南》作者。目前从事华为下一代中间件和 PaaS 平台的架构设计工作。

联系方式:新浪微博 Nettying 微信:Nettying 微信公众号:Netty 之家

对于 Netty 学习中遇到的问题,或者认为有价值的 Netty 或者 NIO 相关案例,可以通过上述几种方式联系我。


感谢郭蕾对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2015-11-24 17:0713675
用户头像

发布了 25 篇内容, 共 69.8 次阅读, 收获喜欢 228 次。

关注

评论

发布
暂无评论
发现更多内容

解读区块链技术对量子攻击的脆弱性以及量子安全区块链的解决方案

CECBC

ONES 当选深圳信创联盟副理事长单位,助力国产软件工业发展

万事ONES

信创 ONES

ISC网络安全大会关于“新型网络犯罪打击与治理”的分析

郑州埃文科技

网络安全 isc

下一个颠覆的领域:区块链如何影响审计行业?(中)

CECBC

希望体验更好的开发流程

escray

学习 极客时间 朱赟的技术管理课 7月日更

如何识别并解决复杂的dcache问题

安第斯智能云

后端

8月日更挑战正式开启,新人大奖等你来领!

InfoQ写作社区官方

8月日更 热门活动

爱奇艺搜索排序算法实践(内附福利)

爱奇艺技术产品团队

排序算法 nlp 搜索

打造“云边一体化”,时序时空数据库TSDB技术原理深度揭秘

数据库 大数据 时序数据库 tsdb 数据智能

2021可信云大会顺利召开,北鲲云践行云计算六大发展趋势

北鲲云

Kafka为何弃用zookeeper(翻译)

石头哥谈架构

kafka kafka架构 分布式消息/流中间件

手撕环形队列系列二:无锁实现高并发

实力程序员

程序员 数据结构 并发 无锁 环形队列

行云管家荣获CFS第十届财经峰会2021科技创新引领奖!

行云管家

行云管家 财经峰会

微服务架构设计模式-进程间通信

以吻封笺

微服务 设计模式

你以为你懂redis?等看完某宝付费的资源你就知道了

Java架构师迁哥

阿里巴巴Java岗面试题库更新(第8版)

Java架构师迁哥

新思科技解读金融服务业的应用安全误区与现实

InfoQ_434670063458

新思科技 金融服务安全

第九周作业-朴朴超市用户路径&转化漏斗

小夏

产品经理训练营 邱岳

第三周作业-知识星球利益相关者排序

小夏

产品经理训练营 邱岳

新工具上线!sdkmgr命令行助力流水线构建

科技汇

🏆「作者推荐!」【Java 技术之旅】彻底你明白什么是JIT编译器(Just In Time编译器)

洛神灬殇

Java 编译器 JIT compiler 即时编译器

Fil收益怎么看?Fil一天收益如何?

区块链 IPFS fil收益 filecoin生态

【得物技术】服务发布时网络“抖动”

得物技术

网络 服务 响应时间 部署 发布

这次不编故事了,阿里Spring Cloud Alibabab笔记,自己领吧

Java架构师迁哥

HarmonyOS开发者创新大赛获奖作品分享——《分镜头App》

科技汇

Apache ShardingSphere:由开源驱动的分布式数据库中间件生态

亚马逊云科技 (Amazon Web Services)

人工智能 开源数据库

智慧农业陷转型困局,区块链如何“对症下药”?

CECBC

品牌轮:用MOT引导的品牌体验模型

石云升

用户体验 关键时刻 7月日更 体验设计

主流分布式文件系统选型,写得太好了!

编程菌

Java 编程 程序员 计算机 技术宅

浪潮云洲赋能智造 拉升制造业“微笑曲线”

云计算

完备的娱乐行业知识图谱库如何建成?爱奇艺知识图谱落地实践

爱奇艺技术产品团队

nlp 搜索 知识图谱

Netty案例集锦之多线程篇(续)_语言 & 开发_李林锋_InfoQ精选文章