问题解决
1、前端开发方面的问题
关于electron使用第三方nodejs模块编译打包的注意事项
关于使用electron开发串口项目中编译的一些注意事项
推荐一款更好用的字体抽取工具font-spider
failed Error: not found: python2.7的问题解决
thinkphp5.0.24兼容php7.4
nginx配置api前缀url转发,解决跨越访问
uni-app使用阿里iconfont多色图标
公众里如何发布竖版的视频,使用SVG方式
使用requireJS引入element-ui
2、后端开发方面的问题
记一次在centos7下安装Django博客的安装踩坑
oracle Cause: java.sql.SQLException: 调用中的无效参数
四种方式可以获取到nacos里的配置信息
gradle3.1升级到gracle7.6版本需要更新的地方
Idea解决Service启动服务不显示端口号的问题
docker中部署的flowable流程图乱码
jdk11版本的jenkins如何打包jdk8项目 ?
windows下面批量更新某个文件夹下面所有的git项目目录
在centos7.9环境上安装 nodejs20版本
3、部署运维方面的问题
virtualBox虚拟机拓展磁盘空间
win10家庭版本安装远程桌面
ssh连接虚拟机CentOS缓慢解决方法
jenkins构建的时候报git: Permission denied错误
安装sqlserver2017的时候遇到的两个坑
CentOS 的 YUM安装时卡死解决方案
docker容器在还原nexus3的数据的时候,注意
4、效率提升方面
win10系统右键没有新建文本文档的选项
微信双开的脚本.bat
Github Copilot如何使用,使用的快捷方式
copilot GitHub Copilot could not connect to server. Extension activation failed: “getaddrinfo ENOTFO
使用ffmpeg 将mp4里的音频摄取成mp3
5、问题的反思
记一次解决投票高并发引发的性能问题
uniapp 在远程调试的时候,报错Invalid Host header
解决绘世启动器的报错:Could not initialize Tensile library
Photoshop2024无法拖动图片导入的解决办法
kgm音乐文件解密操作
玄派星曜+开源宇宙eg01-c+rtx 4060ti 的一些配置说明
centos9上面安装wireguard后,无法启动服务Failed to set DNS configuration: Could not activate remote peer.
记一次破解瑞数6的工作过程(完美解决)
一级路由通过静态路由的方式访问二级路由
解决了一个关于ruoyi-vue添加租户id的拦截问题
在windows上面运行curl命令的正确姿势
使用 Termux + Python + Flask/FastAPI实现安卓手机发短信
一次 TCP 固件升级服务的性能优化实战
本文档使用 MrDoc 发布
-
+
首页
一次 TCP 固件升级服务的性能优化实战
# 一次 TCP 固件升级服务的性能优化实战 最近集中处理了一次 TCP 固件升级服务的性能瓶颈问题。最初的目标很直接:同一时间有更多设备发起升级时,服务端不要因为少量阻塞逻辑而把整体吞吐拖死。 这篇笔记记录一下这次排查、优化和压测的全过程,方便后面自己复盘,也方便团队里其他同事快速理解这次改动到底做了什么、带来了多大收益、还剩哪些边界要注意。 ## 背景 升级服务基于 Netty 实现,设备通过 TCP 协议分片拉取固件。每个固件包会被切成固定大小的片段,设备按片请求,服务端按片响应。 一开始看线上现象时,服务本身并没有明显打满 CPU 或内存,但并发一上来,单台设备升级耗时会明显变长。进一步排查后,问题集中在 Netty I/O 线程里存在阻塞等待。 ## 主要瓶颈 核心问题有两个: 1. `channelReadComplete()` 里存在 `Thread.sleep(100L)` 这意味着每次读完成后,当前 Netty I/O 线程都会硬等 100ms。线程在睡觉时不能处理其他连接,也不能继续处理其他设备的升级请求。 2. 在 `msgType == 22` 的分支里还有一次 `Thread.sleep(2000L)` 这段逻辑用于在返回 `22` 之后,再延迟发送 `23`。功能上是合理的,但实现方式同样是直接卡住 I/O 线程 2 秒。 这两个 `sleep` 放在单连接场景下不太显眼,但一旦并发上来,就会把 Netty 的事件循环线程拖成串行等待队列。 ## 这次做了什么优化 这次改动主要集中在 `NettyServer`: ### 1. 去掉 `channelReadComplete()` 里的阻塞等待 原先流程是: - `channelRead()` 只负责收字节并放进缓存 - `channelReadComplete()` 里先 `sleep(100ms)` - 然后再尝试解析缓存中的协议帧 现在改成: - `channelRead()` 收到字节后立刻写入缓存 - 紧接着立即尝试解析完整帧 - 如果当前还是半包,就把剩余数据继续留在缓存中,等下一次 `channelRead()` 再拼 也就是说,原来那套“缓存、解析、保留剩余半包”的机制并没有丢,只是去掉了人为的 100ms 阻塞等待。 ### 2. 把 `sleep(2000ms)` 改成异步延迟调度 以前是线程卡 2 秒后再发 `23`,现在改成: - 先正常返回 `22` - 使用 `ctx.executor().schedule(..., 2, TimeUnit.SECONDS)` 挂一个延迟任务 - 2 秒后异步发送 `23` 这样设备看到的协议顺序基本不变,但服务端线程不会被白白占住。 ### 3. 降低高频日志噪音 优化过程中还顺手处理了两个高频日志问题: - TCP 收发报文日志从 `INFO` 降到了 `DEBUG` - 去掉了 MyBatis `StdOutImpl` 的 SQL 控制台输出 这些日志本身不是第一瓶颈,但在高并发时会放大磁盘 IO、字符串拼装和标准输出重定向的开销。 ## 压测方式 为了让结果更贴近真实场景,这次没有只用模拟数据,而是从实际服务器拉了几个真实固件包到本地做压测。 代表性固件包括: - `V2.2.2.2.bin` - `V2.1.10.8.bin` - `V1.1.10.9.bin` 其中 `V2.1.10.8.bin` 大约 `301` 个分片,后续的大并发压测主要以它为主。 压测方式是按“完整升级流程”来做,而不是只跑握手或只拉少量分片: - 建立 TCP 连接 - 握手 - 查询升级版本 - 按片完整拉取固件 - 统计每轮总耗时、吞吐和单包延迟 ## 压测结果 ### 优化前 在之前的本地同口径压测里: - `100` 并发吞吐大约 `213 pkt/s` 从业务体感上看,`200` 台设备同时升级时,单台设备大约需要 `180` 秒左右,也就是 `3` 分钟。 ### 优化后 优化后,本地完整升级压测结果有了非常明显的提升。 #### 200 台设备同时升级 - 单台设备大约 `4.09` 秒完成 #### 500 台设备同时升级 - 单台设备大约 `8.68` 秒完成 - `500` 并发完整升级本地实测 `0` 失败 #### 1000 台设备同时升级 - 单台设备大约 `35.90` 秒完成 - `1000` 并发完整升级本地实测 `0` 失败 #### 100 并发吞吐对比 - 优化前:约 `213 pkt/s` - 优化后:约 `10691 pkt/s` 从这个口径看,本地代码路径的吞吐提升大约是几十倍。 ## 怎么理解这些数字 这些结果说明两件事: 1. 原来的主要瓶颈确实就是 I/O 线程里的阻塞 `sleep` 2. 去掉阻塞后,代码层面的升级吞吐被明显释放出来了 不过也要注意边界: - 这组数据是本地压测结果,不等于线上承诺值 - 线上还会受到公网网络、Redis、磁盘日志、SQLite 写入等因素影响 - `1000` 并发虽然本地能跑通,但从延迟看已经不是“完全没压力”的状态 所以更准确的表达应该是: - 代码层面已经可以支撑更高并发的升级请求 - `500` 并发属于比较乐观且已经本地验证通过的范围 - `1000` 并发也能跑通,但上线前仍然建议做一轮服务器实测和小流量灰度 ## 这次优化有没有功能风险 有风险,但属于低风险、可验证的那种,不是协议逻辑被改坏了。 主要风险点在于: - 以前设备请求可能“顺带吃到了”那 100ms 的等待窗口 - 现在改成了收到数据就立即尝试解析,极少数 timing-sensitive 设备如果对这个窗口非常敏感,可能会出现边缘兼容性差异 但目前本地用真实固件跑 `100 / 200 / 500 / 1000` 并发完整升级都通过,说明主升级链路没有被破坏。 ## 后续建议 这次优化已经把最明显的性能瓶颈拿掉了,但如果目标是线上长期稳定支撑更高升级并发,还可以继续做下面几件事: 1. 线上彻底收敛控制台日志输出,只保留需要的归档日志 2. 继续观察 Redis、SQLite 和磁盘 IO 在大并发下的表现 3. 在服务器环境做一轮低风险灰度验证 4. 如果未来并发再往上走,可以评估是否把部分业务处理从 Netty I/O 线程进一步拆到独立业务线程池 ## 总结 这次优化本质上不是“加机器”,而是把服务端最核心的阻塞点拿掉了。 一个很小的 `sleep(100ms)`,放在 Netty I/O 线程里时,会把整个升级服务的并发表现压得非常难看;但一旦改成真正的非阻塞处理,升级服务的吞吐就能立刻释放出来。 对这类长连接、高并发、分片传输的服务来说,最重要的往往不是先看机器够不够,而是先确认事件循环线程里有没有做不该做的阻塞动作。这个思路,这次算是非常典型地验证了一遍。
superadmin
2026年4月29日 16:35
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码