Appearance
从零构建分布式 KV 存储(一):开篇与 CAP 定理
纸上得来终觉浅,绝知此事要躬行。本系列将带你从零实现一个生产级的分布式 KV 存储系统。
为什么写这个系列?
对于没有分布式系统经验的 Golang 开发者来说,分布式系统常常像一个"黑盒":
- 听说过高大上的 Raft 算法,但不知道怎么用代码实现
- 知道 CAP 定理的十二字真言,但遇到实际场景不会权衡
- 读过 etcd、TiKV 的源码,但面对空白的编辑器不知从何下手
这个系列就是为了解决这个问题。
我们将从一个最简单的单机 KV 存储开始,每篇文章增加一个分布式特性,最终构建一个完整的分布式系统。
系列路线图
| 阶段 | 文章 | 核心产出 |
|---|---|---|
| 基础 | 01 开篇与 CAP 定理 | 理解分布式系统的核心权衡 |
| 基础 | 02 单机 KV 存储 | 可运行的 HTTP KV 服务 |
| 分布式 | 03 gRPC 节点通信 | 多节点 RPC 调用 |
| 分布式 | 04 Raft 共识算法 | 强一致性保证 |
| 分布式 | 05 服务发现与注册 | 动态节点管理 |
| 进阶 | 06 一致性哈希分片 | 水平扩展能力 |
| 进阶 | 07 可观测性 | Prometheus 监控 |
| 收官 | 08 集群部署与测试 | Docker 本地集群 |
最终产出:一个支持多节点、强一致、可水平扩展、可观测的分布式 KV 存储系统。
什么是分布式系统?
在深入 CAP 定理之前,我们先明确一下定义。
分布式系统是由一组通过网络通信、协同工作的独立计算机节点组成的系统。对外表现为一个统一的整体。
分布式系统的挑战
与单机系统相比,分布式系统引入了全新的问题:
| 挑战 | 说明 | 类比 |
|---|---|---|
| 网络延迟 | 节点间通信有不可预测的延迟 | 寄信可能迟到 |
| 网络分区 | 节点间的网络可能断开 | 快递员罢工了 |
| 节点故障 | 任何节点随时可能宕机 | 员工突然请假 |
| 时钟不同步 | 不同节点的系统时间不一致 | 挂钟各走各的 |
| 部分失败 | 一个操作可能在部分节点成功,部分失败 | 只给半桌客人上了菜 |
这些问题在单机系统中不存在(或容易处理),但在分布式系统中是常态。CAP 定理正是对这些挑战的理论抽象。
CAP 定理:分布式系统的第一性原理
在动手写代码之前,我们必须先理解 CAP 定理——它解释了分布式系统的根本权衡。
历史背景
CAP 定理由加州大学伯克利分校的 Eric Brewer (埃里克·布鲁尔) 教授在 2000 年提出,2002 年被 MIT 的 Seth Gilbert (塞思·吉尔伯特) 和 Nancy Lynch (南希·林奇) 正式证明。它成为了分布式系统设计的基石。
定理内容
CAP 定理指出,一个分布式系统最多只能同时满足以下三个中的两个:
| 特性 | 含义 | 通俗解释 |
|---|---|---|
| 一致性(C) | 所有节点在同一时间看到相同的数据 | 你更新完数据,任何人都能读到最新值 |
| 可用性(A) | 每个请求都能收到非错误的响应 | 系统永远在线,不会拒绝服务 |
| 分区容错(P) | 系统在网络分区时仍能正常工作 | 节点间网络断开,系统不崩溃 |
深入理解三个特性
一致性(Consistency)
强一致性要求:写操作完成后,所有后续读操作都必须返回该写入的值。
弱一致性/最终一致性则允许短暂的不一致,最终所有节点数据会趋于一致。
可用性(Availability)
可用性要求:每个非故障节点必须在有限时间内响应请求。
注意:可用性不要求"返回最新数据",只要求"返回非错误响应"。
分区容错(Partition Tolerance)
网络分区是指节点之间的网络连接完全断开:
分区容错要求:即使发生网络分区,系统仍能继续运行。
核心洞察:P 是必选项!
P 是必选项!
在网络环境中,网络分区是必然发生的(交换机故障、网线松动、丢包等)。因此,分布式系统必须在 C 和 A 之间做出权衡:
CP 系统
- 选择:保证一致性和分区容错,牺牲可用性
- 行为:网络分区时,系统暂停服务,等待分区恢复
- 代表:ZooKeeper、etcd、HBase、MongoDB(默认配置)
AP 系统
- 选择:保证可用性和分区容错,牺牲一致性
- 行为:网络分区时,系统继续服务,但数据可能短暂不一致
- 代表:Cassandra、Amazon Dynamo、CouchDB、Riak
一个经典例子
假设有两个节点 A 和 B,存储同一个数据 x(初始值 1)。客户端向 A 写入 x=2,但 A 和 B 之间的网络断了:
| 系统类型 | 处理方式 | 结果 |
|---|---|---|
| CP | 拒绝写入或读取 B,返回错误 | 数据一致,但部分请求失败 |
| AP | 允许写入 A,继续服务 | 数据不一致,但系统可用 |
我们怎么选?
本系列实现的 KV 存储将是一个 CP 系统(类似 etcd):
- 优先保证一致性:强一致比最终一致更容易理解和推理
- 可用性:单节点故障时,只要多数派(超过半数节点)存活,系统可用
- 适合场景:配置管理、服务发现、分布式锁
用公式表达:CP = 一致性 + 分区容错 - 部分可用性
整体架构设计
核心模块说明
| 模块 | 职责 | 技术选型 |
|---|---|---|
| API 层 | 接收客户端请求,返回响应 | HTTP + gRPC |
| Raft 共识层 | 领导者选举、日志复制、保证一致性 | hashicorp/raft |
| 状态机 | 存储 KV 数据,应用日志条目 | 内存 Map + 持久化 |
| WAL 日志 | 预写日志,保证数据不丢失 | 文件存储(如 BoltDB) |
| 服务发现 | 节点动态注册与发现 | Consul |
| 监控 | 暴露指标,观察系统状态 | Prometheus |
请求处理流程
环境准备
开始编码前,请确保已安装:
bash
# Go 版本要求
go version # >= 1.21
# 验证安装
go env
# 创建项目目录
mkdir -p ~/projects/distributed-kv
cd ~/projects/distributed-kv
go mod init github.com/yourname/distributed-kv
# 后续各篇会用到的依赖(可以先安装)
go get google.golang.org/[email protected]
go get google.golang.org/protobuf@latest
go get github.com/hashicorp/[email protected]
go get github.com/hashicorp/[email protected]
go get github.com/hashicorp/consul/[email protected]
go get github.com/prometheus/[email protected]下篇预告
第二篇我们将正式开始编码,实现一个单机版本的 KV 存储:
- 基于
sync.RWMutex的内存存储 - HTTP API(
/put、/get、/delete) - 简单的文件持久化
- 单元测试
届时你将拥有一个可运行的服务,为后续添加分布式特性打好基础。
bash
# 第二篇结束时的效果
$ curl -X PUT localhost:8080/put -d '{"key":"name","value":"Alice"}'
{"status":"ok"}
$ curl localhost:8080/get?key=name
{"key":"name","value":"Alice"}思考题
在等待第二篇的同时,你可以先思考以下问题:
- 如果我们要实现一个 AP 系统,在发生网络分区时,应该如何处理写入冲突?
- 为什么 etcd 推荐使用奇数个节点(3、5、7)而不是偶数个?
- 一致性哈希和传统哈希取模(
hash(key) % N)相比,有什么优缺点?
答案会在后续文章或评论区讨论。
本系列所有代码均会在 GitHub 上开源(链接待补充)。
下一篇:从零构建分布式 KV 存储(二):单机 KV 与 HTTP API (待撰写)
