东亚银行、岚图汽车带你解锁 AIGC 时代的数字化人才培养各赛道新模式! 了解详情
写点什么

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

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

关注

评论

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

2020年11期券商App行情刷新及交易体验评测报告

博睿数据

APM 数据 AIOPS 证券

我参与阿里巴巴 ASoC-Seata 的一些感悟

阿里巴巴云原生

阿里云 开发者 云原生 感悟 seata

这道面试题,出错率90%

田维常

面试

获奖名单|七日更挑战成功!

InfoQ写作社区官方

奖品 七日更 热门活动

币币交易所系统开发详情说明

数字货币交易所开发的功能与特点

千里公路建设尽收眼底,3D可视化监测管养运,领导都惊呆了

一只数据鲸鱼

物联网 数据可视化 3D可视化 公路建设 智慧交通

Flink SQL 实战:双流 join 场景应用

Apache Flink

flink 流计算

智慧公安防控管理平台搭建,重点人员管控系统解决方案

t13823115967

智慧公安

区块链数字货币交易所开发的简介

有没有听说过通达快递?

escray

极客时间 极客大学 课程作业 大作业 架构师训练营第 1 期

纵观 ActiveX 平台的兴衰史,看开发控件的技术演变

葡萄城技术团队

SpreadJS activex

智慧社区管理平台建设,智慧平安小区整体解决方案

t13823115967

智慧社区安防系统平台开发

LINUX SHELL脚本攻略

田维常

敏捷团队的质量保障赋能

BY林子

质量保障 质量赋能 敏捷测试

7 天开发后台系统技术小结

老魚

程序员 全栈 建站

Linux进程知识干货|收藏

赖猫

c++ Linux 后台开发 运维

电商平台如何激发内容生态

马踏飞机747

内容 内容分发网络 电商

程序员修炼之路:你该知道的 7 个必经阶段

阿里巴巴云原生

阿里云 程序员 云原生 自我思考 成长笔记

区块链商城系统开发技术详解

区块链钱包开发的功能与特点

区块链app开发要多少钱?如何根据项目需求了解价格?

对冲基金的子基金模式vs集中管理

9527

漫画 | 带你领略前端发展史的江湖恩怨情仇

苏南

程序员 大前端 漫画 时代发展

国外低代码平台趟过那些坑,对国内低代码企业有哪些启示?

DT极客

四年三次获奖,PostgreSQL再度荣获“年度数据库”桂冠!

PostgreSQLChina

数据库 postgresql 开源

区块链挖矿系统开发功能方案

Python的GIL

yunson

Python GIL

深度解析!滴滴内部开源Spring IoC和AOP源码小册

Java架构追梦

Java spring 架构 aop ioc

如何防止短信验证码接口被恶意调用攻击?

香芋味的猫丶

短信 短信防刷 接口安全 验证码

区块链钱包开发的核心优势

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