微服务边界划分
以 DDD 限界上下文为语义边界,对齐数据所有权、发布节奏与团队协作;让 Agent 产出可评审的服务候选、上下文映射草稿与「暂缓拆分」依据,避免分布式单体与为拆而拆。
SKILL 应要求每个候选服务写明:所属限界上下文、拥有的聚合与存储、对外暴露的查询/命令与事件,以及与其它上下文的协作关系(含防腐层或共享内核等显式决策)。
禁止两个团队各自「写」同一聚合根而不经明确流程:要么单一写入方 + 其它方只读副本或事件投影,要么承认强一致链路并评估是否暂不拆服务。
DDD 与限界上下文
限界上下文(Bounded Context)是语义一致性的边界:在同一上下文内,术语与规则统一(通用语言);跨边界则允许同名不同义,必须通过显式翻译(DTO、ACL、集成契约)衔接。
- 先画业务能力与子域(核心 / 支撑 / 通用),再落到上下文,而不是按技术层(Controller 一层一个服务)硬切。
- 上下文比「微服务」更稳定:服务可合并或拆分,但领域边界变化应伴随模型与语言的重议。
- Agent 输出中应能指出:每个上下文的通用语言词条样例与「禁止外泄」的内部不变量。
# 领域词汇表冲突检测(Python 脚本示例)
# 跨上下文中相同词汇有不同含义 → 边界信号
GLOSSARIES = {
"ordering": {
"customer": "下单的用户,含收货地址与支付方式",
"product": "商品快照,包含下单时的价格",
"order": "用户提交的购买意图,包含多个 OrderItem",
},
"inventory": {
"product": "库存中的物理商品,含 SKU 与仓库位置",
"quantity": "可用库存量,不含已预留",
},
"billing": {
"customer": "财务账户持有人,含发票信息",
"invoice": "账期内的账单汇总",
},
}
def detect_term_conflicts(glossaries: dict) -> list[dict]:
"""检测跨上下文中相同词汇的语义差异"""
term_contexts: dict[str, list] = {}
for ctx, terms in glossaries.items():
for term in terms:
term_contexts.setdefault(term, []).append(ctx)
conflicts = []
for term, contexts in term_contexts.items():
if len(contexts) > 1:
definitions = {c: glossaries[c][term] for c in contexts}
conflicts.append({
"term": term,
"contexts": contexts,
"definitions": definitions,
"recommendation": f"'{term}' 跨上下文语义不同 → 需通过 ACL/DTO 翻译,不共享同一模型类"
})
return conflicts
conflicts = detect_term_conflicts(GLOSSARIES)
for c in conflicts:
print(f"⚠ 冲突词汇: {c['term']}")
print(f" 出现在: {', '.join(c['contexts'])}")
for ctx, defn in c['definitions'].items():
print(f" [{ctx}] {defn}")
print(f" 建议: {c['recommendation']}")
微服务与上下文的对应关系
常见起点是一个限界上下文对应一个部署单元,但并非教条:过小会导致运维与发布碎片化;过大则易成「分布式单体」。对齐标准是变更频率、团队归属与事务一致性需求,而非类包或目录结构。
倾向拆成独立服务
- 不同发布节奏与扩展特征(读多写少 vs 批处理峰值)
- 清晰的数据所有权与单一写入路径
- 团队可端到端负责该上下文内的交付
倾向保留同进程 / 同仓库
- 强一致事务跨多个聚合且无法接受最终一致窗口
- 高频、细粒度同步调用链(需先编排、BFF 或合并边界)
- 尚未澄清的领域模型,仅有「按表拆服务」的冲动
数据所有权与聚合边界
表或集合应归属单一服务;其它服务通过 API、只读副本、CDC 或领域事件获取数据,而不是直连对方库。聚合是事务与不变量边界:一个事务内修改的实体集合应能由单一聚合根串起来。
- 跨聚合用最终一致(事件、Outbox、Saga);需要分布式事务时先在 SKILL 中标注成本与替代方案。
- 共享只读维表可抽「通用子域」或复制到各上下文并约定更新源,避免隐式共享写。
Agent 检查清单:每个聚合根是否只有一个写入服务?跨服务「更新同一业务事实」是否已改为事件或明确的主数据归属?
上下文映射与协作模式
上下文映射描述上下游依赖与权力关系:谁定义模型、谁适配、是否共享内核。选错模式会导致隐性耦合或重复建设;SKILL 中应写出模式名与落地方式(例如防腐层所在服务、契约测试责任方)。
- 客户 / 供应商(Customer-Supplier):下游需求进入上游排期;适合有内部「产品化」上游模型的场景。
- 遵奉者(Conformist):下游完全采纳上游模型,成本是失去独立演进空间。
- 防腐层(ACL):下游隔离外来模型,适合外部系统或遗留大包 API。
- 共享内核(Shared Kernel):小范围共享代码/模型,需严格门禁与版本纪律。
- 开放主机服务(OHS)+ 发布语言(PL):上游提供稳定集成面与文档化契约。
- 各行其道(Separate Ways):集成成本高于重复时,允许有限重复并记录理由。
# 反腐层(ACL)实现:翻译外部 API 响应为内部领域模型
# 场景:Billing 上下文调用外部支付网关,用 ACL 隔离外部模型泄漏
from dataclasses import dataclass
from decimal import Decimal
# ===== 外部支付网关响应(上游模型,不可控)=====
class StripePaymentIntent:
def __init__(self, data: dict):
self.id = data["id"]
self.amount = data["amount"] # Stripe: 分为单位
self.currency = data["currency"] # Stripe: 小写 "cny"
self.status = data["status"] # Stripe: "succeeded"/"requires_payment_method"
self.customer = data.get("customer") # Stripe: customer ID
# ===== 内部领域模型(Billing BC)=====
@dataclass
class PaymentResult:
payment_id: str
amount: Decimal # 内部: 元为单位
currency: str # 内部: 大写 "CNY"
status: str # 内部: "SUCCESS"/"FAILED"/"PENDING"
customer_id: str | None
# ===== 防腐层:翻译器 =====
class StripePaymentACL:
STATUS_MAP = {
"succeeded": "SUCCESS",
"requires_payment_method": "FAILED",
"processing": "PENDING",
"requires_action": "PENDING",
}
def translate(self, stripe_intent: StripePaymentIntent) -> PaymentResult:
"""将 Stripe 外部模型翻译为 Billing 内部 PaymentResult"""
return PaymentResult(
payment_id=stripe_intent.id,
amount=Decimal(stripe_intent.amount) / 100, # 分→元
currency=stripe_intent.currency.upper(), # cny→CNY
status=self.STATUS_MAP.get(stripe_intent.status, "UNKNOWN"),
customer_id=stripe_intent.customer,
)
# 使用:外部模型只在 ACL 层出现,不泄漏到 Billing 内部逻辑
acl = StripePaymentACL()
external_data = {"id": "pi_abc", "amount": 9950, "currency": "cny",
"status": "succeeded", "customer": "cus_123"}
internal = acl.translate(StripePaymentIntent(external_data))
# → PaymentResult(payment_id='pi_abc', amount=Decimal('99.50'),
# currency='CNY', status='SUCCESS', customer_id='cus_123')
# Pact 契约测试:Consumer 端 Provider test 示例(Python pact-python)
# 场景:OrderService(consumer) 调用 InventoryService(provider)
# 1. Consumer 端:定义期望(生成 pact 文件)
from pact import Consumer, Provider
pact = Consumer("OrderService").has_pact_with(Provider("InventoryService"))
def test_reserve_inventory_consumer():
(pact
.given("SKU sku-001 has 100 units available")
.upon_receiving("a reserve inventory request")
.with_request("POST", "/inventory/reserve",
body={"skuId": "sku-001", "quantity": 2, "orderId": "order-88421"})
.will_respond_with(200, body={
"reservationId": pact.like("res-uuid-abc"),
"skuId": "sku-001",
"quantity": 2,
"expiresAt": pact.like("2024-03-15T11:00:00Z"),
}))
with pact:
# 实际调用 consumer 代码,pact 启动 mock server
result = inventory_client.reserve("sku-001", 2, "order-88421")
assert result["quantity"] == 2
# 2. Provider 端:验证 pact 文件(在 CI 中运行)
# pact-verifier --provider-base-url=http://localhost:8081 \
# --pact-broker-url=https://pact.acme.com \
# --provider=InventoryService \
# --publish-verification-results \
# --provider-version=$(git rev-parse HEAD)
从领域到服务候选(工作流)
[ 事件风暴 / 领域叙事 ]
│
▼
┌─────────────────┐ 产出:动词命令、名词、热点与争议术语
│ 识别子域与核心域 │
└─────────────────┘
│
▼
┌─────────────────┐ 每个 BC:语言表、聚合草图、对外能力
│ 划定限界上下文 │
└─────────────────┘
│
▼
┌─────────────────┐ 标注:CS / ACL / OHS / Shared Kernel …
│ 上下文映射 │
└─────────────────┘
│
▼
┌─────────────────┐ 表:服务 | 拥有数据 | API / 事件 | 风险
│ 服务候选与暂缓 │──── 暂缓:强一致链、批处理、技术债、联调地狱
└─────────────────┘
输出物建议包含:上下文列表、映射图(文字或 Mermaid)、服务候选表、以及「不拆」或「合并」的条目与触发条件(例如调用深度阈值、团队拓扑变化)。
对外 API 面与同步链
每个服务对外暴露的应是稳定、版本化的契约(REST/GraphQL/gRPC/消息契约);内部实现细节不穿越边界。深度同步调用栈是边界过细或职责错位的信号:优先考虑编排、异步消息或 BFF 聚合读。
- 读模型可经 BFF 或 API Gateway 组合,写路径保持单一归属以避免双写。
- 契约测试与消费者驱动契约(CDC)责任方应在映射中写清,支撑独立部署。
反模式与康威定律
分布式单体:许多小服务但部署、发布、数据库变更仍强耦合,联调成本不低于单体。按层切服务(纯 DAO 服务、纯网关服务)往往放大扇出与延迟。
康威定律:系统结构趋向组织沟通结构。边界与团队拓扑不一致时,要么调整团队(特性团队对齐上下文),要么承认边界将被人为穿透并在 SKILL 中记录治理措施(接口人、评审门槛)。
暂缓拆分清单
SKILL 应显式列出暂不拆或应合并的信号,避免「为了微服务而微服务」。
- 无法在业务上接受最终一致的跨聚合流程,且尚无可靠 Saga / 补偿设计。
- 批处理、报表、迁移作业与在线路径强绑在同一事务或同一锁粒度。
- 两个候选服务由同一小组每日联调,且无独立发布诉求。
- 缺少契约测试、特性开关、可观测性与回滚策略时的贸然切分。
# Istio VirtualService 流量规则示例(服务网格)
# 场景:payment-service v2 灰度发布,5% 流量切新版本
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: payment-service
namespace: production
spec:
hosts:
- payment-service
http:
# 金丝雀:标头路由(内测用户走 v2)
- match:
- headers:
x-canary-user:
exact: "true"
route:
- destination:
host: payment-service
subset: v2
# 流量分割:5% 走 v2,95% 走稳定版
- route:
- destination:
host: payment-service
subset: v1
weight: 95
- destination:
host: payment-service
subset: v2
weight: 5
retries:
attempts: 3
perTryTimeout: 2s
retryOn: "5xx,connect-failure,reset"
timeout: 10s
---
# Saga vs 2PC 选择标准:
# 2PC(两阶段提交):
# 适合:同一数据库/支持 XA 的资源管理器;延迟可接受
# 代价:协调者是单点;锁持有时间长;不跨异构系统
# Saga:
# 适合:跨微服务/异构数据库;可接受最终一致
# 代价:需设计补偿逻辑;可能出现部分提交中间状态
# 决策规则:跨服务 → Saga;同数据库强一致 → 2PC 或本地事务
上下文映射草稿实验室
选择经典上下文映射关系,填写上下游上下文名称与可选说明;可多次「追加一行」累积到下方文本框,便于粘贴进设计文档或 SKILL 附录。
箭头约定:上游 → 下游(依赖方向)。正式制图可用 Mermaid flowchart 或团队统一的上下文映射图例;本实验室仅生成文字草稿。
---
name: microservice-boundaries
description: 从 DDD 限界上下文推导服务边界、ACL 实现与契约测试
---
# 边界识别
词汇冲突检测: 同一术语在两个上下文含义不同 → 需 ACL 翻译
子域分类: 核心域(竞争力)/支撑域/通用域 → 决定自研还是外购
每个 BC: 通用语言词条样例 + 禁止外泄的内部不变量列表
# 上下文映射
CS: 下游需求进入上游排期(内部产品化上游)
ACL: 下游边界内翻译(外部系统/遗留 API)→ 实现见示例代码
OHS+PL: 上游提供稳定集成面 + schema 文档(对外公开 API)
SK: 小范围共享,需双方评审 + 同步发布
# 契约测试(Pact)
Consumer 侧: 定义期望 → 生成 pact 文件 → 本地 mock 验证
Provider 侧: CI 中 pact-verifier 验证 + 发布验证结果到 Broker
# 分布式事务
Saga: 跨服务/异构DB → 补偿顺序与正向相反,语义化
2PC: 同DB/XA资源 → 同一事务但协调者单点,不跨异构
# 服务网格(Istio)
VirtualService: 金丝雀/权重分流/超时重试配置
subset: 通过 DestinationRule 定义版本标签