原始碼解析:K8s 建立 pod 時,背後發生了什麼(三)(2021)
本文基於 2019 年的一篇文章 What happens when … Kubernetes edition! 梳理了 k8s 建立 pod (及其 deployment/replicaset) 的整個過程 , 整理了每個 重要步驟的程式碼呼叫棧 ,以 在實現層面加深對整個過程的理解 。
原文參考的 k8S 程式碼已經較老( v1.8
/ v1.14
以及當時的 master
),且部分程式碼
連結已失效;
本文程式碼基於
v1.21
。
由於內容已經不與原文一一對應(有增加和刪減),因此標題未加 “[譯]” 等字樣。感謝原作者(們)的精彩文章。
篇幅太長,分成了幾部分:
- 原始碼解析:K8s 建立 pod 時,背後發生了什麼(一)(2021)
- 原始碼解析:K8s 建立 pod 時,背後發生了什麼(二)(2021)
- 原始碼解析:K8s 建立 pod 時,背後發生了什麼(三)(2021)
- 原始碼解析:K8s 建立 pod 時,背後發生了什麼(四)(2021)
- 原始碼解析:K8s 建立 pod 時,背後發生了什麼(五)(2021)
-
- 2.1 認證(Authentication)
- 2.2 鑑權(Authorization)
- 2.3 Admission control
-
- 3.1 kube-apiserver 請求處理過程
- 3.2 Create handler 處理過程
-
- 4.2 InitializerConfiguration
2 kube-apiserver
請求從客戶端發出後,便來到服務端,也就是 kube-apiserver。
2.0 呼叫棧概覽
buildGenericConfig |-genericConfig = genericapiserver.NewConfig(legacyscheme.Codecs) // cmd/kube-apiserver/app/server.go NewConfig // staging/src/k8s.io/apiserver/pkg/server/config.go |-return &Config{ Serializer: codecs, BuildHandlerChainFunc: DefaultBuildHandlerChain, } / / / / DefaultBuildHandlerChain // staging/src/k8s.io/apiserver/pkg/server/config.go |-handler := filterlatency.TrackCompleted(apiHandler) |-handler = genericapifilters.WithAuthorization(handler) |-handler = genericapifilters.WithAudit(handler) |-handler = genericapifilters.WithAuthentication(handler) |-return handler WithAuthentication |-withAuthentication |-resp, ok := AuthenticateRequest(req) | |-for h := range authHandler.Handlers { | resp, ok := currAuthRequestHandler.AuthenticateRequest(req) | if ok { | return resp, ok, err | } | } | return nil, false, utilerrors.NewAggregate(errlist) | |-audiencesAreAcceptable(apiAuds, resp.Audiences) |-req.Header.Del("Authorization") |-req = req.WithContext(WithUser(req.Context(), resp.User)) |-return handler.ServeHTTP(w, req)
2.1 認證(Authentication)
kube-apiserver 首先會對請求進行 認證(authentication) ,以確保 使用者身份是合法的(verify that the requester is who they say they are)。
具體過程:啟動時,檢查所有的 命令列引數 ,組織成一個 authenticator list,例如,
--client-ca-file --token-auth-file
不同 anthenticator 做的事情有所不同:
- x509 handler 驗證該 HTTP 請求是用 TLS key 加密的,並且有 CA root 證書的簽名。
-
bearer token handler
驗證請求中帶的 token(HTTP Authorization 頭中),在 apiserver 的 auth file 中是存在的(
--token-auth-file
)。 - basicauth handler 對 basic auth 資訊進行校驗。
如果認證成功,就會將 Authorization
頭從請求中刪除 ,然後在上下文中 加上使用者資訊
。
這使得後面的步驟(例如鑑權和 admission control)能用到這裡已經識別出的使用者身份資訊。
// staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication.go // WithAuthentication creates an http handler that tries to authenticate the given request as a user, and then // stores any such user found onto the provided context for the request. // On success, "Authorization" header is removed from the request and handler // is invoked to serve the request. func WithAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences) http.Handler { return withAuthentication(handler, auth, failed, apiAuds, recordAuthMetrics) } func withAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, metrics recordMetrics) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { resp, ok := auth.AuthenticateRequest(req) // 遍歷所有 authenticator,任何一個成功就返回 OK if !ok { return failed.ServeHTTP(w, req) // 所有認證方式都失敗了 } if !audiencesAreAcceptable(apiAuds, resp.Audiences) { fmt.Errorf("unable to match the audience: %v , accepted: %v", resp.Audiences, apiAuds) failed.ServeHTTP(w, req) return } req.Header.Del("Authorization") // 認證成功後,這個 header 就沒有用了,可以刪掉 // 將使用者資訊新增到請求上下文中,供後面的步驟使用 req = req.WithContext(WithUser(req.Context(), resp.User)) handler.ServeHTTP(w, req) }) }
AuthenticateRequest()
實現:遍歷所有 authenticator,任何一個成功就返回 OK,
// staging/src/k8s.io/apiserver/pkg/authentication/request/union/union.go func (authHandler *unionAuthRequestHandler) AuthenticateRequest(req) (*Response, bool) { for currAuthRequestHandler := range authHandler.Handlers { resp, ok := currAuthRequestHandler.AuthenticateRequest(req) if ok { return resp, ok, err } } return nil, false, utilerrors.NewAggregate(errlist) }
2.2 鑑權(Authorization)
傳送者身份(認證)是一個問題,但他是否有許可權執行這個操作(鑑權),是另一個問題 。 因此確認傳送者身份之後,還需要進行鑑權。
鑑權的過程與認證非常相似,也是逐個匹配 authorizer 列表中的 authorizer:如果都失敗了,
返回 Forbidden
並停止 進一步處理
。如果成功,就繼續。
內建的 幾種 authorizer 型別 :
- webhook : 與其他服務互動,驗證是否有許可權。
- ABAC : 根據 靜態檔案中規定的策略 (policies)來進行鑑權。
- RBAC : 根據 role 進行鑑權,其中 role 是 k8s 管理員提前配置的。
- Node : 確保 node clients,例如 kubelet,只能訪問本機內的資源。
要看它們的具體做了哪些事情,可以檢視它們各自的 Authorize()
方法。
2.3 Admission control
至此,認證和鑑權都通過了。但這還沒結束,K8s 中的 其它元件還需要對請求進行檢查 , 其中就包括 admission controllers 。
與鑑權的區別
- 鑑權(authorization)在前面,關注的是 使用者是否有操作許可權 ,
- Admission controllers 在更後面, 對請求進行攔截和過濾,確保它們符合一些更廣泛的叢集規則和限制 , 是 將請求物件持久化到 etcd 之前的最後堡壘 。
工作方式
- 與認證和鑑權類似,也是遍歷一個列表,
- 但有一點核心區別: 任何一個 controller 檢查沒通過,請求就會失敗 。
設計:可擴充套件
-
每個 controller 作為一個 plugin 存放在
plugin/pkg/admission
目錄 , - 設計時已經考慮,只需要實現很少的幾個介面
- 但注意, admission controller 最終會編譯到 k8s 的二進位制檔案 (而非獨立的 plugin binary)
型別
Admission controllers 通常按不同目的分類,包括: 資源管理、安全管理、預設值管 理、引用一致性 (referential consistency)等型別。
例如,下面是資源管理類的幾個 controller:
InitialResources LimitRanger ResourceQuota
3 寫入 etcd
至此,K8s 已經完成對請求的驗證,允許它進行接下來的處理。
kube-apiserver 將 對請求進行反序列化,構造 runtime objects ( kubectl generator 的反過程),並將它們 持久化到 etcd 。下面詳細 看這個過程。
3.0 呼叫棧概覽
對於本文建立 pod 的請求,相應的入口是 POST handler ,它又會進一步將請求委託給一個建立具體資源的 handler。
registerResourceHandlers // staging/src/k8s.io/apiserver/pkg/endpoints/installer.go |-case POST:
// staging/src/k8s.io/apiserver/pkg/endpoints/installer.go switch () { case "POST": // Create a resource. var handler restful.RouteFunction if isNamedCreater { handler = restfulCreateNamedResource(namedCreater, reqScope, admit) } else { handler = restfulCreateResource(creater, reqScope, admit) } handler = metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, .., handler) article := GetArticleForNoun(kind, " ") doc := "create" + article + kind if isSubresource { doc = "create " + subresource + " of" + article + kind } route := ws.POST(action.Path).To(handler). Doc(doc). Operation("create"+namespaced+kind+strings.Title(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", producedObject). Returns(http.StatusCreated, "Created", producedObject). Returns(http.StatusAccepted, "Accepted", producedObject). Reads(defaultVersionedObject). Writes(producedObject) AddObjectParams(ws, route, versionedCreateOptions) addParams(route, action.Params) routes = append(routes, route) } for route := range routes { route.Metadata(ROUTE_META_GVK, metav1.GroupVersionKind{ Group: reqScope.Kind.Group, Version: reqScope.Kind.Version, Kind: reqScope.Kind.Kind, }) route.Metadata(ROUTE_META_ACTION, strings.ToLower(action.Verb)) ws.Route(route) }
3.1 kube-apiserver 請求處理過程
從 apiserver 的請求處理函式開始:
// staging/src/k8s.io/apiserver/pkg/server/handler.go func (d director) ServeHTTP(w http.ResponseWriter, req *http.Request) { path := req.URL.Path // check to see if our webservices want to claim this path for _, ws := range d.goRestfulContainer.RegisteredWebServices() { switch { case ws.RootPath() == "/apis": if path == "/apis" || path == "/apis/" { return d.goRestfulContainer.Dispatch(w, req) } case strings.HasPrefix(path, ws.RootPath()): if len(path) == len(ws.RootPath()) || path[len(ws.RootPath())] == '/' { return d.goRestfulContainer.Dispatch(w, req) } } } // if we didn't find a match, then we just skip gorestful altogether d.nonGoRestfulMux.ServeHTTP(w, req) }
如果能匹配到請求(例如匹配到前面註冊的路由),它將 分派給相應的 handler
;否則,fall back 到 path-based handler
( GET /apis
到達的就是這裡);
基於 path 的 handlers:
// staging/src/k8s.io/apiserver/pkg/server/mux/pathrecorder.go func (h *pathHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if exactHandler, ok := h.pathToHandler[r.URL.Path]; ok { return exactHandler.ServeHTTP(w, r) } for prefixHandler := range h.prefixHandlers { if strings.HasPrefix(r.URL.Path, prefixHandler.prefix) { return prefixHandler.handler.ServeHTTP(w, r) } } h.notFoundHandler.ServeHTTP(w, r) }
如果還是沒有找到路由,就會 fallback 到 non-gorestful handler,最終可能是一個 not found handler。
對於我們的場景,會匹配到一條已經註冊的、名為
createHandler
為的路由。
3.2 Create handler 處理過程
// staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go func createHandler(r rest.NamedCreater, scope *RequestScope, admit Interface, includeName bool) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { namespace, name := scope.Namer.Name(req) // 獲取資源的 namespace 和 name(etcd item key) s := negotiation.NegotiateInputSerializer(req, false, scope.Serializer) body := limitedReadBody(req, scope.MaxRequestBodyBytes) obj, gvk := decoder.Decode(body, &defaultGVK, original) admit = admission.WithAudit(admit, ae) requestFunc := func() (runtime.Object, error) { return r.Create( name, obj, rest.AdmissionToValidateObjectFunc(admit, admissionAttributes, scope), ) } result := finishRequest(ctx, func() (runtime.Object, error) { if scope.FieldManager != nil { liveObj := scope.Creater.New(scope.Kind) obj = scope.FieldManager.UpdateNoErrors(liveObj, obj, managerOrUserAgent(options.FieldManager, req.UserAgent())) admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit) } admit.(admission.MutationInterface) mutatingAdmission.Handles(admission.Create) mutatingAdmission.Admit(ctx, admissionAttributes, scope) return requestFunc() }) code := http.StatusCreated status, ok := result.(*metav1.Status) transformResponseObject(ctx, scope, trace, req, w, code, outputMediaType, result) } }
- 首先解析 HTTP request,然後執行基本的驗證,例如保證 JSON 與 versioned API resource 期望的是一致的;
- 執行審計和最終 admission;
-
將資源最終 寫到 etcd , 這會進一步呼叫到 storage provider 。
etcd key 的格式一般是
<namespace>/<name>
(例如,default/nginx-0
),但這個也是可配置的。 -
最後,storage provider 執行一次
get
操作,確保物件真的建立成功了。如果有額外的收尾任務(additional finalization),會執行 post-create handlers 和 decorators。 - 返回 生成的 HTTP response。
以上過程可以看出,apiserver 做了大量的事情。
總結:至此我們的 pod 資源已經在 etcd 中了。但是,此時 kubectl get pods -n <ns>
還看不見它。
4 Initializers
物件持久化到 etcd 之後,apiserver 並未將其置位對外可見,它也不會立即就被排程 , 而是要先等一些 initializers 執行完成。
4.1 Initializer
Initializer 是 與特定資源型別(resource type)相關的 controller ,
- 負責 在該資源對外可見之前對它們執行一些處理 ,
- 如果一種資源型別沒有註冊任何 initializer,這個步驟就會跳過, 資源對外立即可見 。
這是一種非常強大的特性,使得我們能 執行一些通用的啟動初始化(bootstrap)操作 。例如,
- 向 Pod 注入 sidecar、暴露 80 埠,或打上特定的 annotation。
- 向某個 namespace 內的所有 pod 注入一個存放了測試證書(test certificates)的 volume。
- 禁止建立長度小於 20 個字元的 Secret (例如密碼)。
4.2 InitializerConfiguration
可以用 InitializerConfiguration
宣告對哪些資源型別(resource type)執行哪些 initializer
。
例如,要實現所有 pod 建立時都執行一個自定義的 initializer custom-pod-initializer
,
可以用下面的 yaml:
apiVersion: admissionregistration.k8s.io/v1alpha1 kind: InitializerConfiguration metadata: name: custom-pod-initializer initializers: - name: podimage.example.com rules: - apiGroups: - "" apiVersions: - v1 resources: - pods
建立以上配置( kubectl create -f xx.yaml
)之後,K8s 會將 custom-pod-initializer
追加到每個 pod 的 metadata.initializers.pending
欄位
。
在此之前需要 啟動 initializer controller ,它會
- 定期掃描是否有新 pod 建立;
- 當 檢測到它的名字出現在 pod 的 pending 欄位 時,就會執行它的處理邏輯;
- 執行完成之後,它會將自己的名字從 pending list 中移除。
pending list 中的 initializers,每次只有第一個 initializer 能執行。
當
所有 initializer 執行完成, pending
欄位為空
之後,就認為
這個物件已經完成初始化了
(considered initialized)。
細心的同學可能會有疑問: 前面說這個物件還沒有對外可見,那用
戶空間的 initializer controller 又是如何能檢測並操作這個物件的呢?
答案是:
kube-apiserver 提供了一個 ?includeUninitialized
查詢引數,它會返回所有物件,
包括那些還未完成初始化的(uninitialized ones)。
- [譯] 為 K8s workload 引入的一些 BPF datapath 擴充套件(LPC, 2021)
- [譯] [論文] 可虛擬化第三代(計算機)架構的規範化條件(ACM, 1974)
- [譯] NAT 穿透是如何工作的:技術原理及企業級實踐(Tailscale, 2020)
- [譯] 寫給工程師:關於證書(certificate)和公鑰基礎設施(PKI)的一切(SmallStep, 2018)
- [譯] 基於角色的訪問控制(RBAC):演進歷史、設計理念及簡潔實現(Tailscale, 2021)
- [譯] Control Group v2(cgroupv2 權威指南)(KernelDoc, 2021)
- [譯] Linux Socket Filtering (LSF, aka BPF)(KernelDoc,2021)
- [譯] LLVM eBPF 彙編程式設計(2020)
- [譯] Cilium:BPF 和 XDP 參考指南(2021)
- BPF 進階筆記(三):BPF Map 核心實現
- BPF 進階筆記(二):BPF Map 型別詳解:使用場景、程式示例
- BPF 進階筆記(一):BPF 程式型別詳解:使用場景、函式簽名、執行位置及程式示例
- 原始碼解析:K8s 建立 pod 時,背後發生了什麼(四)(2021)
- 原始碼解析:K8s 建立 pod 時,背後發生了什麼(三)(2021)
- [譯] 邁向完全可程式設計 tc 分類器(NetdevConf,2016)
- [譯] 雲原生世界中的資料包標記(packet mark)(LPC, 2020)
- [譯] 利用 eBPF 支撐大規模 K8s Service (LPC, 2019)
- 計算規模驅動下的網路方案演進
- 邁入 Cilium BGP 的雲原生網路時代
- [譯] BeyondProd:雲原生安全的一種新方法(Google, 2019)