“AI 技术+人才”如何成为企业增长新引擎?戳此了解>>> 了解详情
写点什么

Netty 案例集锦之多线程篇

  • 2015-09-03
  • 本文字数:6308 字

    阅读完需:约 21 分钟

1. Netty 案例集锦系列文章介绍

1.1. Netty 的特点

Netty 入门比较简单,主要原因有如下几点:

  1. Netty 的 API 封装比较简单,将复杂的网络通信通过 BootStrap 等工具类做了二次封装,用户使用起来比较简单;
  2. Netty 源码自带的 Demo 比较多,通过 Demo 可以很快入门;
  3. Netty 社区资料、相关学习书籍也比较多,学习资料比较丰富。

但是很多入门之后的 Netty 学习者遇到了很多困惑,例如不知道在实际项目中如何使用 Netty、遇到 Netty 问题之后无从定位等,这些问题严重制约了对 Netty 的深入掌握和实际项目应用。

Netty 相关问题比较难定位的主要原因如下:

1) NIO 编程自身的复杂性,涉及到大量 NIO 类库、Netty 自身封装的类库等,当你需要打开黑盒定位问题时,必须对这些类库了如指掌;否则即便定位到问题所在,也不知所以然,更无法修复;

2) Netty 复杂的多线程模型,用户在实际使用 Netty 时,会涉及到 Netty 自己封装的线程组、线程池、NIO 线程,以及业务线程,通信链路的创建、I/O 消息的读写会涉及到复杂的线程切换,这会让初学者云山雾绕,调试起来非常痛苦,甚至都不知道从哪里调试;

3) Netty 版本的跨度大,从实际商用情况看,涉及到了 Netty 3.X、4.X 和 5.X 等多个版本,每个 Major 版本之间特性变化非常大,即便是 Minor 版本都存在一些差异,这些功能特性和类库差异会给使用者带来很多问题,版本升级之后稍有不慎就会掉入陷阱。

1.2. 案例来源

Netty 案例集锦的案例来源于作者在实际项目中遇到的问题总结、以及 Netty 社区网友的反馈,大多数案例都来源于实际项目,也有少部分是读者在学习 Netty 中遭遇的比较典型的问题。

1.3. 多线程篇

学习和掌握 Netty 多线程模型是个难点,在实际项目中如何使用好 Netty 多线程更加困难,很多网上问题和事故都来源于对 Netty 线程模型了解不透彻所致。鉴于此,Netty 案例集锦系列就首先从多线程方面开始。

2. Netty 3 版本升级遭遇内存泄漏案例

2.1. 问题描述

业务代码升级 Netty 3 到 Netty4 之后,运行一段时间,Java 进程就会宕机,查看系统运行日志发现系统发生了内存泄露(示例堆栈):

图 2-1 内存泄漏堆栈

对内存进行监控(切换使用堆内存池,方便对内存进行监控),发现堆内存一直飙升,如下所示(示例堆内存监控):

图 2-2 堆内存监控示例

2.2. 问题定位

使用 jmap -dump:format=b,file=netty.bin PID 将堆内存 dump 出来,通过 IBM 的 HeapAnalyzer 工具进行分析,发现 ByteBuf 发生了泄露。

因为使用了 Netty 4 的内存池,所以首先怀疑是不是申请的 ByteBuf 没有被释放导致?查看代码,发现消息发送完成之后,Netty 底层已经调用 ReferenceCountUtil.release(message) 对内存进行了释放。这是怎么回事呢?难道 Netty 4.X 的内存池有 Bug,调用 release 操作释放内存失败?

考虑到 Netty 内存池自身 Bug 的可能性不大,首先从业务的使用方式入手分析:

  1. 内存的分配是在业务代码中进行,由于使用到了业务线程池做 I/O 操作和业务操作的隔离,实际上内存是在业务线程中分配的;
  2. 内存的释放操作是在 outbound 中进行,按照 Netty 3 的线程模型,downstream(对应 Netty 4 的 outbound,Netty 4 取消了 upstream 和 downstream)的 handler 也是由业务调用者线程执行的,也就是说申请和释放在同一个业务线程中进行。初次排查并没有发现导致内存泄露的根因,继续分析 Netty 内存池的实现原理。

Netty 内存池实现原理分析:查看 Netty 的内存池分配器 PooledByteBufAllocator 的源码实现,发现内存池实际是基于线程上下文实现的,相关代码如下:

也就是说内存的申请和释放必须在同一线程上下文中,不能跨线程。跨线程之后实际操作的就不是同一块儿内存区域,这会导致很多严重的问题,内存泄露便是其中之一。内存在 A 线程申请,切换到 B 线程释放,实际是无法正确回收的。

2.3. 问题根因

Netty 4 修改了 Netty 3 的线程模型:在 Netty 3 的时候,upstream 是在 I/O 线程里执行的,而 downstream 是在业务线程里执行。当 Netty 从网络读取一个数据报投递给业务 handler 的时候,handler 是在 I/O 线程里执行;而当我们在业务线程中调用 write 和 writeAndFlush 向网络发送消息的时候,handler 是在业务线程里执行,直到最后一个 Header handler 将消息写入到发送队列中,业务线程才返回。

Netty4 修改了这一模型,在 Netty 4 里 inbound(对应 Netty 3 的 upstream) 和 outbound(对应 Netty 3 的 downstream) 都是在 NioEventLoop(I/O 线程) 中执行。当我们在业务线程里通过 ChannelHandlerContext.write 发送消息的时候,Netty 4 在将消息发送事件调度到 ChannelPipeline 的时候,首先将待发送的消息封装成一个 Task,然后放到 NioEventLoop 的任务队列中,由 NioEventLoop 线程异步执行。后续所有 handler 的调度和执行,包括消息的发送、I/O 事件的通知,都由 NioEventLoop 线程负责处理。

在本案例中,ByteBuf 在业务线程中申请,在后续的 ChannelHandler 中释放,ChannelHandler 是由 Netty 的 I/O 线程 (EventLoop) 执行的,因此内存的申请和释放不在同一个线程中,导致内存泄漏。

Netty 3 的 I/O 事件处理流程:

图 2-3 Netty 3 的 I/O 线程模型

Netty 4 的 I/O 消息处理流程:

图 2-4 Netty 4 I/O 线程模型

2.4. 案例总结

Netty 4.X 版本新增的内存池确实非常高效,但是如果使用不当则会导致各种严重的问题。诸如内存泄露这类问题,功能测试并没有异常,如果相关接口没有进行压测或者稳定性测试而直接上线,则会导致严重的线上问题。

内存池 PooledByteBuf 的使用建议:

  1. 申请之后一定要记得释放,Netty 自身 Socket 读取和发送的 ByteBuf 系统会自动释放,用户不需要做二次释放;如果用户使用 Netty 的内存池在应用中做 ByteBuf 的对象池使用,则需要自己主动释放;
  2. 避免错误的释放:跨线程释放、重复释放等都是非法操作,要避免。特别是跨线程申请和释放,往往具有隐蔽性,问题定位难度较大;
  3. 防止隐式的申请和分配:之前曾经发生过一个案例,为了解决内存池跨线程申请和释放问题,有用户对内存池做了二次包装,以实现多线程操作时,内存始终由包装的管理线程申请和释放,这样可以屏蔽用户业务线程模型和访问方式的差异。谁知运行一段时间之后再次发生了内存泄露,最后发现原来调用 ByteBuf 的 write 操作时,如果内存容量不足,会自动进行容量扩展。扩展操作由业务线程执行,这就绕过了内存池管理线程,发生了“引用逃逸”;
  4. 避免跨线程申请和使用内存池,由于存在“引用逃逸”等隐式的内存创建,实际上跨线程申请和使用内存池是非常危险的行为。尽管从技术角度看可以实现一个跨线程协调的内存池机制,甚至重写 PooledByteBufAllocator,但是这无疑会增加很多复杂性,通常也使用不到。如果确实存在跨线程的 ByteBuf 传递,而且无法保证 ByteBuf 在另一个线程中会重新分配大小等操作,最简单保险的方式就是在线程切换点做一次 ByteBuf 的拷贝,但这会造成性能下降。

比较好的一种方案就是如果存在跨线程的 ByteBuf 传递,对 ByteBuf 的写操作要在分配线程完成,另一个线程只能做读操作。操作完成之后发送一个事件通知分配线程,由分配线程执行内存释放操作。

3. Netty 3 版本升级性能下降案例

3.1. 问题描述

业务代码升级 Netty 3 到 Netty4 之后,并没有给产品带来预期的性能提升,有些甚至还发生了非常严重的性能下降,这与 Netty 官方给出的数据并不一致。

Netty 官方性能测试对比数据:我们比较了两个分别建立在 Netty 3 和 4 基础上 echo 协议服务器。(Echo 非常简单,这样,任何垃圾的产生都是 Netty 的原因,而不是协议的原因)。我使它们服务于相同的分布式 echo 协议客户端,来自这些客户端的 16384 个并发连接重复发送 256 字节的随机负载,几乎使千兆以太网饱和。

根据测试结果,Netty 4:

  • GC 中断频率是原来的 1/5: 45.5 vs. 9.2 次 / 分钟
  • 垃圾生成速度是原来的 1/5: 207.11 vs 41.81 MiB/ 秒

3.2. 问题定位

首先通过 JMC 等性能分析工具对性能热点进行分析,示例如下(信息安全等原因,只给出分析过程示例截图):

图 3-1 性能热点线程堆栈

通过对热点方法的分析,发现在消息发送过程中,有两处热点:

  1. 消息发送性能统计相关 Handler;
  2. 编码 Handler。

对使用 Netty 3 版本的业务产品进行性能对比测试,发现上述两个 Handler 也是热点方法。既然都是热点,为啥切换到 Netty4 之后性能下降这么厉害呢?

通过方法的调用树分析发现了两个版本的差异:在 Netty 3 中,上述两个热点方法都是由业务线程负责执行;而在 Netty 4 中,则是由 NioEventLoop(I/O) 线程执行。对于某个链路,业务是拥有多个线程的线程池,而 NioEventLoop 只有一个,所以执行效率更低,返回给客户端的应答时延就大。时延增大之后,自然导致系统并发量降低,性能下降。

找出问题根因之后,针对 Netty 4 的线程模型对业务进行专项优化,将耗时的编码等操作迁移到业务线程中执行,为 I/O 线程减负,性能达到预期,远超过了 Netty 3 老版本的性能。

Netty 3 的业务线程调度模型图如下所示:充分利用了业务多线程并行编码和 Handler 处理的优势,周期 T 内可以处理 N 条业务消息:

图 3-2 Netty 3 Handler 执行线程模型

切换到 Netty 4 之后,业务耗时 Handler 被 I/O 线程串行执行,因此性能发生比较大的下降:

图 3-3 Netty 4 Handler 执行线程模型

3.3. 问题总结

该问题的根因还是由于Netty 4 的线程模型变更引起,线程模型变更之后,不仅影响业务的功能,甚至对性能也会造成很大的影响。

对Netty 的升级需要从功能、兼容性和性能等多个角度进行综合考虑,切不可只盯着API 变更这个芝麻,而丢掉了性能这个西瓜。API 的变更会导致编译错误,但是性能下降却隐藏于无形之中,稍不留意就会中招。

对于讲究快速交付、敏捷开发和灰度发布的互联网应用,升级的时候更应该要当心。

4. Netty 业务 Handler 接收不到消息案例

4.1. 问题描述

我的服务碰到一个问题,经常有请求上来到 MessageDecoder 就结束了,没有继续往 LogicServerHandler 里面送,觉得很奇怪,是不是线程池满了?我想请教:

  1. netty 5 如何打印 executor 线程的占用情况,如空闲线程数?
  2. executor 设置的大小一般如何进行计算的?

业务代码示例如下:

4.2. 问题定位

从服务端初始化代码来看,并没有什么问题,业务 LogicServerHandler 没有接收到消息,有如下几种可能:

  1. 客户端并没有将消息发送到服务端,可以在服务端 LoggingHandler 中打印日志查看;
  2. 服务端部分消息解码发生异常,导致消息被丢弃 / 忽略,没有走到 LogicServerHandler 中;
  3. 执行业务 Handler 的 DefaultEventExecutor 中的线程太繁忙,导致任务队列积压,长时间得不到处理。

通过抓包结合日志分析,可能导致问题的原因 1 和 2 排除,需要继续对可能原因 3 进行排查。

Netty 5 如何打印 executor 线程的占用情况,如空闲线程数?回答这些问题,首先要了解 Netty 的线程组和线程池机制。

Netty 的 EventExecutorGroup 实际就是一组 EventExecutor,它的定义如下:

通常通过它的 next 方法从线程组中获取一个线程池,代码如下:

Netty EventExecutor 的典型实现有两个:DefaultEventExecutor 和 SingleThreadEventLoop,在本案例中,因为使用的是 DefaultEventExecutorGroup,所以实际执行业务 Handler 的线程池就是 DefaultEventExecutor,它继承自 SingleThreadEventExecutor,从名称就可以看出它是个单线程的线程池。它的工作原理如下:

  1. DefaultEventExecutor 聚合 JDK 的 Executor 和 Thread, 首次执行 Task 的时候启动线程,将线程池状态修改为运行态;
  2. Thread run 方法循环从队列中获取 Task 执行,如果队列为空,则同步阻塞,线程无限循环执行,直到接收到退出信号。

图 4-1 DefaultEventExecutor 工作原理

用户想通过 Netty 提供的 DefaultEventExecutorGroup 来并发执行业务 Handler,但实际上却是单线程 SingleThreadEventExecutor 在串行执行业务逻辑,当服务端消息接收速度超过业务逻辑执行速度时,就会导致业务消息积压在 SingleThreadEventExecutor 的消息队列中得不到及时处理,现象就是业务 Handler 好像得不到执行,部分业务消息丢失。

讲解完 Netty 线程模型后,问题原因也定位出来了。其实我们发现,可以通过 EventExecutor 获取 EventExecutorGroup 的信息,然后获取整个 EventExecutor 线程组信息,最后打印线程负载信息,代码如下:

执行结果如下:

4.3. 问题总结

事实上,Netty 为了防止多线程执行某个 Handler(Channel)引起线程安全问题,实际只有一个线程会执行某个 Handler,代码如下:

需要指出的是,SingleThreadEventExecutor 的 pendingTasks 可能是个耗时的操作,因此调用的时候需要注意:

实际就像 JDK 的线程池,不同的业务场景、硬件环境和性能标就会有不同的配置,无法给出标准的答案。需要进行实际测试、评估和调优来灵活调整。

最后再总结回顾下问题,对于案例中的代码,实际上在使用单线程处理某个 Handler 的 LogicServerHandler,作者可能想并发多线程执行这个 Handler,提升业务处理性能,但实际并没有达到设计效果。

如果业务性能存在问题,并不奇怪,因为业务实际是单线程串行处理的!当然,如果业务存在多个 Channel,则每个 / 多个 Channel 会对应一个线程(池),也可以实现多线程处理,这取决于客户端的接入数。

案例中代码的线程处理模型如下所示(单个链路模型):

图 4-3 单线程执行业务逻辑线程模型图

5. Netty 4 ChannelHandler 线程安全疑问

5.1. 问题咨询

我有一个非线程安全的类 ThreadUnsafeClass,这个类会在 channelRead 方法中被调用。我下面这样的调用方法在多线程环境下安全吗?谢谢!

代码示例如下:

5.2. 解答

Netty 4 优化了 Netty 3 的线程模型,其中一个非常大的优化就是用户不需要再担心 ChannelHandler 会被并发调用,总结如下:

  • ChannelHandler’s 的方法不会被 Netty 并发调用;
  • 用户不再需要对 ChannelHandler 的各个方法做同步保护;
  • ChannelHandler 实例不允许被多次添加到 ChannelPiple 中,否则线程安全将得不到保证

根据上述分析,MyHandler 的 channelRead 方法不会被并发调用,因此不存在线程安全问题。

5.3. 一些特例

ChannelHandler 的线程安全存在几个特例,总结如下:

  • 如果 ChannelHandler 被注解为 @Sharable,全局只有一个 handler 实例,它会被多个 Channel 的 Pipeline 共享,会被多线程并发调用,因此它不是线程安全的;
  • 如果存在跨 ChannelHandler 的实例级变量共享,需要特别注意,它可能不是线程安全的

非线程安全的跨 ChannelHandler 变量原理如下:

图 5-1 串行调用,线程安全

Netty 支持在添加 ChannelHandler 的时候,指定执行该 Handler 的 EventExecutorGroup,这就意味着在整个 ChannelPipeline 执行过程中,可能会发生线程切换。此时,如果同一个对象在多个 ChannelHandler 中被共享,可能会被多线程并发操作,原理如下:

图 5-2 并行调用,多 Handler 共享成员变量,非线程安全

6. 作者简介

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

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

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


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

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

2015-09-03 08:2618192
用户头像

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

关注

评论 5 条评论

发布
用户头像
不知道这是4.X的版本,有些问题,在新版本中不存在了,比如说跨线程申请和释放的问题,PooledByteBuf中保存了PoolThreadCache和PoolChunk,不会释放错
2021-03-16 03:25
回复
我在4.1.49版本上做了测试,一个线程申请,另一个释放,可以正常回收。而且下载了较早的4.0.56的源码看了看,PooledByteBuf中也有PoolThreadCache和PoolChunk。图也挂了,看不懂哪里有问题啊
2021-03-16 03:33
回复
用户头像
图片损坏可以修复一下吗
2020-07-21 15:54
回复
用户头像
图片损坏
2019-07-15 16:52
回复
用户头像
这篇文章的插图都损坏了。影响阅读
2019-03-24 21:38
回复
没有更多了
发现更多内容

开发者不需要成为 K8s 专家!!!

北京好雨科技有限公司

Kubernetes 开发者 PaaS #云原生

22. 面向对象 - 高阶

茶桁

Python 面向对象 oop

ARTS-WEEK1 —— 23.8.14~23.8.20

EchoZhou

阿里云产品测评赢大奖丨云原生网关 MSE-Higress

阿里巴巴云原生

阿里云 微服务 云原生

Mac端视频动态壁纸 Dynamic Wallpaper 中文最新

胖墩儿不胖y

Mac软件 动态桌面壁纸软件 动态壁纸软件

华秋硬创联合安创加速器,加速和创新赋能技术驱动型创业者

华秋电子

创业

浅析分布式系统之体系结构 基本属性--故障

snlfsnef

亚信科技AntDB数据库连年入选《中国DBMS市场指南》代表厂商

亚信AntDB数据库

数据库 AntDB AntDB数据库 企业号 8 月 PK 榜

Spring高手之路12——BeanDefinitionRegistry与BeanDefinition合并解析

砖业洋__

spring BeanDefinition BeanDefinitionRegistry BeanDefinition合并

​Kubernetes的演变:从etcd到分布式SQL的过渡

互联网工科生

Kubernetes etcd 部署

IPQ4019, IPQ6010, and IPQ6018 Revolutionizing Urban Vehicle Connectivity

wallyslilly

IPQ4019 IPQ6010 ipq6018

中路对线发现正在攻防演练中投毒的红队大佬

墨菲安全

网络安全 npm 投毒 投毒分析

Capture One Pro 22 for Mac(RAW图像处理软件) v15.4.2.12中文版

mac

Raw图像处理软件 苹果mac Windows软件下载 Capture One Pro 22

RocketMQ 5.0 架构解析:如何基于云原生架构支撑多元化场景

阿里巴巴云原生

阿里云 RocketMQ 云原生

目前主流大数据平台有哪些?哪些行业需要大数据平台?

行云管家

数据安全 大数据平台

一点就通,社交源码IOS客户端开发集成SDK

山东布谷网络科技

sdk

NineData成功通过AWS FTR认证,一款值得信赖的数据管理工具!

NineData

数据库 AWS 数据管理 NineData FTR 认证

REST 内容协商详解:理论解读与实际应用指南

Liam

程序员 前端 后端 web开发 RESTful API

手把手带你玩转HetuEngine:资源规划与数据源对接

华为云开发者联盟

大数据 后端 华为云 华为云开发者联盟 企业号 8 月 PK 榜

消息推送:精准推送,提升运营效果,增添平台活力

MobTech袤博科技

前端 前端开发 消息推送 sdk

Proxifier for Mac(socks5全局代理客户端) v3.8注册版

mac

全局代理客户端 苹果mac Windows软件 Proxifier

生成式AI:人工智能新范式,引领生产力革命

百度开发者中心

#人工智能 文心一言

PoseiSwap 更新质押系统,并将在 8 月18 日开启“Trident ”快照

股市老人

大数据平台是什么意思?有什么用?一般包含哪些模块?

行云管家

数据安全 大数据平台

开放网关架构演进

阿里技术

网关 开放网关

KaiwuDB CTO 魏可伟:回归用户本位,打造“小而全”的数据库

KaiwuDB

KaiwuDB DTCC 2023

raw图片处理 DxO PhotoLab 6中文直装版

mac大玩家j

图像编辑 Mac软件 编辑图像 raw图像

带你读论文丨S&P21 Survivalism: Living-Off-The-Land 经典离地攻击

华为云开发者联盟

人工智能 华为云 华为云开发者联盟 企业号 8 月 PK 榜

PoseiSwap 更新质押系统,并将在 8 月18 日开启“Trident ”快照

EOSdreamer111

浅析分布式系统之体系结构 一致性的实现-- 共识

snlfsnef

分布式 系统设计 数据一致性 共识机制 深入理解分布式共识算法

生成式AI技术:引领未来内容创作

百度开发者中心

物联网, 云计算, 文心一言 人工智能、

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