《后端架构设计·高性能与高可用》13.3 进入卷三:分布式与一致性

把卷二没有证明的跨节点假设整理成可交接的检查清单:六项假设逐条登记负责人、指标口径、验收方式和到期日,用 Go 校验器拦截缺字段、阻塞项无到期日、把 unknown 写成 measured 等自欺写法,并说明卷三将回答哪些问题、哪些证明仍待补齐。

交接不是总结

卷二的收口不是把前十二章再总结一遍,而是把“哪些结论只在单进程、单节点、内存适配器里成立”写成一张可执行的交接单。总结可以模糊,交接单不能:每一项必须有负责人、指标口径、验收方式和到期日,否则卷三无法判断该先验证什么。

这张清单只登记卷二没有证明的假设,不重复已经验证的结论。六项里五项 unknown、一项 target,measured 为 0;其中三项 blocking,必须在卷三开工前拿到验收结果或明确降级方案。

ID假设主题负责人指标口径状态阻塞
H-01跨节点写可以保持一致分布式事务与一致性distributed-platform双写故障下的对账差异单数unknown是
H-02复制延迟不影响故障切换复制与读写分离dba跨区复制 P99 延迟unknown是
H-03网络分区不会产生双主分区、租约与 fencingsre分区期间双主写入次数unknown是
H-04消息端到端不丢不重消息投递语义与幂等messaging端到端重复率与丢失率unknown否
H-05分布式 ID 与锁在并发下无冲突分布式 ID 与租约platformID 冲突数与租约误判次数unknown否
H-06对账与补偿能收敛最终一致对账、补偿与最终一致finance-platform超过 24h 未平账订单数target否

验收方式也逐条写死:H-01 注入双写失败后 24h 对账差异必须为 0,否则回滚单写;H-02 计划内切换 RPO 不超过 5s 且切换后 checksum 一致;H-03 注入分区后 fencing 生效、双主写入为 0,仲裁不可用时拒绝写;H-04 用 10 万条故障注入消息检查重复与丢失,包含 DLQ 与重放;H-05 并发 1 万次分配冲突为 0、租约过期误判为 0;H-06 要求 T+1 对账完成率 100%、超 24h 未平账为 0。

检查清单的数据结构与校验规则

Gaps() 是唯一判定入口:必填 id、assumption、topic、owner、metric;acceptance 不能为空;volume 必须是 vol3;measured 必须同时有 command、result、source;unknown/target 不能带 result,且必须有合法到期日;blocking 项即使状态是 target,也必须有到期日。

// file: internal/handoffv3/handoff.go
package handoffv3

import (
	"strings"
	"time"
)

// Kind 只允许三种证据状态;measured 必须有命令、结果和来源。
type Kind string

const (
	Measured Kind = "measured"
	Target   Kind = "target"
	Unknown  Kind = "unknown"
)

const (
	CodeMissing            = "MISSING_FIELD"
	CodeBadKind            = "BAD_KIND"
	CodeBadDue             = "BAD_DUE"
	CodeDuplicateID        = "DUPLICATE_ID"
	CodeMeasuredNoEvidence = "MEASURED_NO_EVIDENCE"
	CodeFakeMeasured       = "FAKE_MEASURED"
	CodeUnknownHasResult   = "UNKNOWN_HAS_RESULT"
	CodeBlockingNoDue      = "BLOCKING_NO_DUE"
	CodeNoAcceptance       = "NO_ACCEPTANCE"
	CodeWrongVolume        = "WRONG_VOLUME"
)

type Item struct {
	ID, Assumption, Topic, Owner, Metric, Acceptance string
	Kind                                             Kind
	Command, Result, Source                          string
	Blocking                                         bool
	Due                                              string
	Volume                                           string
}
type Handoff struct {
	From, To, Date string
	Items          []Item
}
type Gap struct {
	Code, Ref, Detail string
}
type Counts struct {
	Total, Measured, Target, Unknown, Blocking int
}

func validDate(s string) bool {
	_, err := time.Parse("2006-01-02", s)
	return err == nil
}

// fakeResult 是词法护栏,不是证明;它只拦截把“未测/unknown”写进结果字段。
func fakeResult(s string) bool {
	t := strings.ToLower(strings.TrimSpace(s))
	if t == "" {
		return false
	}
	for _, marker := range []string{"unknown", "tbd", "n/a", "not measured", "未测", "待测", "未知"} {
		if strings.Contains(t, marker) {
			return true
		}
	}
	return false
}
func (h Handoff) Gaps() []Gap {
	var out []Gap
	add := func(code, ref, detail string) { out = append(out, Gap{code, ref, detail}) }
	if _, err := time.Parse(time.RFC3339, h.Date); err != nil {
		add(CodeBadDue, "handoff", h.Date)
	}
	seen := map[string]bool{}
	for _, it := range h.Items {
		ref := it.ID
		if ref == "" {
			ref = "?"
		}
		if seen[it.ID] {
			add(CodeDuplicateID, ref, "declared twice")
		}
		seen[it.ID] = true
		for _, f := range []struct{ name, value string }{
			{"id", it.ID}, {"assumption", it.Assumption}, {"topic", it.Topic},
			{"owner", it.Owner}, {"metric", it.Metric},
		} {
			if strings.TrimSpace(f.value) == "" {
				add(CodeMissing, ref, f.name)
			}
		}
		if strings.TrimSpace(it.Acceptance) == "" {
			add(CodeNoAcceptance, ref, "acceptance required")
		}
		if it.Volume != "vol3" {
			add(CodeWrongVolume, ref, it.Volume)
		}
		switch it.Kind {
		case Measured:
			if it.Command == "" || it.Result == "" || it.Source == "" {
				add(CodeMeasuredNoEvidence, ref, "measured needs command, result and source")
			}
			if fakeResult(it.Result) {
				add(CodeFakeMeasured, ref, it.Result)
			}
		case Target, Unknown:
			if it.Result != "" {
				add(CodeUnknownHasResult, ref, "unknown/target must not carry a measured result")
			}
			if !validDate(it.Due) {
				add(CodeBadDue, ref, it.Due)
			}
		default:
			add(CodeBadKind, ref, string(it.Kind))
			continue
		}
		if it.Blocking && !validDate(it.Due) {
			add(CodeBlockingNoDue, ref, it.Due)
		}
	}
	return out
}
func (h Handoff) Ready() bool { return len(h.Gaps()) == 0 }
func (h Handoff) Counts() Counts {
	var c Counts
	for _, it := range h.Items {
		c.Total++
		switch it.Kind {
		case Measured:
			c.Measured++
		case Target:
			c.Target++
		case Unknown:
			c.Unknown++
		}
		if it.Blocking {
			c.Blocking++
		}
	}
	return c
}

// MeasuredShare 只统计真正有证据的条目;unknown 不是 measured。
func (h Handoff) MeasuredShare() int {
	c := h.Counts()
	if c.Total == 0 {
		return 0
	}
	return c.Measured * 100 / c.Total
}
func (h Handoff) BlockingUnknown() int {
	n := 0
	for _, it := range h.Items {
		if it.Blocking && it.Kind != Measured {
			n++
		}
	}
	return n
}

fakeResult 只做词法护栏:结果里出现 unknown、tbd、n/a、not measured、未测、待测、未知,就判定为假 measured。它不是证明,真正的证据仍然要人看命令、运行次数和报告路径。

六项登记数据

登记数据与校验逻辑分离,评审时只改数据文件。六项全部标为 vol3 的目标,H-01 到 H-03 是阻塞项,到期日从 2026-12-15 排到 12-28;这些日期是计划日期,不是已经完成的验证日期。

// file: internal/handoffv3/data.go
package handoffv3

// VolumeTwoToThree 登记卷二仍未证明的六个跨节点假设。
// 它们全部是 unknown/target;卷二只证明了单进程内的行为。
func VolumeTwoToThree() Handoff {
	return Handoff{From: "卷二 高性能与高可用", To: "卷三 分布式与一致性", Date: "2026-10-22T04:00:00+08:00", Items: []Item{
		{ID: "H-01", Assumption: "跨节点写可以保持一致", Topic: "分布式事务与一致性",
			Owner: "distributed-platform", Metric: "双写故障下的对账差异单数", Acceptance: "注入双写失败后 24h 对账差异=0;否则回滚到单写", Kind: Unknown, Blocking: true, Due: "2026-12-15", Volume: "vol3"},
		{ID: "H-02", Assumption: "复制延迟不影响故障切换", Topic: "复制与读写分离",
			Owner: "dba", Metric: "跨区复制 P99 延迟", Acceptance: "计划内切换 RPO<=5s,切换后 checksum 一致", Kind: Unknown, Blocking: true, Due: "2026-12-18", Volume: "vol3"},
		{ID: "H-03", Assumption: "网络分区不会产生双主", Topic: "分区、租约与 fencing",
			Owner: "sre", Metric: "分区期间双主写入次数", Acceptance: "注入分区后 fencing 生效,双主写入=0;仲裁不可用时拒绝写", Kind: Unknown, Blocking: true, Due: "2026-12-20", Volume: "vol3"},
		{ID: "H-04", Assumption: "消息端到端不丢不重", Topic: "消息投递语义与幂等",
			Owner: "messaging", Metric: "端到端重复率与丢失率", Acceptance: "10 万条故障注入消息,重复=0、丢失=0(含 DLQ 与重放)", Kind: Unknown, Due: "2026-12-22", Volume: "vol3"},
		{ID: "H-05", Assumption: "分布式 ID 与锁在并发下无冲突", Topic: "分布式 ID 与租约",
			Owner: "platform", Metric: "ID 冲突数与租约误判次数", Acceptance: "并发 1 万次分配冲突=0;租约过期误判=0", Kind: Unknown, Due: "2026-12-25", Volume: "vol3"},
		{ID: "H-06", Assumption: "对账与补偿能收敛最终一致", Topic: "对账、补偿与最终一致",
			Owner: "finance-platform", Metric: "超过 24h 未平账订单数", Acceptance: "T+1 对账完成率 100%,超 24h 未平账=0", Kind: Target, Due: "2026-12-28", Volume: "vol3"},
	}}
}

用测试固定规则

测试分三组:第一组验证六项登记没有 gap 且计数为 total=6、measured=0、target=1、unknown=5、blocking=3;第二组用 11 个变异用例逐一触发校验码;第三组证明一条证据齐全的 measured 会被接受,避免校验器只会拒绝、不会放行。

// file: internal/handoffv3/handoff_test.go
package handoffv3

import "testing"

func baseItem() Item {
	return Item{
		ID: "H-99", Assumption: "replication lag stays bounded", Topic: "replication",
		Owner: "dba", Metric: "replica lag p99",
		Acceptance: "lag <= 5s during planned failover",
		Kind:       Unknown, Blocking: true, Due: "2026-12-18", Volume: "vol3",
	}
}

func handoffWith(items ...Item) Handoff {
	return Handoff{From: "vol2", To: "vol3", Date: "2026-10-22T04:00:00+08:00", Items: items}
}

func hasCode(gaps []Gap, code string) bool {
	for _, g := range gaps {
		if g.Code == code {
			return true
		}
	}
	return false
}

func measured(it *Item, cmd, result string) {
	it.Kind, it.Command, it.Result, it.Source, it.Due, it.Blocking = Measured, cmd, result, "report", "", false
}

func TestVolumeTwoToThree(t *testing.T) {
	h := VolumeTwoToThree()
	if gaps := h.Gaps(); len(gaps) != 0 {
		t.Fatalf("handoff has %d gap(s): %v", len(gaps), gaps)
	}
	c := h.Counts()
	if c.Total != 6 || c.Measured != 0 || c.Unknown != 5 || c.Target != 1 || c.Blocking != 3 {
		t.Fatalf("counts=%+v", c)
	}
	if h.MeasuredShare() != 0 || h.BlockingUnknown() != 3 || !h.Ready() {
		t.Fatalf("share=%d blocking_unknown=%d ready=%v",
			h.MeasuredShare(), h.BlockingUnknown(), h.Ready())
	}
	t.Logf("total=%d measured=%d target=%d unknown=%d blocking=%d measured_share=%d%% blocking_unknown=%d",
		c.Total, c.Measured, c.Target, c.Unknown, c.Blocking, h.MeasuredShare(), h.BlockingUnknown())
}

func TestGapRules(t *testing.T) {
	tests := []struct {
		name  string
		mut   func(*Item)
		extra []Item
		want  string
	}{
		{"missing owner", func(it *Item) { it.Owner = "" }, nil, CodeMissing},
		{"missing metric", func(it *Item) { it.Metric = "" }, nil, CodeMissing},
		{"bad kind", func(it *Item) { it.Kind = "guess" }, nil, CodeBadKind},
		{"blocking without due", func(it *Item) { it.Due = "" }, nil, CodeBlockingNoDue},
		{"malformed due", func(it *Item) { it.Due = "2026/12/18" }, nil, CodeBadDue},
		{"duplicate id", func(*Item) {}, []Item{baseItem()}, CodeDuplicateID},
		{"measured without command", func(it *Item) { measured(it, "", "p99=12ms") }, nil, CodeMeasuredNoEvidence},
		{"measured result says untested", func(it *Item) { measured(it, "go test ./...", "未测") }, nil, CodeFakeMeasured},
		{"unknown carries result", func(it *Item) { it.Result = "p99=12ms" }, nil, CodeUnknownHasResult},
		{"missing acceptance", func(it *Item) { it.Acceptance = "" }, nil, CodeNoAcceptance},
		{"wrong target volume", func(it *Item) { it.Volume = "vol2" }, nil, CodeWrongVolume},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			it := baseItem()
			tt.mut(&it)
			gaps := handoffWith(append([]Item{it}, tt.extra...)...).Gaps()
			if !hasCode(gaps, tt.want) {
				t.Fatalf("gaps=%v want %s", gaps, tt.want)
			}
			t.Logf("case=%s gaps=%d first=%s/%s", tt.name, len(gaps), gaps[0].Code, gaps[0].Ref)
		})
	}
}

func TestMeasuredWithEvidenceAccepted(t *testing.T) {
	it := baseItem()
	measured(&it, "go test ./internal/replication/ -run TestLag -count=3", "p99=180ms (2026-11-30, 3 runs)")
	it.Source = ".local/reports/be-arch/replication-lag.txt"
	h := handoffWith(it)
	if gaps := h.Gaps(); len(gaps) != 0 {
		t.Fatalf("honest measured item rejected: %v", gaps)
	}
	if c := h.Counts(); c.Measured != 1 || h.MeasuredShare() != 100 {
		t.Fatalf("counts=%+v share=%d", c, h.MeasuredShare())
	}
	t.Logf("measured_accepted total=%d measured_share=%d%%", h.Counts().Total, h.MeasuredShare())
}

真实运行输出

命令:go test ./internal/handoffv3/ -count=1 -v。环境为 macOS darwin/arm64、Go 1.26.0;测试串行执行,并发为 1,运行 1 次。完整输出保存在 .local/reports/be-arch/be-arch-perf-13-3-into-volume-three-tests.txt。

=== RUN   TestVolumeTwoToThree
    handoff_test.go:44: total=6 measured=0 target=1 unknown=5 blocking=3 measured_share=0% blocking_unknown=3
--- PASS: TestVolumeTwoToThree (0.00s)
=== RUN   TestGapRules
    handoff_test.go:75: case=missing owner gaps=1 first=MISSING_FIELD/H-99
    handoff_test.go:75: case=bad kind gaps=1 first=BAD_KIND/H-99
    handoff_test.go:75: case=blocking without due gaps=2 first=BAD_DUE/H-99
    handoff_test.go:75: case=duplicate id gaps=1 first=DUPLICATE_ID/H-99
    handoff_test.go:75: case=measured without command gaps=1 first=MEASURED_NO_EVIDENCE/H-99
    handoff_test.go:75: case=measured result says untested gaps=1 first=FAKE_MEASURED/H-99
    handoff_test.go:75: case=unknown carries result gaps=1 first=UNKNOWN_HAS_RESULT/H-99
    handoff_test.go:75: case=wrong target volume gaps=1 first=WRONG_VOLUME/H-99
--- PASS: TestGapRules (0.00s)
=== RUN   TestMeasuredWithEvidenceAccepted
    handoff_test.go:91: measured_accepted total=1 measured_share=100%
--- PASS: TestMeasuredWithEvidenceAccepted (0.00s)
PASS
ok  	archlab/internal/handoffv3	0.463s

校验器拦住了什么

最容易发生的不是字段漏填,而是把“还没验证”写成 measured。MEASURED_NO_EVIDENCE 拦住缺命令、结果或来源;FAKE_MEASURED 拦住结果里写“未测”;UNKNOWN_HAS_RESULT 拦住 unknown 携带测量值;BLOCKING_NO_DUE 拦住阻塞项没有到期日。空到期日会同时触发 BAD_DUE 和 BLOCKING_NO_DUE,所以变异用例看到 2 个 gap,但第一个仍是 BAD_DUE。

另一个边界:到期日只是计划日期,不是验证结果;Due 合法不代表假设成立。Ready() 只说明清单格式完整,不说明系统已经具备跨节点一致性。校验器能拦住自欺,拦不住设计错误。

卷三回答什么,不回答什么

卷三会展开分布式事务与一致性、复制与读写分离、分区与 fencing、消息投递语义与幂等、分布式 ID 与租约、对账与补偿。它回答“跨节点怎么写、怎么读、怎么恢复”,但不替卷二回答单机性能问题;容量、压测、降级和故障注入仍是卷二的基础。

本节不引用卷三的具体章节链接,也不把计划中的验收写成已完成。六项假设全部保持 unknown/target,直到卷三给出命令、运行次数和报告路径;在那之前,measured share 就是 0。

小结

一份合格的交接清单不是“我们做了什么”,而是“我们还没证明什么、谁负责、怎么验收、什么时候到期”。六项假设、三个阻塞项、0% measured 是卷二交给卷三的起点;校验器保证格式和诚实性,真正的证明要等卷三的故障注入和跨节点实验。

阅读导航:上一节:13.2 订单系统必测的三个故障场景 · 下一节:回到目录 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查