K8s 中的分布式锁
K8s中分部署锁一般用cm或者ep来完成,那数据库具体是如何写入到etcd中,boltdb写入在其中起到了哪些作用?
这是一个很核心的问题,涉及到 Kubernetes 中的资源对象(如 ConfigMap、Endpoints)如何经由 API Server 最终写入到底层存储(etcd),以及 etcd 中的 BoltDB(准确说是 bbolt)在这个链条中的作用。下面我们从整体架构逐层拆解,最后详细解释数据库写入和 BoltDB 的角色。
🌐 一、K8s 中的分布式锁为何使用 ConfigMap 或 Endpoints?
在 K8s 中,实现分布式锁的方式之一是利用某种共享资源作为锁的“媒介”:
- ConfigMap:常用于写入锁信息(如持有者 Pod 名称、时间戳),适合较小、频繁更新的数据。
- Endpoints:有时用于 Leader Election,因为它能快速地被 kube-apiserver 修改且具备 TTL 特性。
这类“锁”其实是将资源对象视为一个 CAS(Compare-And-Swap)机制的载体,通过 kube-apiserver 对资源对象的 乐观并发控制(基于 resourceVersion
)实现。
🧭 二、资源对象是如何写入到 etcd 的?
Kubernetes 中的每个状态变更,都是通过如下路径流转:
用户 / 控制器
↓ REST(如 kubectl patch)或 Go client
Kube-APIServer
↓(验证、准入、默认值填充、审计等)
Storage Layer(GenericStorage)
↓(调用 etcd3 client)
etcd
↓
bbolt 数据库(写入磁盘)
具体步骤如下:
-
用户或控制器发起请求(如 PATCH ConfigMap)
- 通常由 Leader Election Controller、Client-go 等发起请求。
-
kube-apiserver 收到请求
- 对请求做身份校验、准入控制(admission)、序列化等处理;
- 最终将该对象变更提交到 storage layer。
-
storage layer 使用 etcd client(v3)与 etcd 通信
- 存储实现基于
k8s.io/apiserver/pkg/storage/etcd3
; - 使用 etcd 的 gRPC 接口调用
Put()
或Txn()
等方法。
- 存储实现基于
-
etcd 接收数据并写入 BoltDB
- etcd 使用嵌入式 KV 存储库:
bbolt
(从 BoltDB 演进而来); - etcd 把这次写入以 事务的形式(带版本号)写入到 BoltDB;
- 所有变更以 WAL(预写式日志)+ Snapshots 实现 强一致性、可恢复性。
- etcd 使用嵌入式 KV 存储库:
📦 三、BoltDB(bbolt)在其中起到什么作用?
etcd 并不是一个远程数据库,而是一个 嵌入式 Key-Value 存储系统。它本身就是一个单独进程,以下是 bbolt 在其中的作用:
组件层 | 说明 |
---|---|
etcd server | 实现了 gRPC 服务、Raft 协议、API 等 |
bbolt | etcd 的实际存储引擎(KV 存储,支持事务、B+树) |
文件系统 | bbolt 将数据持久化为 .db 文件,并支持 WAL 日志 |
bbolt 的关键特性:
- 内嵌式数据库(单文件存储)
- 支持事务操作(ACID)
- 高性能读写(树结构 + mmap)
- 支持 snapshot、压缩等
在 etcd 中,所有通过 API 写入的数据(例如 ConfigMap、Endpoints 对象的 JSON 序列化体)最终都会:
存储为以 key 为路径(如
/registry/configmaps/kube-system/leader-lock
)、value 为 Protobuf 编码对象的 KV 对 → 写入 bbolt 的树结构中。
🔄 四、数据一致性与分布式锁保障机制
-
etcd 作为 CP 系统(CAP 理论),确保写入强一致;
-
分布式锁使用
resourceVersion
来做乐观并发控制:- Controller 每次尝试写入 ConfigMap 时,都会检查当前的
resourceVersion
; - 若有竞争,API Server 会返回
409 Conflict
,从而触发重试;
- Controller 每次尝试写入 ConfigMap 时,都会检查当前的
-
最终只有一个 controller 能成为锁的持有者。
🧪 示例(Leader Election 写入流程)
kubectl get cm kube-controller-manager -o yaml
可以看到 ConfigMap 中包含如下结构:
metadata:
name: kube-controller-manager
resourceVersion: "8647133"
data:
leader-election: |
{
"holderIdentity": "kube-controller-manager-0_d92ad3cc-c83b-4e23-bbf6",
"leaseDurationSeconds": 15,
"acquireTime": "2025-07-29T14:30:00Z",
...
}
这段 JSON 序列化体就是最终写入 etcd(通过 kube-apiserver → etcd client → bbolt)的内容。
✅ 总结
层级 | 说明 |
---|---|
Kubernetes 对象(如 ConfigMap) | 被用于 Leader 选举、分布式锁 |
kube-apiserver | 提供统一的 API 接入层,执行合法性校验与序列化 |
etcd client(apiserver 内部) | gRPC 调用 etcd3,进行写入与 CAS 操作 |
etcd server | 分布式强一致数据库,管理 Raft 日志与快照 |
bbolt(BoltDB) | etcd 的存储引擎,最终将数据写入磁盘中的树状结构 |
如果你需要我进一步提供:
- etcd 中的数据实际路径示例(如 key 的前缀)
- bbolt 数据结构的内部图示(Bucket、Page、node)
- 具体的 leader election 源码分析路径(如 client-go 中的 lock 机制)
我可以继续深入。是否需要?
resourceVersion
非常好,你提到了一个 Kubernetes 非常核心但容易被忽略的概念:resourceVersion
。
我们来逐步理解它,从定义、作用机制、应用场景(比如分布式锁)、实际示例几个维度帮助你完全搞懂。
🧩 一、什么是 resourceVersion
?
resourceVersion
是 Kubernetes 中每个对象的版本号,用于标识该对象自创建以来经历的变更序号。
它由 etcd 自动生成,每次资源发生任何变更(包括 label、annotation、data、status 等字段),其 resourceVersion
都会更新。
apiVersion: v1
kind: ConfigMap
metadata:
name: example-config
namespace: default
resourceVersion: "123456"
上面这个 "123456"
就是该对象在 etcd 中的“当前版本”。
可以简单理解为:
resourceVersion
是 etcd 中所有写操作的“全局递增序号”。
🚦 二、resourceVersion
的核心作用
用途 | 说明 |
---|---|
乐观并发控制 | 进行并发写入时判断是否有其他人改动过(防止覆盖) |
列表与监控优化(List/Watch) | watch 操作从某个版本之后开始,提高性能 |
高并发控制器协调机制 | 在 leader 选举、资源竞争等场景下作为 CAS 条件使用 |
🔐 三、与分布式锁的关系(ConfigMap Lock)
Kubernetes 中分布式锁(比如 client-go/tools/leaderelection
)通常这样使用 resourceVersion
:
🔁 模式:基于版本号的 CAS(Compare And Swap)写入流程
-
控制器 A 读取 ConfigMap,获取当前
resourceVersion = "123456"
-
它准备修改该 ConfigMap 并持有锁
-
写入请求带上旧的
resourceVersion = "123456"
-
kube-apiserver 在写入前校验:当前 resourceVersion 是否还等于 “123456”
- ✅ 如果等于:写入成功,A 获得锁
- ❌ 如果已变化:写入失败,返回 409 Conflict,A 必须重试(意味着有其他人也在竞争)
✅ 特点:
- 不用加锁(mutex),靠版本号判断是否有“其他人偷偷动过”
- 等价于 “只有当前版本等于 X,我才允许你修改” 的乐观并发写法
🧪 四、一个实际例子(模拟抢锁)
假设两个 Pod 并发抢同一个 ConfigMap 作为锁资源:
# 初始版本
resourceVersion: "123"
# Pod-A 获取锁
→ Get ConfigMap(resourceVersion = "123")
→ PATCH data.holder = Pod-A(条件:resourceVersion = "123")
→ 成功(新版本 = "124")
# Pod-B 尝试获取锁
→ Get ConfigMap(resourceVersion = "123")
→ PATCH data.holder = Pod-B(条件:resourceVersion = "123")
→ ❌ 被拒绝(因为当前版本是 124,不再是 123)
→ 返回 409 Conflict,需重新 Get → Retry
你可以看到,这种机制本质上就是 CAS(Compare-And-Swap)操作,避免了分布式环境下的并发写入冲突。
🛠 五、怎么看实际的 resourceVersion
?
可以通过 kubectl get
或 kubectl get -o yaml
看到:
kubectl get cm leader-lock -o yaml
输出中会看到:
metadata:
name: leader-lock
resourceVersion: "987654"
每次更新该资源,这个数字就会单调递增。
🔍 六、在代码中怎么使用它?
在 Client-go 中的 Patch
或 Update
操作中,封装的对象 metadata 中需要携带:
configMap.ObjectMeta.ResourceVersion = "987654" // 原始值
client.CoreV1().ConfigMaps("default").Update(ctx, configMap, metav1.UpdateOptions{})
API Server 会进行校验,只允许修改的是当前版本,防止脏写。
🧠 总结一句话:
resourceVersion
是 Kubernetes 中用于实现强一致性、并发控制和事件监听的核心机制,等价于数据库的版本号字段,常用于 Leader 选举、控制器抢占、Watch 优化等场景。
如果你希望我演示一个用 resourceVersion
实现的简化版“分布式锁”代码或者进一步画个流程图说明它和 etcd 的关系,我可以继续补充。需要吗?
CAS(Compare-And-Swap)
非常好,你现在已经走到了理解 Kubernetes 并发控制核心机制 的关键一环——CAS(Compare-And-Swap)。这个机制不仅存在于 Kubernetes、etcd、数据库,还广泛用于 操作系统、线程并发、分布式锁等底层场景。我们一步一步讲清楚:
🧠 一、什么是 CAS(Compare-And-Swap)?
📌 定义:
CAS(比较并交换) 是一种原子性操作,用于在并发环境下安全地更新共享数据。
它的基本思想是:
“只有当变量的当前值等于预期值时,才将其更新为新值,否则什么都不做。”
⛓ 操作过程(三个参数):
CAS(address, expected, new_value)
- address:要更新的内存地址(比如某个变量)
- expected:期望值(旧值)
- new_value:想要设置的新值
系统在底层会这样做:
- 比较 address 中的当前值是否等于 expected;
- 如果相等 → 把它原子性地更新为 new_value,返回成功;
- 如果不相等 → 什么也不做,返回失败。
🧪 二、简单示例(线程安全计数器)
假设我们有一个全局计数器 counter = 5
,多个线程并发执行 counter++
。
非线程安全写法(存在竞争):
// counter++
temp := counter
temp = temp + 1
counter = temp
但使用 CAS 就可以做到无锁并发安全:
for {
old := counter
new := old + 1
if atomic.CompareAndSwapInt64(&counter, old, new) {
break // 成功更新
}
// 否则继续重试
}
多个线程同时执行,如果有竞争,CAS 会失败,代码就会重试,直到成功。
⚙️ 三、Kubernetes 中的 CAS 使用例子
Kubernetes 的 API Server 其实并没有直接用 CPU 的 CAS 指令,而是用“版本号 + 比较”来模拟 CAS 操作。比如:
configMap.ResourceVersion = "123456"
client.CoreV1().ConfigMaps("kube-system").Update(context.TODO(), configMap, ...)
此时 server 会检查:
if current.ResourceVersion == "123456" {
apply update and set new resourceVersion
} else {
return 409 Conflict
}
✅ 这就是逻辑层面的 CAS
条件:资源版本未被其他人修改 → 成功
否则:失败 → 说明有其他竞争者 → 重试
🔐 四、CAS 与分布式锁的关系?
在无中心锁管理器的环境中(比如多个 Pod 抢一个 leader),最经典的锁实现方式就是 基于 CAS:
🌐 典型方案:
- 利用一个共享资源(如 ConfigMap、etcd Key)作为锁
- 每个竞争者都用自己的标识去更新该资源
- 更新时附带
resourceVersion
(就是旧值) - 如果成功 → 表示我成功获得锁
- 如果失败 → 说明锁被别人持有 → 重试
这也是为什么 Kubernetes 中的 leader election 使用 resourceVersion
的原因。
🧮 五、CAS 与数据库的对比
系统 | CAS 表现形式 | 举例 |
---|---|---|
CPU 层 | 指令级别原子操作 | cmpxchg |
Java/Go 并发库 | 原子类 | AtomicInteger.compareAndSet() |
数据库 | 乐观并发控制 | UPDATE ... WHERE version = X |
Kubernetes | resourceVersion + 409 Conflict | Update() 接口 |
etcd | Compare + Put (Txn) | atomic transaction |
🧭 六、CAS 的优缺点
优点 | 缺点 |
---|---|
高性能,无需锁(无阻塞) | 可能自旋重试太多,影响性能 |
原子性强,线程安全 | 不适合复杂对象结构修改 |
非阻塞并发编程利器 | ABA 问题(需要配合版本号) |
📌 七、一句话总结:
CAS(Compare-And-Swap) 是一种在并发编程和分布式系统中常用的乐观并发控制机制,只有在当前状态没被其他人改动时才允许写入,否则失败并重试,广泛用于锁、状态机、计数器、Kubernetes resourceVersion 等关键机制中。
是否需要我提供一份 etcd 中使用 CAS 的 transaction 示例(go etcd client v3),或者用图示说明“更新失败/成功的过程”?可以帮助你彻底理解这个机制。