本教程展示了如何在 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 项目。 您可以创建一个新项目,或选择一个您已经创建的项目:
选择或创建 Google Cloud 项目:转到项目选择器页面
为您的项目启用计费。启用计费
创建工作目录
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 |
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}') |
kubectl create namespace argo-rollouts kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/download/v1.0.4/install.yaml |
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 |
cd $WORKDIR/$REPODIR/demoapp gcloud build submit . -t $DEMOAPPIMAGE |
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 就消失了。因为应用回滚到了之前的版本。
