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需定期更新LeaserenewTime(续约),防止租约过期。
  • 若持有锁的Pod异常退出,租约到期后,其他Pod可重新竞争锁。

实现步骤

  1. 定义锁标识(如lease-name),所有竞争节点使用同一标识。
  2. 节点尝试创建Lease对象,设置holderIdentity为自身唯一标识(如Pod名)。
  3. 若创建成功,获得锁;若失败,检查当前Lease的持有者和过期时间,决定是否重试。
  4. 持有锁期间,定期更新LeaserenewTime(如每5秒),维持锁所有权。
  5. 任务完成后,删除Lease释放锁。

2. 基于 ConfigMap/Secret

利用k8s的ConfigMapSecret的创建/更新原子性实现锁,但需手动处理过期逻辑。

原理

  • 多个Pod竞争创建同一个ConfigMap,k8s仅允许一个创建成功(获得锁)。
  • 锁的持有者在ConfigMap中记录自身标识和过期时间。
  • 其他Pod定期检查ConfigMap,若发现持有者已过期,通过replace操作抢占锁(需通过resourceVersion实现乐观锁)。

缺点

  • 无原生过期机制,需手动设计超时逻辑,易产生“孤儿锁”(持有者挂掉未释放)。
  • 性能略差于LeaseLease专为协调优化)。

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{})
}

注意事项

  1. 锁粒度:需根据业务场景设计锁的粒度(如按资源ID划分不同锁),避免单锁成为性能瓶颈。
  2. 容错性:必须处理持有者异常退出的情况(依赖租约过期机制自动释放)。
  3. 重试策略:未获取到锁的节点需合理重试(如指数退避),避免“惊群效应”。
  4. 权限控制:确保Pod有操作Lease(或ConfigMap)的权限(通过RBAC配置)。

基于Lease的实现是k8s环境中最推荐的分布式锁方案,它原生贴合k8s生态,且内置了租约机制,大幅降低了“孤儿锁”风险。

posted @ 2025-08-25 15:03  程煕  阅读(29)  评论(0)    收藏  举报