技术探索与实践踩坑记录:从一次分布式系统升级中的“血泪”说起
开篇:为什么我要写这篇总结?

在技术这条路上,我们总会遇到各种各样的挑战。作为一个架构师,在过去的几年里,我参与过多个大型系统的架构设计和演进。其中有一次项目经历尤其让我印象深刻——那是一个典型的分布式服务迁移与性能优化项目。它不仅考验了我的技术深度,更教会了我如何在复杂多变的现实中找到平衡。
这次分享的内容,并不是一套完美的解决方案手册,而是一次真实的踩坑复盘。我希望通过这篇文章,能让你少走一些弯路,也让我自己把这段经验沉淀下来。
背景介绍:一次“看似简单”的服务拆分

我们的业务系统原本是一个比较传统的 SOA 架构,核心功能都在一个主服务中,随着业务的发展,系统开始出现瓶颈,尤其是在高峰时段,经常发生慢查询、超时甚至局部雪崩的情况。
于是公司决定启动一次大规模的技术改造计划,目标是:
- 拆分主服务,实现微服务化
- 提升整体系统的稳定性
- 支持后续快速迭代和扩容能力
作为项目的主要负责人之一,我负责的是核心订单模块的服务拆分与性能优化部分。
听起来是不是挺常规?其实问题远比预期来得复杂。
问题描述:拆分之后,问题接踵而至


在完成基础服务拆分后,我们开始进入联调阶段。此时,陆续暴露出几个关键问题:
1. 接口响应时间变长,QPS 急剧下降
订单服务拆分为独立模块后,原本本地调用变成了远程 RPC(使用的是 Dubbo),接口平均耗时从原来的 50ms 提升到了 200ms+,QPS 下降明显。
为什么会这样?
初步分析发现两个主要问题:
- Dubbo 的默认连接池太小,高并发下出现阻塞。
- 没有做合理的异步处理逻辑,大量线程被挂起等待结果。
2. 数据一致性成了难题
订单服务涉及金额、库存、用户等多个模块,原来在一个数据库事务里就能搞定的事情,现在分散到不同服务之间。数据不一致的风险陡增。
3. 日志追踪链路混乱
由于服务间依赖变多,排查问题变得异常困难。日志打出来一堆乱码 ID,根本不知道请求是从哪个源头触发的,调用链也没有完整记录。
这些问题加起来,让我们不得不重新审视整个架构的设计方案。
解决方案:层层优化,逐个击破

一、RPC 调用性能提升
为了解决接口延迟的问题,我们做了以下几件事:
✅ 扩展 Dubbo 线程池和连接池配置
dubbo:
protocol:
name: dubbo
threads: 200
iothreads: 32
connections: 100
这里有几个需要注意的地方:
threads是工作线程数,默认可能只有几十个,不足以支撑高并发场景。connections连接池大小影响到客户端和服务端之间的通信效率,特别是在多实例部署的情况下。- 同时我们还启用了 Netty4 协议,相比老版本性能有所提升。
✅ 引入异步非阻塞调用
我们在订单创建流程中,对一些不影响主线程逻辑的操作进行了异步处理。例如,下单成功后发送通知邮件、更新推荐系统等操作。
CompletableFuture.runAsync(() -> {
notifyService.sendOrderConfirmEmail(orderId);
}, asyncExecutor);
同时,我们也引入了一个自定义的线程池进行统一管理:
@Bean("asyncExecutor")
public ExecutorService asyncExecutor() {
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
return new ThreadPoolTaskExecutor(corePoolSize, corePoolSize * 2, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1024), new ThreadPoolTaskExecutor.CallerRunsPolicy());
}
效果非常明显,在 QPS 和响应时间上都得到了明显改善。
二、保障数据最终一致性
针对跨服务的数据一致性问题,我们采取了如下策略:
✅ 使用本地消息表 + 定时补偿机制
我们将关键状态变更事件记录到本地数据库中,然后通过定时任务轮询并发送给下游系统。
订单服务写入订单成功后:
INSERT INTO order_event (order_id, event_type, status) VALUES (xxx, 'ORDER_CREATED', 'PENDING');
然后由定时任务去检查:
@Scheduled(fixedRate = 5000)
public void processPendingEvents() {
List<OrderEvent> pendingEvents = eventRepository.findPendingEvents();
for (OrderEvent event : pendingEvents) {
try {
inventoryService.decreaseStock(event.getOrderId());
event.setStatus("PROCESSED");
} catch (Exception e) {
log.warn("Failed to process event {}, retry later", event.getId());
}
}
}
这种方式虽然不能保证强一致性,但在绝大多数场景下可以接受,并且具备良好的可维护性和容错性。
✅ 引入 Kafka 实现事件驱动模型(后续扩展)
后来我们又引入了 Kafka 来替代定时任务,进一步解耦各个服务之间的关系,实现真正的事件驱动架构。
三、完善全链路日志追踪体系
为了更好地定位问题,我们在服务中引入了 Zipkin 链路追踪系统,配合 Sleuth 实现了自动埋点。
✅ 微服务接入 Sleuth + Zipkin
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
配置文件中增加如下内容:
spring:
zipkin:
base-url: http://localhost:9411/
sleuth:
sampler:
probability: 1.0 # 采样率 100%,生产环境可根据实际情况调整
这样一来,每次请求都会自动生成一个唯一的 TraceID,并在日志和链路中传递,方便后续查证。
✅ ELK 日志聚合系统搭建
我们利用 ELK(Elasticsearch + Logstash + Kibana)实现了日志集中管理和检索。所有服务的日志都被收集到 Elasticsearch 中,通过 Kibana 可以根据 TraceID 快速查询整条调用链的所有日志。
代码实践:关键组件示例
示例一:Dubbo 异步调用封装
public class AsyncRpcInvoker {
@Reference
private InventoryService inventoryService;
public void asyncDecreaseStock(Long orderId) {
RpcContext.getContext().setAttachment("orderId", orderId.toString());
CompletableFuture.runAsync(() -> {
try {
inventoryService.decreaseStock(orderId);
} catch (Exception e) {
log.error("库存扣减失败: {}", e.getMessage());
}
}, asyncExecutor);
}
}
示例二:本地消息表结构设计
CREATE TABLE `order_event` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`order_id` BIGINT NOT NULL,
`event_type` VARCHAR(64) NOT NULL,
`status` VARCHAR(32) DEFAULT 'PENDING',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
踩坑经验分享
❌ 问题一:线程池配置不当导致OOM
初期我们在使用 CompletableFutures 时,直接使用 ForkJoinPool.commonPool(),结果在高并发下导致线程数暴涨,频繁 Full GC 最终 OOM。
解决办法:
- 明确指定自定义线程池。
- 控制最大线程数,设置合适的队列容量。
- 使用拒绝策略避免堆积。
❌ 问题二:日志格式不统一,排查困难
起初没有对日志格式进行标准化,服务多了之后日志混乱不堪。后来我们统一使用 MDC(Mapped Diagnostic Context)注入 TraceID,并采用 JSON 格式输出日志。
日志格式统一后:
{
"timestamp": "2023-12-15T10:20:00.123",
"level": "INFO",
"traceId": "abc123xyz",
"spanId": "span123",
"message": "订单创建成功",
"thread": "main"
}
效果总结:稳定性和性能双提升
经过一系列优化后,我们看到明显的改进:
| 指标 | 原始值 | 优化后 |
|---|---|---|
| 平均响应时间 | 200ms+ | 80ms以内 |
| QPS | ~300 | 突破 1500 |
| 错误率 | 1.5% | <0.1% |
| 链路追踪成功率 | 几乎为零 | 100% |
更重要的是,系统变得更加健壮和可维护。即使某一个服务出问题,也能快速定位和隔离,避免雪崩效应。
经验总结与建议
✨ 技术选型不要一味追求“新潮”
在项目初期有人提议直接上 Service Mesh 或者 DDD 架构,但我们选择了更稳妥的方式。技术选型一定要结合团队能力和当前业务发展阶段。
✨ 不要忽视基础能力的建设
像日志、监控、链路追踪这些“基础设施”,哪怕一开始没觉得有多重要,但一旦出问题就会后悔莫及。
✨ 合理使用异步手段,降低系统耦合度
对于不直接影响主流程的操作,适当使用异步处理,能显著提升性能和响应速度。
✨ 数据一致性不必一开始就追求完美
很多情况下,最终一致性就足够用了。真正需要强一致性的场景并不多,除非是金融级别的交易系统。
写在最后:每一次踩坑,都是成长的机会

回顾整个项目过程,最大的收获不是学会了怎么配置线程池或者部署 Zipkin,而是明白了技术背后“人”的因素。
- 如何在压力下保持冷静,找出最优路径?
- 如何在权衡取舍中达成共识?
- 如何把错误变成经验,让下一次做得更好?
技术的路永远不会有终点,但只要我们愿意不断总结和前行,总能在风浪中走出属于自己的节奏。
如果你也在经历类似的技术转型或系统重构,希望这篇文章能给你一些启发。愿你我都能在不断试错中,走得更稳、更远。

评论 0