This page looks best with JavaScript enabled

Kubernetes kubelet configmap & secret 與 cacheBasedManager 激情四射

 ·  ☕ 8 min read

首先本文所有的 source code 基於 kubernetes 1.19 版本,所有 source code 為了版面的整潔會精簡掉部分 log 相關的程式碼,僅保留核心邏輯,如果有見解錯誤的地方,還麻煩觀看本文的大大們提出,感謝!

本章節將探討在 kubernetes 中每個 node 是如何得知 kubernetes 資源的變化,這裡的資源需要特別說明一下是指 configmap 與 secret ,我們都知道當 configmap 或是 secret 發生變化的時候只要重啟 pod (沒有設定 hot reload 的情況)就能得到最新的資料,這中間生了什麼事情 node 怎麼得知 configmap / secret 發生變化了?

我們先把焦點移到 Manager interface 吧!

interface

這個 interface 主要定義了幾個方法這些方法,這些方法都是針對 pod 的資源我們就來看看定義了什麼吧。

source code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Manager interface {
	// 通過 pod namespace 和 pod name 就能獲得相應的 kubernetes 物件
	GetObject(namespace, name string) (runtime.Object, error)

	// Register function 主要是我們給這個 function 一個 pod spec 實作的物件需要產生對應的 reflector 
	//以 pod 中用到 configmap 為例,RegisterPod 就需要產生 pod 內用到所得有的 configmap reflector  
	RegisterPod(pod *v1.Pod)

	// UnregisterPod function 定義了當給定一個 pod spec  實作的物件必須把 pod 物件所用到 reflector 都消滅掉
	// 當 pod 被刪除時, pod 中用到 configmap 為例,UnregisterPod 就需要 pod 內用到所得有的 configmap reflector  都移除
	UnregisterPod(pod *v1.Pod)
}

我們大概了解這個 interface 定義的方向後,就需要去了解哪個物件實作這個 interface 囉!

cacheBasedManager

實作 manger interface 的是 cacheBasedManager 這個物件,我們看這物件定義了什麼吧!
source code

1
2
3
4
5
6
7
8

type cacheBasedManager struct {
	objectStore          Store						//主要用來儲存 reflector 觀測到的物件狀態
	getReferencedObjects func(*v1.Pod) sets.String			//主要用來找到 pod 內所有關聯的欄位,例如 configmap 就有可能出現在 envform configMapKeyRef 、 envform configMapRef 或是 volumes configMap  

	lock           sync.Mutex						//可能有大量的 pod 同時塞入我們必須保證狀態的一致性
	registeredPods map[objectKey]*v1.Pod					//主要用來儲存哪個 pod 已經在觀測名單了
}

了解了 cacheBasedManager 的屬性之後就可以來了解怎麼新增出這個物件。

new function

new function 其實也很簡單,就是使用者傳什麼這裡接收什麼,沒有偷做事。

source code

1
2
3
4
5
6
7
func NewCacheBasedManager(objectStore Store, getReferencedObjects func(*v1.Pod) sets.String) Manager {
	return &cacheBasedManager{
		objectStore:          objectStore,
		getReferencedObjects: getReferencedObjects,
		registeredPods:       make(map[objectKey]*v1.Pod),
	}
}

快速地了解怎麼建立起 cacheBasedManager 這個物件之後,我們接著要來了解實作層面做了什麼吧!

impliment

GetObject

這一個實作其實也很簡單就是把請求委託給 objectStore 去拿到對應的物件, objectStore 的詳細實作我想保留在下一章節再介紹(會失焦QQ),簡單來說就是一個儲存空間可以從這個儲存空間拿到物件最新的狀態,這個狀態會是 runtime object 需要經過二次轉換成對應的物,例如: configmap secret 等等。
source code

1
2
3
func (c *cacheBasedManager) GetObject(namespace, name string) (runtime.Object, error) {
	return c.objectStore.Get(namespace, name)
}

RegisterPod

這個實作的 function 比較複雜一點,需要透過 getReferencedObjects function 拆解 pod spec 取出對應的資料,這個 getReferencedObjects 可以抽換成 get configmap ReferencedObjects 的或是 secret ReferencedObjects 的。

以 configmap ReferencedObjects 來說就可以會從 pod spec 拆解 envform configMapKeyRef 、 envform configMapRef 或是 volumes configMap 得到 pod spec 中 configmap 對應的名稱,透過這些名稱我們將建立相應的 reflector 。

最後將 pod 的 namespace 與 pod name 作為 key 儲存在 registeredPods ,表示 kubelet 已經監控這個物件了。

source code

 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
func (c *cacheBasedManager) RegisterPod(pod *v1.Pod) {
	//當 kubernetes assign 一個 pod 到 node 上的時候
	//kubelet 會取得 pod 的相關資訊,getReferencedObjects 會將 pod spec 庖丁解牛
	//檢查裡面有沒有我們要的欄位,目前追 code 只看到 configmap 與 secret 有用到,等等看範例會比較容易理解
	names := c.getReferencedObjects(pod)
	//防止競爭加鎖
	c.lock.Lock()
	defer c.lock.Unlock()
    
	//針對 pod 內每一個用到的物件,例如 configmap 、 secret 建立一個對應的 reflector 用以觀察物件的變化
	for name := range names {
		c.objectStore.AddReference(pod.Namespace, name)
	}
    
	//以 pod namespace 與 pod name 作為 key 儲存在 registeredPods 的 map 中 value  會儲存  pod  的資訊
	//用來之後判斷 pod 更新哪些關聯到 reference 要更新或是刪除。
	var prev *v1.Pod
	key := objectKey{namespace: pod.Namespace, name: pod.Name}
	prev = c.registeredPods[key]
	c.registeredPods[key] = pod
    
	// 如果發生某一個狀況,例如 pod 更新了
	// registeredPods map 中會包含舊的 pod spec 也就是說 prev 的會有資料
	
	
	//因為 pod 更新的時候有可能某些的資料已經不需要觀測了,例如 configmap 欄位沒用到了
	//因此在這裡會判斷是否存在舊 pod 資料若是有的話會透過線透過舊資料的 pod name 與 namespace 作為 objectKey
	//以這個 objectkey 透過 getReferencedObjects function 找到舊 pod  secret、configmap 用到的欄位
	
    
	//最後透過 objectStore.DeleteReference 更新關聯到的 reference ,為什麼要更新呢?
	//因為 pod 更新的時候有可能某些的資料已經不需要觀測了,例如 configmap  、 Secret 欄位沒用到了需要把沒用到 reference 刪掉。
	if prev != nil {
		for name := range c.getReferencedObjects(prev) {
			c.objectStore.DeleteReference(prev.Namespace, name)
		}
	}
}

要理解這裡的 code 我認為要搭配 test code 來互相配合會比較好理解,簡單來說就是當有 pod 來註冊,會將部分資訊提取出來並且生成 對應的 Reflector

底下為 test code 部分,重點在於 cacheBasedManager 的 getReferencedObjects 參數在做什麼,總結一句話的話就是過濾 pod spec 中的欄位,例如擷取 pod 中的 configmap 欄位或是 secret 欄位,我們直接來看 code 吧。

source code

 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
49
50
51
52
53
54
55
56
57
58
59
60
//這個 function 等等會用來作爲 cacheBasedManager getReferencedObjects()
//當 cacheBasedManager 呼叫 getReferencedObjects() 時會使用這一個 function 計算 pod 內用到 secret 的地方。
func getSecretNames(pod *v1.Pod) sets.String {
	//建立一個 set 
	result := sets.NewString()
	//下面會看到實作,這裡就簡單的解釋一下這個 function 會遞迴解析  pod spec 中所有用到 secret 的欄位
	//有用到的就全部透過 call back function 傳回來用 set 儲存。
	podutil.VisitPodSecretNames(pod, func(name string) bool {
		result.Insert(name)
		return true
	})
	//回傳 set 
	return result
}

//傳入 store 物件用來儲存 kubernetes 物件狀態並且使用 getSecretNames function 作為  getReferencedObjects
//建立一個實作 Manger 的 cacheBasedManager物件
func newCacheBasedSecretManager(store Store) Manager {
	return NewCacheBasedManager(store, getSecretNames)
}

//模擬 pod 內有的 secret 欄位
type secretsToAttach struct {
	imagePullSecretNames []string
	containerEnvSecrets  []envSecrets
}

func TestCacheInvalidation(t *testing.T) {
	...
	// 這裡的 store 就先把它當作 一個儲存物件狀態的地方就好 就好
	store := newSecretStore(fakeClient, fakeClock, noObjectTTL, time.Minute)
	//建立一個實作 Manger 的 cacheBasedManager 物件
	manager := newCacheBasedSecretManager(store)
    
	// 模擬一個 pod 有 secret 的地方,這裡模擬 pod spec 中友 image pull secret 以及 env secret  
	s1 := secretsToAttach{
		imagePullSecretNames: []string{"s1"},
		containerEnvSecrets: []envSecrets{
			{envVarNames: []string{"s1"}, envFromNames: []string{"s10"}},
			{envVarNames: []string{"s2"}},
		},
	}
	//分成兩階段來看 podWithSecrets("ns1", "name1", s1) 這裡就是建立一個 pod spec 
	// pod 名稱為 name1 , namespace 為 ns1 最後把  剛剛模擬 secret 的地方放入 pod spec 
	// 第二階段為將 pod spec 註冊到 cacheBasedManager 中,這裡就可以參考上面提到的 RegisterPod 時會針對每個欄位建立對應的 Reference 
	manager.RegisterPod(podWithSecrets("ns1", "name1", s1))
	// Fetch both secrets - this should trigger get operations.
	// 以下就不屬於本文要討論的範疇,還是簡單的過水一下
	// 這裡直接觸發這裡直接觸發 store get 表示 store 拿到這個物件的變化
	// 分別拿到 namespace ns1 的 s1 變化 s10 變化以及 s2的變化
	store.Get("ns1", "s1")
	store.Get("ns1", "s10")
	store.Get("ns1", "s2")
	//最後透過 kubernetes mock 出來的 client 看看是否觀察到三次的變化。
	actions := fakeClient.Actions()
	assert.Equal(t, 3, len(actions), "unexpected actions: %#v", actions)
	// 清除 mock client 的變化,也就是重新計署的意思    
	fakeClient.ClearActions()    
    ...
}

source code

 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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// VisitPodSecretNames invokes the visitor function with the name of every secret
// referenced by the pod spec. If visitor returns false, visiting is short-circuited.
// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited.
// Returns true if visiting completed, false if visiting was short-circuited.
func VisitPodSecretNames(pod *v1.Pod, visitor Visitor) bool {
	//透過 for range 遞迴 pod spec 中的 ImagePullSecrets 欄位
	for _, reference := range pod.Spec.ImagePullSecrets {
            //把找到的名稱傳入 visitor function 中,還記得上面有提過的 visitor function 嗎?
            //上面 visitor function 做的事情就是將 name 存入 set 回傳 true 
		if !visitor(reference.Name) {
			return false
		}
	}
	//下面會看到實作方式這裡就簡短的解釋一下,透過 for range 遞迴 pod spec 中的 continaer 的欄位中會出現 secret 的欄位
    //若是欄位有數值就傳入 visitor function 將 name 存入 set 。
	VisitContainers(&pod.Spec, AllContainers, func(c *v1.Container, containerType ContainerType) bool {
		return visitContainerSecretNames(c, visitor)
	})
    
	//恩...我覺得在這裏定了這個沒有什麼特別的意義...就是等等用來承載 pod spec 中 VolumeSource 欄位的數值
    //放到 for 迴圈裡面應該也行吧?xD
	var source *v1.VolumeSource

	// 透過for 迴圈遞迴 pod spec 中的 volumes欄位,這裡可以看到各式各樣的volume 例如 Ceph Cinder Flex 
	// 這些都有可能會用到 secret 我們需要一個一個檢視,若是有找到 secret 就要傳入 visitor function 儲存在 set 中。 
	for i := range pod.Spec.Volumes {
		source = &pod.Spec.Volumes[i].VolumeSource
        //由於 volume 種類眾多我這邊只挑幾個來說明
		switch {
		// 如果VolumeSource的欄位是 Azure file 的話就需要進一步判斷
		// 底下的 secret name 欄位,若是有這個欄位就把裡面的數值傳入 visitor function 
		// 透過 visitor function 儲存在 set 中。 
		case source.AzureFile != nil:
			if len(source.AzureFile.SecretName) > 0 && !visitor(source.AzureFile.SecretName) {
				return false
			}
		// 如果 VolumeSource 的欄位是 CephFS 的話就需要進一步判斷
		// 底下的 secret name 欄位,若是有這個欄位就把裡面的數值傳入 visitor function 
		// 透過 visitor function 儲存在 set 中。 
		case source.CephFS != nil:
			if source.CephFS.SecretRef != nil && !visitor(source.CephFS.SecretRef.Name) {
				return false
			}
		    ...
            
		//其他實作方式都差不多,有興趣的小夥伴可以回 source code code  base 看看
		}
	}
	return true
}

func VisitContainers(podSpec *v1.PodSpec, mask ContainerType, visitor ContainerVisitor) bool {
	if mask&InitContainers != 0 {
		for i := range podSpec.InitContainers {
			if !visitor(&podSpec.InitContainers[i], InitContainers) {
				return false
			}
		}
	}
	if mask&Containers != 0 {
		for i := range podSpec.Containers {
			if !visitor(&podSpec.Containers[i], Containers) {
				return false
			}
		}
	}
	if mask&EphemeralContainers != 0 {
		for i := range podSpec.EphemeralContainers {
			if !visitor((*v1.Container)(&podSpec.EphemeralContainers[i].EphemeralContainerCommon), EphemeralContainers) {
				return false
			}
		}
	}
	return true
}

UnregisterPod

顧名思意就是反註冊 pod ,那到底是反註冊什麼呢?在上面我們有提到 RegisterPod 就是遞迴 pod spec 的每個欄位( configmap / secret ),以及 pod spec 中的 namespace 與 pod name 作為 key 儲存在 registeredPods 的 map。

反過來說反註冊就是要遞迴 pod spec 的每個欄位( configmap / secret ),並且透過 pod spec 中的 namespace 與 pod name 作為 key 刪除 registeredPods map 中對應的資料,廢話就不多說了來 code 吧。

source code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (c *cacheBasedManager) UnregisterPod(pod *v1.Pod) {
	var prev *v1.Pod
	//以 pod spc 中的 namespace 與 pod name 封裝為 object key     
	key := objectKey{namespace: pod.Namespace, name: pod.Name}
	//防止競爭加鎖
	c.lock.Lock()
	defer c.lock.Unlock()
    //透過 pod spec 中的 namespace 與 pod name 作為 key 取得 registeredPods  map 中對應的資料
	prev = c.registeredPods[key]
    
    
	//透過  pod spec 中的 namespace 與 pod name 作為 key 刪除 registeredPods  map 中對應的資料
	delete(c.registeredPods, key)
    
	//如果有資料的話,要刪除對應的 `Reflector` ,這裡也是用到 getReferencedObjects 去解析 pod spec 的每個欄位
	//上面有提過 getReferencedObjects 如果不熟悉的小夥伴可以往上滑去找找
	if prev != nil {
		for name := range c.getReferencedObjects(prev) {
			c.objectStore.DeleteReference(prev.Namespace, name)
		}
	}
}

結論

本篇文章主要我們了解了 cacheBasedManager 透過 Register function 將一個 pod spec 物件拆解並且交由 objectStore 產生對應的 reflector 。以 pod 中用到 configmap 為例,RegisterPod 就需要產生 pod 內用到所得有的 configmap 並且交由 objectStore 產生對應的 reflector 。

cacheBasedManager 透過 UnregisterPod function 將一個 pod spec 把 pod 物件所用到 reflector 都消滅掉,以 configmap 為例,UnregisterPod 就需要 pod 內用到所得有的 configmap reflector 協同 objectStore 都移除。

cacheBasedManager 透過 GetObject function 通過 pod namespace 和 pod name 經由 objectStore 的協助我們就能從 objectStore 獲得相應的 kubernetes 物件。

上述有提到 objectStore 主要用來儲存 reflector 觀測到的物件狀態,下一篇文章會解析 objectStore 的實作,看 cacheBasedManager 把 pod 內的欄位解析完後,怎麼把需要的資料整理成 reflector 。

文章中若有出現錯誤的見解希望各位在觀看文章的大大們可以指出哪裡有問題,讓我學習改進,謝謝。


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

What's on this Page