从HelloWorld看Knative Serving代码实现

简介: Knative Serving以Kubernetes和Istio为基础,支持无服务器应用程序和函数的部署并提供服务。我们从部署一个HelloWorld示例入手来分析Knative Serving的代码细节。

概念先知

官方给出的这几个资源的关系图还是比较清晰的:
image
1.Service: 自动管理工作负载整个生命周期。负责创建route,configuration以及每个service更新的revision。通过Service可以指定路由流量使用最新的revision,还是固定的revision。
2.Route:负责映射网络端点到一个或多个revision。可以通过多种方式管理流量。包括灰度流量和重命名路由。
3.Configuration:负责保持deployment的期望状态,提供了代码和配置之间清晰的分离,并遵循应用开发的12要素。修改一次Configuration产生一个revision。
4.Revision:Revision资源是对工作负载进行的每个修改的代码和配置的时间点快照。Revision是不可变对象,可以长期保留。

看一个简单的示例

我们开始运行官方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:

kubectl get svc knative-ingressgateway -n istio-system

image

查看服务访问:DOMAIN

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

image
这里直接使用cluster ip即可访问

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

image

目前看一下服务是部署ok的。那我们看一下k8s里面创建了哪些资源:
image

我们可以发现通过Serving,在k8s中创建了2个service和1个deployment:
image
那么究竟Serving中做了哪些处理,接下来我们分析一下Serving源代码

源代码分析

Main

先看一下各个组件的控制器启动代码,这个比较好找,在/cmd/controller/main.go中。
依次启动configuration、revision、route、labeler、service和clusteringress控制器。

...
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资源

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,而是依赖一个中间资源ClusterIngress,它可以针对不同的网络平台进行不同的协调。目前实现是基于istio网络平台。
4.创建k8s service:这个Service主要为Istio路由提供域名访问。

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这个黑盒
image

最后

这里只是基于简单的例子,分析了主要的业务流程处理代码。对于activator(如何唤醒业务容器),autoscaler(Pod如何自动缩为0)等代码实现有兴趣的同学可以一起交流。

参考

https://github.com/knative/docs/tree/master/docs/serving

相关实践学习
使用ACS算力快速搭建生成式会话应用
阿里云容器计算服务 ACS(Container Compute Service)以Kubernetes为使用界面,采用Serverless形态提供弹性的算力资源,使您轻松高效运行容器应用。本文将指导您如何通过ACS控制台及ACS集群证书在ACS集群中快速部署并公开一个容器化生成式AI会话应用,并监控应用的运行情况。
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。     相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
Prometheus Kubernetes 监控
容器服务ACK常见问题之pod设置securityContext调整参数失败如何解决
容器服务ACK(阿里云容器服务 Kubernetes 版)是阿里云提供的一种托管式Kubernetes服务,帮助用户轻松使用Kubernetes进行应用部署、管理和扩展。本汇总收集了容器服务ACK使用中的常见问题及答案,包括集群管理、应用部署、服务访问、网络配置、存储使用、安全保障等方面,旨在帮助用户快速解决使用过程中遇到的难题,提升容器管理和运维效率。
|
并行计算 TensorFlow 调度
推荐场景GPU优化的探索与实践:CUDA Graph与多流并行的比较与分析
RTP 系统(即 Rank Service),是一个面向搜索和推荐的 ranking 需求,支持多种模型的在线 inference 服务,是阿里智能引擎团队沉淀多年的技术产品。今年,团队在推荐场景的GPU性能优化上又做了新尝试——在RTP上集成了Multi Stream,改变了TensorFlow的单流机制,让多流的执行并行,作为增加GPU并行度的另一种选择。本文详细介绍与比较了CUDA Graph与多流并行这两个方案,以及团队的实践成果与心得。
|
机器学习/深度学习 人工智能 自然语言处理
一文搞懂【知识蒸馏】【Knowledge Distillation】算法原理
一文搞懂【知识蒸馏】【Knowledge Distillation】算法原理
一文搞懂【知识蒸馏】【Knowledge Distillation】算法原理
|
测试技术 开发工具 Swift
Liger kernel训练加速,一行代码训练吞吐量提高 20%,显存使用量降低 60%
在LLM的训练/微调过程中,开发者通常会遇到一些瓶颈,包括GPU显存不够,经常遇到OOM,GPU使用率100%而且非常慢等。
Liger kernel训练加速,一行代码训练吞吐量提高 20%,显存使用量降低 60%
|
存储 Cloud Native Nacos
恭喜 Nacos 和 Sentinel 荣获 2023 开源创新榜“优秀开源项目”
恭喜 Nacos 和 Sentinel 荣获 2023 开源创新榜“优秀开源项目”
367 87
|
JSON Kubernetes 负载均衡
第一次看 config dump
前言各位,知道的越多,就越会发现自己的无知。在面对服务网格这样的新兴概念之时,就更是如此了。回想昨日,满头大汗地研究VirtualService和DestinationRule是干什么用的自己仿佛还近在眼前。然而,在搞明白了服务网格的基本概念之后,我却发现自己甚至坠落进更大的疑惑之中了。如果你看过了一些istio的基本知识与概念,你应该知道istio为每个数据面的Pod都注入了一个Sidecar,
第一次看 config dump
|
Ubuntu Shell 开发者
helloworld 镜像 | 学习笔记
快速学习 helloworld 镜像
helloworld 镜像 | 学习笔记
|
存储 Kubernetes 搜索推荐
使用容器方式创建firecracker虚拟机
使用容器方式创建firecracker虚拟机
842 1