ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

一次压测,leader 指着 40% 的 CPU 说:你的代码里有锁。我服了。

一次压测,leader 指着 40% 的 CPU 说:你的代码里有锁。我服了。 我们是由枫哥组建的IT技术团队成立于2017年致力于帮助IT从业者提供实力成功入职理想企业我们提供一对一学习辅导由知名大厂导师指导分享Java技术、参与项目实战等服务并为学员定制职业规划全面提升竞争力过去8年我们已成功帮助数千名求职者拿到满意的OfferIT枫斗者、IT枫斗者-Java面试突击。 一次压测leader 指着 40% 的 CPU 说你的代码里有锁。我服了。“CPU 才压到 40%先去检查代码。”我以为压测报告写得够详细了leader 瞄了一眼一句话让我回去重写了整个缓存层。前言性能测试不是跑个数据是逼出系统的真面目很多人以为性能测试就是起个 JMeter设个并发数跑完截个 TPS 图发给老板交差错了。真正有价值的性能测试是帮你发现那些日常开发中完全隐形、只有在高并发下才会暴露的问题代码里的隐藏锁 连接池配小了 ️下游服务扛不住 内存泄漏 超时配置的连锁雪崩 ❄️今天这篇文章我用一个真实的大厂案例把性能测试的价值、排查方法、以及压测定限流参数的完整流程一次性讲清楚。建议收藏 ⭐大促前复习一遍心里有底。一、案例ConcurrentHashMap 里的隐形锁1.1 看起来毫无问题的代码ServicepublicclassProductService{privatefinalConcurrentHashMapLong,ProductlocalCachenewConcurrentHashMap();AutowiredprivateProductMapperproductMapper;publicProductgetProduct(LongproductId){// 线程安全、原子操作完美returnlocalCache.computeIfAbsent(productId,id-productMapper.selectById(id));}}代码 Review 时的判断✅ConcurrentHashMap线程安全✅computeIfAbsent保证原子性✅ 没有synchronized关键字✅ 逻辑清晰无可挑剔1.2 压测结果TPS 死活上不去指标数值正常吗并发数200已调至最优CPU 利用率40%❌ 太低了TPS上不去❌ 瓶颈数据库响应正常✅ 排除下游服务正常✅ 排除leader 的判断排除了外部依赖CPU 低 吞吐低 代码里有锁线程在排队。1.3 抓线程 dump真相大白jstack-lpidthread_dump.txt大量线程状态http-nio-8080-exec-15 #35 prio5 os_prio0 tid0x00007f3b... nid0x2b waiting for monitor entry java.lang.Thread.State: BLOCKED (on object monitor) at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1656)BLOCKED阻塞在ConcurrentHashMap.computeIfAbsent。1.4 源码揭秘锁藏在你不会怀疑的地方// ConcurrentHashMap.computeIfAbsent 源码片段publicVcomputeIfAbsent(Kkey,Function?superK,?extendsVmappingFunction){// ...synchronized(f){// ← 对 hash 桶头节点加锁if(tabAt(tab,i)f){// 在锁内执行 mappingFunctionVvmappingFunction.apply(key);// ← 你的数据库查询在这里执行// ...}}}问题分析ConcurrentHashMap 默认 16 个桶 200 个并发线程 → 平均每个桶 12~13 个线程排队 每个桶同一时间只有 1 个线程能执行数据库查询 其余 184 个线程全部 BLOCKED 真正干活的线程16 / 200 8% CPU 利用率 40%不是 8% 在干活32% 在上下文切换和等锁⚠️Javadoc 警告computation should be short and simple。computeIfAbsent的 mapping 函数里不能放慢操作——但有多少人真的读过这段 Javadoc1.5 修复把数据库查询拆到锁外面publicProductgetProduct(LongproductId){// Step 1: 先查缓存无锁ProductproductlocalCache.get(productId);if(product!null){returnproduct;}// Step 2: 缓存未命中在锁外面查数据库productproductMapper.selectById(productId);if(product!null){// Step 3: putIfAbsent 保证并发安全ProductexistinglocalCache.putIfAbsent(productId,product);returnexisting!null?existing:product;}returnnull;}代价同一个 key 缓存未命中时可能有几个线程同时查数据库。但缓存只填充一次之后全部走缓存——绝大多数场景可接受。更优方案直接用 Caffeine 的LoadingCache加载机制不会阻塞其他 key 的读取。二、压测异常速查表收藏备用压测最耗时的不是加压本身而是看到异常数据后不知道从哪里下手。压测现象可能原因排查方向常用工具CPU 低 吞吐低 锁竞争、IO 等待、连接池满线程 dump 看 BLOCKED/WAITINGjstack、Arthas threadCPU 高 吞吐低 死循环、频繁 GC、低效算法CPU 火焰图、GC 日志async-profiler、jstatCPU 高 吞吐高 响应慢 线程池不够、请求排队线程池监控、请求排队时间Micrometer、Prometheus吞吐随并发增加反而下降⚔️ 锁竞争加剧、上下文切换过多vmstat看cs列、锁分析vmstat、perf前几分钟正常后来变慢 内存泄漏、连接泄漏、GC 压力堆内存趋势、连接池监控jmap、VisualVM错误率随并发线性上升⏱️ 超时配置不合理、下游扛不住下游服务监控、超时配置检查SkyWalking三、类似的隐藏锁陷阱大厂真实踩坑陷阱 1UUID.randomUUID()// 每个请求生成链路追踪 IDStringtraceIdUUID.randomUUID().toString();问题UUID.randomUUID()内部共享一个SecureRandom实例部分实现下nextBytes()是synchronized的。高并发下所有线程在这一行排队。陷阱 2SimpleDateFormat 静态变量// ❌ 错误线程不安全外面包 synchronizedprivatestaticfinalSimpleDateFormatsdfnewSimpleDateFormat(yyyy-MM-dd);publicsynchronizedStringformat(Datedate){// ← 每个请求都在排队returnsdf.format(date);}修复用DateTimeFormatter线程安全或ThreadLocalSimpleDateFormat。陷阱 3Logback 同步 Appender!-- ❌ 同步写入每条日志都拿锁 --appendernameFILEclassch.qos.logback.core.FileAppenderfileapp.log/file/appender修复改用AsyncAppender或RollingFileAppenderprudentfalse。共同特征锁藏在你平时不会怀疑的组件里代码 Review 发现不了只有压测才能逼出来。四、用压测定限流参数大促前必做4.1 核心认知CPU 70% 才是健康上限误区“CPU 压到 90% 就是极限能力”真相CPU 不是应用独占的资源消耗方需要 CPU操作系统线程调度、网络包收发、文件 IOJVMGC尤其是 Full GC监控采集Metrics 上报日志写入磁盘 IO 也需要 CPU 参与如果应用用到 90%留给其他的不到 10%。一次 Full GC、一个网络重传、一波突发流量系统直接崩。经验值应用 CPU 保持在65%~70%留出 30% 缓冲区。4.2 要看整条请求链路的资源HTTP 请求链路 用户请求 ↓ Tomcat 线程池 ← 是否满线程是否在排队 ↓ 应用代码执行 ← CPU 利用率 ↓ 数据库连接池 ← 是否满连接是否在排队 ↓ MySQL ← CPU磁盘 IO慢查询 ↓ Redis ← 连接数响应延迟 ↓ 下游服务 ← 响应时间超时配置任何一个环节到瓶颈系统都会不稳定。4.3 压测定限流5 步操作流程# Step 1: 梯度加压# 每次增加 20% 并发数每个梯度持续 5 分钟以上等指标稳定# Step 2: 同时监控这些指标┌─────────────────┬─────────────────────────────┐ │ 应用 CPU │ 不超过70% │ │ JVM 堆内存 │ 曲线平稳无持续上升 │ │ Tomcat 线程池 │ 使用率不超过80% │ │ 数据库连接池 │ 使用率不超过80% │ │ MySQL CPU │ 不超过70% │ │ MySQL 慢查询数 │ 不持续增长 │ │ Redis 响应延迟 │ P9910ms │ │ 接口 P99 响应时间│ 不超过业务要求上限 │ │ 错误率 │0.1% │ └─────────────────┴─────────────────────────────┘# Step 3: 找到所有指标都稳定的最大并发数# 判断标准上面 9 个指标全部达标# Step 4: 设限流阈值限流阈值最大稳定并发数 ×80%# Step 5: 验证限流给10倍流量再压一轮观察 - 限流是否正确生效 - 超出阈值的请求是否被拒绝/降级 - 系统内部资源是否全部健康做完这 5 步大促期间心里有底。不管流量多大超出的被限流挡在外面系统不会崩。五、不压测发现不了的问题5.1 下游服务扛不住你的代码没毛病但下游 HTTP 连接池默认 10 个连接。压测上量后全部排满你的线程卡在等下游响应连带着你自己的线程池也满了本来无关的接口也开始超时。5.2 中间件配置默认值不够用组件默认值压测后可能需要调Redis 连接池850数据库连接池10100MQ 消费线程数110Tomcat 线程池200根据 CPU 核数调整低流量下默认值够用压测上量后各种问题才浮出水面。5.3 高并发持续运行后的内存泄漏// ThreadLocal 用完没清理privatestaticfinalThreadLocalSimpleDateFormattlThreadLocal.withInitial(()-newSimpleDateFormat(yyyy-MM-dd));publicvoidprocess(){// 用了 ThreadLocaltl.get().format(newDate());// ❌ 忘记 tl.remove()}Tomcat 线程池复用线程上一个请求的数据还留着。压测跑半小时堆内存曲线一路上升不回落问题暴露。5.4 超时配置的连锁雪崩A 服务调 B超时 30s B 调 C超时 30s C 响应变慢实际 35s B 的线程池被 C 拖住 → B 响应变慢 A 的线程池被 B 拖住 → A 响应变慢 一个下游问题沿着调用链往上蔓延 → 雪崩 ❄️不做全链路压测你不知道某个下游变慢会不会引发雪崩。六、总结性能测试的真正价值测试手段能发现什么发现不了什么代码 Review逻辑错误、规范问题并发下的锁竞争单元测试功能正确性资源瓶颈、配置缺陷集成测试模块对接高并发下的性能问题性能测试锁竞争、连接池瓶颈、内存泄漏、雪崩风险—性能测试的收获不只是 TPS 和响应时间数字。更值钱的是通过对线程 dump、资源监控、链路追踪数据的分析你对自己系统的运行状态建立了真实的认知哪个接口有锁竞争哪个连接池配小了哪个下游服务是薄弱环节限流阈值该设多少这些认知不是读文档能获得的是在压测过程中一轮一轮摸出来的。⭐️推荐:Offer训练营介绍Java 面试 后端通用面试八股文Java后端企业级实战面试Java后端校招算法学习
返回列表