公司动态
单体拆微服务:用绞杀者模式和影子读控制风险
单体拆微服务用绞杀者模式和影子读控制风险把运行多年的单体拆成微服务最危险的做法是一次性切走全部流量。遗漏的业务分支、索引不足或新增 RPC 超时都会在真实流量下暴露。更稳妥的做法是按能力分段迁移先让新服务处理可回退的小范围请求再逐步扩大。绞杀者模式提供的是迁移顺序不是保证成功的口号每一段都需要可观测指标和明确回退条件。flowchart TD subgraph Phase1 [阶段 1 2网关路由分流 CDC 双写] Client[客户端请求] -- Gateway[API 网关 / 流量切流组件] Gateway --|新模块 URL| Microservice[新拆出的微服务] Gateway --|旧模块 URL| Monolith[旧单体应用 (Monolith)] Monolith -- MonolithDB[(单体数据库)] MonolithDB --|Debezium / Canal CDC| MicroserviceDB[(微服务独立 DB)] end subgraph Phase3 [阶段 3 4影子读对比 平滑切流] Microservice --|1. 写入微服务 DB| MicroserviceDB Microservice --|2. 异步比对旧 DB 数据| DiffEngine[数据 Consistency 校验引擎] DiffEngine --|Diff 报警| Alerting[监控告警 (自动阻断切流)] end1. 拆分路径演进绞杀者模式的四个关键阶段阶段一透明网关代理与增量业务切分在单体应用前端挂载 API 网关如 Spring Cloud Gateway 或 NGINX。重构时不要尝试一次性重写所有模块而是挑选边界最清晰的子模块例如用户评价模块进行拆分。在网关上配置路由规则仅将/api/v1/reviews/**的流量转发到新的微服务其余 95% 的流量依然透传给旧单体。这一步的风险极小随时可以在网关层一键撤回。阶段二数据双写Double-Write与 CDC 异步追平服务拆分的核心难点在于数据库拆分。旧单体应用与新微服务不能长期共享同一个数据库否则会导致事务耦合与行锁竞争。在过渡期采用CDCChange Data Capture技术如 Debezium 或 Canal监听单体数据库的 Binlog将增量数据实时同步至微服务的独立数据库中。在终端中查看 Debezium 增量同步日志与延迟打靶命令# 查看 Debezium connector 的增量同步延迟指标 curl -s http://debezium-connect:8382/connectors/monolith-review-sync/status | jq .终端控制台返回的数据展示{ name: monolith-review-sync, connector: { state: RUNNING, worker_id: 192.168.1.50:8083 }, tasks: [ { id: 0, state: RUNNING, lag_ms: 12, processed_events: 489201 } ] }指标显示 CDC 延迟保持在 12ms 以内说明增量同步非常顺畅。阶段三影子读Shadow Read与结果比对在将微服务切换为读主节点之前开启“影子读”防线微服务在处理读请求时同时从新 DB 和旧 DB 查询数据并在后台线程池中进行字段级 JSON 比对Diff Check。如果比对一致率连续 72 小时达到 99.999%证明新 DB 的数据结构与计算逻辑完全可靠。阶段四基于用户 ID 梯度的灰度切流在网关层配置基于 User ID 散列的切流规则1% ➔ 5% ➔ 20% ➔ 50% ➔ 100%。按天逐步放大流量。一旦发现任何指标异常分钟级将比例回退至上一步。2. 生产级影子读与双写一致性比对代码下面是在微服务切流阶段用于保障新旧数据库数据一致性的 Spring Boot 影子读校验组件package com.example.distributed.migration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.Objects; import java.util.concurrent.CompletableFuture; Service public class ShadowReadDiffService { private static final Logger log LoggerFactory.getLogger(ShadowReadDiffService.class); private final NewReviewRepository newRepo; private final OldMonolithReviewClient oldMonolithClient; public ShadowReadDiffService(NewReviewRepository newRepo, OldMonolithReviewClient oldMonolithClient) { this.newRepo newRepo; this.oldMonolithClient oldMonolithClient; } /** * 影子读主逻辑优先返回新微服务数据异步发起与旧单体的数据对比 */ public ReviewData getReviewWithShadowCheck(Long reviewId) { // 1. 从新微服务数据库读取 ReviewData newData newRepo.findById(reviewId).orElse(null); // 2. 异步发起影子读与比对绝不阻塞主响应链路 asyncPerformShadowDiff(reviewId, newData); return newData; } Async(shadowCheckExecutor) public void asyncPerformShadowDiff(Long reviewId, ReviewData newData) { try { // 从旧单体 API 读取对应数据 ReviewData oldData oldMonolithClient.fetchReviewFromMonolith(reviewId); if (!isDataConsistent(newData, oldData)) { log.error( [绞杀者切流报警] 影子读数据比对不一致! ReviewID: {}, 新数据: {}, 旧数据: {}, reviewId, newData, oldData); // 可在此处上报 Prometheus 自定义 Metric: migration_data_diff_count.inc() } } catch (Exception e) { log.warn(影子读拉取旧单体数据失败 [ReviewID: {}]: {}, reviewId, e.getMessage()); } } private boolean isDataConsistent(ReviewData newData, ReviewData oldData) { if (newData null oldData null) return true; if (newData null || oldData null) return false; return Objects.equals(newData.getContent(), oldData.getContent()) Objects.equals(newData.getScore(), oldData.getScore()) Objects.equals(newData.getUserId(), oldData.getUserId()); } }单体迁移没有放之四海皆准的路径。绞杀者模式的价值在于把风险拆小每一步有流量范围、数据校验和回退开关下一步才有依据继续。