技术探索与实践踩坑记录:从一次分布式系统升级中的“血泪”说起

一个会部署的人
2025-06-15 03:01
阅读 422

开篇:为什么我要写这篇总结?

开篇:为什么我要写这篇总结?

在技术这条路上,我们总会遇到各种各样的挑战。作为一个架构师,在过去的几年里,我参与过多个大型系统的架构设计和演进。其中有一次项目经历尤其让我印象深刻——那是一个典型的分布式服务迁移与性能优化项目。它不仅考验了我的技术深度,更教会了我如何在复杂多变的现实中找到平衡。

这次分享的内容,并不是一套完美的解决方案手册,而是一次真实的踩坑复盘。我希望通过这篇文章,能让你少走一些弯路,也让我自己把这段经验沉淀下来。


背景介绍:一次“看似简单”的服务拆分

背景介绍:一次“看似简单”的服务拆分

我们的业务系统原本是一个比较传统的 SOA 架构,核心功能都在一个主服务中,随着业务的发展,系统开始出现瓶颈,尤其是在高峰时段,经常发生慢查询、超时甚至局部雪崩的情况。

于是公司决定启动一次大规模的技术改造计划,目标是:

  • 拆分主服务,实现微服务化
  • 提升整体系统的稳定性
  • 支持后续快速迭代和扩容能力

作为项目的主要负责人之一,我负责的是核心订单模块的服务拆分与性能优化部分。

听起来是不是挺常规?其实问题远比预期来得复杂。


问题描述:拆分之后,问题接踵而至

开发流程示意-1

问题描述:拆分之后,问题接踵而至

在完成基础服务拆分后,我们开始进入联调阶段。此时,陆续暴露出几个关键问题:

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 架构,但我们选择了更稳妥的方式。技术选型一定要结合团队能力和当前业务发展阶段。

✨ 不要忽视基础能力的建设

像日志、监控、链路追踪这些“基础设施”,哪怕一开始没觉得有多重要,但一旦出问题就会后悔莫及。

✨ 合理使用异步手段,降低系统耦合度

对于不直接影响主流程的操作,适当使用异步处理,能显著提升性能和响应速度。

✨ 数据一致性不必一开始就追求完美

很多情况下,最终一致性就足够用了。真正需要强一致性的场景并不多,除非是金融级别的交易系统。


写在最后:每一次踩坑,都是成长的机会

系统架构设计-2

回顾整个项目过程,最大的收获不是学会了怎么配置线程池或者部署 Zipkin,而是明白了技术背后“人”的因素。

  • 如何在压力下保持冷静,找出最优路径?
  • 如何在权衡取舍中达成共识?
  • 如何把错误变成经验,让下一次做得更好?

技术的路永远不会有终点,但只要我们愿意不断总结和前行,总能在风浪中走出属于自己的节奏。

如果你也在经历类似的技术转型或系统重构,希望这篇文章能给你一些启发。愿你我都能在不断试错中,走得更稳、更远。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝