模块二-架构基础功 | 第10讲:微服务架构模式 - 服务拆分策略、API 网关与服务间通信
开场:微服务不是「拆得碎」,而是「买得起运维」的边界选择
当你用 AI 快速堆出一个 CodeSentinel 原型时,它几乎总是 单体(monolith):一个 FastAPI 进程里同时完成拉取变更、跑规则、调模型、写报告、发通知。原型阶段这没问题——交付速度最重要。但一旦进入企业环境,你会遇到 独立扩缩容、独立发布、故障隔离、多团队并行 的真实压力,「要不要拆微服务」就会被摆上桌面。
很多团队在讨论微服务时,会把注意力放在「容器个数」与「仓库个数」,却忽略了一个更根本的问题:拆分之后,故障模式从函数调用失败变成网络分区、超时、重试风暴、消息乱序。这意味着你必须同步投资 可观测性、发布策略、容量规划、契约治理。CodeSentinel 作为审核平台,还要额外承担 审计与合规 的压力:跨服务调用链上的每一次失败,都要能解释「用户看到的结果是如何形成的」。因此本讲会把「网关 + 韧性 + 通信模式」放在一起讲,而不是只画一张漂亮的架构图。
本讲不会鼓吹「为拆而拆」,而是给你一套 可落地的决策框架:按业务能力、按限界上下文、按团队拓扑三种拆分策略如何取舍;CodeSentinel 如何划分 ReviewService、RuleService、IndexService、NotificationService;FastAPI 作为 API 网关 时路由、鉴权、聚合、超时怎么设计;同步通信(REST、gRPC)与异步通信(Redis Streams、RabbitMQ)如何组合;服务发现与注册、熔断(Circuit Breaker) 在 Python 里如何朴素实现。最后你会拿到一份 可运行的网关示例:httpx 调用下游并带重试与熔断计数。读完这一讲,你应该能向团队解释:为什么 CodeSentinel 早期更适合单体,以及什么时候拆服务真的划算。
记住一句硬话:微服务解决的是组织与规模问题,不是语法问题。如果团队没有可观测、没有发布纪律、没有契约测试,拆服务只会把单体里的混乱分布式化。
从 CodeSentinel 的产品形态看,审核链路往往同时具备 突发流量(大量 PR 同时触发)与 长尾延迟(索引构建、深度规则、LLM 调用)。这意味着你的架构决策不能只回答「能不能拆」,还要回答 「拆了之后延迟预算怎么分配」。网关的价值之一,就是把对外承诺的 SLA 翻译成内部各服务的超时、重试与降级策略,并在监控上拆成可定位的 segment。否则你会遇到经典现象:用户只看到「网关 504」,却不知道是规则服务慢还是索引服务慢。
本讲刻意把 FastAPI 网关 作为主角,而不是一上来就引入重量级网关组件,原因是:中小团队最常见的路径是「先有一个统一入口」,再逐步演进到 Envoy/Kong。你要学会的是 网关职责边界 与 服务间通信模式,工具可以换,原则不变。与此同时,我们会反复强调 单体优先:CodeSentinel 的课程主线不是让你第一天就部署八个容器,而是让你在边界清晰的前提下,拥有 可演进的拆分期权。
全局视角:CodeSentinel 微服务拓扑与网关位置
第一张图给出教学版边界:网关对外统一入口,内部服务各司其职;异步消息用于削峰与解耦通知。
flowchart TB
subgraph Clients["客户端 / CI / Web UI"]
WEB[Web]
CI[CI Webhook]
end
subgraph GW["API Gateway (FastAPI)"]
R1["/reviews/*"]
R2["/rules/*"]
R3["/index/*"]
R4["/notify/*"]
end
subgraph Sync["同步 HTTP 服务"]
RS[ReviewService]
RL[RuleService]
IX[IndexService]
NT[NotificationService]
end
subgraph Async["异步消息层"]
Q[(Redis Streams / RabbitMQ)]
end
WEB --> GW
CI --> GW
GW --> RS
GW --> RL
GW --> IX
GW --> NT
RS --> Q
NT --> Q
第二张图是 一次跨服务审核调用 的时序:网关编排多个下游读模型与规则评估(教学上简化为 HTTP 串联,生产可并行 asyncio.gather)。
sequenceDiagram
participant C as Client
participant G as API Gateway
participant R as ReviewService
participant I as IndexService
participant U as RuleService
participant N as NotificationService
C->>G: POST /reviews (创建审核)
G->>R: POST /internal/reviews
R->>I: GET /internal/diff?repo=&pr=
I-->>R: diff 摘要
R->>U: POST /internal/evaluate
U-->>R: 规则结果
R-->>G: 审核响应
G->>N: POST /internal/notify (异步或同步)
G-->>C: 202/200 + 追踪 ID
第三张图描述 熔断状态机:关闭 → 打开 → 半开,保护网关不被慢下游拖死。把它记在脑子里:熔断不是为了「隐藏错误」,而是为了 把错误从排队超时变成快速失败,给系统留下恢复窗口。
stateDiagram-v2
[*] --> Closed
Closed --> Open: 连续失败超阈值
Open --> HalfOpen: 冷却时间到
HalfOpen --> Closed: 探测成功
HalfOpen --> Open: 探测失败
Closed --> Closed: 成功重置计数
核心原理:拆分策略与通信方式如何配对
按业务能力拆分(Business Capability)
把系统看成企业对外的能力清单:「审核」「规则」「索引」「通知」各是一个能力。优点是 与产品语言对齐;缺点是早期边界可能猜错。对 CodeSentinel,业务能力拆分通常最自然:审核编排 与 规则评估 往往有不同扩缩容曲线(规则计算 CPU 密集,通知 IO 密集)。
按限界上下文拆分(Bounded Context)
DDD 的上下文边界是更「硬」的拆分依据:上下文之间通过 防腐层(ACL) 与 显式契约 集成。CodeSentinel 的 Review、Rule、Repository、Report 四个上下文,在规模化时可能映射为多个服务,但注意:不是每个上下文都必须是一个进程——先模块化单体,再提取热点服务,是更稳妥的演进路径。
按团队拓扑拆分(Conway 定律)
如果规则团队与平台团队发布节奏完全不同,强行放在一个服务里会出现「互相等待发版」。此时拆分的主要收益是 降低协调成本。反过来,如果团队很小,拆四个服务只会增加四个部署流水线与四套监控——得不偿失。
API 网关模式:FastAPI 可以胜任「轻网关」
完整功能的 API 网关(Kong、Envoy)在大型公司很常见;但在中小团队,用 FastAPI 做 BFF/Edge Gateway 也很实用:统一鉴权、限流、路由、请求日志、聚合下游。要记住网关职责:薄编排、厚契约,不要把业务规则又写回网关层,否则你会得到「分布式单体」。
同步:REST 与 gRPC
- REST:调试友好、生态成熟,适合对外开放与快速迭代。
- gRPC:强类型、性能好,适合内部高频调用(例如索引服务返回大 payload)。
对 Python,httpx + OpenAPI 生成客户端是常见组合;gRPC 可用 grpcio。
异步:Redis Streams 与 RabbitMQ
- Redis Streams:运维轻、与缓存同栈,适合作为起步的消息骨干。
- RabbitMQ:路由模型丰富,适合复杂订阅关系与企业内已有中间件团队。
CodeSentinel 中,ReviewCompleted 类事件非常适合异步投递通知与报表生成,避免审核主链路被慢消费者阻塞。
服务发现与注册
Kubernetes 环境下常用 DNS + Service;本地开发可用 Consul 或简单 静态配置 + 环境变量。教学示例用 硬编码 URL,你只需理解:发现机制解决的是「下游地址变化」与「健康实例集合」问题。
熔断与重试:别把重试做成「压垮下游」
重试必须 指数退避 + 抖动,并对 非幂等 操作谨慎。熔断保护整体:当下游持续失败,快速失败并返回降级结果,比无限等待更有价值。
在 CodeSentinel 场景里,还要特别警惕 重试与审核副作用 的组合:如果某个下游调用会触发外部系统(例如创建 CI 任务、发送 Webhook),那么「失败重试」可能导致重复副作用。解决路径通常是:把副作用移到 幂等消息消费者 或引入 幂等键 + 去重表;同步路径只做「可安全重试」的读聚合与纯计算。
何时坚持单体、何时提取服务(CodeSentinel 建议)
- 0→1 阶段:单体 + 清晰包边界(Clean Architecture)。
- 性能热点出现:先提取 IndexService 或 RuleService 这类 CPU 边界。
- 通知洪水:提取 NotificationService + 消息队列。
- 多租户隔离需求:考虑按租户分片或独立部署,而不是先盲目拆很多微服务。
拆分决策表:把「感觉该拆」变成「条件满足才拆」
建议你与团队在白板上使用下面这张表(文字版),对每个候选服务打勾:1)独立发布价值:是否存在「不想被其它团队阻塞」的发布周期?2)独立扩缩容价值:CPU/内存曲线是否与主服务显著不同?3)故障隔离价值:该组件故障是否应当限制爆炸半径?4)数据自治:是否能明确单一写入方,避免跨服务双向写入?5)运维准备度:是否已有日志、指标、追踪、告警、运行手册?少于三个勾,优先模块化单体;五个勾都满,再考虑拆独立进程。AI 生成架构文档时常常默认微服务,你要用这张表把它拉回地面。
API 网关与 BFF:不要混成「第二单体」
CodeSentinel 可能同时有 Web 控制台、CI 插件、企业 IM 三种入口。你可以做一个统一网关加多路由模块,或做多个 BFF 共享内部服务。无论哪种,都要避免 BFF 里堆领域规则——否则你只是把上帝类搬到网关进程里,分布式之后更难调试。评审 AI 生成代码时,看到网关文件里 tenant 与 channel 分支疯狂增长,就要拉响警报。
服务间认证:内部接口也要有身份
内部服务之间建议 mTLS 或签名服务令牌,并配合内网隔离与最小权限。教学示例省略认证以便运行,但生产必须把「谁能调用 evaluate」「谁能触发 notify」写成明确策略。对 CodeSentinel,内部接口往往携带敏感仓库元数据,认证缺失会直接转化为合规风险。
REST 与 gRPC 的维护成本
gRPC 很强,但它要求契约治理更严格。小团队若还没有 protobuf 版本管理流程,先用 REST 加 OpenAPI 生成客户端往往更稳;当 IndexService 的 payload 大到不可忽视,再引入 gRPC 也不迟。
消息队列与「至少一次」投递
异步世界默认至少一次投递,因此消费者必须幂等:review_id 加 event_id 作为去重键是常见做法。报告生成与通知发送尤其要注意重复投递不应导致重复触发 CI 或重复计费。架构师要在设计文档里写清幂等键来源,否则 AI 很容易写出「每次消息都创建新任务」的脆弱实现。
代码实战:FastAPI 网关 + httpx 重试 + 简易熔断
以下单文件 gateway_lab.py 可运行(需 pip install fastapi uvicorn httpx)。它模拟网关把 /reviews 转发到「下游 Review 服务」,并内置 熔断器 与 重试。为可运行性,下游用另一个 FastAPI 子应用模拟;你也可以把 DOWNSTREAM 改成真实地址。
下游模拟服务 downstream_review_app(可与网关同文件启动第二个进程,或合并为单进程挂载子路由)
为简化,我们提供一个 单进程 方案:网关在内存里调用「假客户端」;同时给出 真实 httpx 调用 的分支,便于你拆进程验证。
# gateway_lab.py
# 依赖: pip install fastapi uvicorn httpx
from __future__ import annotations
import asyncio
import random
import time
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, Optional
import httpx
from fastapi import FastAPI, HTTPException
# ========= 简易熔断器 =========
class CircuitState(str, Enum):
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
@dataclass
class CircuitBreaker:
failure_threshold: int = 3
recovery_seconds: float = 5.0
_failures: int = 0
_opened_at: float = 0.0
state: CircuitState = CircuitState.CLOSED
def allow(self) -> bool:
if self.state == CircuitState.CLOSED:
return True
if self.state == CircuitState.OPEN:
if time.time() - self._opened_at >= self.recovery_seconds:
self.state = CircuitState.HALF_OPEN
return True
return False
return True # HALF_OPEN
def record_success(self) -> None:
self._failures = 0
self.state = CircuitState.CLOSED
def record_failure(self) -> None:
self._failures += 1
if self._failures >= self.failure_threshold:
self.state = CircuitState.OPEN
self._opened_at = time.time()
# ========= 带重试的 httpx 客户端 =========
async def call_with_retry(
method: str,
url: str,
*,
json: Optional[Dict[str, Any]] = None,
max_retries: int = 3,
base_delay: float = 0.2,
) -> httpx.Response:
last_exc: Optional[Exception] = None
async with httpx.AsyncClient(timeout=5.0) as client:
for attempt in range(max_retries):
try:
resp = await client.request(method, url, json=json)
if resp.status_code >= 500:
raise httpx.HTTPStatusError("server error", request=resp.request, response=resp)
return resp
except Exception as e: # noqa: BLE001 - 教学示例聚合异常
last_exc = e
await asyncio.sleep(base_delay * (2**attempt) + random.random() * 0.05)
assert last_exc is not None
raise last_exc
# ========= 网关应用 =========
app = FastAPI(title="CodeSentinel API Gateway Lab")
review_breaker = CircuitBreaker()
RULE_SERVICE_URL = "http://127.0.0.1:9001/evaluate" # 若未启动,可走降级分支
REVIEW_CORE_URL = "http://127.0.0.1:9002/reviews" # 若未启动,可走降级分支
async def forward_review_to_core(body: Dict[str, Any]) -> Dict[str, Any]:
"""尝试转发到下游 ReviewService;失败则返回教学降级结果。"""
if not review_breaker.allow():
return {"degraded": True, "reason": "circuit_open", "echo": body}
try:
resp = await call_with_retry("POST", REVIEW_CORE_URL, json=body)
review_breaker.record_success()
return resp.json()
except Exception:
review_breaker.record_failure()
return {"degraded": True, "reason": "downstream_failure", "echo": body}
@app.post("/v1/reviews")
async def create_review(payload: Dict[str, Any]) -> Dict[str, Any]:
# 网关可在此加入 auth、tenant 解析、trace id
trace_id = payload.get("trace_id", "trace-local")
merged = {**payload, "trace_id": trace_id}
result = await forward_review_to_core(merged)
return {"trace_id": trace_id, "result": result}
@app.get("/health")
async def health() -> Dict[str, str]:
return {"status": "ok", "circuit": review_breaker.state.value}
# ========= 可选:同一文件内启动「假下游」用于联调 =========
downstream = FastAPI()
@downstream.post("/reviews")
async def internal_review(body: Dict[str, Any]) -> Dict[str, Any]:
if body.get("force_fail"):
raise HTTPException(status_code=503, detail="forced failure")
return {"accepted": True, "review_id": "rv-1001", "input": body}
def mount_local_downstream_for_demo() -> None:
"""把下游挂到同一 app,便于单命令演示(非真实部署形态)。"""
app.mount("/_downstream", downstream)
# 默认开启本地挂载,便于 `uvicorn gateway_lab:app` 一把跑
mount_local_downstream_for_demo()
REVIEW_CORE_URL = "http://127.0.0.1:8000/_downstream/reviews"
运行方式(单进程演示):
pip install fastapi uvicorn httpx
uvicorn gateway_lab:app --reload --port 8000
然后:
curl -X POST http://127.0.0.1:8000/v1/reviews -H "Content-Type: application/json" -d "{\"repo\":\"codesentinel/core\",\"pr\":55}"
若要验证熔断,可连续发送 {"force_fail": true} 到挂载下游——注意教学代码里默认 forward_review_to_core 指向 REVIEW_CORE_URL;你需要把 body 传到下游才能触发失败逻辑。下面给出 补丁思路:在 create_review 把 payload 原样转发即可(上文已转发)。
自测熔断:连续 POST {"force_fail": true} 到 /_downstream/reviews 会直接 503;对网关而言应连接真实下游。为保持文档简洁,建议你本地把 REVIEW_CORE_URL 指到独立 uvicorn gateway_lab:downstream --port 9002 并去掉 mount——这是拆进程的真实形态。
拆进程真实联调(推荐)
终端 A:
# review_service.py
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.post("/reviews")
async def reviews(body: dict):
if body.get("force_fail"):
raise HTTPException(status_code=503, detail="forced")
return {"review_id": "rv-2002", "ok": True}
终端 A 运行:uvicorn review_service:app --port 9002
终端 B:uvicorn gateway_lab:app --port 8000 且设置 REVIEW_CORE_URL 为 http://127.0.0.1:9002/reviews 并注释 mount_local_downstream_for_demo() 调用。
这样熔断与重试行为更可观察。
说明:上文
gateway_lab.py为教学拼接版,默认 mount 下游 时 URL 已改写为指向自身挂载路径,便于零配置体验。若你要严格模拟跨进程,请按上一段拆进程。
代码走读:为什么熔断挂在网关而不是每个客户端随意做
把熔断器放在网关(或共享 SDK)里,是为了 统一失败策略:同样的下游故障,在 Web 与 CI 入口看到一致的错误语义与重试行为。若每个服务各自实现一套熔断,容易出现「A 服务已经打开熔断,B 服务仍在猛打下游」的撕裂状态。更进一步,熔断阈值应结合 下游 SLO 调参:对强依赖服务阈值更严,对弱依赖服务可以更快降级。
代码走读:call_with_retry 的边界与陷阱
教学版对 >=500 统一视为可重试。真实系统要区分:
- 429:应尊重
Retry-After,而不是盲指数退避; - 400/422:通常是请求不合法,不应重试;
- 401/403:重试只会放大审计噪声,应先修凭证。
AI 生成重试逻辑时常犯「所有异常都重试三遍」的错误,这会把雪崩送给下游。你要在评审模板里明确要求 可重试异常分类。
并行调用:当网关需要同时打 Index 与 Rule
在 FastAPI 里可以用 asyncio.gather 并行请求多个下游,缩短关键路径。但要设置 整体 deadline:并行并不自动降低尾延迟,反而可能放大资源占用。CodeSentinel 若对外承诺「30 秒内返回初审结果」,网关要把 deadline 拆给每个下游,并在某个下游超时时决定是否 部分成功。
配置管理:不要把 URL 写死在代码里
教学示例用常量 URL;生产应使用 环境变量或配置中心,并支持 多区域路由。否则你会在灾难切换时被迫改代码发版,这是架构师最该避免的「高成本变更」。
与 Kubernetes 的亲和性:超时、连接池与 keep-alive
在 K8s 中 Pod 频繁滚动,客户端连接池可能持有失效连接。httpx.AsyncClient 建议在进程级复用连接池,并在滚动发布策略上配合 readiness probe,避免把流量打到未就绪实例。细节琐碎,但决定了「为什么本地一切正常、上线就偶发超时」。
生产环境实战:从实验室到可运维网关
网关层必须出现的横切能力
- 认证与鉴权:JWT、mTLS、或内网身份代理头校验;
- 限流与配额:按租户、按 API Key;
- 请求日志与 trace:
trace_id注入并在下游传播(W3C Trace Context); - 超时预算:全局 deadline,避免串联服务把延迟相乘。
CodeSentinel 服务边界建议
- ReviewService:审核生命周期编排、聚合 diff 与规则结果;
- RuleService:规则包管理、评估计算、版本化;
- IndexService:代码索引、符号检索、变更摘要(可能 CPU 密集);
- NotificationService:Webhook、邮件、Slack、企业 IM,全部异步化。
消息队列与 Outbox
跨服务一致性不要迷信「分布式事务优先」。更常见是 Outbox:本地事务写业务表 + outbox 表,worker 投递 MQ,消费者幂等处理。
gRPC 何时值得上
当 IndexService 返回大规模符号图或需要流式传输时,gRPC streaming 比 REST 更自然。代价是调试与网关穿透复杂度上升,需要工具链投入。
何时不要拆
团队 < 5、日变更个位数、无独立扩缩容痛点时,模块化单体 + 清晰边界 往往是最佳解。CodeSentinel 的课程主线也强调:先单体跑通,再提取服务。
与 AI 生成代码的治理
AI 很容易在网关里写业务规则、在多个服务里复制 DTO。要通过 共享契约包(OpenAPI / protobuf)+ CI 契约测试抑制漂移;网关代码审查重点:只做横切与轻编排。
更进一步,你可以把「允许网关出现的 import 白名单」写进静态检查:只允许 fastapi、httpx、pydantic、观测 SDK 等;一旦出现业务领域模型 import,立即失败。对 AI 工作流来说,这种硬门禁比「请遵守分层」有效得多,因为它把错误变成 编译期/CI 期错误,而不是人类评审的运气问题。
灰度与版本
对外 API 路径建议带 /v1,内部服务可用 header 版本协商。网关负责兼容层,避免下游被迫同时维护多版本实现。
多区域与灾备:拆分后变难的不是代码,是数据
一旦 ReviewService 与 RuleService 分属不同数据库或不同存储后端,你会遇到 跨区复制延迟、读写一致性、备份恢复点 的组合难题。对 CodeSentinel,建议早期就把「哪些数据必须强一致、哪些可以最终一致」写清楚:例如规则包版本与评估结果可能需要强一致关联;通知投递允许最终一致。没有这张表,微服务拆分很容易在事故后暴露为「数据对不上」。
成本视角
每多一个常驻服务,至少增加:部署、监控、告警、日志索引、证书轮换、容量规划。架构师要在白板上把 年运维成本 算进去,而不是只画框图。
灾难演练:当 IndexService 全挂时,ReviewService 该不该继续?
这是典型的 降级策略 问题。可选策略包括:返回「部分结果 + 明确告警」、阻断合入、或进入人工审核队列。没有标准答案,但必须 产品化决策,不能靠工程师临场发挥。网关可以把降级策略编码为统一错误码,并在响应体附带 degraded_components 列表,方便 UI 呈现。
与 CodeSentinel 合规:跨服务日志里的 PII
审核系统常处理代码与元数据,日志中可能意外包含 token、邮箱、内部 URL。网关作为统一入口,最适合做 日志脱敏与字段允许列表。AI 生成日志语句时往往直接 logger.info(payload),评审必须拦截。
AGENTS.md 建议片段(微服务相关)
- 禁止在网关实现具体规则判定;规则必须调用 RuleService 或领域服务。
- 所有跨进程调用必须设置 timeout,禁止裸
requests.get无超时。 - 异步消费者必须幂等;必须写清 idempotency key 字段来源。
从单测到集成测试:契约测试最小集合
至少覆盖:网关 → ReviewService happy path;ReviewService → RuleService 超时重试;Notification 异步消费者的重复消息。没有这三类测试,微服务很容易「各服务单测全绿、联调全红」。
发布策略:滚动、蓝绿与金丝雀如何影响客户端
当你拆分服务后,发布不再是一次进程替换,而可能涉及多服务协同版本。网关作为入口,适合做 金丝雀流量:把 1% 请求路由到新版本 ReviewService,对比错误率与延迟。没有网关层的流量控制能力,金丝雀只能退化为「手工挑选用户」,成本高且容易出错。对 CodeSentinel 这种平台,建议尽早把发布策略写进运维手册,而不是靠工程师记忆。
本讲小结
mindmap
root((微服务与网关))
拆分策略
业务能力
限界上下文
团队拓扑
通信
REST
gRPC
消息队列
网关
鉴权限流
路由聚合
韧性
重试退避
熔断降级
CodeSentinel
Review Rule Index Notify
先单体后拆分
思考题
- 若
RuleService比ReviewService慢一个数量级,你会优先异步化哪一段?如何保持用户体验(同步返回 vs 轮询 vs Webhook)? - 网关聚合多个下游时,如何做 部分成功 语义与错误码映射?
- 你的组织里服务发现用 K8s DNS 还是独立注册中心?各自失败模式是什么?
延伸讨论:CodeSentinel 的「模块化单体」长什么样?
在拆服务之前,推荐你在单仓库内用包结构预演服务边界:review_app/、rule_app/、index_app/、notify_app/ 各自暴露内部 API(仍可在同一进程用函数调用)。当你发现团队开始用「跨包 HTTP」来绕过循环依赖时,说明边界设计可能有问题;当你发现某个包独立扩缩容需求真实存在,再把它变成独立部署单元。这个节奏能显著降低 分布式大爆改 的概率。
延伸讨论:链路追踪的最小可行方案
没有 traceId 的微服务排障几乎等于盲人摸象。最小可行方案是:网关生成或透传 trace_id,下游服务在日志与响应头回显;更进一步接入 OpenTelemetry。对 CodeSentinel,建议把 review_id 与 trace_id 绑定存储,方便从用户投诉一路追到具体下游 span。
反模式清单:微服务拆分后的「分布式单体」信号
如果出现以下现象,你要警惕:
- 四个服务必须同版本一起发布才能工作;
- 每次需求都同时改四个仓库;
- 本地开发需要启动十几个进程才能调试一个按钮;
- 跨服务大量分布式事务「强一致」诉求。
这些信号说明:你得到的不是微服务,而是 更贵的单体。此时应暂停继续拆分,先收敛契约与边界,甚至考虑合并回单体或引入 BFF 聚合降噪。
下一讲预告
下一讲进入 事件驱动架构:用 Event Sourcing + CQRS 构建 CodeSentinel 的审核事件流,实现可追溯、可重放、可时间查询的审计模型。你会看到事件存储、命令侧、读模型投影如何与本期网关/消息层自然衔接。建议你在学习时把本讲的 trace_id 与下一讲的 event_id 对照理解:前者解决跨服务排障,后者解决业务事实追溯,两者叠加才是企业级审核平台。
动手小练习(可选)
用两台终端分别启动 review_service 与 gateway_lab,对 force_fail 做压测,观察熔断从 closed 到 open 的迁移,并记录失败请求占比。把实验结果写进团队 wiki,比单纯背诵熔断定义更有用。你也可以在练习里尝试调大 failure_threshold 与 recovery_seconds,体会参数变化对用户体验与下游保护的影响。