使用 K8S Lease 分布式锁

255 阅读2分钟

说明

只是为了简单理解lease的使用方式,实际调用可以直接选择client-go中的leader-election

准备环境

  • k8s 集群 v1.14+
  • go 1.20+
  • 提前设置好 KUBECONFIG 环境变量

创建lease资源

cat > leasePractice.yaml <<EOF 
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  name: my-lease
  namespace: default
spec:
  holderIdentity: ""
  leaseDurationSeconds: 60  # 设置为 60 秒
  renewTime: null
  leaseTransitions: 0
EOF

kubectl apply -f  leasePractice.yaml

运行环境

用来练习就跑在shell窗口

export POD_NAME=test1
go run leasePractice.go

# 可以在另一个shell窗口在启动一个备用进程,观察是否会阻塞
export POD_NAME=test2
go run leasePractice.go

DemoCode

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/typed/coordination/v1"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/retry"
)

// 需要提前定义 lease 资源
// 运行前需要设置环境变量POD_NAME, 更新 lease 锁时会用到
// 使用声明KUBECONFIG变量的配置

func main() {
	// 加载 Kubernetes 配置
	config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
	if err != nil {
		panic(err.Error())
	}
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err.Error())
	}

	leaseClient := clientset.CoordinationV1().Leases("default")
	leaseName := "my-lease"
	podName := os.Getenv("POD_NAME") // 获取 Pod 名称

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 确保在退出时取消上下文

	if err := acquireLease(leaseClient, leaseName, podName, ctx, cancel); err != nil {
		fmt.Println("Failed to acquire lease:", err)
	}

	// 业务逻辑执行
	// ...
	i := 1
	for {
		if i > 20 {
			break
		}

		fmt.Println("模拟业务运行", i)
		time.Sleep(5 * time.Second)
		i++
	}
	// 释放锁
	releaseLock(leaseClient, leaseName, podName, cancel)
}

func acquireLease(leaseClient v1.LeaseInterface, leaseName string, podName string, ctx context.Context, cancel context.CancelFunc) error {
	for {
		fmt.Println("尝试获取锁")
		err := retry.OnError(retry.DefaultRetry, func(err error) bool {
			return true
		}, func() error {
			lease, err := leaseClient.Get(context.TODO(), leaseName, metav1.GetOptions{})
			if err != nil {
				return err
			}
			// 检查是否过期
			if lease.Spec.RenewTime != nil && lease.Spec.LeaseDurationSeconds != nil && time.Since(lease.Spec.RenewTime.Time) > time.Duration(*lease.Spec.LeaseDurationSeconds)*time.Second {
				fmt.Println("Lock has expired, attempting to acquire...")
				*lease.Spec.HolderIdentity = ""
				lease.Spec.RenewTime = &metav1.MicroTime{Time: time.Now()}
				if lease.Spec.LeaseTransitions == nil {
					lease.Spec.LeaseTransitions = new(int32)
				}
				*lease.Spec.LeaseTransitions = *lease.Spec.LeaseTransitions + int32(1)
				_, err = leaseClient.Update(context.TODO(), lease, metav1.UpdateOptions{})
				if err != nil {
					return err
				}
			}

			// 检查并获取锁
			if lease.Spec.HolderIdentity == nil || *lease.Spec.HolderIdentity == podName || *lease.Spec.HolderIdentity == "" {
				lease.Spec.HolderIdentity = new(string)
				*lease.Spec.HolderIdentity = podName
				lease.Spec.RenewTime = &metav1.MicroTime{Time: time.Now()}
				_, err = leaseClient.Update(context.TODO(), lease, metav1.UpdateOptions{})
				if err != nil {
					return err
				}
				fmt.Println("Lock acquired")
				go renewLease(leaseClient, leaseName, podName, ctx) // 启动心跳更新
				return nil
			}

			return fmt.Errorf("lock is held by %s", lease.Spec.HolderIdentity)
		})

		if err == nil {
			break // 成功获取锁
		}

		time.Sleep(5 * time.Second) // 等待后重试
	}
	return nil
}

// 定期刷新 Lease,防止过期被解锁定
func renewLease(leaseClient v1.LeaseInterface, leaseName, podName string, ctx context.Context) {
	ticker := time.NewTicker(30 * time.Second) // 每 30 秒进行更新
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			lease, err := leaseClient.Get(context.TODO(), leaseName, metav1.GetOptions{})
			if err != nil {
				fmt.Println("Error getting Lease:", err)
				return
			}

			// 更新 Lease
			if *lease.Spec.HolderIdentity == podName {
				lease.Spec.RenewTime = &metav1.MicroTime{Time: time.Now()}
				_, err = leaseClient.Update(context.TODO(), lease, metav1.UpdateOptions{})
				if err != nil {
					fmt.Println("Error renewing lease:", err)
				} else {
					fmt.Println("Lease renewed")
				}
			} else {
				fmt.Println("This instance does not hold the lock anymore")
				return
			}
		case <-ctx.Done():
			fmt.Println("Stopping lease renewal")
			return
		}
	}
}

func releaseLock(leaseClient v1.LeaseInterface, leaseName, podName string, cancel context.CancelFunc) {
	lease, err := leaseClient.Get(context.TODO(), leaseName, metav1.GetOptions{})
	if err != nil {
		fmt.Println("Error getting Lease:", err)
		return
	}

	if *lease.Spec.HolderIdentity == podName {
		lease.Spec.HolderIdentity = nil
		_, err = leaseClient.Update(context.TODO(), lease, metav1.UpdateOptions{})
		if err != nil {
			fmt.Println("Error releasing lock:", err)
		} else {
			fmt.Println("Lock released")
		}
	} else {
		fmt.Println("This instance does not hold the lock")
	}

	// 取消上下文以停止心跳更新
	cancel()
}