微服务常被误解为“把单体拆小”,但拆小本身不产生任何价值,反而把进程内调用变成了网络调用,把事务变成了分布式难题。微服务真正的收益只有一个:让不同团队能独立演进、独立部署、独立扩容。所有架构决策都应围绕这条判据展开。本文从边界、通信、数据、可观测四层给出可落地的实现。
最常见的错误是“controller 一个服务、service 一个服务”。正确的切分单位是业务能力与数据所有权:一个服务独占它的数据表,其他服务只能通过接口访问。用一句话检验:这个服务能否在不改别人的前提下改自己的表结构? 不能,说明边界错了。
# 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 定义,消费方生成客户端,避免“字段悄悄改”。调用必须显式设置超时,并遵循超时预算:下游超时必须小于上游剩余时间,否则重试会叠加成雪崩。
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()读接口可重试,写接口只在幂等前提下重试——这条比任何框架配置都关键。
跨服务强一致代价高,主线是最终一致。上面 outbox 表由独立中继进程投递到消息队列,消费方按业务键幂等;复杂链路用编排式 Saga,为每个正向步骤定义补偿动作。
# 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 串起来。用 OpenTelemetry 自动埋点,网关生成并透传 traceId。
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 延迟、下游依赖饱和度。告警要按用户影响分级,而不是按服务数量刷屏。
缺任何一条,服务数量越多,故障面越大。
反模式清单:共享数据库(边界失效)、同步调用链过长(延迟叠加)、无契约联调(字段漂移)、所有服务共用一个发布窗口(失去独立部署意义)、为拆而拆(团队 3 人却 20 个服务)。
判断是否该拆,问四个问题:是否有独立扩容需求?是否有独立发布节奏?是否有独立数据所有权?是否有独立团队负责? 四问有三个“是”才拆,否则先做模块化单体。
微服务架构的成熟度不看服务数量,而看三件事:下游抖动时上游能否在 1 秒内优雅降级、10 分钟内用 traceId 定位根因、数据最终不丢不错。边界决定长期可维护性,契约与超时决定可用性,Saga 与 outbox 决定正确性,可观测决定救火效率。把这四点做扎实,微服务才是资产;缺了它们,拆分只是把单体的问题复制成了分布式的问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。