灰度发布也叫金丝雀部署 ,是指通过控制流量的比例,实现新老版本的逐步替换。比如对于服务 A 有两个版本(蓝和绿两个版本),当前两个版本同时部署,但是 version1 比例 90% ,version2 比例 10% ,然后我们可以观察 version2 的实际运行效果,如果符合预期,则可以逐步调整流量占比,比如调整为 80:20 -> 70:30 -> 10:90 -> 0:100 ,最终 version1 版本下线,全部替换成 version2 版本。如果验证失败,切换 100%流量回 v1 版本(回滚)。灰度发布的特点是:
flagger
在 Istio 中要实现灰度发布有多种方案,比如 Flagger、Argo Rollouts 等。
Flagger
Flagger 是一个渐进式交付的 Kubernetes Operator,它可以自动执行 Kubernetes 上运行的应用程序的发布过程。它通过在测量指标和运行一致性测试的同时逐渐将流量转移到新版本,降低了在生产中引入新软件版本的风险。
Flagger 通过使用服务网格(App Mesh、Istio、Linkerd、Kuma、Open Service Mesh)或 Ingress 控制器(Contour、Gloo、NGINX、Skipper、 Traefik、APISIX)用于流量路由。对于发布分析,Flagger 可以查询 Prometheus、InfluxDB、Datadog、New Relic、CloudWatch、Stackdriver 或 Graphite,并使用 Slack、MS Teams、Discord 和 Rocket 来发出警报。
Flagger
Flagger 可以使用 Kubernetes CRD 进行配置,并且与任何为 Kubernetes 制作的 CI/CD 解决方案兼容。由于 Flagger 是声明性的对 Kubernetes 事件做出反应,因此它可以与诸如此类的工具一起在 GitOps 管道中使用。
安装 Flagger
要使用 Flagger,需要先选择一个受支持的路由提供商(比如我们这里使用 Istio),然后使用 Helm 或 Kustomize 安装 Flagger。
Flagger 需要 Kubernetes 集群 v1.16 或更高版本以及 Istio v1.5 或更高版本。
首先当然需要安装 Istio,并开启 Prometheus 插件:
# demo 或者 default 都可以
istioctl manifest install --set profile=demo -y
# istio 根目录
kubectl apply -f samples/addons/prometheus.yaml
然后在 istio-system 命名空间安装 Flagger:
$ git clone https://github.com/fluxcd/flagger && cd flagger
$ kubectl apply -k kustomize/istio
$ kubectl get pods -n istio-system -l app=flagger
NAME READY STATUS RESTARTS AGE
flagger-ff76bfdff-kkcmz 1/1 Running 0 17m
测试应用
下面我们创建一个名为 test 的命名空间,并为其启用 Istio sidecar 自动注入:
kubectl create ns test
kubectl label namespace test istio-injectinotallow=enabled
接下来我们使用 flagger 官方提供的 podinfo 应用来进行测试:
kubectl apply -k kustomize/podinfo
该命令会为 podinfo 应用创建对应的 Deployment 和一个 HPA 对象。
$ kubectl get deployment -n test
NAME READY UP-TO-DATE AVAILABLE AGE
podinfo 2/2 2 0 96s
$ kubectl get hpa -n test
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
podinfo Deployment/podinfo /99% 2 4 2 60s
部署后,我们可以看到 podinfo 应用的容器数量已经变成了 2 个(自动注入了 istio sidecar),而且 HPA 也已经生效。
```bash
$ kubectl get pods -n test
NAME READY STATUS RESTARTS AGE
podinfo-584c4546df-fzw4d 0/2 PodInitializing 0 5m26s
podinfo-584c4546df-n28cf 0/2 PodInitializing 0 5m11s
接着我们再部署一个负载测试服务用于在金丝雀分析期间生成流量:
kubectl apply -k kustomize/tester
创建金丝雀
接下来我们就可以创建一个 Canary 自定义资源来实现我们的金丝雀发布了。Canary 对象是 Flagger 的核心,它描述了金丝雀发布的目标。如下所示,我们为 podinfo 应用创建一个 Canary 对象:
# podinfo-canary.yaml
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: podinfo
namespace: test
spec:
targetRef: # deployment 引用
apiVersion: apps/v1
kind: Deployment
name: podinfo
progressDeadlineSeconds: 60 # 金丝雀部署升级最大处理时间(以秒为单位)(默认600秒)
autoscalerRef: # HPA 引用(可选)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
name: podinfo
service:
port: 9898
targetPort: 9898
gateways: # Istio 网关(可选)
- istio-system/public-gateway
hosts: # VirtualService 主机名 (optional)
- podinfo.k8s.local
trafficPolicy: # Istio 流量策略(可选)
tls:
# use ISTIO_MUTUAL when mTLS is enabled
mode: DISABLE
retries: # Istio 重试策略(可选)
attempts: 3
perTryTimeout: 1s
retryOn: "gateway-error,connect-failure,refused-stream"
analysis: # 金丝雀分析
interval: 1m # 金丝雀分析间隔时间(默认 60s)
threshold: 5 # 金丝雀分析失败阈值(默认 5)
maxWeight: 50 # 金丝雀最大流量权重(默认 50)
stepWeight: 10 # 金丝雀流量权重步长(默认 10)
metrics:
- name: request-success-rate
# minimum req success rate (non 5xx responses)
# percentage (0-100)
thresholdRange:
min: 99
interval: 1m
- name: request-duration
# maximum req duration P99
# milliseconds
thresholdRange:
max: 500
interval: 30s
# testing (optional)
webhooks:
- name: acceptance-test
type: pre-rollout
url: http://flagger-loadtester.test/
timeout: 30s
metadata:
type: bash
cmd: "curl -sd 'test' http://podinfo-canary:9898/token | grep token"
- name: load-test
url: http://flagger-loadtester.test/
timeout: 5s
metadata:
# 使用 hey 工具对 podinfo-canary 进行为期1分钟的负载测试,每秒发送10个请求,且测试过程中会维持2个并发连接。
cmd: "hey -z 1m -q 10 -c 2 http://podinfo-canary.test:9898/"
上面的配置文件中,我们定义了 podinfo 应用的金丝雀发布策略,其中 targetRef 指定了要进行金丝雀发布的 Deployment 对象,service 指定了金丝雀发布的服务,analysis 指定了金丝雀分析策略,这里我们指定了两个内置的指标检查:request-success-rate 和 request-duration,其中 request-success-rate 指定了 HTTP 请求成功率,request-duration 指定了请求持续时间。对于每个指标,你可以使用 thresholdRange 和窗口大小或时间序列指定可接受的值范围和时间间隔。内置检查适用于每个服务网格/Ingress 控制器,并通过 Prometheus 查询实现。
在 service 中我们指定了 Istio 的 Gateway(istio-system/public-gateway)以及 VirtualService 要使用的主机名,首先我们可以为该应用创建一个 Gateway 对象:
# public-gateway.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: public-gateway
namespace: istio-system
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
另外我们在上面的对象中通过 webhooks 字段指定了金丝雀分析期间要执行的测试,其中 acceptance-test 用于在金丝雀分析开始之前执行,load-test 用于在金丝雀分析期间执行。
金丝雀部署
接下来我们可以直接创建 Canary 对象了:
kubectl apply -f podinfo-canary.yaml
当创建了 Canary 对象后,Flagger 会自动创建一个名为 pod-info-primary 的 Deployment 以及两个版本的 Service 对象:
$ kubectl get deploy -n test
NAME READY UP-TO-DATE AVAILABLE AGE
flagger-loadtester 1/1 1 1 42m
podinfo 0/0 0 0 46m
podinfo-primary 2/2 2 2 7m3s
$ kubectl get svc -ntest
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flagger-loadtester ClusterIP 10.106.172.190 80/TCP 35m
podinfo-canary ClusterIP 10.101.184.213 9898/TCP 39s
podinfo-primary ClusterIP 10.110.105.36 9898/TCP 39s
可以看到我们原本的 podinfo 应用已经从 podinfo 这个 Deployment 迁移到了 podinfo-primary 这个 Deployment 之上。
此外还有 Istio 相关的对象:
$ kubectl get vs -ntest
NAME GATEWAYS HOSTS AGE
podinfo ["istio-system/public-gateway"] ["podinfo.k8s.local","podinfo"] 91s
$ kubectl get dr -ntest
NAME HOST AGE
podinfo-canary podinfo-canary 95s
podinfo-primary podinfo-primary 95s
我们可以查看下自动生成的 VirtualService 对象:
$ kubectl get vs -ntest podinfo -oyaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: podinfo
namespace: test
spec:
gateways:
- istio-system/public-gateway
hosts:
- podinfo.k8s.local
- podinfo
http:
- retries:
attempts: 3
perTryTimeout: 1s
retryOn: gateway-error,connect-failure,refused-stream
route:
- destination:
host: podinfo-primary
weight: 100
- destination:
host: podinfo-canary
weight: 0
从上面的配置中我们可以看到当前 podinfo 应用的流量全部被路由到了 podinfo-primary 对象上,而 podinfo-canary 对象的流量权重为 0,当然同样可以查看 DestinationRule 对象:
$ kubectl get dr podinfo-primary -ntest -oyaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: podinfo-primary
namespace: test
spec:
host: podinfo-primary
trafficPolicy:
tls:
mode: DISABLE
$ kubectl get dr podinfo-canary -ntest -oyaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: podinfo-canary
namespace: test
spec:
host: podinfo-canary
trafficPolicy:
tls:
mode: DISABLE
所以默认情况下现在我们访问到的就是 podinfo-primary 这个 Deployment 对象,也就是目前的默认版本。我们可以在浏览器中访问 podinfo 来查看当前的版本:
podinfo
自动金丝雀发布
我们可以看到现在的版本是 podinfo v6.0.0,接下来我们来升级应用触发金丝雀发布。要触发金丝雀发布,可以由以下任何对象的更改来触发:
- Deployment PodSpec(容器镜像、命令、端口、环境变量、资源等)
- 作为卷挂载或映射到环境变量的 ConfigMaps
- 作为卷挂载或映射到环境变量的 Secrets
比如我们可以直接修改 Deployment 对象的镜像版本来触发自动化的金丝雀发布:
kubectl -n test set image deployment/podinfo podinfod=ghcr.io/stefanprodan/podinfo:6.0.1
Flagger 检测到 Deployment 更改后就会开始新的部署:
$ kubectl describe canaries podinfo -ntest
# ......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Synced 15m flagger podinfo-primary.test not ready: waiting for rollout to finish: observed deployment generation less than desired generation
Normal Synced 14m (x2 over 15m) flagger all the metrics providers are available!
Normal Synced 14m flagger Initialization done! podinfo.test
Normal Synced 56s flagger New revision detected! Scaling up podinfo.test
需要注意在金丝雀分析期间对 Deployment 应用新的更改,Flagger 将重新启动分析。第一步是先会去扩容 podinfo 应用:
$ kubectl get pods -ntest
NAME READY STATUS RESTARTS AGE
flagger-loadtester-78dd9787d4-dq5fc 2/2 Running 0 67m
podinfo-5d5dbc4d84-f2mp6 2/2 Running 0 31s
podinfo-5d5dbc4d84-gd8ln 2/2 Running 0 31s
podinfo-primary-64f865cf4-bhr79 2/2 Running 0 3m31s
podinfo-primary-64f865cf4-tgsdj 2/2 Running 0 3m31s
然后就会根据我们在 Canary 对象中定义的金丝雀分析策略来进行分析,并一步步将金丝雀版本的权重提高。
Warning Synced 4m4s flagger podinfo-primary.test not ready: waiting for rollout to finish: observed deployment generation less than desired generation
Normal Synced 3m4s (x2 over 4m4s) flagger all the metrics providers are available!
Normal Synced 3m4s flagger Initialization done! podinfo.test
Normal Synced 64s flagger New revision detected! Scaling up podinfo.test
Normal Synced 4s flagger Starting canary analysis for podinfo.test
Normal Synced 4s flagger Pre-rollout check acceptance-test passed
Normal Synced 4s flagger Advance podinfo.test canary weight 10
最后会自动将流量全部切换到金丝雀版本上。
金丝雀版本
整个过程就是通过控制 VirtualService 的权重来实现的金丝雀发布。
自动回滚
在金丝雀分析期间,我们可以生成 HTTP 500 错误和高延迟来测试 Flagger 是否暂停发布。
比如我们触发另一个金丝雀发布:
kubectl -n test set image deployment/podinfo podinfod=ghcr.io/stefanprodan/podinfo:6.0.2
然后进入 loadtester 容器:
kubectl -n test exec -it flagger-loadtester-xx-xx sh
使用下面的命令来生成 HTTP 500 错误:
watch curl http://podinfo-canary:9898/status/500
也可以添加延迟:
watch curl http://podinfo-canary:9898/delay/1
当失败检查的次数达到金丝雀分析配置的阈值时,流量将路由回主节点,金丝雀将缩放为零,并将部署标记为失败。
Normal Synced 8m10s (x3 over 45m) flagger New revision detected! Scaling up podinfo.test
Normal Synced 7m10s (x2 over 44m) flagger Pre-rollout check acceptance-test passed
Normal Synced 7m10s (x2 over 44m) flagger Advance podinfo.test canary weight 10
Normal Synced 7m10s (x2 over 44m) flagger Starting canary analysis for podinfo.test
Warning Synced 6m10s flagger Halt podinfo.test advancement success rate 55.86% < 99%
Warning Synced 5m10s flagger Halt podinfo.test advancement success rate 97.61% < 99%
Warning Synced 4m10s flagger Halt podinfo.test advancement success rate 8.00% < 99%
Warning Synced 3m10s flagger Halt podinfo.test advancement success rate 98.13% < 99%
Warning Synced 2m10s flagger Halt podinfo.test advancement success rate 7.69% < 99%
Warning Synced 70s (x2 over 14m) flagger Canary failed! Scaling down podinfo.test
Warning Synced 70s flagger Rolling back podinfo.test failed checks threshold reached 5
关于 Flagger 的更多使用请关注后续文章。