k8s分布式锁
在Kubernetes(k8s)环境中,分布式锁用于协调多个Pod/节点的并发操作,确保同一时间只有一个参与者执行特定任务(如资源竞争、任务调度等)。k8s本身提供了多种原生机制可用于实现分布式锁,无需依赖额外中间件(如Redis、ZooKeeper)。
核心实现思路
k8s实现分布式锁的核心是利用其API对象的原子性操作和强一致性存储(底层etcd保证)。常用的实现方式有以下3种:
1. 基于 Lease 资源(推荐)
k8s的coordination.k8s.io/v1
API组中提供了Lease
资源,专为分布式协调设计,原生支持租约过期机制,是实现分布式锁的最佳选择。
原理:
- 多个Pod竞争创建同一个
Lease
对象,k8s保证只有一个Pod能成功创建(原子操作)。 - 持有锁的Pod需定期更新
Lease
的renewTime
(续约),防止租约过期。 - 若持有锁的Pod异常退出,租约到期后,其他Pod可重新竞争锁。
实现步骤:
- 定义锁标识(如
lease-name
),所有竞争节点使用同一标识。 - 节点尝试创建
Lease
对象,设置holderIdentity
为自身唯一标识(如Pod名)。 - 若创建成功,获得锁;若失败,检查当前
Lease
的持有者和过期时间,决定是否重试。 - 持有锁期间,定期更新
Lease
的renewTime
(如每5秒),维持锁所有权。 - 任务完成后,删除
Lease
释放锁。
2. 基于 ConfigMap/Secret
利用k8s的ConfigMap
或Secret
的创建/更新原子性实现锁,但需手动处理过期逻辑。
原理:
- 多个Pod竞争创建同一个
ConfigMap
,k8s仅允许一个创建成功(获得锁)。 - 锁的持有者在
ConfigMap
中记录自身标识和过期时间。 - 其他Pod定期检查
ConfigMap
,若发现持有者已过期,通过replace
操作抢占锁(需通过resourceVersion
实现乐观锁)。
缺点:
- 无原生过期机制,需手动设计超时逻辑,易产生“孤儿锁”(持有者挂掉未释放)。
- 性能略差于
Lease
(Lease
专为协调优化)。
3. 基于 etcd 原生API
k8s的底层存储etcd支持Compare-and-Swap(CAS) 原子操作,可直接用于实现分布式锁(需应用直接访问etcd,而非通过k8s API)。
原理:
- 所有节点竞争写入同一个etcd键(如
/k8s-lock/my-lock
),值为持有者标识。 - 通过etcd的
Put
操作加prevExist=false
条件,确保只有第一个写入者成功(获得锁)。 - 持有锁期间,通过
KeepAlive
机制维持租约;释放锁时删除键。
缺点:
- 需直接操作etcd,增加权限管理复杂度(k8s默认不暴露etcd给应用)。
- 与k8s生态耦合度低,不如
Lease
贴合k8s环境。
代码示例(基于 Lease 实现)
以下是使用k8s官方client-go
库实现Lease分布式锁的核心逻辑(Go语言):
import (
"context"
"fmt"
"time"
coordinationv1 "k8s.io/api/coordination/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// Lock 分布式锁结构体
type Lock struct {
client *kubernetes.Clientset
leaseName string // 锁的唯一标识(Lease名称)
namespace string // 命名空间
holderID string // 持有者标识(如Pod名)
lease *coordinationv1.Lease
renewChan chan struct{} // 续约停止信号
}
// NewLock 创建锁实例
func NewLock(client *kubernetes.Clientset, leaseName, namespace, holderID string) *Lock {
return &Lock{
client: client,
leaseName: leaseName,
namespace: namespace,
holderID: holderID,
renewChan: make(chan struct{}),
}
}
// Acquire 尝试获取锁
func (l *Lock) Acquire(ctx context.Context, ttl int64) (bool, error) {
// 定义Lease对象:设置持有者和过期时间
lease := &coordinationv1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: l.leaseName,
Namespace: l.namespace,
},
Spec: coordinationv1.LeaseSpec{
HolderIdentity: &l.holderID,
LeaseDurationSeconds: &ttl, // 租约有效期(秒)
RenewTime: &metav1.MicroTime{Time: time.Now()},
},
}
// 尝试创建Lease(原子操作,只有一个能成功)
createdLease, err := l.client.CoordinationV1().Leases(l.namespace).Create(ctx, lease, metav1.CreateOptions{})
if err == nil {
l.lease = createdLease
go l.renewLease(ctx, ttl) // 启动续约协程
return true, nil
}
// 创建失败,检查是否已存在且可抢占
existingLease, err := l.client.CoordinationV1().Leases(l.namespace).Get(ctx, l.leaseName, metav1.GetOptions{})
if err != nil {
return false, fmt.Errorf("获取Lease失败: %v", err)
}
// 判断当前持有者是否已过期
now := time.Now()
renewTime := existingLease.Spec.RenewTime.Time
if renewTime.Add(time.Duration(*existingLease.Spec.LeaseDurationSeconds) * time.Second).Before(now) {
// 过期,尝试抢占(通过resourceVersion实现乐观锁)
existingLease.Spec.HolderIdentity = &l.holderID
existingLease.Spec.RenewTime = &metav1.MicroTime{Time: now}
updatedLease, err := l.client.CoordinationV1().Leases(l.namespace).Update(ctx, existingLease, metav1.UpdateOptions{})
if err == nil {
l.lease = updatedLease
go l.renewLease(ctx, ttl)
return true, nil
}
}
return false, nil // 未获取到锁
}
// renewLease 定期续约,维持锁
func (l *Lock) renewLease(ctx context.Context, ttl int64) {
ticker := time.NewTicker(time.Duration(ttl/3) * time.Second) // 每1/3 TTL续约一次
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-l.renewChan:
return
case <-ticker.C:
// 更新RenewTime
l.lease.Spec.RenewTime = &metav1.MicroTime{Time: time.Now()}
_, err := l.client.CoordinationV1().Leases(l.namespace).Update(ctx, l.lease, metav1.UpdateOptions{})
if err != nil {
fmt.Printf("续约失败: %v\n", err)
return
}
}
}
}
// Release 释放锁
func (l *Lock) Release(ctx context.Context) error {
close(l.renewChan) // 停止续约
return l.client.CoordinationV1().Leases(l.namespace).Delete(ctx, l.leaseName, metav1.DeleteOptions{})
}
注意事项
- 锁粒度:需根据业务场景设计锁的粒度(如按资源ID划分不同锁),避免单锁成为性能瓶颈。
- 容错性:必须处理持有者异常退出的情况(依赖租约过期机制自动释放)。
- 重试策略:未获取到锁的节点需合理重试(如指数退避),避免“惊群效应”。
- 权限控制:确保Pod有操作
Lease
(或ConfigMap)的权限(通过RBAC配置)。
基于Lease
的实现是k8s环境中最推荐的分布式锁方案,它原生贴合k8s生态,且内置了租约机制,大幅降低了“孤儿锁”风险。