This page looks best with JavaScript enabled

kubernetes 分散式資源鎖

 ·  ☕ 8 min read

當系統中一個元件有多個副本,這些副本同時要競爭成為領導人會透過許多不同的方法,例如zk(zookeeper), raft, redis…等等,這些方法都有一個共同的特色誰先搶到誰就當領導人。

那如果我的元件執行在 Kubernetes 叢集裡,有沒有什麼方式可以不依靠第三方系統讓我們也能做到分散式資源鎖呢?

答案是有的,可以透過Kubernetes的ResourceLock來達成,ResourceLock 大致上可以分為endpoints,configmaps,leases三種。

如果我們有手動搭建過HA(High Availability)環境的Kubernetes的話,會發現Controller Manger 與 Scheduler

todo

這是因為 Controller Manger以及 scheduler 這兩個元件採用 Lease 又或是稱為 ResourceLock 的機制來從所有的 instance 會去競爭一個資源鎖,只有競爭到資源鎖的 instance 有能力去繼續動作,沒有競爭到的則是繼續等待資源鎖的釋放,只有持有資源鎖的 instance 發生故障沒有 renew 資源鎖,其他 instacne 才會繼續競爭資源鎖並接手原來的工作。

底下會透過Kubernetes controller manger source code 來簡單的介紹 Kubernetes ResourceLock。

resourcelock

在Controller Manger source code 中有使用到resourcelock.Newresourcelock.New使用到工廠模式在new的過程中會產出我們指定的resourcelock

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Lock required for leader election
	rl, err := resourcelock.New(
               //在config會指定說要新增哪一種resource loack
               c.ComponentConfig.Generic.LeaderElection.ResourceLock,
		//在config會指定說resource lock要綁在哪一個namespace
		c.ComponentConfig.Generic.LeaderElection.ResourceNamespace,
               //在config會指定說resource lock的名稱
		c.ComponentConfig.Generic.LeaderElection.ResourceName,
               //帶入建立configmap,endpoint client object
		c.LeaderElectionClient.CoreV1(),
               //帶入建立leases client object
		c.LeaderElectionClient.CoordinationV1(),
                //Resource lock 內容
		resourcelock.ResourceLockConfig{
                //Resource lock 內容 ID 目前會以hostname_uuid
			Identity:      id,
                //EventRecorder 會紀錄 resource lock 事件
			EventRecorder: c.EventRecorder,
		})
	if err != nil {
		klog.Fatalf("error creating lock: %v", err)
	}

configmaplock,endpointlock,leaselock,MultiLock這幾個需要實作以下這個interface。
我們來簡單看一下這些是什麼功能

  1. Get
    從 ConfigMap、enpoint 或 lease 的 Annotation 拿到 LeaderElectionRecord 的資料。
  2. Create
    建立 LeaderElectionRecord 的資料 到 ConfigMap、enpoint 或 lease 的 Annotation。
  3. Update
    更新現有的 LeaderElectionRecord 資料到 ConfigMap、enpoint 或 lease 的 Annotation。
  4. RecordEvent
    當加入 LeaderElectionRecord 觸發紀錄的event 。
  5. Identity
    拿到 resource lock 的 id 。
  6. Describe
    拿到 resource name/resource meta-data name
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type Interface interface {
	// Get returns the LeaderElectionRecord
	Get(ctx context.Context) (*LeaderElectionRecord, []byte, error)

	// Create attempts to create a LeaderElectionRecord
	Create(ctx context.Context, ler LeaderElectionRecord) error

	// Update will update and existing LeaderElectionRecord
	Update(ctx context.Context, ler LeaderElectionRecord) error

	// RecordEvent is used to record events
	RecordEvent(string)

	// Identity will return the locks Identity
	Identity() string

	// Describe is used to convert details on current resource lock
	// into a string
	Describe() string
}

configmap lock/lease lock 物件

這邊舉兩個例子ConfigmapLock與LeaseLock,xxxMeta主要紀錄namespace name以及resource name。
Client 主要是用來操作 Kubernetes configmap / endpoint 的client api。
LockConfig 主要操作 Kubernetes Lease 的 Client api。
cm 操作 Kubernetes configmap 實際物件,不會透露給使用者。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type ConfigMapLock struct {
	// ConfigMapMeta should contain a Name and a Namespace of a
	// ConfigMapMeta object that the LeaderElector will attempt to lead.
	ConfigMapMeta metav1.ObjectMeta
	Client        corev1client.ConfigMapsGetter
	LockConfig    ResourceLockConfig
	cm            *v1.ConfigMap
}

...

type LeaseLock struct {
	// LeaseMeta should contain a Name and a Namespace of a
	// LeaseMeta object that the LeaderElector will attempt to lead.
	LeaseMeta  metav1.ObjectMeta
	Client     coordinationv1client.LeasesGetter
	LockConfig ResourceLockConfig
	lease      *coordinationv1.Lease
}
...

configmap lock/lease lock 工廠

kubernetes lock resource 透過工廠模式建立不同的物件
一開始一次性建立EndpointsLock,ConfigMapLock,LeaseLock再由lockType去選擇要回傳什麼給使用者。 例如 type 輸入 endpoints 最後回傳給使用者的就是 endpointslock ,type 輸入 configmaps 最後回傳使用者 configmaplock 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Manufacture will create a lock of a given type according to the input parameters
func New(lockType string, ns string, name string, coreClient corev1.CoreV1Interface, coordinationClient coordinationv1.CoordinationV1Interface, rlc ResourceLockConfig) (Interface, error) {
	endpointsLock := &EndpointsLock{
		EndpointsMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
		},
		Client:     coreClient,
		LockConfig: rlc,
	}
	configmapLock := &ConfigMapLock{
		ConfigMapMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
		},
		Client:     coreClient,
		LockConfig: rlc,
	}
	leaseLock := &LeaseLock{
		LeaseMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
		},
		Client:     coordinationClient,
		LockConfig: rlc,
	}
	switch lockType {
	case EndpointsResourceLock:
		return endpointsLock, nil
	case ConfigMapsResourceLock:
		return configmapLock, nil
	case LeasesResourceLock:
		return leaseLock, nil
	case EndpointsLeasesResourceLock:
		return &MultiLock{
			Primary:   endpointsLock,
			Secondary: leaseLock,
		}, nil
	case ConfigMapsLeasesResourceLock:
		return &MultiLock{
			Primary:   configmapLock,
			Secondary: leaseLock,
		}, nil
	default:
		return nil, fmt.Errorf("Invalid lock-type %s", lockType)
	}
}

resource lock 怎麼使用

在 kubernetes controller manger code 裡面使用了 leaderelection package 的 RunOrDie package 這個方法會在後面解析。

這邊簡單說明一下怎麼使用 RunOrDie ,非常簡單。

  1. lock
    在上面定義的resource lock 會在這裡被使用,例如上面定義了,例如上面定義了 configmap resource lock 會在這裡被注入(可能會被create)。
  2. leaseduration
    要佔領這個resource lock 多久
  3. renewdeadline
    多久要刷新一次resource lock
  4. retryperiod
    多久要嘗試建立resource lock
  5. callback
    • OnStartedLeading:
      當成功拿到resource lock 後要執行的動作
    • OnStoppedLeading:
      拿取resource lock 失敗後要執行的動作
  6. watchdog
    健康檢查相關的Object
  7. name

    todo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20

// Try and become the leader and start cloud controller manager loops
        //使用 leaderelection package 裡的 RunOrDie function 會按照你設定的 LeaseDuration, RenewDeadline , RetryPeriod 三個時間嘗試成為領導人。
	leaderelection.RunOrDie(context.TODO(), 
            leaderelection.LeaderElectionConfig{
		Lock:          rl,
		LeaseDuration: c.ComponentConfig.Generic.LeaderElection.LeaseDuration.Duration,
		RenewDeadline: c.ComponentConfig.Generic.LeaderElection.RenewDeadline.Duration,
		RetryPeriod:   c.ComponentConfig.Generic.LeaderElection.RetryPeriod.Duration,
		Callbacks: leaderelection.LeaderCallbacks{
                        // 在 controller manger 裡面就是執行一個run 的function 當作controller 的進入點
                        // 也可以理解為controller manger 程式的進入點
			OnStartedLeading: run,
			OnStoppedLeading: func() {
				klog.Fatalf("leaderelection lost")
			},
		},
		WatchDog: electionChecker,
		Name:     "cloud-controller-manager",
	})

RunOrDie背後的機制

對於使用者很方便的 RunOrDie,我們只要簡單的使用這個function 就可以定期的幫我們去競爭resource lock 勢必要去理解 RunOrDie 背後的機制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// RunOrDie starts a client with the provided config or panics if the config
// fails to validate. RunOrDie blocks until leader election loop is
// stopped by ctx or it has stopped holding the leader lease
func RunOrDie(ctx context.Context, lec LeaderElectionConfig) {
        //這裡很簡單的去new一個LeaderElector物件,主要是檢查使用者輸入的config有沒有問題。
	le, err := NewLeaderElector(lec)
	if err != nil {
		panic(err)
	}
        //todo還不是很清楚watch dog的作用
	if lec.WatchDog != nil {
		lec.WatchDog.SetLeaderElection(le)
	}
        //主要在這個run function 等等會來解密這個function 做了什麼事
	le.Run(ctx)
}

LeaderElector run 解密

剛剛有提到 RunOrDie 背後的機制主要是透過 LeaderElector 物件底下的 run 方法去執行的,底下就開始講解 LeaderElector run做什麼吧!

大致上主要是執行 建立以及續約 resourcelock ,當沒拿到 resourcelock 就對退出,拿到 resourcelock就執行續約。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Run starts the leader election loop. Run will not return
// before leader election loop is stopped by ctx or it has
// stopped holding the leader lease
func (le *LeaderElector) Run(ctx context.Context) {
        //kubernetes 內置處理 crash 的方法本篇不討論。
	defer runtime.HandleCrash()
        //當使用者觸發了 context cancel 時會呼叫 OnStoppedLeading 方法。
	defer func() {
		le.config.Callbacks.OnStoppedLeading()
	}()
        //在這裡會試圖取得會是創建 resource lock,沒有拿到資源鎖的話就會卡在這個function裡面喔!
	if !le.acquire(ctx) {
		return // ctx signalled done
	}
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
        //啟動一個 goroutine 去執行 controller manger 的進入點 run function
	go le.config.Callbacks.OnStartedLeading(ctx)
        //執行 resource lock 的續約動作。
	le.renew(ctx)
}

LeaderElector acquire

這邊會簡單的說明一下獲取 resource lock 的流程,這裡主要各個process會嘗試向kubernetes 要 resource lock 如果不成功就再嘗試看看(嘗試到process死掉或是拿到resource lock為止)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// acquire loops calling tryAcquireOrRenew and returns true immediately when tryAcquireOrRenew succeeds.
// Returns false if ctx signals done.
func (le *LeaderElector) acquire(ctx context.Context) bool {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	succeeded := false
	desc := le.config.Lock.Describe()
	klog.Infof("attempting to acquire leader lease %v...", desc)
        // kubernetes封裝了一個類似timmer的function比較,細節不去探討。
        // 我們只要知道他是一個定時觸發的function就好。
	wait.JitterUntil(func() {
                //嘗試獲取 resource lock 或是續約resource lock
		succeeded = le.tryAcquireOrRenew(ctx)
                //不知道實際用途,追code感覺是有人定義Callbacks.OnNewLeader的話會callback告知 leader換了。
		le.maybeReportTransition()
                // 獲取 resource lock 失敗,會等下次timmer到了再嘗試獲取租約。
		if !succeeded {
			klog.V(4).Infof("failed to acquire lease %v", desc)
			return
		}
                //獲取租約成功紀錄事件
		le.config.Lock.RecordEvent("became leader")
                //獲取租約成功紀錄事件
		le.metrics.leaderOn(le.config.Name)
                //獲取租約成功紀錄事件
		klog.Infof("successfully acquired lease %v", desc)
                //只要獲取成功後就不用再跑timmer,(這個Timmer用意就是讓沒有拿到resource lock的人可以定時去拿拿看)
		cancel()
	}, le.config.RetryPeriod, JitterFactor, true, ctx.Done())
	return succeeded
}

LeaderElector renew

LeaderElector renew 的行為可能我是說可能有一點點複雜,對其他大大們來說可能不會 xD。

當通過一關 LeaderElector acquire 的考驗 process 已經拿到 resource lock 接下來只要不斷的更新租約就好了 ~

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// renew loops calling tryAcquireOrRenew and returns immediately when tryAcquireOrRenew fails or ctx signals done.
func (le *LeaderElector) renew(ctx context.Context) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
        // kubernetes封裝了一個類似timmer的function比較,細節不去探討。
        // 可能有人會問跟wait.JitterUntil有什麼區別,這邊之後會另開戰場去討論(先挖洞給自己跳)
	wait.Until(func() {
		timeoutCtx, timeoutCancel := context.WithCancel(ctx)
		defer timeoutCancel()
                // PollImmediateUntil 會等 ConditionFunc 執行完畢
                // 若是 ConditionFunc 執行失敗(是false)就會等 RetryPeriod 時間在執行一次。
                // 如果 ConditionFunc 執行的結果是error或是true的話就不會重新嘗試 ConditionFunc 。
		err := wait.PollImmediateUntil(le.config.RetryPeriod, func() (bool, error) {
                // ConditionFunc
                // 嘗試建立resource lock 或是更新resource lock 租約
			return le.tryAcquireOrRenew(timeoutCtx), nil
		}, timeoutCtx.Done())
                //不知道實際用途,追code感覺是有人定義Callbacks.OnNewLeader的話會callback告知 leader換了。
		le.maybeReportTransition()
                // 拿到 resource lock 以configmaplock 為例 desc就會等於 configmaplock namespace/configmap name
		desc := le.config.Lock.Describe()
                // 這裡代表更新了租約成功就跳出去了,等待下一次timmer的呼叫。
		if err == nil {
			klog.V(5).Infof("successfully renewed lease %v", desc)
			return
		}
                //租約更新失敗紀錄
		le.config.Lock.RecordEvent("stopped leading")
                //租約更新失敗紀錄
		le.metrics.leaderOff(le.config.Name)
                //租約更新失敗紀錄
		klog.Infof("failed to renew lease %v: %v", desc, err)
                //再也不透過timmer定期更新租約
		cancel()
	}, le.config.RetryPeriod, ctx.Done())

	// if we hold the lease, give it up
	if le.config.ReleaseOnCancel {
		le.release()
	}
}

小結

本篇非常簡單的帶過source code 了解了在有 kubernetes 的環境之下如何透過 kubernetes 已經有的資源完成一個分散式鎖,如果有任何疑問或內文有錯的地方歡迎提出跟我起討論!


Meng Ze Li
WRITTEN BY
Meng Ze Li
Kubernetes / DevOps / Backend

What's on this Page