knative学习-02

knative学习-02

在上一节中我们在 Kubernetes 和 Istio 之上构建了 Knative,并部署了第一个 severless 应用,观察了当接收到请求时服务会运行起来,在停止发送请求后会自动将服务实例缩减为0。这是 Knative-Serving 的功能,本章我们一起来看下整个实现的过程。

回顾下 Knative-Serving 的主要功能:

  • 管理 Serverless 工作负载,支持快照和回滚
  • 自动伸缩,可缩至 0,也可从 0 扩展到无限
  • 基于 Istio 组件,提供路由和网络编程
  • 支持蓝绿发布、滚动更新

Knative-Serving 是基于 Kubernetes 和 Istio 开发的,它使用 Kubernetes 来管理容器(deployment、pod),使用istio 来管理网络路由(VirtualService、DestinationRule)。我们首先来了解 Knative-Serving 的主要构建模块,它的每一个构建模块都是由 CRD 定义并部署为 Knative-Serving 的一部分:

  • Configuration(configuration.serving.knative.dev):

    Configuration 是 Serverless 应用程序的核心,他负责将 Deployment(对,就是 Kubernetes 里的 Deployment) 标记为最新版本或固定为已知版本来保持 Deployment 的期望状态(比如 container 的image、env等),每次修改都会触发一次新的部署。Configuration 提供了代码和配置之间清晰的分离,并遵循应用开发的 12 要素。

    1
    2
    3
    4
    [root@master01 ~]# kubectl get configuration
    NAME LATESTCREATED LATESTREADY READY REASON
    fib-knative fib-knative-gqmkm fib-knative-gqmkm True
    helloworld-go helloworld-go-shsxr helloworld-go-shsxr True
  • Revision(revision.serving.knative.dev):

    Revision 资源是对服务进行的每个修改的代码和配置的快照,不可更改,每当有 Configuration 变更都会触发创建新的 Revision,类似 git commit。Revision 的历史记录由 Knative-Serving 维护,可以在任何指定的时间点部署所需要的 revision,实际上我们可以将 Configuration 看做 Revision 的上一级对象。遵循应用开发的 12 要素,

    1
    2
    3
    4
    [root@master01 ~]# kubectl get revision
    NAME CONFIG NAME K8S SERVICE NAME GENERATION READY REASON
    fib-knative-gqmkm fib-knative fib-knative-gqmkm 2 True
    helloworld-go-shsxr helloworld-go helloworld-go-shsxr 1 True
  • Route(route.serving.knative.dev):

    Route 通过 DNS 名称定义网络端口(Endpoint)使得客户端可以消费 Service,例如:helloworld-go.knativetutorial.example.com。一个 Service 可能有一条或多条 route 映射到一个或多个 Revision。

    1
    2
    3
    4
    [root@master01 ~]# kubectl get route
    NAME URL READY REASON
    fib-knative http://fib-knative.knativetutorial.example.com True
    helloworld-go http://helloworld-go.knativetutorial.example.com True
  • Service(service.serving.knative.dev)

    Service 就不是 kubernetes 里的 service 了,他是在 Configuration、Revision 和 Route 之上更高级的资源对象。当部署一个 Serverless 工作负载,Service 可以自动创建 Configuration 和 Route,并可以定义规则使请求路由到不同的 Revision 上,从而管理 Serverless 工作负载的整个生命周期。

    1
    2
    3
    4
    [root@master01 ~]# kubectl get ksvc
    NAME URL LATESTCREATED LATESTREADY READY REASON
    fib-knative http://fib-knative.knativetutorial.example.com fib-knative-gqmkm fib-knative-gqmkm True
    helloworld-go http://helloworld-go.knativetutorial.example.com helloworld-go-shsxr helloworld-go-shsxr True

    以下是我们上回部署的 helloworld-go 定义 Service 的 yaml 文件,创建了一个名为 helloworld-goserving.knative.dev 的 Service,默认 Configuration 使用最新版的 revision,且默认会把流量全部分发给最新的 revision,container 下指定了 image 和 env,和 pod 的定义类似,当然还可以有其他的定义,如:configmap、secret、traffic。非常精简的接口设计,全都是因为 Knative 在 Kubernetes 和 Istio 之上提供了更高层次的抽象,自动帮我们封装掉了 kubernetes 和 istio 的实现细节,Service 会自动生成并管理资源对象如:deployment、ingress、service discovery、auto scaling……

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [root@master01 ~]# cat service.yaml
    apiVersion: serving.knative.dev/v1alpha1 # Current version of Knative
    kind: Service
    metadata:
    name: helloworld-go # The name of the app
    spec:
    template:
    spec:
    containers:
    - image: gcr.io/knative-samples/helloworld-go # The URL to the image of the app
    env:
    - name: TARGET # The environment variable printed out by the sample app
    value: "Go Sample v1"

下面我们来看看 knative-serving 都有哪些组件:

1
2
3
4
5
6
7
8
[root@master01 ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
activator-68b98f8d65-gql2t 1/1 Running 0 22d
autoscaler-6984fb88bf-cxhgf 1/1 Running 0 22d
autoscaler-hpa-f584bccd7-5pp5l 1/1 Running 0 22d
controller-6f4d6b5f94-6xgss 1/1 Running 0 22d
networking-istio-7d9bfbcd86-75rbk 1/1 Running 0 22d
webhook-584d7fc486-j7sdt 1/1 Running 0 22d

其中各组件的作用大致为:

  • Controller 负责 Service 整个生命周期的管理。
  • Autoscaler 根据应用的请求并发量对应用实例扩缩容。
  • Activator 监控外部请求,并负责将应用实例从0扩展到1。
  • Webhook 主要负责创建和更新的参数校验。
  • networking-istio 使用 istio 的流量管理能力。
  • 还有一个组件 queue-proxy,作为应用的 pod 的 sidecar,负载拦截转发给 Pod 的请求,用于统计 Pod 的请求并发量和资源使用情况。

组件之间的关系大致如下图所示:

当创建 knative service 后,它会创建出 route 和 configuration,由 controller 来管理这些对象的 CRD 。然后根据 configuration 创建 revision,每次修改 Configuration 都会触发创建新的 revision,根据 revision 创建 deployment 启动业务 pod,每个 revision 对应了一组 deployment 管理的 pod。knative-serving 依赖 istio 提供网络能力,根据 route 创建 k8s service 和 istio VirtualService ,负责流量如何转发。
knative 依赖 istio 提供网络能力,所有请求会由 istio-ingressgateway 根据 route 创建的访问域名转发到应用。当第一个访问请求进入 istio-ingressgateway 时会被转发给 activator,activator 拦截请求后先修改 deployment 的 replicas 数量1从而拉起1个 pod,等待启动成功后 activator 再把流量转发给 pod,此后再进入的请求都将直接被转发到 pod。而所有发送到 pod 的请求又都会被 queue-proxy 拦截,他会统计当前 pod 的请求并发数和资源使用量,并开放一个 metric 接口,autoscaler 通过监听该端口去获取 Pod 的 metrics 数据并计算是否需要扩缩容。当需要扩缩容时,autoscaler 会通过修改 revision 对应的 deployment 的 replicas 数量来实现扩缩容。在应用长时间无请求访问时,autoscaler 修改 deployment 的 replicas 数量为0,pod 即缩减到 0。

下面逐层去查看相关资源对象的配置文件

首先是最顶级的资源对象 knative 的 Service,也是我们使用中实际要去写的文件。上面介绍过,我们仔细看他的 status,可以知道以下信息:

  1. 定义了内部和外部的访问域名
  2. Service、Configuration、和 route 都已经 Ready
  3. 最新一次生成的 Revision 是 helloworld-go-shsxr,目前在用的 Revision 也是 helloworld-go-shsxr
  4. 流量默认全部分发到最新的 Revision(最后演示在 Service 里定义 traffic 来定义分发流量的规则)

我们再来看 Configuration,可以看到其上一级资源对象为 knative 的 Service,container 中的定义与 Service 中定义的一样,还能配置资源限制和就绪性检测。

Revision 的上一级对应的哪个 Configuration、对应哪个 knative 的 Service 和哪个 Route,container 中的定义与 Service、Configuration 中定义的一样,status 显示此时已经没有请求进来了。

Controller 根据 Revision 的 container 的定义先调用 Kubernetes API 创建出对应的 deployment。前面说了 activator 会触发从0创建一个pod,再由 autoscaler 负责监听 metrics 判断并执行扩缩容。

可以看到 PodAutoscaler 由 Revision 管理,它和 deployment 对应,当有请求访问应用时,controller 会根据 autoscaler 来修改 replicas 的数量

那怎么控制 autoscaler 的策略呢?主要是在 namespace knative-serving 下的 ConfigMap config-autoscaler 定义的。

关键参数:

  1. pod的请求并发数
    1
    2
    3
    4
    # 默认请求并发数为 100
    container-concurrency-target-default: 100
    # 可以在 Revision template 中添加 annotation 来修改
    autoscaling.knative.dev/target: 50
  1. 缩容到0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 启用缩容到 0 的功能
    enable-scale-to-zero: true
    # 缩小到0前的剩余运行时间(最小:6s)
    scale-to-zero-grace-period: 30s

    # 在稳定模式下运行时,autoscaler 检测请求并发时间(最小:6s)。
    stable-window: 60s
    # 可以在 Revision template 中添加 annotation 来修改
    autoscaling.knative.dev/window: 60s

终止时间是在最后一个请求完成后关闭 pod 所需的时间。pod 的终止周期等于 stable-windowscale-to-zero-grace-period 的值之和。所以我们上回观察到 90s pod 才终止。

  1. 定义 pod 数量的最小和最大值,可用于防止冷启动或帮助控制计算成本

    1
    2
    3
    4
    5
    6
    # minScale 和 maxScale 可以在 Revision template 中添加 annotation 来修改
    spec:
    template:
    metadata:
    autoscaling.knative.dev/minScale: "2"
    autoscaling.knative.dev/maxScale: "10"
  2. KPA 是基于请求的伸缩策略,也可以使用 HPA 配置基于 CPU 资源用量的伸缩策略

    1
    2
    3
    4
    5
    spec:
    template:
    metadata:
    autoscaling.knative.dev/metric: concurrency
    autoscaling.knative.dev/class: hpa.autoscaling.knative.dev

其实还有一个资源对象 serverlessservice,它有 autoscaler 管理和 deployment 对应,controller 会根据他创建三个 k8s service 分别对应内部域名、外部访问域名和 prometheus metrics

Route 的上一级对应的哪个 knative 的 Service,定义了所有流量分发给最新的 Revision。内部域名就是 k8s service,那么外部域名在哪里定义的?只看到状态里有外部的域名,没见在哪里定义。

经查发现,所有路由的域名配置由 namespace knative-serving 下的 ConfigMap config-domain 来定义的,路由的默认DNS名称将类似于 [servicename].[namespace].[domain value from config-domain],例如:helloworld-go.knativetutorial.example.com

可以替换configmap中的域名 example.com,例如我们让 Route 使用域名 mydomain.com:

1
2
3
4
5
apiVersion: v1
data:
mydomain.com: ""
kind: ConfigMap
[...]

Route 调用 istio 的 API 去实现网络分发,那他是怎么做的呢?Controller 根据 Route 创建了 virtualservice 定义了内外部访问的路由策略。

那这个带有 -mesh 的 virtualservice 是做什么的呢?我们说默认 knative 默认是用 Istio 网关服务 istio-ingressgateway ,那这个是在哪里指定的呢?

首先我们找到 config-istio 这个 configmap,里面定义了 knative v0.3 以后使用 istio-ingressgateway 作为 knative 默认网关,他提供了一个外部可访问的 public url。而 cluster-local-gateway 正好是我们不想让一些服务被外部访问,只能在集群内部被访问时需要用到的,但我们一开始没安装,演示也暂时不会用到,需要的话可以后装。

也正因为 ConfigMap config-istio ,于是就有了这个两个对应的 gateway。knative 就是在这里调用了 istio gateway 的网络能力。

最后基于 helloworld-go 这个 Service 演示创建新的 Configuration 和 Route 并定义 traffic 分发流量到两个不同的 Revision。如图所示,定义新的 revision helloworld-go-2,为区别修改环境变量的值为v2,添加 traffic 定义路由规则,给前一个 Revision 分配 30% 的权重,新的 Revision 分配 70%。

traffic 中定义的 tag 可以让我们通过不同的 tag 访问到不同的 Revision

可观测性(Observability)

相对于编写微服务来说,一个服务只专注于一个功能,其复杂度已经非常低,但是当非常多的服务共同工作的时候,服务之间会互相调用,随着微服务数量逐渐增多,复杂度上升,如何管理这些微服务就成了一个必须解决的问题。

  • 如何快速找到某个微服务?
  • 如何知道一个微服务的功能是什么?接收的参数是什么?
  • 怎么保证服务的升级不会破坏原有的功能?升级之后如何回滚?怎么记录服务的历史版本追溯?
  • 当有多个服务需要同时工作的时候,怎么定义它们之间的关系?
  • 服务出现问题的时候如何调试?

去年的时候我还没看过文档,一直以为 knative 也是通过调用 Istio API 实现对应用的可观测性,其实不是的。对于 Serverless 服务的运维,knative 也提供了 logging、metrics、tracing 三个方面的功能,和 istio 类似。默认情况下,knative 使用 EFK(Elasticsearch、Fluentd、Kibana)来收集、查找和分析日志;使用 prometheus + grafana 来收集和索引、展示 metrics 数据;使用 zipkin, jaeger 来进行调用关系的 tracing。第一次我们已经安装了 Monitoring、Logging 和 Zipkin,所以还需要安装 Jaeger。

prometheus 主要提供了以下维度的监控

• Revision HTTP Requests: HTTP request count, latency, and size metrics per revision and per configuration
• Nodes: CPU, memory, network, and disk metrics at node level
• Pods: CPU, memory, and network metrics at pod level
• Deployment: CPU, memory, and network metrics aggregated at deployment level
• Istio, Mixer and Pilot: Detailed Istio mesh, Mixer, and Pilot metrics
• Kubernetes: Dashboards giving insights into cluster health, deployments, and capacity usage

EFK 收集了主机、Kubernetes集群、请求、Configuration、Request等多个维度的日志

通过 Kibana 查看请求数据,发现 activator 拉起了1个 pod

分布式追踪主要是 zipkin 和 Jaeger

zipkin 持久化端到端的追踪数据到 elasticsearch 中,通过 Kibana 查看请求 traceid,请求流经的端点,处理请求的时间,返回状态码等

部署 Jaeger-operator

1
2
3
4
5
6
7
8
9
[root@master01 ~]#  git clone https://github.com/jaegertracing/jaeger-operator.git
[root@master01 ~]# sed -i "s/^[ ]*namespace.*/ namespace\: knative-monitoring/g" jaeger-operator/deploy/crds/jaegertracing_v1_jaeger_crd.yaml
[root@master01 ~]# sed -i "s/^[ ]*namespace.*/ namespace\: knative-monitoring/g" jaeger-operator/deploy/*.yaml
[root@master01 ~]# cd jaeger-operator/deploy/
[root@master01 ~]# kubectl create -f crds/jaegertracing_v1_jaeger_crd.yaml
[root@master01 ~]# kubectl create -f service_account.yaml
[root@master01 ~]# kubectl create -f role.yaml
[root@master01 ~]# kubectl create -f role_binding.yaml
[root@master01 ~]# kubectl create -f operator.yaml

我们这里把 tracing 的数据也和 zipkin 一样持久化到 elasticsearch 里,修改 namespace 为 knative-monitoring, 然后创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[root@master01 deploy]# wget https://github.com/knative/serving/releases/download/v0.9.0/monitoring-tracing-jaeger.yaml
[root@master01 deploy]# vi monitoring-tracing-jaeger.yaml
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
labels:
serving.knative.dev/release: "v0.9.0"
name: jaeger
namespace: knative-monitoring
spec:
storage:
options:
es:
server-urls: http://elasticsearch-logging.knative-monitoring.svc.cluster.local:9200
type: elasticsearch
strategy: production
---
apiVersion: v1
kind: Service
metadata:
labels:
serving.knative.dev/release: "v0.9.0"
name: zipkin
namespace: istio-system
spec:
ports:
- name: http
port: 9411
selector:
app: jaeger
[root@master01 ~]# kubectl create -f monitoring-tracing-jaeger.yaml

好了,今天就先到这里,下次继续讲解。