本教程展示了如何在 GKE 上进行声明式应用发布,如何在 Stackdriver Monitor 的帮助下,可以根据服务等级指标监控数据,自动控制应用程序的发布过程。这样我们就可以管理由发布过程引发的服务级别目标 (SLO) 未命中的风险,进而提高应用程序的整体可用性。


部署过程中的 SLO 监控

SLO 是 SRE 实践中的一个关键概念,保证应用程序的 SLO 是最重要的运维目标。当定义了 SLO 时,误差预算可以从以下公式得出:

错误预算 = 1-slo

错误预算可以被认为是在不破坏 SLO 的情况下在测量期间可能发生的错误数量。

在现实世界中,在生产中推出新版本的应用程序是有风险的:新版本可能存在缺陷,或者由于各种原因配置错误。

我们可以为 rollout 过程分配错误预算,并在 rollout 过程中监控错误预算消耗率。如果错误预算消耗率大于某个阈值,我们需要终止 rollout 过程并回滚更改。因此可以保证 SLO。下图描述了典型的金丝雀部署过程:

在 Stackdriver Monitoring 的帮助下,我们可以自动化流程,减少运维团队所需的手动操作。


声明式发布

发布过程可以通过两种方式完成:命令式方式和声明式方式。

命令式的方式就像一个组合了多个步骤的管道,每个步骤执行一个特定的任务。它易于理解和实现,但主要缺点是副作用和管道中的样板代码。这些缺点使部署过程难以扩展和管理。

相比之下,声明式推出需要输入

  • 要达到的预期状态

  • 要遵守的不变量(例如策略)

底层系统负责通过协调过程进行部署。声明式方法的主要好处是:

  • 幂等性 - 推出没有副作用

  • 收敛 - rollout 的状态收敛

  • 确定性 - 推出的状态是可预测的

最后,我们可以将声明放在 git 存储库中,并使用 Anthos Config Sync 等 GitOps 组件来管理发布。

Argo rollouts 正好是 Kubernetes 系统的声明式 rollout 控制器。


目标

学习在 Stackdriver 中为您的应用程序定义服务监控中的 SLO

学习使用 SLO 和错误预算来控制部署过程

学习以声明方式在 GKE 上发布应用


在你开始之前

对于本参考指南,您需要一个 GCP 项目。 您可以创建一个新项目,或选择一个您已经创建的项目:

  1. 选择或创建 Google Cloud 项目:转到项目选择器页面

  2. 为您的项目启用计费。启用计费

  3. 创建工作目录

mkdir -p ${HOME}/rollout-demo

cd ${HOME}/rollout-demo

export WORKDIR=`pwd`


准备环境

  • 设置环境变量

export PROJECT_ID=$(gcloud info --format='value(config.project)')

export NUM_PROJECT_ID=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")

export REPO=https://github.com/ChaosEternal/gcp-rollout-demo

export REPODIR=gke-rollout-demo

export DEMOAPPIMAGE=gcr.io/${PROJECT_ID}/demoapp:1.0

export SLOCHECKAPP=gcr.io/${PROJECT_ID}/slocheck:2.1

export CANARY_SERVICE_NAME=rollout-demo-canary

export STABLE_SERVICE_NAME=rollout-demo-stable

export SERVICE_ID=canary-service-$(uuidgen)

export SLO_SERVICE_NAME=$CANARY_SERVICE_NAME

export SLO_ID=canary-slo-$(uuidgen)

export K8SNS=default

  • 创建 GKE 并安装 ASM, 设置环境变量 K8SCLUSTER 为集群的名字,比如 democd

export K8SCLUSTER=democd

export K8SZONE=us-central1-a

export INGRESSIP=$(kubectl -n istio-system get svc istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

  • 安装 ArgoRollout

kubectl create namespace argo-rollouts

kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/download/v1.0.4/install.yaml

  • 安装 ArgoRollouts 的 kubectl plugin

curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64

sudo install -m 755 ./kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

  • $REPODIR/demoapp 里的 Dockerfile 构建 demoapp, 并上传到 gcr.io

cd $WORKDIR/$REPODIR/demoapp

gcloud build submit . -t $DEMOAPPIMAGE

  • 构建 slocheck 应用, Dockerfile 存在 $REPODIR/slocheck,也上传到 gcr.io

cd $WORKDIR/$REPODIR/slocheck

gcloud build submit . -t $SLOCHECKAPP


使用 Service Monitor

在本教程中,我们在发布过程中监控“金丝雀服务”的 SLO。 原因是新版本的发布过程直接影响“金丝雀服务”的 SLO。


创建要监控的服务

我们将“canary 服务”命名为 rollout-demo-canary。 使用以下 curl 命令在云监控中创建服务。

cat <<EOF >/tmp/service-$SERVICE_ID 

{

  "displayName": "display-${SLO_SERVICE_NAME}",

  "custom": {},

  "telemetry": {

    "resourceName": "//container.googleapis.com/projects/${PROJECT_ID}/zones/${K8SZONE}/clusters/democd/k8s/namespaces/${K8SNS}/services/${SLO_SERVICE_NAME}"

  }

}

EOF


curl --http1.1 --header "Authorization: Bearer $(gcloud auth print-access-token)"

   -X POST -d @/tmp/service-$SERVICE_ID --header "Content-Type: application/json"

   https://monitoring.googleapis.com/v3/projects/$PROJECT_ID/services?service_id=$SERVICE_ID


创建一个 SLO

我们定义一个基于请求数的 SLI: 

 GoodRequests 由以下规则决定:

{

       "goodServiceFilter":

             "goodServiceFilter": "metric.type="istio.io/service/client/request_count" resource.type="k8s_pod" metric.labels.destination_service_name="${SLO_SERVICE_NAME}" metric.labels.destination_service_namespace="${K8SNS}" metric.labels.response_code<"400" resource.labels.cluster_name=""${K8SCLUSTER}""<">

   

}

该规则统计发往集群${K8SCLUSTER} 中的 namespace ${K8SNS}的 Kubernetes service ${SLO_SERVICE_NAME} 所指向的 pod 的所有请求中,返回码小于400的请求数。

相应的,TotalRequests 是如下规则:

{...

             "totalServiceFilter": "metric.type="istio.io/service/client/request_count" resource.type="k8s_pod" metric.labels.destination_service_name="${SLO_SERVICE_NAME}" metric.labels.destination_service_namespace="${K8SNS}" resource.labels.cluster_name="${K8SCLUSTER}""

…}

该规则统计发往集群${K8SCLUSTER} 中的 namespace ${K8SNS}的 Kubernetes service ${SLO_SERVICE_NAME} 所指向的 pod 的所有请求。


用以下 curl 命令创建 SLO

cat <<EOF > /tmp/$SLO_ID

{

  "serviceLevelIndicator": {

    "requestBased": {

      "goodTotalRatio": {

        "goodServiceFilter": "metric.type="istio.io/service/client/request_count" resource.type="k8s_pod" metric.labels.destination_service_name="${SLO_SERVICE_NAME}" metric.labels.destination_service_namespace="${K8SNS}" metric.labels.response_code<"400" resource.labels.cluster_name=""${K8SCLUSTER}""<">,

        "totalServiceFilter": "metric.type="istio.io/service/client/request_count" resource.type="k8s_pod" metric.labels.destination_service_name="${SLO_SERVICE_NAME}" metric.labels.destination_service_namespace="${K8SNS}" resource.labels.cluster_name="${K8SCLUSTER}""

      }

    }

  },

  "goal": 0.8,

  "calendarPeriod": "DAY",

  "displayName": "80% - Good/Total Ratio - Calendar day"

}

EOF

curl --http1.1 --header "Authorization: Bearer $(gcloud auth print-access-token)"

   -X POST -d @/tmp/$SLO_ID --header "Content-Type: application/json"

https://monitoring.googleapis.com/v3/projects/$PROJECT_ID/services/${SERVICE_ID}/serviceLevelObjectives?service_level_objective_id=${SLO_ID}

此 SLO 表示在任何一个日历天里面,累积 SLI 必须大于0.8(目标), 即 80% 的请求都是好的。


创建一个能访问前面的 SLO 监控的 service account

使用以下命令创建 service account

gcloud iam service-accounts create k8s-slo-read

赋予角色 roles/monitoring.viewer

gcloud projects add-iam-policy-binding ${PROJECT_ID}

  --member serviceAccount:k8s-slo-read@${PROJECT_ID}.iam.gserviceaccount.com

  --role roles/monitoring.viewer


创建 workload identity 并赋予权限

在 namespace $K8SNS 里创建一个 Kubernetes service account

kubectl create serviceaccount -n $K8SNS slo-reader

标记 (annotate) 该 service account slo-reader 以正确的 Google Cloud service account:

kubectl annotate serviceaccount -n $K8SNS slo-reader iam.gke.io/gcp-service-account=k8s-slo-read@${PROJECT_ID}.iam.gserviceaccount.com

赋予权限

gcloud iam service-accounts add-iam-policy-binding

   --role roles/iam.workloadIdentityUser --member

   serviceAccount:${PROJECT_ID}.svc.id.goog[$K8SNS/slo-reader]

   k8s-slo-read@${PROJECT_ID}.iam.gserviceaccount.com


创建应用


创建 Kubernetes services 和 Istio 对象

我们用 Istio 来做流量切分,所以我们先创建两个 k8s services, 一个叫 

$STABLE_SERVICE_NAME 指向应用的稳定版, 另一个叫 $CANARY_SERVICE_NAME, 指向应用的金丝雀版:

创建服务:

kubectl -n $K8SNS apply -f - <<EOF

---

apiVersion: v1

kind: Service

metadata:

  name: $CANARY_SERVICE_NAME

spec:

  ports:

  - port: 80

    targetPort: http

    protocol: TCP

    name: http

  selector:

    app: rollouts-demo

---

apiVersion: v1

kind: Service

metadata:

  name: $STABLE_SERVICE_NAME

spec:

  ports:

  - port: 80

    targetPort: http

    protocol: TCP

    name: http

  selector:

    app: rollouts-demo

EOF

创建 Istio 对象

kubectl -n $K8SNS apply -f - <<EOF

apiVersion: networking.istio.io/v1alpha3

kind: Gateway

metadata:

  name: rollouts-demo-gateway

spec:

  selector:

    istio: ingressgateway # use istio default controller

  servers:

  - port:

      number: 80

      name: http

      protocol: HTTP

    hosts:

    - "*"

---

apiVersion: networking.istio.io/v1alpha3

kind: VirtualService

metadata:

  name: rollouts-demo-vsvc

spec:

  gateways:

  - rollouts-demo-gateway

  hosts:

  - rollouts-demo.default.example.com

  http:

  - name: primary

    route:

    - destination:

        host: $STABLE_SERVICE_NAME

      weight: 100

    - destination:

        host: $CANARY_SERVICE_NAME

      weight: 0

EOF


创建一个 Argo 的 AnalysisTemplate

Argo 的 analysis 定义了如何判断一个服务是健康的,而 AnalysisTemplate 则让 analysis 逻辑变得可复用。

计算金丝雀服务的 Error Budget 消耗速率 (Burnrate):

以下命令

kubectl -n $K8SNS apply -f - <<EOF

apiVersion: argoproj.io/v1alpha1

kind: AnalysisTemplate

metadata:

  name: gcp-slo-burnrate-check

spec:

  args:

  - name: target_project

  - name: threshold

  - name: defined_slo

  - name: checker_image

    value: ${SLOCHECKAPP}

  metrics:

  - name: test

    provider:

      job:

        spec:

          backoffLimit: 1

          template:

            metadata:

              annotations:

                sidecar.istio.io/inject: "false"

            spec:

              serviceAccountName: slo-reader

              restartPolicy: Never

              containers:

              - env:

                - name: TARGET_PROJECT

                  value: "{{ args.target_project }}"

                - name: THRESHOLD

                  value: "{{ args.threshold }}"

                - name: DEFINEDSLO

                  value: "{{ args.defined_slo }}"

                image: "{{ args.checker_image }}"

                name: ckslo

EOF


创建应用

使用以下命令创建工作负载。 请注意,此步骤是应用程序的初始创建,实际上并未进行任何新的发布。

kubectl -n $K8SNS apply -f - <<EOF

apiVersion: argoproj.io/v1alpha1

kind: Rollout

metadata:

  name: rollouts-demo

spec:

  replicas: 1

  strategy:

    canary:

      canaryService: $CANARY_SERVICE_NAME

      stableService: $STABLE_SERVICE_NAME

      trafficRouting:

        istio:

          virtualService:

            name: rollouts-demo-vsvc

            routes:

            - primary # At least one route is required

      steps:

      - setWeight: 5

      - pause:

          duration: 15s

  revisionHistoryLimit: 2

  selector:

    matchLabels:

      app: rollouts-demo

  template:

    metadata:

      labels:

        app: rollouts-demo

        istio-injection: enabled

    spec:

      containers:

      - name: rollouts-demo

        image: $DEMOAPPIMAGE

        ports:

        - name: http

          containerPort: 8080

          protocol: TCP

        resources:

          requests:

            memory: 32Mi

            cpu: 5m

EOF


测试应用

在另一个窗口中运行:

while sleep 0.1;

do

  curl -H "Host: rollouts-demo.default.example.com" $INGRESSIP;

done

由于我们的应用是第一次安装,因此前面命令的输出应该只包含“green”。

保持终端打开并运行命令。


应用升级为"yellow"

本步骤中“yello”的发布过程有两个步骤。 第一步将 10% 的流量拆分到新的(黄色)。 第二个是暂停动作,没有时间限制的暂停动作意味着需要用户干预才能通过这步骤。

使用以下命令将应用升级为“yello”:

kubectl -n $K8SNS apply -f - <<EOF

apiVersion: argoproj.io/v1alpha1

kind: Rollout

metadata:

  name: rollouts-demo

spec:

  replicas: 1

  strategy:

    canary:

      canaryService: $CANARY_SERVICE_NAME

      stableService: $STABLE_SERVICE_NAME

      trafficRouting:

        istio:

          virtualService:

            name: rollouts-demo-vsvc

            routes:

            - primary # At least one route is required

      steps:

      - setWeight: 10

      - pause: {}

  revisionHistoryLimit: 2

  selector:

    matchLabels:

      app: rollouts-demo

  template:

    metadata:

      labels:

        app: rollouts-demo

        istio-injection: enabled

    spec:

      containers:

      - name: rollouts-demo

        image: $DEMOAPPIMAGE

        args:

          - -cyello

        ports:

        - name: http

          containerPort: 8080

          protocol: TCP

        resources:

          requests:

            memory: 32Mi

            cpu: 5m

EOF

你会看到一些 yellows 出现在测试窗口的输出上。 

使用以下命令

kubectl argo rollouts promote rollouts-demo

所有的输出都变成 "yellow".


升级应用为一个出错的版本

此部署使工作负载运行到故障版本,20% 的响应代码为 500。因此错误预算开始以 1.0 的消耗率消耗。 因此,此次升级的分析步骤发现此消耗率大于阈值 (0.5) 并失败。 为了确保收集到的消耗率数据的准确性,我们在进行分析之前添加了一个时间受限(300 秒)的暂停。

运行命令:

kubectl -n $K8SNS apply -f - <<EOF

apiVersion: argoproj.io/v1alpha1

kind: Rollout

metadata:

  name: rollouts-demo

spec:

  replicas: 1

  strategy:

    canary:

      canaryService: $CANARY_SERVICE

      stableService: $STABLE_SERVICE

      trafficRouting:

        istio:

          virtualService:

            name: rollouts-demo-vsvc

            routes:

            - primary # At least one route is required

      steps:

      - setWeight: 20

      - pause:

          duration: 300s

      - analysis:

          templates:

          - templateName: gcp-slo-burnrate-check

          args:

          - name: target_project

            value: $PROJECT_ID

          - name: threshold

            value: "0.5"

          - name: defined_slo

            value:  projects/$NUM_PROJECT_ID/services/${SERVICE_ID}/serviceLevelObjectives/${SLO_ID}

      - setWeight: 40

      - pause:

          duration: 360s

  revisionHistoryLimit: 2

  selector:

    matchLabels:

      app: rollouts-demo

  template:

    metadata:

      labels:

        app: rollouts-demo

        istio-injection: enabled

    spec:

      containers:

      - name: rollouts-demo

        image: $DEMOAPPIMAGE

        args:

          - -cyello

          - -e20

        ports:

        - name: http

          containerPort: 8080

          protocol: TCP

        resources:

          requests:

            memory: 32Mi

            cpu: 5m

EOF

在最初的300秒里面,你会看到一些“ERROR”,但是接着这些 ERROR 就消失了。因为应用回滚到了之前的版本。



相关推荐