kubernetes-kube-apiserver進程源碼分析

kubernetes API server是由kube-apiserver進程實現的,它運行在kubernetes的管理節點—master上並對外提供kubernetes Restful  API服務,它提供的主要是與集羣管理相關的API服務,例如校驗pod、services、replication controller的配置並存儲到後端的etcd server上。下面我們分別對其啓動過程、關鍵代碼分析及設計總結等進行深入講解。

 

進程啓動過程分析

kube-apiserver進程的入口類源碼位置如下:

\kubernetes-master\cmd\kube-apiserver\apiserver.go

入口main函數的邏輯如下:

上述代碼的核心爲下面三行,創建一個APIServer結構體,並將命令行啓動參數傳入,最後啓動監聽:

我們先來看看都有哪些常用的命令行參數被傳遞到了APIServer對象,下面是運行在Master節點的kube-apiserver進程的命令行信息:

可以看到關鍵的幾個參數有etcd_servers的地址,APIServer綁定和監聽的本地地址、kubelet的運行端口及kubernetes服務的clusterIP地址。

下面是app.NewAPIServer()的代碼,我們看到這裏的控制還是很全面的,包括安全控制(Certdirectory、HTTPS默認啓動)、

權限控制(AuthorizationMode、AdmissionControl)、服務限流控制(APIRate、APIBurst)等,這些邏輯說明了APIServer是按照企業級平臺的標準所設計和實現的。

創建了APIServer結構體實例後,apiserver.go將此實例傳入子包app/server.go的func(s*APIServer) Run(_ []string)方法裏,最終綁定本地端口並創建一個HTTP server與一個HTTPS server,從而完成整個進程的啓動過程。

 

Run方法的代碼有很多,這裏就不再列出源碼,對該方法的源碼解讀如下。

1.調用verifyClusterIPFlags方法,驗證CLusterIP參數是否設置以及是否有效。

2.驗證etcd-servers的參數是否已設置。

3.如果初始化CloudProvider,且沒有CloudProvider的參數,則日誌警告並繼續。

4.根據kubeletconfig的配置參數,調用pkg/Client/kubeclient.go中的方法NewKubeletClient()創建一個kubelet Client對象,這其實是一個HTTPKubeletClient實例,目前只用於kubelet的健康檢查(KubeletHealthChecker)。

5.判斷哪些API version需要關閉,目前在1.0代碼中默認關閉了v1 beta3的API版本。

6.創建一個kubernetes的RestClient對象,具體的代碼在pkg/client/helper.go的transportFor()方法裏完成,通過它完成Pod,replication controller及kubernetes service等對象的CRUD操作。

7.創建用於訪問etcd server的客戶端,具體代碼在newEtcd()方法裏實現,從代碼調用中可以看出,kubernetes採用的是coreos/go-etcd/client.go這個客戶端實現。

8.建立鑑權(authentication)、授權(authorzer)、服務許可框架和插件(admissioncontrol)的相關代碼邏輯。

9.獲取和設置APIServer的externalHost的名稱,如果沒有提供ExternalHost參數,且kubernetes運行在谷歌的GCE雲平臺上,則嘗試通過CloudProvider接口獲取本機節點的外部IP地址。

10.如果運行在雲平臺上,則安裝本機的SSH key到kubernetes集羣中的所有虛擬機上。

11.用APIServer的數據及上述過程中創建的一些對象(kubeletClient、etcdClient、authenticator、admissionController等)作爲參數,構造kubernetes Master的config構造(pkg/master/master.go),以此生成一個Master實例。

12.用上述創建的Master實例,分別創建HTTP server及安全的HTTPS Server來開始監控客戶端的請求,至此整個進程啓動完畢。

 

關鍵代碼分析

kubernetes api service的關鍵代碼就隱藏在pkg/master/master.go中,APIServer這個結構體只不過是一個參數傳遞通道而已,它的數據最終傳給了pkg/master/master.go裏的master結構體,下面是其完整定義:

      

在這段代碼裏,除了之前我們熟悉的那些變量,又多了幾個陌生的重要變量,接下來我們逐一對其進行分析講解。

首先是類型爲apiserver.Mux(來自pkg/apiserver/apiserver.go)的mux變量,下面是對它的定義:

如果你熟悉socket編程,特別使用過或者研究過HTTP Rest的一些框架,那麼對於這個Mux接口就很熟悉了,它是一個HTTP的多分器(Multiplexer),其實它也是GolangHTTP基礎包裏的http.ServeMux的一個接口子集,用於派發(dispatch)某個request路徑到對應的http.Handler進行處理。實際上在master.go代碼中是生成了一個http.ServeMux對象並賦值給apiserver.Mux變量,在代碼中還有強制類型轉換的語句。從上述分析來看,apiserver.Mux的引入是設計的一個敗筆,並沒有增加什麼價值,反而增加了理解代碼的難度,此外,爲了更好實現Rest服務,kubernetes在這裏引入了一個第三方的REST框架:go-restful

go-restful採用了路由映射的設計思想,並在API設計中使用了流行的Fluent Style風格,使用起來酣暢淋漓,也難怪kubernetes選擇它。

go-restful框架中的核心對象如下:

restful.container: 代表一個HTTP Rest服務器,包括一組restful.WebService對象和一個http.ServeMux對象,使用RouteSelector進行請求派發。

restful.Webservice:表示一個rest服務,由多個rest路由(rest.route)組成,這一組rest路由共享一個root path。

rest.Route:表示一個route路由,rest路由主要由rest path、HTTP Method、輸入輸出類型(HTML/JSON)及對應的回調函數組成。

rest.RouteFunction:一個用於處理具體的REST調用的函數接口定義,具體定義爲type RouteFunction func(*request, *response)。

Master結構體包含了對restful.container與restful.webservice這兩個go-restful核心對象的引用,在接下來的Master對象的構造方法中被初始化。那麼問題又來了,kubernetes的一堆rest API又是在哪定義的,是如何被綁定到restful.Route中的呢?

要理解這個問題,我們首先要弄清楚Master結構體的變量:

storage map[string] rest.Storage

storage變量是一個Map,key爲Rest API的path,value是rest.storage接口,此接口是一個通用的符合restful要求的資源存儲服務接口,每個服務接口負責處理一類數據對象-資源數據,只有一個接口方法:new(),new()方法返回該storage服務所能識別和管理的某種具體的資源數據的一個空實例。

在運行期間,kubernetes API Runtime運行時框架會把new()方法返回的空對象的指針傳入Codec.DecodeInto方法中,從而完成HTTP Rest請求中的Byte數組反序列化邏輯。kubernetes API Server中所有對外提供服務的restful資源都實現了此接口,這些資源包括pods,bindings,podTemplates, replicationcontrollers, service等,完整的列表就在master.go的func(m *Master)init(c *Config)中,下面是相關代碼片段。

                    

  

看到這段代碼,你在潛意識裏已經明白,這其實就是似曾相識的kubernetes rest API列表,storage這個map的key就是rest API的訪問路徑,value卻不是之前說好的restful.route。聰明的你一定想到了答案:必然存在一個轉換適配的方法來實現上述轉換。方法在pkg/apiserver/api_installer.go中:

該方法把一個path對應的rest.storage轉換成一系列的restful.route並添加到指針restful.webservice中。

 

爲了區分API的版本,在apiserver.go裏定義了一個結構體:APIGroupVersion。以下是代碼:

 

我們注意到APIGroupVersion是與rest.Storage map捆綁的,並且綁定了相應版本的codec、convertor用於版本轉換,這樣就很容易理解kubernetes是如何區分多版本API的rest服務的。以下是過程詳解:

首先,在APIGroupVersion的InstallREST方法裏,用Version變量來構造一個Rest API Path前綴並賦值給APIINstaller的prefix變量,並調用它的install()方法完成Rest API的轉換。

接着,在APIINstaller的install方法中用prefix前綴生成webservice的相對根路徑:

 

最後,在kubernetes的master初始化方法func(m* master) init(c* config)裏生成不同的APIGroupVersion對象,並調用installRest()方法,完成最終的多版本API的Rest服務裝配流程:

至此,Rest API的多版本問題還有最後一個問題需要澄清,在不同版本接口的輸入輸出參數的格式是有差別的,kubernetes如何處理的?

要弄明白這一點,首先要研究kubernetes API的數據對象的序列化、反序列化的實現機制,爲了同時解決數據對象的序列化、反序列化與多版本數據對象的兼容和轉換問題,kubernetes設計了一套複雜的機制,首先設計了conversion.scheme這個結構體,以下是對她的定義:

在上述代碼中可以看到,typetoversion與versionMap屬性是爲了解決數據對象的序列化和反序列化問題,converter屬性則負責不同版本的數據對象轉換問題,kubernetes這個設計思路簡單方便解決了多版本的序列化和版本轉換問題。

下面是conversion.scheme裏序列化、反序列化的核心方法NewObject()的代碼:通過查找versionMap裏匹配的註冊類型,以反射方式生成一個空的數據對象:

而pkg/conversion/encode.go與decode.go則在conversion.scheme提供的基礎功能之上,完成了最終的序列化、反序列化功能。下面是encode.go裏的主方法encodeToVersion()的關鍵代碼片段:

再進一步,kubernetes在conversion.scheme的基礎上又做了一個封裝工具類runtime.scheme,可以看作前者的代理類,主要增加了fieldLabelConversionFuncs這個Map屬性,用於解決數據對象的屬性名稱的兼容性轉換和校驗,比如將需要兼容Pod的spec.host屬性改爲spec.nodeName的情況。

注意到conversion.scheme只是實現了一個序列化與類型轉換的框架API,提供了註冊資源數據類型與轉換函數的功能,那麼具體的資源數據對象類型、轉換函數又是在哪個包裏實現的呢?答案是pkg/api。kubernetes爲不同的API版本提供了獨立的數據類型和相關的轉換函數並按照版本號命名package,如pkg/api/v1,pkg/api/v1beta3等,而當前默認版本則存在於pkg/api目錄下。

以pkg/api/v1爲例,以每個目錄裏都包含如下關鍵源碼:

1.types.go定義了rest API接口裏所涉及的所有數據類型,v1版本有2000行代碼:

2.在conversion.go與conversion_generated.go裏定義了conversion.scheme所需的從內部版本到v1版本的類型轉換函數,其中conversion_generated.go中的代碼有5000行之多,當然這是通過工具自動生成的代碼;

3.register.go負責將types.go裏定義的數據類型與conversion.go定義的數據轉換函數註冊到runtime.schema裏。

pkg/api裏的register.go初始化生成並持有一個全局的runtime.schema對象,並把當前默認版本的數據類型註冊進去,相關代碼如下:

而pkg/api/v1/register.go與v1beta3下的register.go在初始化過程中分別把與版本相關的數據類型和轉換函數註冊到全局的runtime.scheme中:

這樣一來,其他地方就可以通過runtime.scheme這個全局變量來完成kubernetes API中的數據對象的序列化和反序列化邏輯了,比如kubernetes API Client包就大量使用了它,下面是pkg/client/pods.go裏pod刪除的delete()方法的代碼:

清楚了kubernetes Rest API的數據對象的序列化機制及多版本的實現原理之後,我們接着分析下面這個重要流程的實現細節。

kubernetes中實現了rest.storage接口的服務在轉換成restful.routefunction以後,是怎樣處理一個rest請求並最終完成基於後端存儲服務etcd上的具體操作過程的?

首先,kubernetes設計了一個名爲註冊表的package(pkg/registry),這個package按照rest.storage服務所管理的資源數據的類型而劃分爲不同的子包,每個子包都由相同命名的一組golang代碼來完成具體的rest接口的實現邏輯。

下面我們以pod的rest服務實現爲例,其與註冊表相關的代碼位於pkg/registry/pod中,在registry.go裏定義了pod註冊表服務的接口:

我們看到這個pod註冊表服務是針對pod的CRUD的操作接口的一個定義,在入口參數中除了調用的上下文環境api.context,就是我們之前分析過的pkg/api包中的pod這個資源數據對象,爲了實現強類型的方法調用,在registry.go裏定義了一個名爲storage的結構體,storage實現registry接口,可以看做一種代理設計模式,因爲具體的操作都是通過內部rest.standardstorage來實現的,下面是截取的registry.go中的create、update、delete的源碼:

那麼,這個實現了rest.standardstorage通用接口的真正storage又是什麼?從master對象的初始化函數中,我們發現了下面的相關代碼:

master對象創建了一個私有變量podstorage,其類型爲PodStorage,pod註冊表服務實例(podregistry)裏真正的storage是podstorage.pod。下面是podetcd的函數newstorage中的關鍵代碼:

在上述代碼可以看到,位於pkg/registry/generic/etcd/etcd.go裏的etcd纔是真正的storage實現。而具體操作etcd的代碼是靠tools.etcdhelper這個類完成的,通過分析etcd.go裏的func(e* etcd) Create方法,我們知道創建一個etcd裏的鍵值對的關鍵邏輯如下:

注意到之前podstorage創建store時重載了objectNameFunc()、KetFunc()\NewFunc()等函數,於是完成了針對pod的創建過程,kubernetes API服務中的其他數據對象也都遵循同樣的設計模式。

進一步研究代碼,我們發現podstorage中的Pod、Binding、Status等屬性是pkg/api/rest/rest.go中幾個不同的Rest接口的實現,並且通過ercdgeneric.etcd這個實例來完成Pod的一些具體操作,比如這裏的statusREST。下面是其相關代碼片段:

下表展現了podstorage中的各個XXXREST接口與pkg/api/rest.go的相關Rest接口的一一對應關係。

 

對kubernetes RestAPI Server的複雜實現機制和調用流程總結入下:

在pkg/api包定義了Rest API涉及的資源對象、提供的rest接口、類型轉換框架和具體轉換函數、序列化反序列化等代碼,其中,資源對象和轉換函數按照版本分包,形成了kubernetes API server基礎的框架,其中核心是各類資源(Node。pod,service

等)以及這些資源對應的rest.storage。

在pkg/runtime包最重要的對象就是schema,它保存了kubernetes API service中註冊的資源對象類型、轉換函數等重要基礎數據,另外runtime包也提供了獲取json/yaml序列化、反序列化的codec結構體,runtime總體上與pkg/api密切關聯,分離出來的目的是供其他模塊方便使用。

pkg/registry包其實是把pkg/api中定義的各種資源對象所提供的rest接口進一步規範定義並且實現對應的接口,其中generate/etcd/etcd.go裏的rtcd對象是一個真正實現了rest.storage接口的基於etcd後端存儲的服務框架。

kubernetes採用了go-restful這個第三方rest框架,簡化了rest服務的開發,主要代碼在pkg/apiserver中,通過APIGroupVersion這個結構體可以完成不同API版本的rest路徑映射,而api_installer.go則實現了從kubernetes rest storage接口到go-restful的映射連接邏輯,對應rest.storage的具體restful.routefunction則在resthandler.go裏實現。

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