10-模块二-架构基础功 第10讲-微服务架构模式 - 服务拆分策略 API 网关与服务间通信

3 阅读21分钟

模块二-架构基础功 | 第10讲:微服务架构模式 - 服务拆分策略、API 网关与服务间通信

开场:微服务不是「拆得碎」,而是「买得起运维」的边界选择

当你用 AI 快速堆出一个 CodeSentinel 原型时,它几乎总是 单体(monolith):一个 FastAPI 进程里同时完成拉取变更、跑规则、调模型、写报告、发通知。原型阶段这没问题——交付速度最重要。但一旦进入企业环境,你会遇到 独立扩缩容独立发布故障隔离多团队并行 的真实压力,「要不要拆微服务」就会被摆上桌面。

很多团队在讨论微服务时,会把注意力放在「容器个数」与「仓库个数」,却忽略了一个更根本的问题:拆分之后,故障模式从函数调用失败变成网络分区、超时、重试风暴、消息乱序。这意味着你必须同步投资 可观测性、发布策略、容量规划、契约治理。CodeSentinel 作为审核平台,还要额外承担 审计与合规 的压力:跨服务调用链上的每一次失败,都要能解释「用户看到的结果是如何形成的」。因此本讲会把「网关 + 韧性 + 通信模式」放在一起讲,而不是只画一张漂亮的架构图。

本讲不会鼓吹「为拆而拆」,而是给你一套 可落地的决策框架:按业务能力、按限界上下文、按团队拓扑三种拆分策略如何取舍;CodeSentinel 如何划分 ReviewServiceRuleServiceIndexServiceNotificationServiceFastAPI 作为 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)。
  • 性能热点出现:先提取 IndexServiceRuleService 这类 CPU 边界。
  • 通知洪水:提取 NotificationService + 消息队列。
  • 多租户隔离需求:考虑按租户分片或独立部署,而不是先盲目拆很多微服务。

拆分决策表:把「感觉该拆」变成「条件满足才拆」

建议你与团队在白板上使用下面这张表(文字版),对每个候选服务打勾:1)独立发布价值:是否存在「不想被其它团队阻塞」的发布周期?2)独立扩缩容价值:CPU/内存曲线是否与主服务显著不同?3)故障隔离价值:该组件故障是否应当限制爆炸半径?4)数据自治:是否能明确单一写入方,避免跨服务双向写入?5)运维准备度:是否已有日志、指标、追踪、告警、运行手册?少于三个勾,优先模块化单体;五个勾都满,再考虑拆独立进程。AI 生成架构文档时常常默认微服务,你要用这张表把它拉回地面。

API 网关与 BFF:不要混成「第二单体」

CodeSentinel 可能同时有 Web 控制台、CI 插件、企业 IM 三种入口。你可以做一个统一网关加多路由模块,或做多个 BFF 共享内部服务。无论哪种,都要避免 BFF 里堆领域规则——否则你只是把上帝类搬到网关进程里,分布式之后更难调试。评审 AI 生成代码时,看到网关文件里 tenantchannel 分支疯狂增长,就要拉响警报。

服务间认证:内部接口也要有身份

内部服务之间建议 mTLS 或签名服务令牌,并配合内网隔离与最小权限。教学示例省略认证以便运行,但生产必须把「谁能调用 evaluate」「谁能触发 notify」写成明确策略。对 CodeSentinel,内部接口往往携带敏感仓库元数据,认证缺失会直接转化为合规风险。

REST 与 gRPC 的维护成本

gRPC 很强,但它要求契约治理更严格。小团队若还没有 protobuf 版本管理流程,先用 REST 加 OpenAPI 生成客户端往往更稳;当 IndexService 的 payload 大到不可忽视,再引入 gRPC 也不迟。

消息队列与「至少一次」投递

异步世界默认至少一次投递,因此消费者必须幂等:review_idevent_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_reviewpayload 原样转发即可(上文已转发)。

自测熔断:连续 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_URLhttp://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;
  • 请求日志与 tracetrace_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 白名单」写进静态检查:只允许 fastapihttpxpydantic、观测 SDK 等;一旦出现业务领域模型 import,立即失败。对 AI 工作流来说,这种硬门禁比「请遵守分层」有效得多,因为它把错误变成 编译期/CI 期错误,而不是人类评审的运气问题。

灰度与版本

对外 API 路径建议带 /v1,内部服务可用 header 版本协商。网关负责兼容层,避免下游被迫同时维护多版本实现。

多区域与灾备:拆分后变难的不是代码,是数据

一旦 ReviewServiceRuleService 分属不同数据库或不同存储后端,你会遇到 跨区复制延迟读写一致性备份恢复点 的组合难题。对 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
      先单体后拆分

思考题

  1. RuleServiceReviewService 慢一个数量级,你会优先异步化哪一段?如何保持用户体验(同步返回 vs 轮询 vs Webhook)?
  2. 网关聚合多个下游时,如何做 部分成功 语义与错误码映射?
  3. 你的组织里服务发现用 K8s DNS 还是独立注册中心?各自失败模式是什么?

延伸讨论:CodeSentinel 的「模块化单体」长什么样?

在拆服务之前,推荐你在单仓库内用包结构预演服务边界:review_app/rule_app/index_app/notify_app/ 各自暴露内部 API(仍可在同一进程用函数调用)。当你发现团队开始用「跨包 HTTP」来绕过循环依赖时,说明边界设计可能有问题;当你发现某个包独立扩缩容需求真实存在,再把它变成独立部署单元。这个节奏能显著降低 分布式大爆改 的概率。

延伸讨论:链路追踪的最小可行方案

没有 traceId 的微服务排障几乎等于盲人摸象。最小可行方案是:网关生成或透传 trace_id,下游服务在日志与响应头回显;更进一步接入 OpenTelemetry。对 CodeSentinel,建议把 review_idtrace_id 绑定存储,方便从用户投诉一路追到具体下游 span。

反模式清单:微服务拆分后的「分布式单体」信号

如果出现以下现象,你要警惕:

  • 四个服务必须同版本一起发布才能工作;
  • 每次需求都同时改四个仓库;
  • 本地开发需要启动十几个进程才能调试一个按钮;
  • 跨服务大量分布式事务「强一致」诉求。

这些信号说明:你得到的不是微服务,而是 更贵的单体。此时应暂停继续拆分,先收敛契约与边界,甚至考虑合并回单体或引入 BFF 聚合降噪。


下一讲预告

下一讲进入 事件驱动架构:用 Event Sourcing + CQRS 构建 CodeSentinel 的审核事件流,实现可追溯、可重放、可时间查询的审计模型。你会看到事件存储、命令侧、读模型投影如何与本期网关/消息层自然衔接。建议你在学习时把本讲的 trace_id 与下一讲的 event_id 对照理解:前者解决跨服务排障,后者解决业务事实追溯,两者叠加才是企业级审核平台。

动手小练习(可选)

用两台终端分别启动 review_servicegateway_lab,对 force_fail 做压测,观察熔断从 closedopen 的迁移,并记录失败请求占比。把实验结果写进团队 wiki,比单纯背诵熔断定义更有用。你也可以在练习里尝试调大 failure_thresholdrecovery_seconds,体会参数变化对用户体验与下游保护的影响。