写点什么

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

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

关注

评论

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

前端之算法(一)

Augus

数据结构与算法 8月日更

【前端 · 面试 】HTTP 总结(十一)—— HTTPS 概述

编程三昧

面试 https 8月日更

Web 框架 Gin | Gin 介绍

xcbeyond

Go 语言 gin 8月日更

Android开发:引入重复包报错Error:Execution failed for task ‘:app:transform...’解决方法

三掌柜

8月日更 8月

企业不可忽视的三大关键时刻

石云升

管理经验 关键时刻 体验设计 8月日更

从新手村出来,我在 Apache APISIX 社区发出了第一个 PR

API7.ai 技术团队

开源 后端 API网关 APISIX

从安卓转到Java开发,我吃透了这份pdf,终于4面拿下美团offer

Java~~~

Java spring 面试 微服务 JVM

仿imtoken钱包源码,TP钱包源码开发

端口占用解决方案

一个大红包

8月日更

ipfs矿机挖币哪家最好?ipfs矿机公司实力排行如何?

ipfs矿机挖币哪家最好 ipfs矿机公司实力排行如何

DeFi去中心化平台源码开发|智能合约系统搭建

量化系统19942438797

记一次PHP渗透测试实战教程

网络安全学海

php 网络安全 信息安全 渗透测试 安全漏洞

【设计模式】代理模式

Andy阿辉

C# 后端 设计模式 8月日更

fil矿机怎么购买?fil矿机在哪买?

fil矿机怎么购买 fil矿机在哪买

手把手 Golang 实现静态图像与视频流人脸识别

声网

音视频 人脸识别

新药开发瓶颈问题或将被打破,北鲲云超算平台开启药物研发“加速度”

北鲲云

区块链难懂?人民日报评论员讲给你听

CECBC

网络攻防学习笔记 Day102

穿过生命散发芬芳

态势感知 网络攻防 8月日更

融云CTO杨攀:把握核心技术,促进产学研用融合发展

融云 RongCloud

MinIO Client 使用(一)

耳东@Erdong

Minio 8月日更 mc minio client

基于时间和窗口的算子(六)

Databri_AI

flink 窗口函数 算子

用区块链加强知识产权保护

CECBC

MySQL知识点整理

一个大红包

8月日更

数据缓存历险记(五)--LRU缓存算法的最终篇

卢卡多多

缓存 LRU Redis 协议 8月日更

CSS 文档中定位指南:static、relative、absolute、fixed、sticky

devpoint

CSS 8月日更

写作 7 堂课——【6. 清单式写作】

LeifChen

清单 写作技巧 8月日更 检查清单

异步编程的终极解决方案 async/await:用同步的方式去写异步代码

前端依依

大前端 js 经验分享 异步 知识讲解

RESTful API

escray

学习 极客时间 如何落地业务建模 8月日更

🏆「作者推荐」【JVM 性能调优】JVM分析与调优技巧分析(原理篇)

洛神灬殇

JVM JVm虚拟机 8月日更

LeetCode题解:80. 删除有序数组中的重复项 II,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

Ipfs靠谱吗?ipfs中国授权公司都有哪些?

分布式存储 区块链+ IPFS fil

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