30.1 微服务的定义与前提

一句话

微服务不是“把单体拆小”,而是用网络边界换独立部署:它把编译期耦合换成运行期耦合,因此必须先有“发现、可观测、自动发布、幂等重试、独立数据”这五件基础设施——否则拆出来的一定是分布式单体(30.3 分布式单体反模式)。

定义:四个“独立”

维度 微服务 模块化单体
部署 独立进程 / 独立镜像,可单独发布、单独回滚 一个制品,整体发布
数据 独占数据库或 schema,只有自己能写 共享库,跨模块可有本地事务
通信 网络(RPC / 事件),必须处理超时、重试、幂等、部分失败 进程内函数调用,失败即异常
团队 一个小团队端到端拥有(含值班) 按技术分层,多人共有一个制品

四条里只有“部署独立”和“数据独立”是定义性的,另外两条是结果。用这个判据看,很多“微服务系统”其实只有第一条:进程分开了,库还是一个——那不是微服务,是分布式单体。

网络边界到底贵多少:实测

“用网络换独立”不是免费的。下面这段程序在同机测两种调用的延迟:进程内函数调用,与 loopback 上的 HTTP 调用(进程边界的最小代价模型)。

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"sort"
	"time"
)

func addOne(x int) int { return x + 1 }

func main() {
	const N = 20000

	// ① 进程内调用:纳秒级
	start := time.Now()
	s := 0
	for i := 0; i < N; i++ {
		s += addOne(i)
	}
	inProcNs := float64(time.Since(start).Nanoseconds()) / float64(N)

	// ② 本机 loopback 上的 HTTP 调用:只测进程边界,不含跨机网络
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "ok")
	}))
	defer srv.Close()

	client := &http.Client{} // 默认 Transport 会复用连接,相当于带连接池的 RPC
	lat := make([]time.Duration, 0, N)
	for i := 0; i < N; i++ {
		t0 := time.Now()
		resp, err := client.Get(srv.URL)
		if err != nil {
			panic(err)
		}
		resp.Body.Close()
		lat = append(lat, time.Since(t0))
	}
	sort.Slice(lat, func(i, j int) bool { return lat[i] < lat[j] })
	p50 := lat[N/2]
	p99 := lat[N*99/100]

	fmt.Printf("进程内调用   %8.2f ns/次(累加和 %d,防止循环被编译器优化掉)\n", inProcNs, s)
	fmt.Printf("HTTP 调用    p50=%8v  p99=%8v\n", p50, p99)
	fmt.Printf("p50 倍数     %.0f×\n", float64(p50.Nanoseconds())/inProcNs)
	fmt.Printf("p99 倍数     %.0f×\n", float64(p99.Nanoseconds())/inProcNs)
	fmt.Println("(本机 loopback,不含跨机网络与序列化;跨机还要再加一个网络往返)")
}

本机实测(macOS,Apple M1,Go 1.27):

进程内调用       1.90 ns/次(累加和 200010000,防止循环被编译器优化掉)
HTTP 调用    p50=53.708µs  p99=109.875µs
p50 倍数     28267×
p99 倍数     57829×
(本机 loopback,不含跨机网络与序列化;跨机还要再加一个网络往返)

读数:

对比项 量级 含义
进程内调用 纳秒 一次函数调用的成本可以忽略,所以单体里的“分层/抽象”几乎不花钱
本机 HTTP 几十微秒 只多了进程边界与协议栈,已经慢近 3 万倍(4 个数量级)
跨机 RPC 再加网络往返 同可用区通常是零点几毫秒的量级(取决于网络,本节未测)

🔑 这个差距决定了后面所有设计的形状:一次跨服务调用等于几百次函数调用。所以“多拆一个服务”不是多一个文件夹,是给每一次交互加一笔固定成本;拆分只有在“独立部署带来的收益(发布解耦、独立伸缩、故障隔离)”能覆盖这笔成本时才成立。

前提清单:五件基础设施,缺一件就别拆

(部署形态怎么选——容器、编排、Serverless——是 33.2 微服务部署 的主场,这里只列“有没有”。)

# 前提 没有它会怎样 主场
1 自动化发布:一条命令构建 + 部署 + 回滚,无需人工 服务数从 1 变 10,发布从“一天一次”变“一天十次人工”,团队被发布拖死 55.1 发布策略
2 可观测性:trace 贯穿、指标按服务打标签、日志带 trace_id 线上一次超时,要在 10 个服务的日志里手工拼因果,平均定位时间从分钟变小时 50.3 链路
3 服务发现 + 配置中心:实例上下线自动生效,配置不改镜像 改一个 IP 要重新发布,扩容要人工改配置;配置漂移无人发现(见 30.4 服务治理 (注册、发现、熔断、限流)) 48.2 服务发现
4 幂等与重试语义:调用方可安全重试,消费方可去重 网络超时后重试 → 重复下单、重复扣款;不重试 → 偶发失败变成业务失败 25.4 幂等设计与去重
5 数据所有权:一个服务一份库,禁止跨库 join 库一共享,任何表结构变更都要多方协调,独立部署立刻失效 30.7 跨服务查询与数据聚合

⚠️ 这五条是前置条件,不是“以后再补”的优化项。第 1、2 条没有时拆微服务,团队会同时失去“快速定位”和“快速修复”两个能力——这是最危险的状态:故障变多了,排查变慢了。

组织前提:一条比技术更硬的判据

微服务真正的门槛是团队能不能端到端负责一个服务:需求、开发、测试、发布、值班、容量,全在同一个小组内闭环。做不到这一点,拆出来的服务会变成“开发团队写、运维团队背”的交接物,发布流程反而更长。

组织形态 适合 不适合
一个 5 人小组端到端拥有 1–3 个服务 微服务 —
前端组 / 后端组 / DBA 组按技术分层 模块化单体 微服务(每次改接口要跨三个组)
一个团队维护 10 个服务 过度拆分(见 30.2 服务粒度) —

判断方法见 74.1 康威定律及其逆定律:架构边界最终会长成团队边界,所以先问“我能不能把团队切到服务粒度”,再问“代码怎么切”。

破除四个误区

误区 现实
微服务 = 服务很小 “小”不是判据。一个 2 万行、只做发票的服务完全可能是好微服务;一个 2000 行、和另外 5 个服务共享数据库的“小服务”是分布式单体
微服务 = 用了 Docker / K8s 容器解决的是打包与调度,不解决数据所有权、幂等、可观测性。上了 K8s 但共用一个库,等于给单体换了个部署方式
拆得越细越“微” 粒度是成本函数:每个服务固定成本 6 类资产(见 29.7 从模型到服务边界),收益随拆分递减
微服务能解决团队协作问题 它只会把协作问题显性化:原来一次代码评审能解决的事,变成一次跨服务发布协调

什么时候别上微服务

情况 替代方案
团队 < 10 人、单产品线 模块化单体(27.1 单体架构的适用性),用包边界强制上下文
领域边界还在剧烈变化 先做 29.1 领域、子域、限界上下文 的建模,边界稳定前拆分会反复返工
没有自动化发布 先补流水线,这是“一天发布多次”的前提
强一致事务为主(资金、库存扣减) 单体 + 本地事务;拆开就要付分布式事务的成本(24.9 事务模型选型)
一次性项目、交付即结束 单体;微服务的运维投资需要长期摊薄

自检

Q1 一个 8 人团队把系统拆成 12 个微服务,共用一个 MySQL 库,服务之间用 RPC 互调。用本节的定义判断:这是微服务吗?

▸ 答案:不是,是分布式单体。四条定义里“部署独立”成立,“数据独立”不成立——共用一个库意味着任何表结构变更都要 12 个服务协调,且一个服务的慢查询会拖垮全部。它已经付出了微服务的全部成本(网络调用、跨服务排查、12 套部署),却没有拿到任何收益(独立演进、故障隔离)。修复顺序是先拆数据所有权(30.3),不是先拆更多服务。

Q2 为什么“本机 HTTP 调用比进程内调用慢 3–4 个数量级”这件事,会直接决定服务粒度的上限?

▸ 答案:因为每次拆分都会把一部分原本零成本的交互变成有固定成本的交互。一个业务流程如果原本是 20 次函数调用,拆成 3 个服务后可能变成 2 次 RPC + 18 次函数调用;如果拆成 8 个服务,可能变成 7 次 RPC。按本节实测(本机 loopback 已经是 2.8 万倍,跨机再加网络),7 次 RPC 就足以让一个原本 1ms 的接口变成 5–10ms,并且把可用性从 99.9% 拉到 0.999^7 ≈ 99.3%。所以粒度的上限不是“代码还能切多细”,而是“这条链路上能容忍几次跨进程跳转”。

Q3 五件前提里,为什么“可观测性”排第二,而不是排在“服务发现”之后?

▸ 答案:因为拆分把失败模式变多了、定位路径变长了:一次请求经过 5 个服务,任何一跳超时都表现为“用户说慢”,而你在单个服务的日志里看不到完整因果。没有 trace,排障时间不是线性增长而是指数级(要在 N 个服务里做笛卡尔积式的排查)。服务发现缺失的后果是“改 IP 要重新发布”,痛苦但可绕过;可观测性缺失的后果是“故障无法定位”,不可绕过。

Q4 一个团队说“我们的服务都很小,每个只有 3000 行,所以粒度很合理”。用本节的判据指出这句话缺了什么。

▸ 答案:缺三个判据:① 数据所有权——3000 行的服务如果和别的服务共库,仍然耦合;② 端到端归属——谁值班、谁能独立发布、需求进来后要跨几个团队;③ 调用深度——12 个 3000 行的服务串成一条链,可用性与延迟都由链长决定,跟单个服务大小无关。代码行数只反映“服务内复杂度”,完全不反映“服务间耦合”,而后者才是微服务的主要成本。

关联