從HelloWorld看Knative Serving代碼實現

概念先知

官方給出的這幾個資源的關係圖仍是比較清晰的:

1.Service: 自動管理工做負載整個生命週期。負責建立route,configuration以及每一個service更新的revision。經過Service能夠指定路由流量使用最新的revision,仍是固定的revision。
2.Route:負責映射網絡端點到一個或多個revision。能夠經過多種方式管理流量。包括灰度流量和重命名路由。
3.Configuration:負責保持deployment的指望狀態,提供了代碼和配置之間清晰的分離,並遵循應用開發的12要素。修改一次Configuration產生一個revision。
4.Revision:Revision資源是對工做負載進行的每一個修改的代碼和配置的時間點快照。Revision是不可變對象,能夠長期保留。api

看一個簡單的示例

咱們開始運行官方hello-world示例,看看會發生什麼事情:網絡

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
  name: helloworld-go
  namespace: default
spec:
  runLatest: // RunLatest defines a simple Service. It will automatically configure a route that keeps the latest ready revision from the supplied configuration running.
    configuration:
      revisionTemplate:
        spec:
          container:
            image: registry.cn-shanghai.aliyuncs.com/larus/helloworld-go
            env:
            - name: TARGET
              value: "Go Sample v1"

查看 knative-ingressgateway:dom

kubectl get svc knative-ingressgateway -n istio-system

查看服務訪問:DOMAINcurl

kubectl get ksvc helloworld-go  --output=custom-columns=NAME:.metadata.name,DOMAIN:.status.domain


這裏直接使用cluster ip便可訪問ide

curl -H "Host: helloworld-go.default.example.com" http://10.96.199.35

目前看一下服務是部署ok的。那咱們看一下k8s裏面建立了哪些資源:函數

咱們能夠發現經過Serving,在k8s中建立了2個service和1個deployment:

那麼究竟Serving中作了哪些處理,接下來咱們分析一下Serving源代碼fetch

源代碼分析

Main

先看一下各個組件的控制器啓動代碼,這個比較好找,在/cmd/controller/main.go中。
依次啓動configuration、revision、route、labeler、service和clusteringress控制器。ui

...
controllers := []*controller.Impl{
        configuration.NewController(
            opt,
            configurationInformer,
            revisionInformer,
        ),
        revision.NewController(
            opt,
            revisionInformer,
            kpaInformer,
            imageInformer,
            deploymentInformer,
            coreServiceInformer,
            endpointsInformer,
            configMapInformer,
            buildInformerFactory,
        ),
        route.NewController(
            opt,
            routeInformer,
            configurationInformer,
            revisionInformer,
            coreServiceInformer,
            clusterIngressInformer,
        ),
        labeler.NewRouteToConfigurationController(
            opt,
            routeInformer,
            configurationInformer,
            revisionInformer,
        ),
        service.NewController(
            opt,
            serviceInformer,
            configurationInformer,
            routeInformer,
        ),
        clusteringress.NewController(
            opt,
            clusterIngressInformer,
            virtualServiceInformer,
        ),
    }
...

Service

首先咱們要從Service來看,由於咱們一開始的輸入就是Service資源。在/pkg/reconciler/v1alpha1/service/service.go。
比較簡單,就是根據Service建立Configuration和Route資源this

func (c *Reconciler) reconcile(ctx context.Context, service *v1alpha1.Service) error {
    ...
    configName := resourcenames.Configuration(service)
    config, err := c.configurationLister.Configurations(service.Namespace).Get(configName)
    if errors.IsNotFound(err) {
        config, err = c.createConfiguration(service)
    ...
    routeName := resourcenames.Route(service)
    route, err := c.routeLister.Routes(service.Namespace).Get(routeName)
    if errors.IsNotFound(err) {
        route, err = c.createRoute(service)
    ...
}

Route

/pkg/reconciler/v1alpha1/route/route.go
看一下Route中reconcile作了哪些處理:
1.判斷是否有Ready的Revision可進行traffic
2.設置目標流量的Revision(runLatest:使用最新的版本;pinned:固定版本,不過已棄用;release:經過容許在兩個修訂版之間拆分流量,逐步擴大到新修訂版,用於替換pinned。manual:手動模式,目前來看並未實現)
3.建立ClusterIngress:Route不直接依賴於VirtualService[https://istio.io/docs/reference/config/istio.networking.v1alpha3/#VirtualService] ,而是依賴一箇中間資源ClusterIngress,它能夠針對不一樣的網絡平臺進行不一樣的協調。目前實現是基於istio網絡平臺。
4.建立k8s service:這個Service主要爲Istio路由提供域名訪問。url

func (c *Reconciler) reconcile(ctx context.Context, r *v1alpha1.Route) error {
    ....
    // 基因而否有Ready的Revision
    traffic, err := c.configureTraffic(ctx, r)
    if traffic == nil || err != nil {
        // Traffic targets aren't ready, no need to configure child resources.
        return err
    }

    logger.Info("Updating targeted revisions.")
    // In all cases we will add annotations to the referred targets.  This is so that when they become
    // routable we can know (through a listener) and attempt traffic configuration again.
    if err := c.reconcileTargetRevisions(ctx, traffic, r); err != nil {
        return err
    }

    // Update the information that makes us Addressable.
    r.Status.Domain = routeDomain(ctx, r)
    r.Status.DeprecatedDomainInternal = resourcenames.K8sServiceFullname(r)
    r.Status.Address = &duckv1alpha1.Addressable{
        Hostname: resourcenames.K8sServiceFullname(r),
    }

    // Add the finalizer before creating the ClusterIngress so that we can be sure it gets cleaned up.
    if err := c.ensureFinalizer(r); err != nil {
        return err
    }

    logger.Info("Creating ClusterIngress.")
    desired := resources.MakeClusterIngress(r, traffic, ingressClassForRoute(ctx, r))
    clusterIngress, err := c.reconcileClusterIngress(ctx, r, desired)
    if err != nil {
        return err
    }
    r.Status.PropagateClusterIngressStatus(clusterIngress.Status)

    logger.Info("Creating/Updating placeholder k8s services")
    if err := c.reconcilePlaceholderService(ctx, r, clusterIngress); err != nil {
        return err
    }

    r.Status.ObservedGeneration = r.Generation
    logger.Info("Route successfully synced")
    return nil
}

看一下helloworld-go生成的Route資源文件:

apiVersion: serving.knative.dev/v1alpha1
kind: Route
metadata:
  name: helloworld-go
  namespace: default
...
spec:
  generation: 1
  traffic:
  - configurationName: helloworld-go 
    percent: 100
status:
...
  domain: helloworld-go.default.example.com
  domainInternal: helloworld-go.default.svc.cluster.local
  traffic:
  - percent: 100 # 全部的流量經過這個revision
    revisionName: helloworld-go-00001 # 使用helloworld-go-00001 revision

這裏能夠看到經過helloworld-go配置, 找到了已經ready的helloworld-go-00001(Revision)。

Configuration

/pkg/reconciler/v1alpha1/configuration/configuration.go
1.獲取當前Configuration對應的Revision, 若不存在則建立。
2.爲Configuration設置最新的Revision
3.根據Revision是否readiness,設置Configuration的狀態LatestReadyRevisionName

func (c *Reconciler) reconcile(ctx context.Context, config *v1alpha1.Configuration) error {
    ...
    // First, fetch the revision that should exist for the current generation.
    lcr, err := c.latestCreatedRevision(config)
    if errors.IsNotFound(err) {
        lcr, err = c.createRevision(ctx, config)
    ...    
    revName := lcr.Name
    // Second, set this to be the latest revision that we have created.
    config.Status.SetLatestCreatedRevisionName(revName)
    config.Status.ObservedGeneration = config.Generation

    // Last, determine whether we should set LatestReadyRevisionName to our
    // LatestCreatedRevision based on its readiness.
    rc := lcr.Status.GetCondition(v1alpha1.RevisionConditionReady)
    switch {
    case rc == nil || rc.Status == corev1.ConditionUnknown:
        logger.Infof("Revision %q of configuration %q is not ready", revName, config.Name)

    case rc.Status == corev1.ConditionTrue:
        logger.Infof("Revision %q of configuration %q is ready", revName, config.Name)

        created, ready := config.Status.LatestCreatedRevisionName, config.Status.LatestReadyRevisionName
        if ready == "" {
            // Surface an event for the first revision becoming ready.
            c.Recorder.Event(config, corev1.EventTypeNormal, "ConfigurationReady",
                "Configuration becomes ready")
        }
        // Update the LatestReadyRevisionName and surface an event for the transition.
        config.Status.SetLatestReadyRevisionName(lcr.Name)
        if created != ready {
            c.Recorder.Eventf(config, corev1.EventTypeNormal, "LatestReadyUpdate",
                "LatestReadyRevisionName updated to %q", lcr.Name)
        }
...
}

看一下helloworld-go生成的Configuration資源文件:

apiVersion: serving.knative.dev/v1alpha1
kind: Configuration
metadata:
  name: helloworld-go
  namespace: default
  ...
spec:
  generation: 1
  revisionTemplate:
    metadata:
      creationTimestamp: null
    spec:
      container:
        env:
        - name: TARGET
          value: Go Sample v1
        image: registry.cn-shanghai.aliyuncs.com/larus/helloworld-go
        name: ""
        resources: {}
      timeoutSeconds: 300
status:
  ...
  latestCreatedRevisionName: helloworld-go-00001
  latestReadyRevisionName: helloworld-go-00001
  observedGeneration: 1

咱們能夠發現LatestReadyRevisionName設置了helloworld-go-00001(Revision)。

Revision

/pkg/reconciler/v1alpha1/revision/revision.go
1.獲取build進度
2.設置鏡像摘要
3.建立deployment
4.建立k8s service:根據Revision構建服務訪問Service
5.建立fluentd configmap
6.建立KPA
感受這段代碼寫的很優雅,函數執行過程寫的很清晰,值得借鑑。另外咱們也能夠發現,目前knative只支持deployment的工做負載

func (c *Reconciler) reconcile(ctx context.Context, rev *v1alpha1.Revision) error {
    ...
    if err := c.reconcileBuild(ctx, rev); err != nil {
        return err
    }

    bc := rev.Status.GetCondition(v1alpha1.RevisionConditionBuildSucceeded)
    if bc == nil || bc.Status == corev1.ConditionTrue {
        // There is no build, or the build completed successfully.

        phases := []struct {
            name string
            f    func(context.Context, *v1alpha1.Revision) error
        }{{
            name: "image digest",
            f:    c.reconcileDigest,
        }, {
            name: "user deployment",
            f:    c.reconcileDeployment,
        }, {
            name: "user k8s service",
            f:    c.reconcileService,
        }, {
            // Ensures our namespace has the configuration for the fluentd sidecar.
            name: "fluentd configmap",
            f:    c.reconcileFluentdConfigMap,
        }, {
            name: "KPA",
            f:    c.reconcileKPA,
        }}

        for _, phase := range phases {
            if err := phase.f(ctx, rev); err != nil {
                logger.Errorf("Failed to reconcile %s: %v", phase.name, zap.Error(err))
                return err
            }
        }
    }
...
}

最後咱們看一下生成的Revision資源:

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
  name: helloworld-go
  namespace: default
  ...
spec:
  generation: 1
  runLatest:
    configuration:
      revisionTemplate:
        spec:
          container:
            env:
            - name: TARGET
              value: Go Sample v1
            image: registry.cn-shanghai.aliyuncs.com/larus/helloworld-go
          timeoutSeconds: 300
status:
  address:
    hostname: helloworld-go.default.svc.cluster.local
 ...
  domain: helloworld-go.default.example.com
  domainInternal: helloworld-go.default.svc.cluster.local
  latestCreatedRevisionName: helloworld-go-00001
  latestReadyRevisionName: helloworld-go-00001
  observedGeneration: 1
  traffic:
  - percent: 100
    revisionName: helloworld-go-00001

這裏咱們能夠看到訪問域名helloworld-go.default.svc.cluster.local,以及當前revision的流量配比(100%)
這樣咱們分析完以後,如今打開Serving這個黑盒

最後

這裏只是基於簡單的例子,分析了主要的業務流程處理代碼。對於activator(如何喚醒業務容器),autoscaler(Pod如何自動縮爲0)等代碼實現有興趣的同窗能夠一塊兒交流。


原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索