HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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:0713655
用户头像

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

关注

评论

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

超硬核!阿里技术大牛肝了2晚整理的Java知识,这也太强了!

飞飞JAva

Java Java泛型

SwiftUI @ Netflix:推动新技术落地是怎样一种体验?

故胤道长

swift 移动开发 iOS Developer SwiftUI

多家银行增设数字金融部 架构调整背后透露出哪些信号?

CECBC

银行

【JS】作用域(入门篇)

德育处主任

JavaScript 大前端 Web js

如何在 GitHub 上面为你的项目选择开源许可证

HoneyMoose

Redis-技术专区-知识问题总结大全(上篇)

洛神灬殇

redis 5月日更 问题分析

限时白嫖!腾讯内部员工培训Java资料,网友:大厂就是不一样

牛哄哄的java大师

Java

【LeetCode】砖墙Java题解

Albert

算法 LeetCode 5月日更

复杂Gremlin查询的调试方法

Tom(⊙o⊙)

gremlin调试

网络攻防学习笔记 Day2

穿过生命散发芬芳

5月日更 网络攻防

顺序一致性(Sequential Consistency)

UNDEFINED

sequential consistency Java Concurrency distributed system

高级研发工程师都有哪些特点?【超级准】

liuzhen007

技术人生 工作体会 程序猿

从零搭建一款PC页面编辑器PC-Dooring

徐小夕

大前端 可视化 lowcode 代码编辑器

【人间碎片】关于努力这件事

南吕

人生修炼 人生故事

微服务-技术专题-微服务进程间通信

洛神灬殇

微服务 分布式架构 5月日更

你的烂代码终于有了解决方案

博文视点Broadview

书单 | 月度畅销好书,助你技能满格,摆脱低效,走向财富人生

博文视点Broadview

我与 InfoQ 写作平台的这些事

xcbeyond

个人成长 1 周年盛典 InfoQ 写作平台 1 周年 5月日更

如何提升工作效率

wangwei1237

工作效率 文化 大历史理论

数字化转型能力成为中国纺织服装业未来发展的核心动能

CECBC

纺织面料

外行在谈论流派,大师在讨论颜料

顿晓

极限编程 5月日更 门道

如何选择开源许可证

HoneyMoose

名可名

顿晓

5月日更 命名

Redis-技术专题-Redis分布式锁实现方案

洛神灬殇

redis 分布式锁 5月日更

当你觉得老板的决策是错的,你会怎么做?

石云升

职场经验 5月日更

未来5年或将出现颠覆型区块链应用,资产通证化将重构实体经济

CECBC

区块链

【音视频】弱网下的音视频通讯

Bob

音视频 直播技术

Excel用户如何学习数据分析语言DAX?

博文视点Broadview

OAuth 2.0 了解了,OAuth 2.1 呢?

Zhang

OAuth 2.0 认证授权 OAuth 2.1

通向未来的十二个趋势

CECBC

人工智能

区块链如何推动人力资源和薪酬管理体系变革?

CECBC

人力资源

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