【微电平台】-高并发实战经验-奇葩问题解决之旅
字数 2276 2025-08-11 08:36:04
高并发系统问题排查与优化实战教学文档
1. 案例背景
1.1 系统概述
微电平台是一个综合智能SCRM SAAS化系统,集电销、企业微信等功能于一体,具有以下特点:
- 60+京东业务线入驻
- 每日处理上亿客户名单
- 提供千人千面的配置功能
- 基于规则引擎设计实现
- 包含金融刷单标记、风控标记、人群画像标记等多维度复杂接口
1.2 问题场景
- 业务场景:每日凌晨1~8点进行客户名单离线打标
- 规模:80台机器处理,平均95万名单/分钟
- 性能指标:
- 拒绝营销JSF服务总TPS约2万
- TP99在100~110ms
- 问题表现:
- 2023年2月24日发现打标未按时完成
- JMQ消费端TP99过高
- 吞吐量降低40%
- 4台机器频繁出现"问题"
2. 问题排查与分析
2.1 为什么少量问题机器会导致吞吐急剧下降?
根本原因分析:
-
JMQ重试机制:
- 每条消息消费报错会导致本地sleep并重新消费拉下的所有消息(batchSize=10)
- 每次报错增加至少1000ms耗时
-
JSF负载均衡:
- 使用默认随机负载均衡算法
- 80台机器中4台问题机器的命中概率为5%
- 10条消息中只要1条命中问题机器就会显著降低吞吐
解决方案:
- 优化JMQ重试配置:
<jmq:consumer id="cusAttributionMarkConsumer" transport="jmqTransport">
<jmq:listener topic="${jmq.topic}" listener="jmqListener"
retryDelay="10" maxRetryDelay="20" maxRetrys="1"/>
</jmq:consumer>
- 修改JSF负载均衡算法:
<jsf:consumer loadbalance="shortestresponse"/>
负载均衡算法对比:
- Random(默认):完全随机,不考虑服务端表现
- ShortestResponse:基于服务请求响应时间动态调整,类似熔断机制
2.2 如何判定问题实例
判断依据:
- 时间段内耗时高或失败的请求集中在特定IP
- 大量IP出现类似现象则非机器硬件问题
监控指标:
- SGM监控显示特定机器持续高耗时
- 问题机器TPS几乎为0
- JSF线程池满错误
2.3 线程假死问题定位
排查步骤:
-
内存分析:
- 使用行云工具dump内存对象
- 内存占用正常,无显著问题
-
线程堆栈分析:
- jstack显示所有JSF线程处于WAITING状态
- 典型线程堆栈示例:
// 线程1 at com.jd.jr.scg.service.common.crowd.UserCrowdHitResult.isHit(UserCrowdHitResult.java:36) at com.jd.jr.scg.service.impl.BlacklistTempNewServiceImpl.callTimes(...) // 线程2 at com.jd.jr.scg.service.biz.BlacklistBiz.isBlacklist(BlacklistBiz.java:337)
问题代码模式:
- 主线程使用线程池A执行任务X
- 任务X内部又使用同一个线程池A执行子任务
- 当线程池资源不足时形成死锁等待
验证方法:
- 将业务线程池从50扩容到200
- 观察TPS抖动情况,确认无线程等待时服务正常
2.4 同步锁性能问题
发现过程:
- 线程堆栈中存在大量"waiting to lock"信息
- 服务链路中3个方法使用同一把锁
解决方案:
- 使用Caffeine本地缓存替代同步锁维护的手写缓存
2.5 JSF线程池满的特殊行为
意外发现:
- 当服务端报错:"[JSF-23003]Biz thread pool of provider has bean exhausted"
- 客户端不会自动重试(与其他RpcException行为不同)
解决方案:
- 需要在客户端代码中实现自定义重试逻辑
3. 优化实施与效果
3.1 优化措施汇总
-
解决线程等待问题:
- 新增独立线程池,避免线程池嵌套使用
-
性能优化:
- 使用Caffeine替换同步锁缓存实现
-
配置调优:
- 调整JMQ重试参数
- 修改JSF负载均衡算法
3.2 优化效果
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| TPS | 2万 | 3万 | +50% |
| TP99 | 100-110ms | 65ms | -35%~-40% |
| JMQ重试TP99 | 1100ms | 300ms | -72% |
| 任务完成时间 | 8点 | 5点前 | 时间缩短57% |
4. 经验总结与最佳实践
4.1 高并发系统设计原则
-
线程池使用规范:
- 避免线程池嵌套使用
- 为不同层级任务分配独立线程池
-
锁优化:
- 减少同步锁范围
- 优先考虑无锁设计或并发容器
-
中间件配置:
- 根据业务特点调整重试策略
- 选择合适的负载均衡算法
4.2 问题排查方法论
-
监控先行:
- 建立完善的监控体系(SGM、JMQ、JSF等)
-
工具使用:
- 内存分析:内存dump
- 线程分析:jstack
-
推理验证:
- 提出假设 → 设计验证 → 确认结论
-
根因定位:
- 从表象到本质层层深入
- 区分症状与病因
4.3 性能优化checklist
- [ ] 线程池配置合理性检查
- [ ] 锁竞争分析
- [ ] 中间件参数调优
- [ ] 负载均衡策略评估
- [ ] 异常处理机制审查
5. 附录:关键技术点
5.1 JMQ关键配置参数
| 参数 | 说明 | 建议值 |
|---|---|---|
| retryDelay | 重试延迟时间(ms) | 10-50 |
| maxRetryDelay | 最大重试延迟时间(ms) | 20-100 |
| maxRetrys | 最大重试次数 | 1-3 |
5.2 JSF负载均衡算法对比
| 算法 | 原理 | 适用场景 |
|---|---|---|
| random | 完全随机 | 服务端性能均匀 |
| roundrobin | 轮询 | 服务端性能均匀 |
| shortestresponse | 基于响应时间 | 服务端性能不均 |
| consistenthash | 一致性哈希 | 需要会话保持 |
5.3 线程池嵌套问题代码示例
// 错误示例:线程池嵌套使用
public void process() {
executorService.submit(() -> {
// 业务逻辑
executorService.submit(() -> { // 使用同一个线程池
// 子任务
});
});
}
// 正确做法:使用独立线程池
public void process() {
executorService.submit(() -> {
// 业务逻辑
subTaskExecutor.submit(() -> { // 使用不同线程池
// 子任务
});
});
}
通过本案例的系统性分析和解决方案,我们不仅解决了眼前的问题,还建立了预防类似问题的长效机制,为高并发系统的稳定运行提供了可靠保障。