作者:shengdong
不知道大家有沒有意識到一個現實,就是大部分時候,我們已經不像以前一樣,通過指令行,或者可視視窗來使用一個系統了。現在我們上微網誌、或者網購,操作的其實不是眼前這台裝置,而是一個又一個叢集。

通常,這樣的叢集擁有成百上千個節點,每個節點是一台實體機或虛拟機。叢集一般遠離使用者,坐落在資料中心。為了讓這些節點互相協作,對外提供一緻且高效的服務,叢集需要作業系統。Kubernetes就是這樣的作業系統。
比較Kubernetes和單機作業系統,Kubernetes相當于核心,它負責叢集軟硬體資源管理,并對外提供統一的入口,使用者可以通過這個入口來使用叢集,和叢集溝通。
而運作在叢集之上的程式,與普通程式有很大的不同。這樣的程式,是“關在籠子裡”的程式。它們從被制作,到被部署,再到被使用,都不尋常。我們隻有深挖根源,才能了解其本質。
“關在籠子裡”的程式
代碼
我們使用go語言寫了一個簡單的web伺服器程式app.go,這個程式監聽在2580這個端口。通過http協定通路這個服務的根路徑,服務會傳回“This is a small app for kubernetes...”字元串。
package main
import (
"github.com/gorilla/mux"
"log"
"net/http"
)
func about(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("This is a small app for kubernetes...\n"))
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/", about)
log.Fatal(http.ListenAndServe("0.0.0.0:2580", r))
}
使用go build指令編譯這個程式,産生app可執行檔案。這是一個普通的可執行檔案,它在作業系統裡運作,會依賴系統裡的庫檔案。
# ldd app
linux-vdso.so.1 => (0x00007ffd1f7a3000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f554fd4a000)
libc.so.6 => /lib64/libc.so.6 (0x00007f554f97d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f554ff66000)
“籠子”
為了讓這個程式不依賴于作業系統自身的庫檔案,我們需要制作容器鏡像,即隔離的運作環境。Dockerfile是制作容器鏡像的“菜單”。我們的菜單就隻有兩個步驟,下載下傳一個centos的基礎鏡像,把app這個可執行檔案放到鏡像中/usr/local/bin目錄中去。
FROM centos
ADD app /usr/local/bin
位址
制作好的鏡像存再本地,我們需要把這個鏡像上傳到鏡像倉庫裡去。這裡的鏡像倉庫,相當于應用商店。我們使用阿裡雲的鏡像倉庫,上傳之後鏡像位址是:
registry.cn-hangzhou.aliyuncs.com/kube-easy/app:latest
鏡像位址可以拆分成四個部分:倉庫位址/命名空間/鏡像名稱:鏡像版本。顯然,鏡像上邊的鏡像,在阿裡雲杭州鏡像倉庫,使用的命名空間是kube-easy,鏡像名:版本是app:latest。至此,我們有了一個可以在Kubernetes叢集上運作的,“關在籠子裡”的小程式。
得其門而入
入口
Kubernetes作為作業系統,和普通的作業系統一樣,有API的概念。有了API,叢集就有了入口;有了API,我們使用叢集,才能得其門而入。Kubernetes的API被實作為運作在叢集節點上的元件API Server。這個元件是典型的web伺服器程式,通過對外暴露http(s)接口來提供服務。
這裡我們建立一個阿裡雲Kubernetes叢集。登入叢集管理頁面,我們可以看到API Server的公網入口。
API Server 内網連接配接端點: https://xx.xxx.xxx.xxx:6443
雙向數字證書驗證
阿裡雲Kubernetes叢集API Server元件,使用基于CA簽名的雙向數字證書認證來保證用戶端與api server之間的安全通信。這句話很繞口,對于初學者不太好了解,我們來深入解釋一下。
從概念上來講,數字證書是用來驗證網絡通信參與者的一個檔案。這和學校頒發給學生的畢業證書類似。在學校和學生之間,學校是可信第三方CA,而學生是通信參與者。如果社會普遍信任一個學校的聲譽的話,那麼這個學校頒發的畢業證書,也會得到社會認可。參與者證書和CA憑證可以類比畢業證和學校的辦學許可證。
這裡我們有兩類參與者,CA和普通參與者;與此對應,我們有兩種證書,CA憑證和參與者證書;另外我們還有兩種關系,證書簽發關系,以及信任關系。這兩種關系至關重要。
我們先看簽發關系。如下圖,我們有兩張CA憑證,三個參與者證書。其中最上邊的CA憑證,簽發了兩張證書,一張是中間的CA憑證,另一張是右邊的參與者證書;中間的CA憑證,簽發了下邊兩張參與者證書。這六張證書以簽發關系為聯系,形成了樹狀的證書簽發關系圖。
然而,證書以及簽發關系本身,并不能保證可信的通信可以在參與者之間進行。以上圖為例,假設最右邊的參與者是一個網站,最左邊的參與者是一個浏覽器,浏覽器相信網站的資料,不是因為網站有證書,也不是因為網站的證書是CA簽發的,而是因為浏覽器相信最上邊的CA,也就是信任關系。
了解了CA(證書),參與者(證書),簽發關系,以及信任關系之後,我們回過頭來看“基于CA簽名的雙向數字證書認證”。用戶端和API Server作為通信的普通參與者,各有一張證書。而這兩張證書,都是由CA簽發,我們簡單稱它們為叢集CA和用戶端CA。用戶端信任叢集CA,是以它信任擁有叢集CA簽發證書的API Server;反過來API Server需要信任用戶端CA,它才願意與用戶端通信。
阿裡雲Kubernetes叢集,叢集CA憑證,和用戶端CA憑證,實作上其實是一張證書,是以我們有這樣的關系圖。
KubeConfig檔案
登入叢集管理控制台,我們可以拿到KubeConfig檔案。這個檔案包括了用戶端證書,叢集CA憑證,以及其他。證書使用base64編碼,是以我們可以使用base64工具解碼證書,并使用openssl檢視證書文本。
- 首先,用戶端證書的簽發者CN是叢集id c0256a3b8e4b948bb9c21e66b0e1d9a72,而證書本身的CN是子賬号252771643302762862。
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 787224 (0xc0318)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=c0256a3b8e4b948bb9c21e66b0e1d9a72, OU=default, CN=c0256a3b8e4b948bb9c21e66b0e1d9a72
Validity
Not Before: Nov 29 06:03:00 2018 GMT
Not After : Nov 28 06:08:39 2021 GMT
Subject: O=system:users, OU=, CN=252771643302762862
- 其次,隻有在API Server信任用戶端CA憑證的情況下,上邊的用戶端證書才能通過API Server的驗證。kube-apiserver程序通過client-ca-file這個參數指定其信任的用戶端CA憑證,其指定的證書是/etc/kubernetes/pki/apiserver-ca.crt。這個檔案實際上包含了兩張用戶端CA憑證,其中一張和叢集管控有關系,這裡不做解釋,另外一張如下,它的CN與用戶端證書的簽發者CN一緻。
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 787224 (0xc0318)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=c0256a3b8e4b948bb9c21e66b0e1d9a72, OU=default, CN=c0256a3b8e4b948bb9c21e66b0e1d9a72
Validity
Not Before: Nov 29 06:03:00 2018 GMT
Not After : Nov 28 06:08:39 2021 GMT
Subject: O=system:users, OU=, CN=252771643302762862
- 再次,API Server使用的證書,由kube-apiserver的參數tls-cert-file決定,這個參數指向證書/etc/kubernetes/pki/apiserver.crt。這個證書的CN是kube-apiserver,簽發者是c0256a3b8e4b948bb9c21e66b0e1d9a72,即叢集CA憑證。
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 2184578451551960857 (0x1e512e86fcba3f19)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=c0256a3b8e4b948bb9c21e66b0e1d9a72, OU=default, CN=c0256a3b8e4b948bb9c21e66b0e1d9a72
Validity
Not Before: Nov 29 03:59:00 2018 GMT
Not After : Nov 29 04:14:23 2019 GMT
Subject: CN=kube-apiserver
- 最後,用戶端需要驗證上邊這張API Server的證書,因而KubeConfig檔案裡包含了其簽發者,即叢集CA憑證。對比叢集CA憑證和用戶端CA憑證,發現兩張證書完全一樣,這符合我們的預期。
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 786974 (0xc021e)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=CN, ST=ZheJiang, L=HangZhou, O=Alibaba, OU=ACS, CN=root
Validity
Not Before: Nov 29 03:59:00 2018 GMT
Not After : Nov 24 04:04:00 2038 GMT
Subject: O=c0256a3b8e4b948bb9c21e66b0e1d9a72, OU=default, CN=c0256a3b8e4b948bb9c21e66b0e1d9a72
通路
了解了原理之後,我們可以做一個簡單的測試。我們以證書作為參數,使用curl通路api server,并得到預期結果。
# curl --cert ./client.crt --cacert ./ca.crt --key ./client.key https://xx.xx.xx.xxx:6443/api/
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "192.168.0.222:6443"
}
]
}
擇優而居
兩種節點,一種任務
如開始所講,Kubernetes是管理叢集多個節點的作業系統。這些節點在叢集中的角色,卻不必完全一樣。Kubernetes叢集有兩種節點,master節點和worker節點。
這種角色的區分,實際上就是一種分工:master負責整個叢集的管理,其上運作的以叢集管理元件為主,這些元件包括實作叢集入口的api server;而worker節點主要負責承載普通任務。
在Kubernetes叢集中,任務被定義為pod這個概念。pod是叢集可承載任務的原子單元。pod被翻譯成容器組,其實是意譯,因為一個pod實際上封裝了多個容器化的應用。原則上來講,被封裝在一個pod裡邊的容器,應該是存在相當程度的耦合關系。
排程算法需要解決的問題,是替pod選擇一個舒适的“居所”,讓pod所定義的任務可以在這個節點上順利地完成。
為了實作“擇優而居”的目标,Kubernetes叢集排程算法采用了兩步走的政策:第一步,從所有節點中排除不滿足條件的節點,即預選;第二步,給剩餘的節點打分,最後得分高者勝出,即優選。
下邊,我們使用文章開始的時候制作的鏡像,建立一個pod,并通過日志來具體分析一下,這個pod怎麼樣被排程到某一個叢集節點。
Pod配置
首先,我們建立pod的配置檔案,配置檔案格式是json。這個配置檔案有三個地方比較關鍵,分别是鏡像位址,指令以及容器的端口。
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "app"
},
"spec": {
"containers": [
{
"name": "app",
"image": "registry.cn-hangzhou.aliyuncs.com/kube-easy/app:latest",
"command": [
"app"
],
"ports": [
{
"containerPort": 2580
}
]
}
]
}
}
日志級别
叢集排程算法被實作為運作在master節點上的系統元件,這一點和api server類似。其對應的程序名是kube-scheduler。kube-scheduler支援多個級别的日志輸出,但社群并沒有提供詳細的日志級别說明文檔。檢視排程算法對節點進行篩選、打分的過程,我們需要把日志級别提高到10,即加入參數--v=10。
kube-scheduler --address=127.0.0.1 --kubeconfig=/etc/kubernetes/scheduler.conf --leader-elect=true --v=10
建立Pod
使用curl,以證書和pod配置檔案等作為參數,通過POST請求通路api server的接口,我們可以在叢集裡建立對應的pod。
# curl -X POST -H 'Content-Type: application/json;charset=utf-8' --cert ./client.crt --cacert ./ca.crt --key ./client.key https://47.110.197.238:6443/api/v1/namespaces/default/pods [email protected]
預選
預選是Kubernetes排程的第一步,這一步要做的事情,是根據預先定義的規則,把不符合條件的節點過濾掉。不同版本的Kubernetes所實作的預選規則有很大的不同,但基本的趨勢,是預選規則會越來越豐富。
比較常見的兩個預選規則是PodFitsResourcesPred和PodFitsHostPortsPred。前一個規則用來判斷,一個節點上的剩餘資源,是不是能夠滿足pod的需求;而後一個規則,檢查一個節點上某一個端口是不是已經被其他pod所使用了。
下圖是排程算法在處理測試pod的時候,輸出的預選規則的日志。這段日志記錄了預選規則CheckVolumeBindingPred 的執行情況。某些類型的存儲卷(PV),隻能挂載到一個節點上,這個規則可以過濾掉不滿足pod對PV需求的節點。
從app的編排檔案裡可以看到,pod對存儲卷并沒有什麼需求,是以這個條件并沒有過濾掉節點。
優選
排程算法的第二個階段是優選階段。這個階段,kube-scheduler會根據節點可用資源及其他一些規則,給剩餘節點打分。
目前,CPU和記憶體是排程算法考量的兩種主要資源,但考量的方式并不是簡單的,剩餘CPU、記憶體資源越多,得分就越高。
日志記錄了兩種計算方式:LeastResourceAllocation和BalancedResourceAllocation。前一種方式計算pod排程到節點之後,節點剩餘CPU和記憶體占總CPU和記憶體的比例,比例越高得分就越高;第二種方式計算節點上CPU和記憶體使用比例之差的絕對值,絕對值越大,得分越少。
這兩種方式,一種傾向于選出資源使用率較低的節點,第二種希望選出兩種資源使用比例接近的節點。這兩種方式有一些沖突,最終依靠一定的權重來平衡這兩個因素。
除了資源之外,優選算法會考慮其他一些因素,比如pod與節點的親和性,或者如果一個服務有多個相同pod組成的情況下,多個pod在不同節點上的分散程度,這是保證高可用的一種政策。
得分
最後,排程算法會給所有的得分項乘以它們的權重,然後求和得到每個節點最終的得分。因為測試叢集使用的是預設排程算法,而預設排程算法把日志中出現的得分項所對應的權重,都設定成了1,是以如果按日志裡有記錄得分項來計算,最終三個節點的得分應該是29,28和29。
之是以會出現日志輸出的得分和我們自己計算的得分不符的情況,是因為日志并沒有輸出所有的得分項,猜測漏掉的政策應該是NodePreferAvoidPodsPriority,這個政策的權重是10000,每個節點得分10,是以才得出最終日志輸出的結果。
結束語
在這篇文章中,我們以一個簡單的容器化web程式為例,着重分析了用戶端怎麼樣通過Kubernetes叢集API Server認證,以及容器應用怎麼樣被分派到合适節點這兩件事情。
在分析過程中,我們棄用了一些便利的工具,比如kubectl,或者控制台。我們用了一些更接近底層的小實驗,比如拆解KubeConfig檔案,再比如分析排程器日志來分析認證和排程算法的運作原理。希望這些對大家進一步了解Kubernetes叢集有所幫助。