从HelloWorld看Knative Serving代码实现

柔情只为你懂 2022-01-14 05:11 212阅读 0赞

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

概念先知

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

看一个简单的示例

我们开始运行官方hello-world示例,看看会发生什么事情:

  1. apiVersion: serving.knative.dev/v1alpha1
  2. kind: Service
  3. metadata:
  4. name: helloworld-go
  5. namespace: default
  6. spec:
  7. runLatest: // RunLatest defines a simple Service. It will automatically configure a route that keeps the latest ready revision from the supplied configuration running.
  8. configuration:
  9. revisionTemplate:
  10. spec:
  11. container:
  12. image: registry.cn-shanghai.aliyuncs.com/larus/helloworld-go
  13. env:
  14. - name: TARGET
  15. value: "Go Sample v1"

查看 knative-ingressgateway:

  1. kubectl get svc knative-ingressgateway -n istio-system

21debdf2db18528fe306d610a244c577d6a.jpg

查看服务访问:DOMAIN

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

fa22bb7bbd9b626700210cd4f660955e719.jpg
这里直接使用cluster ip即可访问

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

bd0595ac98d84656baec56a34594c09b857.jpg

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

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

源代码分析

Main

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

  1. ...
  2. controllers := []*controller.Impl{
  3. configuration.NewController(
  4. opt,
  5. configurationInformer,
  6. revisionInformer,
  7. ),
  8. revision.NewController(
  9. opt,
  10. revisionInformer,
  11. kpaInformer,
  12. imageInformer,
  13. deploymentInformer,
  14. coreServiceInformer,
  15. endpointsInformer,
  16. configMapInformer,
  17. buildInformerFactory,
  18. ),
  19. route.NewController(
  20. opt,
  21. routeInformer,
  22. configurationInformer,
  23. revisionInformer,
  24. coreServiceInformer,
  25. clusterIngressInformer,
  26. ),
  27. labeler.NewRouteToConfigurationController(
  28. opt,
  29. routeInformer,
  30. configurationInformer,
  31. revisionInformer,
  32. ),
  33. service.NewController(
  34. opt,
  35. serviceInformer,
  36. configurationInformer,
  37. routeInformer,
  38. ),
  39. clusteringress.NewController(
  40. opt,
  41. clusterIngressInformer,
  42. virtualServiceInformer,
  43. ),
  44. }
  45. ...

Service

首先我们要从Service来看,因为我们一开始的输入就是Service资源。在/pkg/reconciler/v1alpha1/service/service.go。
比较简单,就是根据Service创建Configuration和Route资源

  1. func (c *Reconciler) reconcile(ctx context.Context, service *v1alpha1.Service) error {
  2. ...
  3. configName := resourcenames.Configuration(service)
  4. config, err := c.configurationLister.Configurations(service.Namespace).Get(configName)
  5. if errors.IsNotFound(err) {
  6. config, err = c.createConfiguration(service)
  7. ...
  8. routeName := resourcenames.Route(service)
  9. route, err := c.routeLister.Routes(service.Namespace).Get(routeName)
  10. if errors.IsNotFound(err) {
  11. route, err = c.createRoute(service)
  12. ...
  13. }

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路由提供域名访问。

  1. func (c *Reconciler) reconcile(ctx context.Context, r *v1alpha1.Route) error {
  2. ....
  3. // 基于是否有Ready的Revision
  4. traffic, err := c.configureTraffic(ctx, r)
  5. if traffic == nil || err != nil {
  6. // Traffic targets aren't ready, no need to configure child resources.
  7. return err
  8. }
  9. logger.Info("Updating targeted revisions.")
  10. // In all cases we will add annotations to the referred targets. This is so that when they become
  11. // routable we can know (through a listener) and attempt traffic configuration again.
  12. if err := c.reconcileTargetRevisions(ctx, traffic, r); err != nil {
  13. return err
  14. }
  15. // Update the information that makes us Addressable.
  16. r.Status.Domain = routeDomain(ctx, r)
  17. r.Status.DeprecatedDomainInternal = resourcenames.K8sServiceFullname(r)
  18. r.Status.Address = &duckv1alpha1.Addressable{
  19. Hostname: resourcenames.K8sServiceFullname(r),
  20. }
  21. // Add the finalizer before creating the ClusterIngress so that we can be sure it gets cleaned up.
  22. if err := c.ensureFinalizer(r); err != nil {
  23. return err
  24. }
  25. logger.Info("Creating ClusterIngress.")
  26. desired := resources.MakeClusterIngress(r, traffic, ingressClassForRoute(ctx, r))
  27. clusterIngress, err := c.reconcileClusterIngress(ctx, r, desired)
  28. if err != nil {
  29. return err
  30. }
  31. r.Status.PropagateClusterIngressStatus(clusterIngress.Status)
  32. logger.Info("Creating/Updating placeholder k8s services")
  33. if err := c.reconcilePlaceholderService(ctx, r, clusterIngress); err != nil {
  34. return err
  35. }
  36. r.Status.ObservedGeneration = r.Generation
  37. logger.Info("Route successfully synced")
  38. return nil
  39. }

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

  1. apiVersion: serving.knative.dev/v1alpha1
  2. kind: Route
  3. metadata:
  4. name: helloworld-go
  5. namespace: default
  6. ...
  7. spec:
  8. generation: 1
  9. traffic:
  10. - configurationName: helloworld-go
  11. percent: 100
  12. status:
  13. ...
  14. domain: helloworld-go.default.example.com
  15. domainInternal: helloworld-go.default.svc.cluster.local
  16. traffic:
  17. - percent: 100 # 所有的流量通过这个revision
  18. 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

  1. func (c *Reconciler) reconcile(ctx context.Context, config *v1alpha1.Configuration) error {
  2. ...
  3. // First, fetch the revision that should exist for the current generation.
  4. lcr, err := c.latestCreatedRevision(config)
  5. if errors.IsNotFound(err) {
  6. lcr, err = c.createRevision(ctx, config)
  7. ...
  8. revName := lcr.Name
  9. // Second, set this to be the latest revision that we have created.
  10. config.Status.SetLatestCreatedRevisionName(revName)
  11. config.Status.ObservedGeneration = config.Generation
  12. // Last, determine whether we should set LatestReadyRevisionName to our
  13. // LatestCreatedRevision based on its readiness.
  14. rc := lcr.Status.GetCondition(v1alpha1.RevisionConditionReady)
  15. switch {
  16. case rc == nil || rc.Status == corev1.ConditionUnknown:
  17. logger.Infof("Revision %q of configuration %q is not ready", revName, config.Name)
  18. case rc.Status == corev1.ConditionTrue:
  19. logger.Infof("Revision %q of configuration %q is ready", revName, config.Name)
  20. created, ready := config.Status.LatestCreatedRevisionName, config.Status.LatestReadyRevisionName
  21. if ready == "" {
  22. // Surface an event for the first revision becoming ready.
  23. c.Recorder.Event(config, corev1.EventTypeNormal, "ConfigurationReady",
  24. "Configuration becomes ready")
  25. }
  26. // Update the LatestReadyRevisionName and surface an event for the transition.
  27. config.Status.SetLatestReadyRevisionName(lcr.Name)
  28. if created != ready {
  29. c.Recorder.Eventf(config, corev1.EventTypeNormal, "LatestReadyUpdate",
  30. "LatestReadyRevisionName updated to %q", lcr.Name)
  31. }
  32. ...
  33. }

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

  1. apiVersion: serving.knative.dev/v1alpha1
  2. kind: Configuration
  3. metadata:
  4. name: helloworld-go
  5. namespace: default
  6. ...
  7. spec:
  8. generation: 1
  9. revisionTemplate:
  10. metadata:
  11. creationTimestamp: null
  12. spec:
  13. container:
  14. env:
  15. - name: TARGET
  16. value: Go Sample v1
  17. image: registry.cn-shanghai.aliyuncs.com/larus/helloworld-go
  18. name: ""
  19. resources: {}
  20. timeoutSeconds: 300
  21. status:
  22. ...
  23. latestCreatedRevisionName: helloworld-go-00001
  24. latestReadyRevisionName: helloworld-go-00001
  25. 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的工作负载

  1. func (c *Reconciler) reconcile(ctx context.Context, rev *v1alpha1.Revision) error {
  2. ...
  3. if err := c.reconcileBuild(ctx, rev); err != nil {
  4. return err
  5. }
  6. bc := rev.Status.GetCondition(v1alpha1.RevisionConditionBuildSucceeded)
  7. if bc == nil || bc.Status == corev1.ConditionTrue {
  8. // There is no build, or the build completed successfully.
  9. phases := []struct {
  10. name string
  11. f func(context.Context, *v1alpha1.Revision) error
  12. }{
  13. {
  14. name: "image digest",
  15. f: c.reconcileDigest,
  16. }, {
  17. name: "user deployment",
  18. f: c.reconcileDeployment,
  19. }, {
  20. name: "user k8s service",
  21. f: c.reconcileService,
  22. }, {
  23. // Ensures our namespace has the configuration for the fluentd sidecar.
  24. name: "fluentd configmap",
  25. f: c.reconcileFluentdConfigMap,
  26. }, {
  27. name: "KPA",
  28. f: c.reconcileKPA,
  29. }}
  30. for _, phase := range phases {
  31. if err := phase.f(ctx, rev); err != nil {
  32. logger.Errorf("Failed to reconcile %s: %v", phase.name, zap.Error(err))
  33. return err
  34. }
  35. }
  36. }
  37. ...
  38. }

最后我们看一下生成的Revision资源:

  1. apiVersion: serving.knative.dev/v1alpha1
  2. kind: Service
  3. metadata:
  4. name: helloworld-go
  5. namespace: default
  6. ...
  7. spec:
  8. generation: 1
  9. runLatest:
  10. configuration:
  11. revisionTemplate:
  12. spec:
  13. container:
  14. env:
  15. - name: TARGET
  16. value: Go Sample v1
  17. image: registry.cn-shanghai.aliyuncs.com/larus/helloworld-go
  18. timeoutSeconds: 300
  19. status:
  20. address:
  21. hostname: helloworld-go.default.svc.cluster.local
  22. ...
  23. domain: helloworld-go.default.example.com
  24. domainInternal: helloworld-go.default.svc.cluster.local
  25. latestCreatedRevisionName: helloworld-go-00001
  26. latestReadyRevisionName: helloworld-go-00001
  27. observedGeneration: 1
  28. traffic:
  29. - percent: 100
  30. revisionName: helloworld-go-00001

这里我们可以看到访问域名helloworld-go.default.svc.cluster.local,以及当前revision的流量配比(100%)
这样我们分析完之后,现在打开Serving这个黑盒fc64e62ced6d3c58e636f4a4dbaa2128182.jpg

最后

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

原文链接
本文为云栖社区原创内容,未经允许不得转载。

转载于:https://my.oschina.net/u/1464083/blog/3052268

发表评论

表情:
评论列表 (有 0 条评论,212人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Knative 简介

    本文作者来自蚂蚁金服系统部之芥 什么是 Knative? [knative][] 是谷歌开源的 serverless 架构方案,旨在提供一套简单易用的 serverle