从单体到微服务:我在字节基础架构组踩过的坑

Django老掌柜
2026-01-26 12:44
阅读 681

上周五晚上十点半,我正窝在沙发上远程 debug 一个诡异的分布式事务问题,突然手机弹出一条消息:“哥,你那篇《微服务拆分指南》啥时候更新?我们产品下周就要上线了!”——发信人是我前同事,现在在一家创业公司带后端团队。我苦笑了一下,这不就是当年的我么?被产品经理追着问“为什么新功能要等两周”,被运维甩锅说“你们服务太碎了不好监控”,被面试官灵魂拷问“CAP理论怎么落地”……

今天这篇文章,就来聊聊我这几年在字节基础架构组搬砖的真实经历——如何把一个臃肿的单体应用,一步步拆成可维护、可扩展、扛得住双11流量的微服务系统。全程基于 Java 技术栈,但思路通用,无论你是刚入行的小白,还是准备跳槽的老鸟,都能从中找到共鸣。


为啥非要搞微服务?别被“潮流”忽悠了

先说清楚,微服务不是银弹。我刚进字节那会儿,也热血沸腾地想“赶紧上微服务,显得高大上”。结果被老架构师一句话点醒:“你连单体都跑不稳,还拆服务?”

真正推动我们拆分的,是产品迭代速度和系统稳定性之间的矛盾。2022年,我们负责的内部配置中心(ConfigCenter)还是个 Spring Boot 单体应用,代码量 30w+ 行,启动要 3 分钟,改一行日志级别都要全量回归测试。产品经理每次提需求都像在求我们:“能不能快点?竞品已经上了动态灰度发布……”

更致命的是,一次小 bug 就可能拖垮整个系统。记得去年双11前夜,一个配置项校验逻辑写错了,导致所有服务拉取配置时返回 500,整个公司几百个服务集体“假死”——运维群里炸锅,CTO 亲自下场问进度。那一刻,我盯着屏幕上疯狂刷屏的告警,真的想砸电脑。

所以,拆微服务的核心驱动力不是技术炫技,而是业务复杂度和组织效率的倒逼。如果你的产品还在 MVP 阶段,用户量不到十万,别折腾微服务,先把单体优化好。


拆!但怎么拆?别一上来就“领域驱动设计”

很多教程一上来就讲 DDD(领域驱动设计),画一堆上下文边界图,搞得人头大。实战中,我们更倾向于“渐进式拆分”——先找最痛的点下手。

我们的 ConfigCenter 有四大核心功能:

  1. 配置管理(增删改查)
  2. 配置推送(实时通知客户端)
  3. 权限控制(谁可以改哪些配置)
  4. 审计日志(谁在什么时候改了什么)

分析线上监控数据发现,80% 的流量集中在配置推送,而这块逻辑和其他模块耦合极深。于是我们决定:第一刀,先切出“配置推送服务”

第一步:识别边界,定义接口

关键不是代码怎么拆,而是接口契约怎么定。我们用 OpenAPI 3.0 写了清晰的 API 文档,约定:

  • 推送服务只暴露 /push 接口,接收配置变更事件
  • 主服务通过 MQ 异步发送事件,不直接调用推送服务
  • 返回格式统一为 {"code": 200, "msg": "success"}
// 事件模型示例(简化版)
public class ConfigChangeEvent {
    private String appId;
    private String configKey;
    private String newValue;
    private String operator; // 操作人
    private long timestamp;
    // ... getter/setter
}

避坑提醒:千万别为了“解耦”而过度设计。我们一开始想用 gRPC,结果发现团队对 Protobuf 不熟,调试成本太高,最后还是用 JSON + REST,简单粗暴。

第二步:数据库怎么分?别搞“共享库”!

这是最容易踩的坑。微服务 ≠ 服务拆了,数据库还共用一个。我们见过太多团队,服务拆成 10 个,结果 10 个服务连同一个 MySQL 库,表名靠前缀区分(比如 user_config, order_config)——这根本不是微服务,是“分布式单体”。

我们的做法:

  • 每个服务独占数据库(物理隔离)
  • 通过 Saga 模式处理跨服务事务(后面详述)
  • 历史数据迁移用 双写 + 对账 保证一致性

举个例子:配置管理服务需要记录操作日志,但审计日志服务才是日志的“主人”。我们这样设计:

  1. 配置服务本地写操作日志(临时表)
  2. 发送 MQ 消息给审计服务
  3. 审计服务消费后写自己的库
  4. 定时任务对账,修复不一致数据
-- 配置服务的临时操作日志表
CREATE TABLE tmp_audit_log (
    id BIGINT PRIMARY KEY,
    event_id VARCHAR(64) NOT NULL, -- 关联 ConfigChangeEvent
    status TINYINT DEFAULT 0, -- 0:待处理, 1:已同步
    create_time DATETIME
);

血泪教训:别信“最终一致性很快”,网络抖动时,对账任务可能延迟几小时。我们加了企业微信告警,一旦积压超过 1000 条,自动@负责人。


分布式事务:别再迷信“强一致性”了

说到微服务,面试官最爱问:“分布式事务怎么保证?” 我反手就是一个“看场景”。

在字节,我们几乎不用两阶段提交(2PC)——性能差、锁粒度大,线上事故频发。主流方案是“最终一致性 + 补偿机制”,具体到 ConfigCenter,我们用的是 Saga 模式

Saga 是什么?简单说就是“一串补偿操作”

比如用户修改配置,流程如下:

  1. 配置服务保存新值
  2. 发送 MQ 事件
  3. 推送服务收到事件,推送给客户端
  4. 审计服务记录日志

如果第 3 步失败(比如推送服务宕机),怎么办?

  • 不是回滚第 1 步(因为配置已生效,不能撤回)
  • 而是 重试推送,直到成功
  • 如果重试 N 次失败,人工介入
// 推送服务的重试逻辑(伪代码)
public void onMessage(ConfigChangeEvent event) {
    try {
        pushToClients(event);
        markAsSuccess(event); // 标记事件已处理
    } catch (Exception e) {
        if (retryCount < MAX_RETRY) {
            scheduleRetry(event, retryCount + 1); // 延迟重试
        } else {
            alert("推送失败,需人工处理", event); // 触发告警
        }
    }
}

面试题挑战:如果面试官问“如何避免重复消费”,你可以答:“MQ 消息带唯一 ID,服务端用 Redis 记录已处理 ID,TTL 24 小时”。但实际生产中,我们更依赖幂等设计——比如推送接口,多次调用结果不变。


运维之痛:服务多了,监控告警怎么搞?

拆完服务,最头疼的不是代码,是运维。以前一个单体,看一个 JVM 监控就行;现在 5 个服务,每个都要看 CPU、内存、GC、线程池、DB 连接池……

在字节,我们重度依赖 ByteTrace(自研链路追踪) + Prometheus + Grafana。但小团队没这条件,我的建议是:

  1. 统一日志格式:用 Logback + MDC,每条日志带 traceId
  2. 关键指标埋点:QPS、错误率、P99 延迟
  3. 健康检查接口/health 返回 DB、MQ、依赖服务状态
# application.yml 示例
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  endpoint:
    health:
      show-details: always

有一次,推送服务突然慢了,查了半天发现是客户端连接数暴增,但监控没暴露这个指标。后来我们加了自定义指标:

// 统计活跃连接数
Gauge.builder("push.active_connections", connectionManager, ConnectionManager::getActiveCount)
     .register(Metrics.globalRegistry);

从此,Grafana 面板上多了一条曲线,问题秒定位。


性能优化:别让“分布式”变成“分布慢”

微服务最大的性能杀手是网络调用。我们做过压测,单体应用 10ms 响应,拆成 3 个服务后,直接飙到 50ms——光 RPC 调用就占了 30ms。

优化手段:

  • 异步化:非关键路径用 MQ 解耦(如审计日志)
  • 批量接口:客户端一次拉取多个配置,减少请求次数
  • 本地缓存:推送服务缓存配置,避免每次查 DB
// 本地缓存示例(Caffeine)
private final LoadingCache<String, Config> configCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> loadFromDB(key)); // 从 DB 加载

但缓存也有坑!缓存和 DB 一致性怎么保证?我们的策略是:

  • 写操作先更新 DB,再删除缓存(Cache-Aside)
  • 删除失败则发 MQ 重试
  • 客户端接受短暂不一致(产品说“5秒内生效就行”)

效果如何?数据说话

拆分完成后,我们做了对比:

指标 单体应用 微服务架构
启动时间 180s 15s(平均)
发布频率 1次/周 10次/天
故障隔离 全站挂 仅影响推送
双11 P99 延迟 800ms 120ms

更重要的是,产品经理再也不追着我问“为啥改个按钮要等三天”——权限模块独立后,他们可以自己配角色,不用等我们排期。


最后几句掏心窝的话

微服务不是终点,而是持续演进的过程。我们在字节也还在优化:服务网格(Service Mesh)试点、配置中心 Serverless 化……技术永远在变,但核心不变:用合适的工具,解决当前的问题

如果你正在面试,被问到“微服务经验”,别背八股文。就说:

“我们拆服务是因为产品迭代太慢,第一刀切了高频模块,用 Saga 保证最终一致性,监控靠统一日志+关键指标,现在发布快了 10 倍。”

这比“CAP 理论我懂”实在多了。

对了,开头那个创业公司的朋友,昨天发消息说他们微服务上线了,虽然第一天就出了个小事故,但至少能快速回滚单个服务,没让整个产品瘫痪。我回他:“恭喜,你已经跨过了最难的坎——从‘不敢拆’到‘敢拆’。”

毕竟,代码可以重构,但信心一旦建立,就再也回不去了

评论 0

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