K8s 的 UUID 是如何生成的?

引言

不知道各位有沒有注意到,在 K8s 系統中,每個實例對象都有自己的 UID(其實就是UUID),用於唯一標識自己,比如 Pod、ConfigMap 等對象的 metadata 內都有一個叫做 uid 的字段。爲什麼要有這個字段呢?因爲僅靠 kind/namespace/name 是沒辦法確定實例還是那個實例的,比如實例被重建,kind/namespace/name 等信息並不會變化,這時候就需要使用 UID 來判斷「你還是不是原來的你了」。

UUID(Universally Unique IDentifier)又名 GUID(Globally Unique IDentifier),是一種長度爲 128 bits 的,無需中心化的註冊機制就能在時間和空間上具備唯一性的標識符。例如,Pod 的 uid 信息如下:

apiVersion: v1
kind: Pod
metadata:
  name: coredns-6955765f44-dc48n
  uid: 78ee0556-96f1-44af-b8b2-1f507dc43931

K8s UUID 的生成

一個問題油然而生啊!這個 uid(UUID) 在 K8s 中究竟是如何生成的,又是如何保證在集羣內是全局唯一的呢?

通過閱讀源碼,K8s 對象的 uid 字段是在實際創建前,由 api-server 自動注入的,關鍵代碼如下:

k8s.io/apiserver/pkg/registry/rest/meta.go

// FillObjectMetaSystemFields populates fields that are managed by the system on ObjectMeta.
func FillObjectMetaSystemFields(meta metav1.Object) {
	meta.SetCreationTimestamp(metav1.Now())
	meta.SetUID(uuid.NewUUID())
	meta.SetSelfLink("")
}

上述代碼在每個對象被創建前被調用,用以填充 ObjectMeta 字段,其中一項就是 uid。而真正生成 uid 的代碼實現位於 https://github.com/google/uuid 倉庫,它參照 RFC 4122 和 DCE 1.1 標準實現。

根據 RFC 4122 的說明 , UUID 生成算法最高可支持一千萬每秒每臺機器的生成速度(很大程度應該取決於將時間細分的粒度,例如 100 納秒粒度,每秒就可以產生 10 億個不同的時間刻度),足以用作交易流水 ID 使用。值得注意的是,由於 UUID 的長度固定,因此 UUID 的值可能會重複(大約在公元 3400 年,與算法實現有關)。一般,UUID 會以字符串的形式使用,即將 128 bit 轉爲對應的 16 進製表示的字符串,例如:“6aad787c-0e4a-46eb-8e1d-00a9a825bd04”。

根據算法實現和應用範圍的不同,UUID 可分爲 5 類:

version 1: 基於時間的 UUID(MD5 hash)

基於時間的 UUID 通過計算當前時間戳、隨機數和機器 MAC 地址得到。由於在算法中使用了 MAC 地址,該版本 UUID 可以保證在全球範圍的唯一性。但與此同時,使用 MAC 地址會帶來安全性問題,這就是這個版本 UUID 受到批評的地方。如果應用只是在局域網中使用,也可以使用退化的算法,以 IP 地址來代替 MAC 地址。

version 2: 嵌入 POSIX UID 的 DCE 安全的 UUID

DCE(Distributed Computing Environment)安全的 UUID 和基於時間的 UUID 算法相同,但會把時間戳的前 4 位置換爲 POSIX 的 UID 或 GID。

version 3: 基於名字的 UUID(MD5 hash)

基於名字的 UUID 通過計算名字和名字空間的 MD5 散列值得到。這個版本的 UUID 保證了:相同名字空間中不同名字生成的 UUID 的唯一性;不同名字空間中的 UUID 的唯一性;相同名字空間中相同名字的 UUID 重複生成是相同的。

version 4: 隨機或僞隨機 UUID

根據隨機數或者僞隨機數生成 UUID。這種 UUID 產生重複的概率是可以計算出來的,但隨機的東西就像是買彩票:你指望它發財是不可能的,但幸運通常會在不經意中到來。

version 5: 基於名字的 UUID(SHA-1 hash)

和版本 3 的 UUID 算法類似,只是散列值計算使用 SHA-1(Secure Hash Algorithm 1)算法。

令人驚訝的是 K8s 使用的居然是版本 4,通過隨機數生成 UUID,關鍵源碼如下:

// NewRandom returns a Random (Version 4) UUID.
//
// The strength of the UUIDs is based on the strength of the crypto/rand
// package.
//
// A note about uniqueness derived from the UUID Wikipedia entry:
//
//  Randomly generated UUIDs have 122 random bits.  One's annual risk of being
//  hit by a meteorite is estimated to be one chance in 17 billion, that
//  means the probability is about 0.00000000006 (6 × 10−11),
//  equivalent to the odds of creating a few tens of trillions of UUIDs in a
//  year and having one duplicate.
func NewRandom() (UUID, error) {
	var uuid UUID
	_, err := io.ReadFull(rander, uuid[:])
	if err != nil {
		return Nil, err
	}
	uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
	uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
	return uuid, nil
}

通過翻閱代碼歷史記錄,可以發現 K8s 最初其實使用的是版本 1 的 UUID 生成算法,不過考慮到以下幾個問題,才切換爲版本 4 的:

  • 使用主機 MAC 地址,有隱私泄露的風險
  • 依賴主機時鐘獲取時間信息不可靠
  • 基於 clock 處理重複讓代碼變得複雜

更多細節請查看代碼提交 PR : https://github.com/kubernetes/kubernetes/pull/75270

接着分析,隨機生成 UUID 的代碼中通過讀取 rander 獲得隨機數,而 rander 根據平臺的不同有所差異。Linux 平臺下會首先嚐試通過系統調用 getrandom(2) 獲取隨機數,如果系統調用不可用,則通過讀取 /dev/urandom 文件的內容獲取隨機數:

// Reader is a global, shared instance of a cryptographically
// secure random number generator.
//
// On Linux and FreeBSD, Reader uses getrandom(2) if available, /dev/urandom otherwise.
// On OpenBSD, Reader uses getentropy(2).
// On other Unix-like systems, Reader reads from /dev/urandom.
// On Windows systems, Reader uses the CryptGenRandom API.
// On Wasm, Reader uses the Web Crypto API.
var Reader io.Reader

getrandom(2) 系統調用在 3.17 版內核被引入,本質上其實與讀取 /dev/urandom 異曲同工,那麼如何理解 /dev/urandom 呢?這需要深入 Linux 內核的隨機數生成器。

在這裏插入圖片描述
如上圖所示,/dev/urandom/dev/random 是 Linux/Unix 系統中比較特殊的兩個文件,用戶可以通過讀取該文件獲得操作系統生成的隨機數。而 Linux/Unix 操作系統生成隨機數的原理是是利用當前系統的熵池來計算出固定一定數量的隨機比特,然後將這些比特作爲字節流返回。熵池就是當前系統的環境噪音,熵指的是一個系統的混亂程度,系統噪音可以通過很多參數來評估,如鍵盤輸入、鼠標移動、磁盤 I/O、文件使用、不同類型的進程數量等等。那爲什麼會有 /dev/urandom/dev/random 兩個隨機數文件呢?區別在於:/dev/random 是阻塞型的,當熵池中沒有足夠的輸入時,就無法持續產生隨機數,進而阻塞程序;而 /dev/urandom 是非阻塞的,當熵池中的輸入不夠時,可以通過一些額外的算法,持續產生隨機數,當然此時隨機的效果相比 /dev/random 要差些,但最重要的是能滿足持續生成隨機數的需求。

文末小結

到這裏很容易知道,K8s 本質上是利用操作系統的隨機數生成器(Unix/Linux 平臺就是 /dev/urandom )生成隨機數的方法產生 UUID 的,其實也是充分利用了機器的熵,綜合各類信息來產生隨機數,出現重複的概率應該還是非常低的!引用它的原話是:“一年內創建數萬億個 UUID,僅會有一個重複”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章