概述:
Kubernetes項目目前依然延續着之前爆炸式的擴張。急需能夠理解Kubernetes原理並且貢獻代碼的軟件開發者。學習Kubernetes源碼並不容易。Kubernetes是使用相對年輕的Go語言編寫,並且擁有大量的源代碼。在這個系列的多篇文章里,我將為大家深入分析Kubernetes的關鍵源碼,以及介紹那些幫助我理解源碼的技術。我的目標是提供一系列的文章,讓對於Kubernetes還較為陌生的開發者能夠快速學習Kubernetes源碼。
在第一篇文章里,我會分析從運行一個簡單的kubectl命令到向API Server發送REST調用的源碼執行過程。在開始深入Kubernetes之前,我建議你先閱讀一下Julia Evans對Kubernetes架構的高級概述分析的文章。
Kubectl命令的基本運行
Kubernetes里的命令行接口叫做kubectl。它用來控制Kubernetes集群。閱讀這部分源碼實現是一個好的開始。我們要追蹤的命令是kubectl create -f
——它會從文件創建K8s資源。我們要創建的資源是使用了Nginx基礎鏡像的單副本Pod。下面是它的yaml描述:
apiVersion: v1 kind: ReplicationController metadata: name: nginx spec: replicas: 1 selector: app: nginx template: metadata: name: nginx labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80
在一個Kubernetes 開發環境中我們可以用下面的方式調用kubectl:
現在我們知道該如何執行kubectl命令,下面來看看在Kubernetes源碼的哪里能找到它的實現吧。
在源碼中尋找kubectl的實現
實現kubectl命令的源碼可以在 https://github.com/kubernetes/kubernetes/tree/master/pkg/kubectl/cmd目錄找到。在這個目錄里,名為kubectl對應命令的go文件就是實現的地方。例如,kubectl create命令的起點在create.go。下圖展示了這個目錄和示例go文件的多種多樣實現:
Kubernetes ❤️ Cobra命令框架
Kubernetes命令使用Cobra命令框架實現。Cobra提供了很多構建命令行接口的特性。基本的Cobra功能說明可以在 https://blog.gopheracademy.com/advent-2014/introducing-cobra/ 找到。如圖所示,很容易就可以定位哪個文件實現了哪個命令行選項。而且Cobra結構使得命令的使用說明、命令描述與運行的代碼相鄰。圖中所示的代碼可以在 https://github.com/kubernetes/kubernetes/blob/fd9a91e0b57face905c4225b8a6633b2ea9c832d/pkg/kubectl/cmd/create.go#L62-#76 找到。這種結構它的好處在於你可以閱讀並找到所有Kubernetes kubectl命令的描述,並且快速跳轉到這些命令的代碼實現。圖中62~76行的字符串Use、Short、Long和Example都包含了描述命令的信息,和Run指向一個函數實際執行這條命令。
在74行調用的RunCreate函數是kubectl create命令的主要實現。這個函數的實現可以在 https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/create.go 文件找到。下圖列出了RunCreate函數。在132行,我添加了一句fmt.Println來確保這段代碼如我所料被調用了。在后面的編譯運行Kubernetes的部分我會展示當為kubectl源碼添加了一些用於調試的單獨語句等時,怎樣加速Kubernetes代碼的重新編譯過程。
Builders 和 Visitors
下面的133~140行是resource.NewBuilder的代碼。一些Go和Kubernetes的新手可能覺得特別害怕。這段代碼值得深入解釋一下。從高處看,這段代碼所做的事情是將命令行接收到的參數轉化為一個資源的列。它也負責創建一個可以用來迭代訪問所有資源的Visitor結構。這個命令比較復雜,因為它使用了Builder模式的變種,使用獨立的函數做各自的數據初始化工作。函數Schema、ContinueOnError、NamespaceParam、DefaultNamespace、FilenameParam、SelectorParam和Flatten都引入了一個指向Builder結構的指針,執行一些對它的修改,並且將這個結構體返回給調用鏈中的下一個方法來執行這些修改。所有的這些方法可以在這里找到 https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/resource/builder.go,但我在下面列出了一些你可以理解它如何運行的代碼:
func (b *Builder) Schema(schema validation.Schema) *Builder { b.schema = schema return b } func (b *Builder) ContinueOnError() *Builder { b.continueOnError = true return b } func (b *Builder) Flatten() *Builder { b.flatten = true return b }
一旦所有的初始化都完成,resource.NewBuilder函數會調用Do函數。這個Do函數很關鍵,它會返回一個Result對象,並且將執行對資源的創建。Do函數還會創建一個Visitor對象,可以用來遍歷所有關聯到resource.NewBuilder執行過程的資源。Do函數的實現展示如下:
就像816行所展示的,創建了一個新的DecoratedVisitor,並作為Builder Do函數返回的Result的一部分。這個DecoratedVisitor有一個Visit函數將會調用傳給它的Visitor函數。它的實現在 https://github.com/kubernetes/kubernetes/blob/6b52d8f1383d3a4a769b403a04f812c99ed98815/pkg/kubectl/resource/visitor.go#L306,如下:
這個Result對象由Do函數返回,擁有用來調用DecoratedVisitor Visit的函數Visit。這為我們找到了從create.go的RunCreate函數到實際最終調用的匿名函數,以及包含了API Server進行調用的createAndRefresh函數。這個在create.go的150行實現的Result Visit函數展示如下:
現在我們明白了Visit函數和DecoratedVisitor類如何把這一切連接起來。可以看到150行的inline visitor函數在165行有一個createAndRefresh函數:
這個createAndRefresh函數調用了NewHelper函數,在 https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/resource/helper.go,並且返回了一個新的Helper對象:
這里的代碼返回了一個新的Helper對象,十分顯而易見
func NewHelper(client RESTClient, mapping *meta.RESTMapping) *Helper { return &Helper{ Resource: mapping.Resource, RESTClient: client, Versioner: mapping.MetadataAccessor, NamespaceScoped: mapping.Scope.Name() == meta.RESTScopeNameNamespace, } }
在217行createAndRefresh里Helper的創建和調用它的Create函數,我們最終可以看到Create函數調用了一個createResource函數。在119行的Helper Create函數里,如下所示是這個Helper createResource函數,以及實際向API Server發送的用來創建yaml文件描述的資源的REST調用。
編譯和運行Kubernetes
現在我們回顧了代碼,是時候了解如何編譯和運行這些代碼了。在上面的許多代碼示例中你都可以發現fmt.Println()調用。所有這些我添加的用來調試的語句,你也可以將它們加入源代碼。為了編譯這段代碼,我們將使用一個特殊的選項,以告知Kubernetes構建過程只編譯kubectl這部分代碼。這樣可以極大地加快Kubernetes的編譯速度。為做這個優化的make命令為:
make WAHT='cmd/kubectl'
並且指出了如何從命令行運行這個指令
一旦我們重新編譯了包含前面添加的print語句的這部分kubectl代碼,就可以用下面的命令啟動我們的Kubernetes開發環境:
PATH=$PATH KUBERNETES_PROVIDER=local hack/local-up-cluster.sh
下面的圖片說明了在命令行運行這條命令:
在另一個終端窗口里我們來繼續執行kubectl命令,然后觀察它的fmt.Printlns的輸出。我們使用下面的命令:
cluster/kubectl.sh create -f ~/nginx_kube_example/nginx_pod.yaml
下圖展示了我們的調試輸出應該有的樣子:
代碼學習工具
我知道你可能會想:Brad,你雖然在Kube和Go都是新手,但你可以快速搞定這一切。你一定是個天才!然而,我有很多的Twitter粉絲,都會積極地拿出證據來駁斥這句話。借助於別人的幫助,我發現了幾個可以真正有助於提升你閱讀Kubernetes源碼能力的工具和技術。在這部分里,我會介紹我最喜歡的技術:Chrome Sourcegraph Plugin,正確地格式化打印語句,使用go panic來獲得所需要的stack trace,以及Github Blame來進行時空旅行。
Chrome Sourcegraph 插件
這是Morgan Bauer向我介紹了閱讀Kubernetes 源碼最酷炫的工具之一。Chrome Sourcegraph plugin提供了多種高級IDE特性,讓在瀏覽Github倉庫時理解Kubernetes Go代碼變得非常容易。這里是它的使用例子。當我首先開始閱讀Kubernetes 源碼時,我們發現下面的代碼片段非常難以分段和理解。它有數不清的函數,快要淹沒我了。
當在裝有Sourcegraph擴展插件的Chrome瀏覽器里看向這段代碼時,你可以把鼠標移過每個函數,很快就得到了這個函數的描述,它接受了什么參數,返回了什么結果。這幫助你節省了無比巨大的時間,你可以避免在代碼里抓取對應的函數定義,來了解它的功能。下面的圖是一個示例:
Chrome Sourcegraph擴展還有一個高級視圖,提供深入被調用函數代碼的功能。這是非常有用的機制:
唯一的問題是有時候Chrome Sourcegraph插件會卡住,並且不能彈出代碼細節。我的經驗是只要輕點頁面刷新就可以修復。
打印語句從不過時
我在這篇文章中多次加入了打印語句,來幫助我們確定代碼是否按照預期執行。這個%#v格式選項展示了提供了最典型的調試信息。不要忘了你可能需要添加“fmt”包:
fmt.Prinln("\n createAndRefresh Info = %#v", info)
有疑問?PANIC!
我有一段時間非常難以理解Create.go里createAndRefresh函數是如何被調用的。最后,我決定拋出一個異常來強行得到stack trace並打印到屏幕上。下面的代碼展示了我是怎么添加這句Panic的。這幫助我最終決定了是哪種Visitor實際被用來調用createAndRefresh函數。
func createAndRefresh(info *resource.Info) error { fmt.Println("\n createAndRefresh Info = %#v", info) panic("Want Stack Trace") obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) if err != nil { return err } info.Refresh(obj, true) return nil }
查看過去的源碼
有時你看到一些代碼,然后自己開始思考:這些人在提交代碼的時候是怎么想的。感天謝地,Github瀏覽器接口提供了一個blame選項作為用戶接口,下面展示了這個接口:
當我們按下blame按鈕,你會得到一份關於每一行代碼的commit的列表。這讓你可以穿越時空,看到某一特定行在添加的時候開發者試着完成的是什么。下面的圖展示了blame選項的使用,左手邊列出了所有的commits:
總結
本文中我們試驗了Kubernetes關於運行一個簡單的kubectl命令的多個關鍵代碼,並且閱讀到它向API Server實際發送REST調用的代碼。我們也描述了如何在Kubernetes開發環境中編譯和運行命令。我們最后介紹了幾個有用的工具和技巧。在下篇文章里,我們將會試驗Kubernetes代碼中另一段重要的代碼。同時,希望這篇文章能夠給你帶來學習Kubernetes源碼的勇氣:千里之行始於足下。
原文作者:Dr. Brad Topol,IBM傑出工程師,專注於開源技術和開發推廣,同時他也是Kubernetes的貢獻者和Kubernetes Conformance Workgroup成員。