首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >微服务架构:边界、契约与失败设计

微服务架构:边界、契约与失败设计

原创
作者头像
资源大佬 jzit-top
发布于 2026-10-22 16:38:56
发布于 2026-10-22 16:38:56
1050
举报

微服务常被误解为“把单体拆小”,但拆小本身不产生任何价值,反而把进程内调用变成了网络调用,把事务变成了分布式难题。微服务真正的收益只有一个:让不同团队能独立演进、独立部署、独立扩容。所有架构决策都应围绕这条判据展开。本文从边界、通信、数据、可观测四层给出可落地的实现。

一、边界:按限界上下文切,不按技术层切

最常见的错误是“controller 一个服务、service 一个服务”。正确的切分单位是业务能力与数据所有权:一个服务独占它的数据表,其他服务只能通过接口访问。用一句话检验:这个服务能否在不改别人的前提下改自己的表结构? 不能,说明边界错了。

代码语言:javascript
复制
# order_service/app.py —— 订单服务独占 orders 表
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import asyncpg

app = FastAPI()
pool: asyncpg.Pool

class CreateOrder(BaseModel):
    user_id: str
    sku: str
    qty: int
    idempotency_key: str

@app.post("/orders")
async def create_order(cmd: CreateOrder):
    async with pool.acquire() as conn:
        # 幂等:同 key 直接返回已有订单
        row = await conn.fetchrow(
            "SELECT * FROM orders WHERE idem_key=$1", cmd.idempotency_key)
        if row:
            return dict(row)
        async with conn.transaction():
            row = await conn.fetchrow(
                """INSERT INTO orders(user_id, sku, qty, status, idem_key)
                   VALUES($1,$2,$3,'CREATED',$4) RETURNING *""",
                cmd.user_id, cmd.sku, cmd.qty, cmd.idempotency_key)
            # 同库同事务写 outbox,保证"订单成功则事件必在"
            await conn.execute(
                """INSERT INTO outbox(topic, payload)
                   VALUES('order.created', $1)""",
                {"order_id": str(row["id"]), "sku": cmd.sku, "qty": cmd.qty})
        return dict(row)

二、通信:契约先行 + 超时预算

接口契约用 OpenAPI/Protobuf 定义,消费方生成客户端,避免“字段悄悄改”。调用必须显式设置超时,并遵循超时预算:下游超时必须小于上游剩余时间,否则重试会叠加成雪崩。

代码语言:javascript
复制
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

client = httpx.AsyncClient(timeout=httpx.Timeout(0.8, connect=0.3))

@retry(stop=stop_after_attempt(2),
       wait=wait_exponential(multiplier=0.05, max=0.2),
       retry=retry_if_exception_type((httpx.ConnectError, httpx.ReadTimeout)))
async def deduct(sku: str, qty: int, order_id: str):
    # 写接口必须带幂等键,服务端按 order_id 去重
    r = await client.post("http://inventory:8000/deduct",
                          json={"sku": sku, "qty": qty, "idem_key": order_id})
    r.raise_for_status()
    return r.json()

读接口可重试,写接口只在幂等前提下重试——这条比任何框架配置都关键。

三、数据一致性:Saga 优于两阶段提交

跨服务强一致代价高,主线是最终一致。上面 outbox 表由独立中继进程投递到消息队列,消费方按业务键幂等;复杂链路用编排式 Saga,为每个正向步骤定义补偿动作。

代码语言:javascript
复制
# saga.py —— 编排式 Saga:任一步失败,逆序补偿
class Saga:
    def __init__(self):
        self.steps = []          # (action, compensate)
        self.done = []

    def add(self, action, compensate):
        self.steps.append((action, compensate)); return self

    async def run(self, ctx):
        for action, comp in self.steps:
            try:
                ctx = await action(ctx)
                self.done.append((comp, ctx))
            except Exception as e:
                for comp, c in reversed(self.done):   # 逆序补偿
                    await comp(c)
                raise e
        return ctx

saga = (Saga()
    .add(lambda c: create_order(c),  lambda c: cancel_order(c))
    .add(lambda c: deduct_stock(c),  lambda c: restore_stock(c))
    .add(lambda c: charge(c),        lambda c: refund(c)))

补偿动作必须幂等且可重入,因为补偿本身也可能失败,需要重试。

四、可观测:三条链路一个 traceId

日志、指标、追踪必须能被同一个 traceId 串起来。用 OpenTelemetry 自动埋点,网关生成并透传 traceId。

代码语言:javascript
复制
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel:4317")))
FastAPIInstrumentor.instrument_app(app)      # 自动记录 HTTP 与 DB span

tracer = trace.get_tracer("order")
async def create_order_traced(cmd):
    with tracer.start_as_current_span("create_order") as span:
        span.set_attribute("sku", cmd.sku)
        return await create_order(cmd)

必看的三个指标:错误率、P95 延迟、下游依赖饱和度。告警要按用户影响分级,而不是按服务数量刷屏。

五、治理清单:上线前必须过的六道关

  1. 超时:每个出站调用都有显式超时,且满足预算递减;
  2. 重试:仅幂等调用可重试,带退避与上限;
  3. 熔断:下游连续失败自动快速失败,避免线程耗尽;
  4. 限流:网关按租户/接口限流,保护自身与下游;
  5. 幂等:所有写接口接受幂等键;
  6. 降级:故障时返回可解释的兜底结果,而非 500。

缺任何一条,服务数量越多,故障面越大。

六、反模式与判据

反模式清单:共享数据库(边界失效)、同步调用链过长(延迟叠加)、无契约联调(字段漂移)、所有服务共用一个发布窗口(失去独立部署意义)、为拆而拆(团队 3 人却 20 个服务)。

判断是否该拆,问四个问题:是否有独立扩容需求?是否有独立发布节奏?是否有独立数据所有权?是否有独立团队负责? 四问有三个“是”才拆,否则先做模块化单体。

结语

微服务架构的成熟度不看服务数量,而看三件事:下游抖动时上游能否在 1 秒内优雅降级、10 分钟内用 traceId 定位根因、数据最终不丢不错。边界决定长期可维护性,契约与超时决定可用性,Saga 与 outbox 决定正确性,可观测决定救火效率。把这四点做扎实,微服务才是资产;缺了它们,拆分只是把单体的问题复制成了分布式的问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、边界:按限界上下文切,不按技术层切
  • 二、通信:契约先行 + 超时预算
  • 三、数据一致性:Saga 优于两阶段提交
  • 四、可观测:三条链路一个 traceId
  • 五、治理清单:上线前必须过的六道关
  • 六、反模式与判据
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档