微服务
微服务不是"把项目拆成几个 repo",而是用网络调用换团队自治——它换来独立发布与按域伸缩,代价是分布式系统全部的复杂度。本文先讲清楚什么时候不该拆,再沿着"拆分 → 发现 → 网关 → 通信 → 韧性 → 可观测 → 契约"这条主线展开,每一步都给出可直接照搬的判断表与代码骨架。
一、先问:要不要拆
| 该拆的信号 | 不该拆的信号 |
|---|---|
| 多团队在同一代码库互相阻塞,发布要排队 | 三五人的小团队,一个仓库就能说清楚 |
| 不同模块伸缩特性差异巨大(如转码 vs 管理后台) | 所有模块一起扩缩,流量特征一致 |
| 某模块需要独立的技术栈或合规隔离 | 业务逻辑高度耦合,拆完要一起改一起发 |
| 故障需要隔离(支付挂了不能拖垮浏览) | 没有可观测性与自动化部署的基础设施 |
判断原则:单体优先(Monolith First)。如果连模块边界在单体里都划不清,拆成微服务只会得到"分布式单体"——部署是分开的,改动仍然牵一发动全身,还额外附赠网络超时、数据一致性和排障难度。先在一个进程内把边界跑通,再拆。
拆分的真实代价(拆之前必须买单):
- 网络不可靠:调用可能超时、重试、乱序,必须有超时/重试/幂等;
- 数据不再共享事务:跨服务一致性需要 Saga/消息事务(见 系统设计);
- 排障变难:一次请求横跨 N 个服务,没有链路追踪几乎无法定位;
- 运维成本:服务数 ×(部署、监控、告警、扩容、证书、日志)。
二、怎么拆:按业务能力,而不是按技术分层
| 拆法 | 例子 | 评价 |
|---|---|---|
| 按业务能力/限界上下文 | 订单、库存、支付、履约 | 正确:边界内高内聚,边界间低耦合 |
| 按技术分层 | controller 服务、service 服务、dao 服务 | 反模式:改一个字段要发三个服务 |
| 按数据量 | 大表单独拆 | 只对分库分表有效,不等于服务拆分 |
| 按团队现状 | 有几个人就拆几块 | 组织决定架构(康威定律),但要以业务域为准 |
三个反模式信号:
- 共享数据库:两个服务读写同一张表 = 边界失效,任何一方改表结构都要同步另一方;
- 分布式单体:任何需求都要改 3 个服务并同时发布,比单体还慢;
- 纳米服务:拆得过细,一次业务操作跨十几个调用,延迟与运维成本压垮收益。
数据所有权是拆分的核心:每个服务拥有自己的数据,只能通过 API 被访问。这条守不住,其他都是空谈。
三、服务注册与发现:别把 IP 写死
服务启动 → 向注册中心注册(名称、IP、端口、元数据、健康检查地址)
→ 注册中心定期健康检查,失败则摘除
调用方 → 按服务名查询可用实例列表 → 负载均衡挑一个 → 发起调用| 发现模式 | 机制 | 代表 | 特点 |
|---|---|---|---|
| 客户端发现 | 调用方从注册中心拉列表,自行负载均衡 | Eureka + Ribbon、Nacos、Consul | 少一跳,但负载均衡逻辑要每种语言实现一遍 |
| 服务端发现 | 调用方只访问固定入口,由负载均衡器转发 | K8s Service、阿里云 SLB、Nginx | 对语言透明,容器环境的默认选择 |
| 注册中心 | 一致性 | 健康检查 | 适用 |
|---|---|---|---|
| Nacos | AP/CP 可切换 | 心跳 + 主动探测 | 国内 Java 生态常用,兼作配置中心 |
| Consul | CP(Raft) | 多种 check | 多语言、多机房 |
| Eureka | AP | 客户端心跳 | Spring Cloud 生态(已停止重大更新) |
| K8s DNS/Service | 依赖 etcd | readiness/liveness 探针 | 容器环境下直接用,不必再引一个注册中心 |
健康检查三类,别只做一种:
- 存活探针(liveness):进程是否卡死,失败则重启;
- 就绪探针(readiness):能否接流量,失败则从负载均衡摘除(启动中、依赖未就绪);
- 业务健康:能否真正完成工作(依赖的 DB/下游是否可用),用于更精细的摘除与告警。
四、API 网关与 BFF
网关是所有流量的统一入口,但它是"横切关注点"的归宿,不是业务逻辑的垃圾桶。
| 该放网关 | 不该放网关 |
|---|---|
| 路由、限流、鉴权(见 鉴权认证) | 业务编排(把三个服务聚合成一个业务接口) |
| TLS 终止、跨域、请求/响应改写 | 数据转换与业务规则 |
| 日志、TraceID 注入、审计 | 数据库访问 |
| 熔断降级与灰度分流 | 任何需要频繁变动的逻辑(会让网关成为发布瓶颈) |
需要按端定制的聚合,交给 BFF(Backend For Frontend):Web / App / 开放平台各自一个 BFF,由前端团队维护,专门为各自的界面裁剪数据,避免"一个通用 API 服务所有端"导致的过度请求与字段膨胀。
五、服务间通信
| 方式 | 特点 | 适用 |
|---|---|---|
| HTTP/REST + JSON | 通用、易调试、可读 | 外部 API、跨语言一般调用 |
| gRPC(HTTP/2 + Protobuf) | 强类型契约、二进制、多路复用、流式 | 内部高频调用首选 |
| 消息队列 | 异步、削峰、解耦、可重放 | 事件通知、耗时任务、最终一致(见 消息队列) |
| GraphQL | 客户端按需取字段 | 多端聚合场景,注意复杂度与缓存难度 |
同步调用的三条铁律:
- 必须设超时,且下游超时必须逐级递减(调用链总预算固定,避免层层放大);
- 重试要克制:只对幂等操作重试,且必须带退避与抖动,否则会引发重试风暴把下游打垮;
- 幂等是前提:重试、消息重投、用户重复提交都会导致重复执行,用幂等键(业务唯一号)在服务端去重。
超时预算示例(总预算 800ms):
网关 800ms → 订单服务 600ms → 库存服务 300ms → 数据库 100ms
每一层都比上一层小,避免上游已超时、下游还在白干六、韧性:超时、重试、熔断、限流、隔离、降级
6.1 熔断:快速失败胜过雪崩
// 熔断状态机核心(CLOSED → OPEN → HALF_OPEN → CLOSED)
class CircuitBreaker {
constructor({ failThreshold = 3, cooldownMs = 1000, halfOpenMax = 2 } = {}) {
this.failThreshold = failThreshold
this.cooldownMs = cooldownMs
this.halfOpenMax = halfOpenMax
this.state = 'CLOSED'
this.fails = 0
this.halfOpenPass = 0
this.openedAt = 0
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() - this.openedAt < this.cooldownMs) return 'rejected(fast-fail)' // 快速失败,不压下游
this.state = 'HALF_OPEN'; this.halfOpenPass = 0 // 冷却结束,试探
}
try {
const r = await fn()
if (this.state === 'HALF_OPEN' && ++this.halfOpenPass >= this.halfOpenMax) {
this.state = 'CLOSED'; this.fails = 0 // 试探成功,恢复
} else if (this.state === 'CLOSED') this.fails = 0
return r
} catch {
if (this.state === 'HALF_OPEN') { this.state = 'OPEN'; this.openedAt = Date.now(); return 'rejected(reopen)' }
if (++this.fails >= this.failThreshold) { this.state = 'OPEN'; this.openedAt = Date.now() }
return 'error'
}
}
}Node v22.22.1 实测(连续失败阈值 3,冷却 1s):
第 1 次调用: error state=CLOSED
第 2 次调用: error state=CLOSED
第 3 次调用: error state=OPEN ← 达到阈值,熔断打开
第 4 次调用: rejected(fast-fail) state=OPEN ← 不再打下游
第 5 次调用: rejected(fast-fail) state=OPEN
冷却结束 → HALF_OPEN 试探: ok state=HALF_OPEN
再成功一次: ok state=CLOSED ← 探测通过,恢复正常三个关键参数:失败阈值(多少次要熔断)、冷却时间(熔断保持多久)、半开试探数(几个成功才恢复)。熔断必须配降级(返回兜底数据/默认值/排队页),否则快速失败只是把错误更快地抛给用户。
6.2 限流:算法决定行为
| 算法 | 行为 | 边界表现 | 适用 |
|---|---|---|---|
| 固定窗口计数 | 每窗口重置计数 | 窗口交界处可放行 2 倍流量 | 简单统计,不推荐做严格限流 |
| 滑动窗口计数 | 统计最近 N 毫秒 | 平滑,无边界突刺 | 精确 QPS 限制(网关常见) |
| 令牌桶 | 匀速生成令牌,桶可积攒 | 允许突发(≤ 桶容量),平均速率受限 | 绝大多数业务限流(Guava/Redis 实现) |
| 漏桶 | 恒定速率出水 | 削峰填谷,输出恒定 | 需要严格匀速(如下游写入保护) |
虚拟时钟实测(阈值 100 次/秒,窗口 1000ms):
t=990ms 打 100 次 → 固定窗口通过 100,滑动窗口通过 100
t=1001ms 再打 100 次 → 固定窗口通过 100,滑动窗口通过 0
11ms 内总通过:固定窗口 200(2 倍阈值),滑动窗口 100(不超阈值)令牌桶实测(桶容量 100,速率 50/s):
瞬时 200 次 → 通过 100 次(= 桶容量,允许一次性突发)
1 秒后打 50 次 → 通过 50 次(1 秒补充 50 个令牌)
紧接着 50 次 → 通过 0 次(令牌耗尽,平均速率被限制在 50/s)选型直觉:要"允许合理突发"用令牌桶;要"严格匀速"用漏桶;要"精确 QPS 阈值"用滑动窗口。分布式限流用 Redis(Lua 脚本保证原子)或网关层统一做,单机限流只能保护自己那一份。
6.3 隔离与降级
- 舱壁隔离(Bulkhead):不同下游用独立线程池/连接池/信号量,一个慢下游不会耗尽全部资源;
- 降级:关闭非核心功能(推荐、评论、积分),保住主链路(浏览、下单、支付);
- 超时 + 重试 + 熔断 三者必须成套配置,只配一个反而更危险(无超时的重试 = 放大故障)。
七、可观测性:三支柱
| 支柱 | 回答什么 | 工具 |
|---|---|---|
| 日志 Logging | 发生了什么(离散事件) | ELK / Loki,结构化 JSON + TraceID |
| 指标 Metrics | 整体如何(可聚合数值) | Prometheus + Grafana |
| 链路 Tracing | 一次请求在哪慢 | Jaeger / Zipkin / SkyWalking(OpenTelemetry) |
TraceID 透传链路(每个服务都必须往下传,不能断):
网关生成 TraceID/SpanID → 放入 HTTP Header(traceparent)
→ 服务 A 记录并生成子 Span → 调用 B 时带上
→ 服务 B 记录 → 调 DB/MQ 时继续带
所有日志打印 TraceID → 用 TraceID 串起一次请求的全部日志与耗时关注黄金四指标:延迟(Latency)、流量(Traffic)、错误(Errors)、饱和度(Saturation)。告警要基于症状(用户可见的错误率/延迟),而不是基于原因(CPU 高了)——原因型告警会产生大量无效告警。
八、配置与契约
| 事项 | 做法 |
|---|---|
| 配置外置 | 配置中心(Nacos/Apollo)或 ConfigMap + Secret,禁止打进镜像 |
| 密钥管理 | Secret 独立存储、加密、可轮换,不进 Git(见 容器与部署) |
| 契约管理 | gRPC/OpenAPI 定义先行,生成代码;禁止口头约定字段 |
| 契约测试 | 消费者驱动契约(Pact/Spring Cloud Contract),在 CI 里验证兼容性 |
| 版本兼容 | 向后兼容优先(加字段不改语义),破坏性变更走新版本路径 + 灰度 |
| 灰度发布 | 按流量比例/用户维度灰度,配合指标对比自动回滚(见 容器与部署) |
九、检查清单
- 每个服务有自己的数据存储,不共享数据库表;
- 服务间调用全部有超时,且沿调用链递减;
- 重试只对幂等操作,带退避 + 抖动 + 重试预算;
- 关键下游有熔断 + 降级,熔断参数(阈值/冷却/试探数)有明确配置;
- 有接入层限流(网关/Redis 分布式),明确限流维度(用户/IP/接口/租户);
- 有分布式链路追踪,TraceID 贯穿日志、MQ 消息头、线程池切换处;
- 健康检查区分存活与就绪,就绪探针真实反映依赖状态;
- 配置外置、密钥集中管理且可轮换;
- 接口有契约(OpenAPI/Proto)与契约测试,破坏性变更有灰度路径;
- 每个服务有独立的 owner、告警与值班响应机制(拆了没人管 = 失控)。
十、常见坑速查
- 没划清边界就拆:得到分布式单体,改动成本比单体更高;
- 共享数据库:边界失效,改表结构要跨团队同步——数据所有权必须排他;
- 没设超时:线程池被慢调用耗尽,整条链路雪崩;
- 无限制重试 + 无退避:一次抖动引发重试风暴,把刚恢复的下游再次打死;
- 重试非幂等接口:重复扣款、重复下单——先做幂等键,再谈重试;
- 熔断没有降级:只是把错误更快抛给用户,体验更差;
- 限流用固定窗口:边界处放行 2 倍流量(实测见 §6.2),压测能过、线上被打穿;
- TraceID 在异步/线程池处断掉:日志无法串联,排障回到"人肉 grep";
- 把业务逻辑塞进网关:网关变成发布瓶颈,改一次业务要全网灰度;
- 日志无结构、无统一字段:无法聚合分析,只能靠关键字碰运气——统一 JSON 结构 + TraceID 是底线;
- 配置打进镜像 / 密钥进 Git:改配置要重新发版,密钥泄露无法快速轮换。
状态与参考
- 状态:已收录(2026-09-02,由后端领域规划清单「微服务|服务发现、网关、熔断限流、可观测性」新建)。
- 版本:熔断状态机、限流窗口与令牌桶均为 Node v22.22.1 最小实现实测(输出见文中);注册发现、网关、BFF、OpenTelemetry 为架构模式,与具体版本无关。生产建议直接使用成熟库(Resilience4j / Sentinel / Polly / opossum),勿自研核心韧性逻辑。
- 参考:microservices.io(模式总览)、Martin Fowler - Microservices、Google SRE Book - Handling Overload、OpenTelemetry 文档、Release It!(稳定性模式)。
- 阅读联动:限流的具体计数与过期策略见 缓存;异步解耦与消费语义见 消息队列;服务的打包与发布见 容器与部署;一致性取舍与容量估算见 系统设计。
下一步
- [ ] 用 opossum / Resilience4j 跑通熔断 + 降级组合,对比自研实现的参数差异
- [ ] 搭一套 Prometheus + Grafana + OpenTelemetry 的最小可观测链路并截图归档
- [ ] 写一篇"从单体拆出第一个微服务"的实操复盘(含数据迁移与双写方案)
写作规范请参阅领域概览。