使用Knative的服务管理组件管理应用

在部署一个Knative Service之前,先了解一下它的部署模型和对应的Kubernetes资源。

如下所示,在部署Knative Serving Service的过程中,Knative Serving控制器将创建configuration、Revision和Route三个资源对象。

Knative Serving的资源对象 

配置(configuration):Knative configuration维护了部署的目标状态,提供了一个干净的代码和配置分离、遵循12要素开发原则的机制。基于目标状态,Knative configuration控制器为应用创建了一个新的Kubernetes部署应用。并且configuration的变更会体现在一个新的Kubernetes部署应用中。

修订版(Revision):Knative configuration遵循12要素开发原则,每次应用的变更将会创建一个新的Knative Revision。Revision类似于版本控制中的标签。Revision一旦创建,是不可改变的。每个Revision都有一个对应的Kubernetes Deployment。它允许将应用程序回滚到任何正确的最新配置。

路由(Route):Knative Route是访问Knative Service的URL。

1 部署一个Knative Service

Go语言编写的程序代码为例,创建一个简单的Web服务。该服务接收到HTTP GET请求时,会根据环境变量Target传递的内容向Response输出Hello$TATGET!内容。

1)创建一个文件名为helloworld.go的文件。

package main
import (
  "fmt"
  "log"
  "net/http"
  "os"
)
func handler(w http.ResponseWriter, r *http.Request) {
  log.Print("helloworld: received a request")
  target := os.Getenv("TARGET")
  if target == "" {
    target = "World"
  }
  fmt.Fprintf(w, "Hello %s!\n", target)
}
func main() {
  log.Print("helloworld: starting server...")
  http.HandleFunc("/", handler)
  port := os.Getenv("PORT")
  if port == "" {
    port = "8080"
  }
  log.Printf("helloworld: listening on port %s", port)
  log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

2)使用Dockerfile构建源码并生成容器:

# Use the official Golang image to create a build artifact.
# This is based on Debian and sets the GOPATH to /go.
# https://hub.docker.com/_/golang
FROM golang:1.13 as builder
# Create and change to the app directory.
WORKDIR /app
# Retrieve application dependencies using go modules.
# Allows container builds to reuse downloaded dependencies.
COPY go.* ./
RUN go mod download
# Copy local code to the container image.
COPY . ./
# Build the binary.
#-mod=readonly ensures immutable go.mod and go.sum in container builds.
RUN CGO_ENABLED=0 GOOS=linux go build-mod=readonly-v-o server
# Use the official Alpine image for a lean production container.
# https://hub.docker.com/_/alpine
# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-
  stage-builds
FROM alpine:3
RUN apk add--no-cache ca-certificates
# Copy the binary to the production image from the builder stage.
COPY--from=builder /app/server /server
# Run the web service on container startup.
CMD ["/server"]
# 在本地主机构建容器。{username}替换为自己在dockerhub的用户名
docker build-t {username}/helloworld-go .
# 将容器Push到Docker容器镜像仓库。{username}替换为你自己在dockerhub的用户名
docker push {username}/helloworld-go

3)部署Knative Service。

Knative Service和其他Kubernetes资源类似,可以通过一个yaml文件进行定义和部署。接下来,使用上一步构建的容器来部署Knative Service服务。service.yaml配置文件如下:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: helloworld-go  # Service名称
  namespace: default
spec:
  template:
    metadata:
      name: helloworld-go-v1 # Knative Revision名称,如果未设置系统将会自动生成
    spec:
      containers:
     - image: {username}/helloworld-go
        env:
       - name: TARGET
          value: "Go Sample v1"
        livenessProbe:
          httpGet:
            path: /
        readinessProbe:
          httpGet:
            path: /

运行以下命令部署helloworld-go Knative Service:

# kubectl apply-f service.yaml

在该yaml配置文件中,Knative Service的kind值是Service。为了避免与Kubernetes内置的Service混淆,apiVersion的值需要设置为serving.knative.dev/v1。

配置文件中的spec区块与Kubernetes PodSpec的定义几乎一样,只是删除了以下属性:

· InitContainers
· RestartPolicy
· TerminationGracePeriodSeconds
· ActiveDeadlineSeconds
· DNSPolicy
· NodeSelector
· AutomountServiceAccountToken
· NodeName
· HostNetwork
· HostPID
· HostIPC
· ShareProcessNamespace
· SecurityContext
· Hostname
· Subdomain
· Affinity
· SchedulerName
· Tolerations
· HostAliases
· PriorityClassName
· Priority
· DNSConfig
· ReadinessGates
· RuntimeClassName

spec.template.metadata.name定义了Knative Revision的名称,这个名称是可选的。如果其被省略,系统会自动生成。

Knative Service的liveness探针与标准Kubernetes探针有微小区别。Knative Service探针定义中没有port属性定义。Knative Serving控制器在Service部署阶段能够自动确定port值。readiness探针也遵循同样的规则。

4)检查部署结果并验证服务:

# kubectl get ksvc helloworld-go
NAME           URL                    LATESTCREATED     LATESTREADY       READY  REASON
helloworld-go  http://helloworld-go.  helloworld-go-v1  helloworld-go-v1  True
               default.example.com

通过curl命令访问helloworld-go服务:

##获取集群任一节点的IP地址和nodePort端口
# IP_ADDRESS="$(kubectl get nodes-o 'jsonpath={.items[0].status.addresses[0].
  address}'):$(kubectl get svc istio-ingressgateway--namespace istio-system--
  output 'jsonpath={.spec.ports[?(@.port==80)].nodePort}')"
# curl-H "Host:helloworld-go.default.example.com" http://$IP_ADDRESS
Hello Go Sample v1!

上述脚本为了获取访问helloworld-go服务的IP地址以及端口号,选取了集群任一节点的IP地址、istio-ingressgateway服务的nodePort端口号。使用带有Service URL的主机名的头信息(例如Host:helloworld-go.default.example.com)即可访问helloworld-go服务。

CURL访问到服务后,Knative Serving则自动创建一个Pod副本来提供服务。当一段时间没有访问服务,Pod副本将会被销毁。我们可以通过watch kubectl get pods来监控Pod的生命周期。

2 更新Knative Service configuration

在部署完一个Knative Service后,因为应用版本的升级、配置的变更等需要更新现有服务的configuration。Knative服务还提供了一种机制实现回滚变更。

12要素应用设计原则规定,应用程序与配置的变更应被视为一个新的修订版。修订版是不可变更的应用和配置的状态集合。它可以让你回滚到任何一个有效的修订版状态。

应用的更新包括容器镜像的更新、健康检查探针的调整、环境变量的变更。这些变更会导致Knative生成新的修订版。每一个新修订版将创建一个新的Kubernetes Deployment对象。

接下来,通过一个更新服务配置的示例来演示配置的变更。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: helloworld-go  # Service名称
  namespace: default
spec:
  template:
    metadata:
      name: helloworld-go-v2 # Knative Revision名称
    spec:
      containers:
     - image: cnlab/helloworld-go
        env:
       - name: TARGET
          value: "Go Sample v2"
        livenessProbe:
          httpGet:
            path: /
        readinessProbe:
          httpGet:
            path: /

配置文件(service.yaml)的变更如下。

1)更新修订版的名称(.spec.template.metadata.name)为helloworld-go-v2,区别于上一个修订版名称helloworld-go-v1。

2)更新环境变量TARFET(.spec.template.spec.containers[0].env[0].value)的值为Go Sample v2。

将配置更新到Knative:

# kubectl apply-f service.yaml

检查部署结果:

# kubectl get ksvc helloworld-go
NAME            URL                    LATESTCREATED        LATESTREADY       READY  REASON
helloworld-go   http://helloworld-go.  helloworld-go-v2     helloworld-go-v2  True
                default.example.com

通过curl命令访问helloworld-go服务:

##获取集群任一节点的IP地址和nodePort端口
# IP_ADDRESS="$(kubectl get nodes-o 'jsonpath={.items[0].status.addresses[0].
  address}'):$(kubectl get svc istio-ingressgateway--namespace istio-system
 --output 'jsonpath={.spec.ports[?(@.port==80)].nodePort}')"
# curl-H "Host:helloworld-go.default.example.com" http://$IP_ADDRESS
Hello Go Sample v2!

查看部署后生成的Kubernetes Deployment:

# kubectl get deployments
NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
helloworld-go-v1-deployment   0/0     0            0           2m52s
helloworld-go-v2-deployment   1/1     1            1           2m19s

查看部署后生成的Kubernetes Pod:

# kubectl get pods
NAME                                          READY   STATUS    RESTARTS   AGE
helloworld-go-v2-deployment-589c5f7ff9-czpj2   3/3     Running   0          12s

helloworld-go对应的部署如果在扩缩容时间窗口期(默认60s)内没有请求,Knative将自动将对应的部署缩容为零。

查看部署后生成的Revision:

# kubectl get revision
NAME               CONFIG NAME    K8S SERVICE NAME   GENERATION   READY   REASON
helloworld-go-v1  helloworld-go   helloworld-go-v1  1            True
helloworld-go-v2  helloworld-go   helloworld-go-v2  2            True

可以看到helloworld-go的配置有两个修订版,分别是helloworld-go-v1和helloworld-go-v2。配置的变更产生了新的修订版,然而并没有产生新的路由、服务和配置对象。我们可以通过下面的命令来验证这些资源对象的状态。

·获取服务的路由信息的命令:kubectl get routes。

·获取Knative服务的状态信息的命令:kubectl get ksvc。

·获取Knative服务的配置信息的命令:kubectl get configurations。

Knative默认路由策略是将所有流量转发给最新的修订版。

3 流量分发到不同版本

在典型的微服务部署中,实现流量在不同版本中分发是实现金丝雀或蓝绿部署方式的基础。Knative提供了这种流量分发方式的支持。

Knative Service的yaml文件配置中,traffic区块描述了如何在多个版本之间进行流量分发。配置范例如下: 

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: helloworld-go  # Service名称
  namespace: default
spec:
  template:
    metadata:
      name: helloworld-go-v2 # Knative Revision名称
    spec:
      containers:
     - image: cnlab/helloworld-go
        env:
       - name: TARGET
          value: "Go Sample v2"
        livenessProbe:
          httpGet:
            path: /
        readinessProbe:
          httpGet:
            path: /
  traffic:
 - tag: v1  
    revisionName: helloworld-go-v1  # Revision的名称
    percent: 50  #流量切分的百分比的数字值
 - tag: v2
    revisionName: helloworld-go-v2  # Revision的名称
    percent: 50  #流量切分的百分比的数字值

traffic区块中可以有一个或多个条目。每个条目中带有如下属性。

·tag:流量分配的标识符。此标记将在路由中充当前缀,以便将流量分发到特定修订版。

·revisionName:参与流量分配的Knative服务修订版本的名称。

·percent:对应修订版被分配的流量百分比。这个值在0~100之间。在上述例子中,Knative分配给修订版helloworld-go-v1和helloworld-go-v2各50%的流量。

Knative Serving会为每个Tag创建独特的URL。可以通过下面的命令查看:

# kubectl get ksvc helloworld-go-o jsonpath='{.status.traffic[*].url}'
http://v1-helloworld-go.default.example.com
http://v2-helloworld-go.default.example.com

通过访问URL可以直接访问到对应的修订版。

4 蓝绿部署与灰度发布

一般情况下,升级服务端应用需要先停掉老版本服务,再启动新版服务。但是这种简单的发布方式存在两个问题,一方面在新版本升级过程中,服务是暂时中断的;另一方面,如果新版本升级失败,回滚起来非常麻烦,容易造成更长时间的服务不可用。

1.蓝绿部署

所谓蓝绿部署,是指同时运行两个版本的应用,即部署的时候,并不停掉老版本,而是直接部署一套新版本,等新版本运行起来后,再将流量切换到新版本上,如下所示。但是蓝绿部署要求服务端在升级过程中同时运行两套程序,对硬件资源的要求是日常所需的2倍。

蓝绿部署 

Knative提供了一个简单的切换流量的方法,可将流量快速从Revison1切换到Revision 2。如果Revision2发生错误,服务可以快速回滚变更到Revison1。

接下来,将通过helloworld-go这个Knative服务来应用蓝绿色部署模式。已经部署了拥有两个修订版的helloworld-go服务,名称分别为helloworld-go-v1和helloworld-go-v2。通过部署helloworld-go-v2,可以看到Knative自动将100%的流量路由到helloworld-go-v2。现在,假设出于某些原因,需要将helloworld-go-v2回滚到helloworld-go-v1。

以下示例中Knative Service与先前部署的helloworld-go相同,只是添加了traffic部分以指示将100%的流量路由到helloworld-go-v1。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: helloworld-go  # Service名称
  namespace: default
spec:
  template:
    metadata:
      name: helloworld-go-v2 # Knative Revision名称
    spec:
      containers:
     - image: cnlab/helloworld-go
        env:
       - name: TARGET
          value: "Go Sample v2"
        livenessProbe:
          httpGet:
            path: /
        readinessProbe:
          httpGet:
            path: /
  traffic:
 - tag: v1  
    revisionName: helloworld-go-v1     # Revision的名称
    percent: 100                 # 流量切分的百分比值
 - tag: v2
    revisionName: helloworld-go-v2     # Revision的名称
    percent: 0                     # 流量切分的百分比值
 - tag: latest                     # 默认最新的Revision
    latestRevision: true
    percent: 0                     # 关闭默认流量分配

2.灰度发布

灰度发布也叫金丝雀发布。如下所示,在灰度发布开始后,先启动一个新版本应用,但是并不直接将流量切过来,而是测试人员对新版本进行线上测试。启动的这个新版本应用,就是金丝雀。如果测试没有问题,可以将少量的流量导入新版本,然后再对新版本做运行状态观察,收集各种运行时数据。如果此时对新旧版本做数据对比,就是所谓的A/B测试。

 

灰度发布 

当确认新版本运行良好后,再逐步将更多的流量导入新版本。在此期间,还可以不断地调整新旧两个版本运行的服务器副本数量,使得新版本能够承受更大的流量压力,直到将100%的流量切换到新版本上,最后关闭剩下的老版本服务,完成灰度发布。

如果在灰度发布过程中(灰度期)发现新版本有问题,应该立即将流量切回老版本,这样就会将负面影响控制在最小范围内。

以下示例通过不断变更helloworld-go-v1和helloworld-go-v2的流量比例来实现helloworld-go服务新版本的灰度发布。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: helloworld-go               # Service名称
  namespace: default
spec:
  template:
    metadata:
      name: helloworld-go-v2          # Knative Revision名称
    spec:
      containers:
     - image: cnlab/helloworld-go
        env:
       - name: TARGET
          value: "Go Sample v2"
        livenessProbe:
          httpGet:
            path: /
        readinessProbe:
          httpGet:
            path: /
  traffic:
 - tag: v1  
    revisionName: helloworld-go-v1     # Revision的名称
    percent: 80                    # 流量切分的百分比值
 - tag: v2
    revisionName: helloworld-go-v2     # Revision的名称
    percent: 20                    # 流量切分的百分比值
 - tag: latest                    # 默认最新的Revision
    latestRevision: true
    percent: 0                    # 关闭默认流量分配

5 Knative Service的弹性伸缩配置

无服务器计算不仅能够终止未使用的服务,还可以按需扩展计算规模。Knative Serving支持这种弹性伸缩能力。

为了让Knative的Autoscaler更好地调度服务,需要根据实际情况在服务中添加相应的扩缩容配置项。下面以helloworld-go.yaml范例来演示扩缩容相关配置。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: helloworld-go  # Service名称
  namespace: default
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/class: "kpa.autoscaling.knative.dev"
 # Autoscaler的实现方式,可选值有"kpa.autoscaling.knative.dev" 或
   "hpa.autoscaling.knative.dev"
        autoscaling.knative.dev/metric: "concurrency"  # 设置度量指标为Concurrency
                                          (默认值),还可以根据业务情
                                          况选择RPS或CPU
        autoscaling.knative.dev/target: "10"  # 设置单个Pod最大并发数为10,默认值为100
        autoscaling.knative.dev/minScale: "1"   # minScale表示最小保留实例数为1
        autoscaling.knative.dev/maxScale: "100" # maxScale表示最大扩容实例数为3
    spec:
      containerConcurrency: 10                  # 并发请求数的硬性限制
      containers:
     - image: cnlab/helloworld-go

在上述配置中,revision中配置了修订版的弹性伸缩策略。各个属性代表的意义如下。

·autoscaling.knative.dev/class:表示Autoscaler的实现方式,这个属性的可选值有kpa.autoscaling.knative.dev或hpa.autoscaling.knative.dev。KPA支持缩容到零,HPA支持基于CPU的扩展机制。

·autoscaling.knative.dev/metric:度量指标默认为并发数,该属性还可以根据业务情况选择每秒请求数或CPU使用率。

·autoscaling.knative.dev/target:自动缩放的目标值是Autoscaler维护应用的每个副本度量指标的目标值。

·autoscaling.knative.dev/minScale:表示每个修订版副本需要保留的最小数量。在任何时间点,副本不会少于这个数量。通过该设置,我们可以有效地减少服务的冷启动时间。

·autoscaling.knative.dev/maxScale:表示每个修订版副本所能达到的最大数量。在任何时间点,副本都不会超过指定的最大值,从而避免资源被过度使用。

·containerConcurrency:限制容器在给定时间允许的并发请求的数量的硬性限制。只有当应用程序需要强制的并发约束时,才会使用到该属性。

部署helloworld-go服务并配置到Knative集群:

# kubectl apply-f helloworld-go.yaml

验证部署结果:

#IP_ADDRESS="$(kubectl get nodes-o 'jsonpath={.items[0].status.addresses[0].
  address}'):$(kubectl get svc istio-ingressgateway--namespace istio-system--
  output 'jsonpath={.spec.ports[?(@.port==80)].nodePort}')"
# curl-H "Host:helloworld-go.default.example.com" $IP_ADDRESS
Hello World!

压力测试:

# hey-c 50-z 30s-host "helloworld-go.default.knative.k8s.arch.dapp.com"
  "http://$IP_ADDRESS"

通过hey工具发起50个并发请求,持续30秒对hellowrold-go服务进行压测。

查看压测期间Pod的副本数量:

# kubectl get pod -l serving.knative.dev/service=helloworld-go
NAME                                             READY   STATUS    RESTARTS   AGE
helloworld-go-7t7sg-deployment-6bfbdb84fd-5l5gc  3/3     Running   0          42s
helloworld-go-7t7sg-deployment-6bfbdb84fd-99cdr  3/3     Running   0          42s
helloworld-go-7t7sg-deployment-6bfbdb84fd-ls4ks  3/3     Running   0          44s
helloworld-go-7t7sg-deployment-6bfbdb84fd-n4s4k  3/3     Running   0          44s
helloworld-go-7t7sg-deployment-6bfbdb84fd-q9kr8  3/3     Running   0          40s
helloworld-go-7t7sg-deployment-6bfbdb84fd-r77tt  3/3     Running   0          22m

通过上面的命令,可以看到集群中产生了6个Pod副本。那么问题来了,发起的并发请求数是50个,服务自动缩放的目标值是10,按照“副本数=并发数/目标值”算法,Pod副本数应该是5个才对呀。这是由于Knative Serving还有一个控制参数叫目标使用率,一旦并发数达到预设目标的70%(默认值),Autoscaler就会继续扩容。引入目标使用率的主要目的是在扩容时减小由Pod启动时间带来的延迟,使负载到达前就将Pod实例启动起来。

posted @ 2023-01-25 15:52  muzinan110  阅读(204)  评论(0编辑  收藏  举报