原始碼解析:K8s 建立 pod 時,背後發生了什麼(三)(2021)

語言: CN / TW / HK

本文基於 2019 年的一篇文章 What happens when … Kubernetes edition! 梳理了 k8s 建立 pod (及其 deployment/replicaset) 的整個過程 , 整理了每個 重要步驟的程式碼呼叫棧 ,以 在實現層面加深對整個過程的理解

原文參考的 k8S 程式碼已經較老( v1.8 / v1.14 以及當時的 master ),且部分程式碼 連結已失效; 本文程式碼基於 v1.21

由於內容已經不與原文一一對應(有增加和刪減),因此標題未加 “[譯]” 等字樣。感謝原作者(們)的精彩文章。

篇幅太長,分成了幾部分:

  1. 原始碼解析:K8s 建立 pod 時,背後發生了什麼(一)(2021)
  2. 原始碼解析:K8s 建立 pod 時,背後發生了什麼(二)(2021)
  3. 原始碼解析:K8s 建立 pod 時,背後發生了什麼(三)(2021)
  4. 原始碼解析:K8s 建立 pod 時,背後發生了什麼(四)(2021)
  5. 原始碼解析: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 handlerGET /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)
    }
}
  1. 首先解析 HTTP request,然後執行基本的驗證,例如保證 JSON 與 versioned API resource 期望的是一致的;
  2. 執行審計和最終 admission;
  3. 將資源最終 寫到 etcd , 這會進一步呼叫到 storage provider

    etcd key 的格式一般是 <namespace>/<name> (例如, default/nginx-0 ),但這個也是可配置的。

  4. 最後,storage provider 執行一次 get 操作,確保物件真的建立成功了。如果有額外的收尾任務(additional finalization),會執行 post-create handlers 和 decorators。
  5. 返回 生成的 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)。