进行24小时稳定性测试TPS越来越少原因分析
在24小时稳定性测试中观察到 TPS(Transactions Per Second)持续或逐渐下降,这是非常典型且重要的性能衰减迹象,表明系统在长时间运行后性能逐步劣化。主要原因可以从以下几个方面分析:
👾 1. 资源泄露/耗尽
- 内存泄漏(最常见原因之一):
- 应用层: Java(堆外内存、PermGen/Metaspace)、C/C++应用等中未释放的对象、集合膨胀未被清理、缓存无限增长(无失效策略)、未关闭资源(文件句柄、数据库连接、网络连接、
ThreadLocal
未清理等)都会导致内存占用持续增长。 - 作用: 内存泄漏导致频繁GC(垃圾回收),特别是Full GC,消耗大量CPU时间且停顿时间长(STW)。内存接近耗尽时,系统开始使用Swap空间,磁盘I/O剧增,性能急剧下降。如果泄漏严重,最终会导致
OutOfMemoryError
甚至进程崩溃。
- 应用层: Java(堆外内存、PermGen/Metaspace)、C/C++应用等中未释放的对象、集合膨胀未被清理、缓存无限增长(无失效策略)、未关闭资源(文件句柄、数据库连接、网络连接、
- 线程泄漏/线程池耗尽:
- 未正确关闭/复用线程(或数据库连接、HTTP连接),导致线程数量不断增加。
- 线程池配置不当或被任务完全占用后,新任务被迫排队等待或直接被拒绝,响应时间拉长甚至失败,从而导致TPS下降(任务堆积)。
- 连接池耗尽(数据库、外部服务等):
- 连接未正确释放回连接池(代码bug),导致池中可用连接越来越少。新请求在获取连接池对象时阻塞或等待时间变长,最终超时失败。
- 文件句柄耗尽:
- 打开的文件、套接字(
Socket
)未关闭。系统级ulimit
设置过低会导致进程无法打开新文件或网络连接。
- 打开的文件、套接字(
- CPU资源逐渐饱和:
- 内存泄漏引起频繁GC消耗CPU。
- 死循环、有损设计(如低效算法在数据集变大时变慢)、线程竞争加剧(锁冲突)都可能使平均CPU使用率逐步攀升至100%,CPU成为瓶颈,导致处理能力下降。
🧠 2. 线程/锁/并发瓶颈
- 锁竞争加剧:
- 随着系统负载累积或数据量增长(如缓存变大、数据库表行数增加),锁(同步锁、数据库锁)的争用可能变得更加激烈。
- 热点区块或数据(热门商品账户)的竞争尤其强烈。
- 持有锁的时间过长或无合理释放机制会导致更多线程阻塞等待,大大降低并发处理能力。
- 死锁/线程挂死:
- 部分线程因逻辑错误进入永久等待(死锁)或资源无法获取而无限挂起。
- 虽然不一定使系统完全崩溃,但会让参与死锁的线程失去能力,导致系统吞吐量下降。
- 线程上下文切换过多:
- 线程数过多,大量时间用在切换线程而非真正处理任务上。
- 锁争用严重也会导致大量等待线程间的无效切换。
- 上下文切换过多导致CPU利用率虚高(
sys%
比例过大),实际有效工作减少。
💿 3. 数据库瓶颈加剧
- 慢查询恶化:
- 数据量增长: 测试过程中积累的数据让原本"正常"的查询变慢(特别是未正确使用索引或索引失效的查询)。
- 缓存失效/失效不足: 应用层缓存(如Redis)达到瓶颈或被清除,更多请求落到数据库。数据过期策略不完善、大量缓存穿透/击穿/雪崩可能导致数据库压力骤增。
- 数据库锁争用:
- 随着数据量和操作的增加,事务锁(行锁、表锁)竞争可能加剧。如处理不当会产生死锁或大量锁等待。
- 连接数压力:
- 应用端连接池耗尽或数据库服务器连接数达到上限。
- 数据库内部资源争用:
- I/O(硬盘读写)、CPU、内存缓冲区、日志写入等资源逐渐达到饱和。
- 数据库积累负担:
- 未提交的事务、大事务、长时间运行的SQL消耗大量资源导致后续语句变慢。
- 数据库特定问题:
- 表空间不足、归档日志空间满、统计信息过时导致执行计划错误选择等。
🔗 4. 外部依赖服务性能退化
- 第三方API/服务:
- 调用的外部API响应时间变慢、失败率增加(如他们也面临资源泄漏、限流、过载),使得调用方等待延长,拖垮整体TPS。
- 消息队列积压:
- 消费者处理速度跟不上生产者速度,消息队列积压越来越多。消费者可能因各种原因变慢(如数据库慢),从而导致整个流处理链路TPS下降。
- 网络拥塞:
- 网络设备或链路在长时间高负载下出现问题或拥塞,导致延迟增加、丢包率上升。
🔄 5. "伪"负载特征(测试设计问题)
- 用户行为模型偏差:
- 测试脚本未能真实模拟用户操作间隔和休眠。如果测试脚本是"无思考时间"的循环加压,可能导致内部资源(如会话)积累过快,不同于真实场景。
- 缓存预热不足但测试时间长:
- 测试初期的缓存未命中导致早期TPS较低,但随后缓存命中率提高,TPS本该回升。但若观察到的是持续下降,这种可能性较低。
- 测试工具/环境问题:
- 控制器/PG的资源不足(CPU、内存、网络带宽)。
- 测试工具自身的资源泄露。
- 网络不稳定或有抖动。
- 数据污染:
- 测试产生的无效或低质量数据积累,影响正常业务逻辑处理效率(需要特别设计清理策略)。
🔍 定位问题的方法与步骤
-
监控和度量(第一要务!):
- 系统层: CPU利用率(
us%/sy%/wa%
)、内存使用量(物理/Swap)、磁盘I/O(util%, await, r/s, w/s
)、网络流量。 - 应用层: GC日志(频率、时长)、线程池状态(活跃线程、队列大小)、连接池状态(活跃连接、等待连接)、日志中的错误和警告(特别是资源耗尽、超时、拒绝连接)、堆内存快照、线程转储。
- 数据库层: 慢查询日志、活动会话监控、锁等待信息、各资源(CPU、内存、I/O)监控、关键表/索引大小增长情况、缓存命中率(查询缓存、
InnoDB Buffer Pool
)。 - 外部服务/中间件: 调用响应时间、成功率(API)、消息队列积压深度(Kafka等)。
- 系统层: CPU利用率(
-
分析趋势:
- 对比测试开始时、中期、结束前的监控数据📉,重点关注线性增长或阶梯式增长的指标(特别是内存使用、线程数、连接数、慢查询数量、Full GC频率)。
- TPS下降曲线与哪些资源指标(CPU、内存、I/O、连接池使用率)的增长曲线高度相关?
-
抓取快照(关键时间点):
- 在TPS开始显著下降的"拐点"前后,抓取多线程调用堆栈。
- 在出现明显瓶颈时(如内存高使用),抓取内存堆转储进行离线分析。
-
压力分析与比对:
- 检查TPS下降阶段,平均响应时间、错误率、超时率的变化趋势。
- 比对TPS下降和数据库平均响应时间上升是否有对应关系?如有,则重点查数据库相关项。
-
日志分析:
- 仔细检查应用日志、数据库日志、Web服务器、容器日志中是否有堆栈溢出、
OutOfMemoryError
、连接错误、超时等错误信息,尤其是集中出现在测试后期的事件。
- 仔细检查应用日志、数据库日志、Web服务器、容器日志中是否有堆栈溢出、
🛠 可能的解决方向
- 修复泄露: 分析内存转储、线程转储找出根本原因,修复代码中的资源泄露点(未关闭的资源池、缓存策略、集合膨胀等)。
- 优化GC: 调整JVM堆大小及垃圾收集器参数。
- 调整连接池: 设置合理大小、验证周期、超时与空闲时间。
- 优化线程池: 确定匹配CPU核心数的合适线程数量,设定队列深度,考虑拒绝策略。
- 数据库优化: 分析慢查询、优化SQL和索引设计、考虑分库分表策略清理过期数据、调整数据库参数配置。
- 缓存策略改进: 设置适当的过期时间、采用淘汰策略、解决缓存穿透/击穿/雪崩风险。
- 减少锁争用: 重构热点代码分段加锁、用无锁数据结构代替、使用更细粒度锁。
- 外部依赖治理: 添加服务熔断降级机制、重试策略优化、协商外部服务扩容。
- 负载模型优化: 在测试脚本引入更真实用户思考时间。
- 基础设施扩容: 确认网络带宽充足、保障测试工具资源充裕、监控系统资源使用情况。
📌 总结
持续下降的TPS绝大多数情况是资源泄漏(内存>线程>连接)、数据库性能逐步恶化或锁竞争加剧的明确信号。 在稳定性测试过程中发现这一现象是有价值的预警,务必借助监控工具、日志和快照深入定位问题根因。这是保证线上系统长期稳健运行的关键诊断环节。