虽然 Flagger 可以单独执行加权路由和 A/B 测试,但通过 Istio,它可以将两者结合起来,从而形成具有会话关联性的 Canary 版本。这种部署策略将金丝雀发布与 A/B 测试相结合,当我们尝试逐步向用户推出新功能时,金丝雀发布是很有帮助的,但由于其路由的特性(基于权重),即使用户之前已经被路由到新版本,他们仍然还有路由到应用程序的旧版本上,这种情况可能不符合我们的预期。
由于 A/B 测试对于需要会话关联的应用程序特别有用,因此我们将基于 cookie
的路由与常规的基于权重的路由集成在一起,这意味着一旦用户接触到我们应用程序的新版本(基于流量权重),他们总是会被路由到该版本,不会被路由回我们应用程序的旧版本。
我们可以通过在 Canary 对象中指定 .spec.anasyis.sessionAffinity
来启用此功能:
analysis:
# schedule interval (default 60s)
interval: 1m
# max number of failed metric checks before rollback
threshold: 10
# max traffic percentage routed to canary
# percentage (0-100)
maxWeight: 50
# canary increment step
# percentage (0-100)
stepWeight: 2
# session affinity config
sessionAffinity:
# name of the cookie used
cookieName: flagger-cookie
# max age of the cookie (in seconds)
# optional; defaults to 86400
maxAge: 21600
其中 .spec.analysis.sessionAffinity.cookieName
是存储的 Cookie 的名称,cookie 的值是随机生成的字符串,充当唯一标识符,对于上述配置,在 Canary 运行期间路由到 Canary 版本的请求的响应头将如下所示:
Set-Cookie: flagger-cookie=LpsIaLdoNZ; Max-Age=21600
Canary 运行结束并且所有流量都转移回主应用后,所有响应都将具有以下 Header:
Set-Cookie: flagger-cookie=LpsIaLdoNZ; Max-Age=-1
这告诉客户端删除 cookie,确保用户系统中没有垃圾 cookie。
如果触发新的 Canary 运行,响应标头将被路由到 Canary 版本的所有请求设置一个新的 cookie:
Set-Cookie: flagger-cookie=McxKdLQoIN; Max-Age=21600
比如我们这里的 podinfo
这个金丝雀对象如果想要启用会话亲和性,我们可以这样配置:
# podinfo-canary-session-affinity.yaml
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: podinfo
namespace: test
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: podinfo
progressDeadlineSeconds: 60
autoscalerRef:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
name: podinfo
service:
port: 9898
targetPort: 9898
gateways:
- istio-system/public-gateway
hosts:
- podinfo.k8s.local
trafficPolicy:
tls:
mode: DISABLE
retries:
attempts: 3
perTryTimeout: 1s
retryOn: "gateway-error,connect-failure,refused-stream"
analysis:
interval: 1m
threshold: 5
maxWeight: 50
stepWeight: 10
sessionAffinity: # session 亲和性配置
cookieName: flagger-cookie # cookie 名称
maxAge: 21600 # cookie 最大存活时间(秒),默认为 86400
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 30s
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:
cmd: "hey -z 1m -q 10 -c 2 http://podinfo-canary.test:9898/"
重新更新 Canary
对象:
kubectl apply -f podinfo-canary-session-affinity.yaml
更新后我们可以重新触发金丝雀发布:
kubectl -n test set image deployment/podinfo podinfod=ghcr.io/stefanprodan/podinfo:6.0.0
当在金丝雀的过程中,如果前端应用被路由到了 6.0.0 版本那么就会始终被路由到 6.0.0 版本,直到金丝雀发布结束,在请求中我们也可以看到对应的 cookie 信息:
cookie
这个时候我们查看 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:
- match:
- headers:
Cookie:
regex: .*flagger-cookie.*tmVCwNFKaj.*
name: sticky-route
retries:
attempts: 3
perTryTimeout: 1s
retryOn: gateway-error,connect-failure,refused-stream
route:
- destination:
host: podinfo-primary
weight: 0
- destination:
host: podinfo-canary
weight: 100
- retries:
attempts: 3
perTryTimeout: 1s
retryOn: gateway-error,connect-failure,refused-stream
route:
- destination:
host: podinfo-primary
weight: 50
- destination:
host: podinfo-canary
headers:
response:
add:
Set-Cookie: flagger-cookie=tmVCwNFKaj; Max-Age=21600
weight: 50
通过 VirtualService
对象将请求头中添加上 Cookie 信息,然后根据 Cookie 信息来进行路由,这样就可以实现会话亲和性了。
对于执行读取操作的应用程序,可以将 Flagger 配置为通过流量镜像驱动金丝雀版本。Istio 流量镜像将复制每个传入请求,将一个请求发送到主服务,并将一个请求发送到金丝雀服务,来自主节点的响应被发送回用户,来自金丝雀的响应被丢弃。收集两个请求的指标,以便仅当金丝雀指标在阈值范围内时才会继续部署。
流量镜像
我们可以通过用迭代替换 stepWeight/maxWeight
并将 analysis.mirror
设置为 true 来启用流量镜像:
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: podinfo
namespace: test
spec:
analysis:
interval: 1m
threshold: 5
# 迭代总数
iterations: 10
# 启用流量镜像
mirror: true
# 将流量镜像到金丝雀版本的权重(默认为100%)
mirrorWeight: 100
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
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:
cmd: "hey -z 1m -q 10 -c 2 http://podinfo.test:9898/"
通过上述配置,Flagger 将通过以下步骤运行金丝雀版本:
上述过程我们还可以通过自定义指标检查、webhook、手动升级批准以及 Slack 或 MS Teams 通知进行扩展。
接下来我们了解下如何使用 Istio 和 Flagger 进行 A/B 测试。除了加权路由之外,Flagger 还可以配置为根据 HTTP 匹配条件将流量路由到金丝雀版本,在 A/B 测试场景中,我们会使用 HTTP Header 或 cookie 来定位特定的用户群体,这对于需要会话关联的前端应用程序特别有用。
A/B 测试实验一般有 2 个目的:
我们一般比较熟知的是上述第 1 个目的,对于第 2 个目的,对于收益的量化,计算 ROI,往往对数据分析师和管理者非常重要。对于一般的 A/B 测试,其实本质上就是把平台的流量均匀分为几个组,每个组添加不同的策略,然后根据这几个组的用户数据指标,例如:留存、人均观看时长、基础互动率等等核心指标,最终选择一个最好的组上线。
所以 A/B 测试其实没有一个固定的标准,一般都是根据业务场景来定制的,比如我们可以根据用户的地域、设备、版本、渠道、用户行为等等来进行分组,然后针对不同的分组进行不同的策略,最后根据不同的指标来选择最好的组。
Istio A/B 测试
同样这里我们对上面的 podinfo 应用来进行 A/B 测试,创建一个如下所示的 Canary
对象:
# podinfo-ab.yaml
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: podinfo
namespace: test
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: podinfo
progressDeadlineSeconds: 60
autoscalerRef:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
name: podinfo
service:
port: 9898
gateways:
- istio-system/public-gateway
hosts:
- podinfo.k8s.local
trafficPolicy:
tls:
mode: DISABLE
analysis:
interval: 1m
iterations: 10
# 回滚前的最大失败迭代次数
threshold: 2
# 金丝雀匹配条件
match:
- headers:
user-agent:
regex: ".*Chrome.*"
- headers:
cookie:
regex: "^(.*?;)?(type=insider)(;.*)?$"
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 30s
webhooks: # 金丝雀分析期间生成流量
- name: load-test
url: http://flagger-loadtester.test/
timeout: 15s
metadata:
cmd: "hey -z 1m -q 10 -c 2 -H 'Cookie: type=insider' http://podinfo.test:9898/"
在上面的对象中我们增加了一个 match
字段,用于指定金丝雀匹配条件,这里我们指定了两个匹配条件,一个是 user-agent
,另一个是 cookie
,表示将针对 Chrome
用户和拥有 type=insider
cookie 的用户运行 10 分钟的金丝雀分析。
我们可以直接更新 Canary
对象:
kubectl apply -f podinfo-ab.yaml
更新后我们可以重新触发金丝雀发布:
kubectl -n test set image deployment/podinfo podinfod=ghcr.io/stefanprodan/podinfo:6.0.1
然后就会开始金丝雀发布过程了:
$ kubectl describe canary podinfo -ntest
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Synced 39m flagger Pre-rollout check acceptance-test passed
Normal Synced 39m flagger Advance podinfo.test canary weight 10
Normal Synced 38m flagger Advance podinfo.test canary weight 20
Normal Synced 37m flagger Advance podinfo.test canary weight 30
Normal Synced 36m flagger Advance podinfo.test canary weight 40
Normal Synced 35m flagger Advance podinfo.test canary weight 50
Normal Synced 34m flagger Copying podinfo.test template spec to podinfo-primary.test
Normal Synced 32m (x2 over 33m) flagger (combined from similar events): Promotion completed! Scaling down podinfo.test
Normal Synced 2m14s (x2 over 40m) flagger New revision detected! Scaling up podinfo.test
Normal Synced 74s (x2 over 39m) flagger Starting canary analysis for podinfo.test
Normal Synced 74s flagger Advance podinfo.test canary iteration 1/10
Normal Synced 14s flagger Advance podinfo.test canary iteration 2/10
这个时候如何我们打开 Chrome 浏览器访问 podinfo 应用,那么就会被路由到金丝雀版本上,而如果我们使用其他浏览器访问 podinfo 应用,那么就会被路由到主版本上:
A/B 测试
这样就可以实现 A/B 测试了,当然在实际的工作中 A/B 测试的条件可能会更加复杂,比如我们可以根据用户的地域、设备、版本、渠道、用户行为等等来进行分组,这需要结合实际的业务场景来进行配置。
作为分析过程的一部分,Flagger 可以验证服务级别目标 (SLO),例如可用性、错误率百分比、平均响应时间以及基于应用程序特定指标的任何其他目标。如果在 SLO 分析过程中发现性能下降,版本将自动回滚,将对最终用户的影响降到最低。
Flagger 附带两个内置指标检查:HTTP 请求成功率和持续时间。
analysis:
metrics:
- name: request-success-rate
interval: 1m
# minimum req success rate (non 5xx responses)
# percentage (0-100)
thresholdRange:
min: 99
- name: request-duration
interval: 1m
# maximum req duration P99
# milliseconds
thresholdRange:
max: 500
默认情况下,Flagger 使用 Prometheus 查询来测量请求成功率和持续时间。
在 Istio 中 HTTP 请求成功率对应的 PromQL 语句如下所示:
sum(
rate(
istio_requests_total{
reporter="destination",
destination_workload_namespace=~"{{ namespace }}",
destination_workload=~"{{ target }}",
response_code!~"5.*"
}[{{ interval }}]
)
)
/
sum(
rate(
istio_requests_total{
reporter="destination",
destination_workload_namespace=~"{{ namespace }}",
destination_workload=~"{{ target }}"
}[{{ interval }}]
)
)
同样 Istio 中 HTTP 请求的持续时间对应的 PromQL 语句为:
histogram_quantile(0.99,
sum(
irate(
istio_request_duration_milliseconds_bucket{
reporter="destination",
destination_workload=~"{{ target }}",
destination_workload_namespace=~"{{ namespace }}"
}[{{ interval }}]
)
) by (le)
)
istio_requests_total
以及 istio_request_duration_milliseconds_bucket
这两个指标都是 Istio 自带的,前面可观测性章节中我们已经介绍过了。
如果两个内置的指标检查不足以满足需求,Flagger 还支持自定义指标检查进行扩展。使用 MetricTemplate
自定义资源,可以将 Flagger 配置为连接到指标提供程序并运行返回 float64 值的查询,查询结果用于根据指定的阈值范围验证金丝雀。
比如我们想要自定义一个 Prometheus 的指标,那么可以通过将提供程序类型设置为 prometheus
并在 PromQL 中编写查询来创建针对 Prometheus 服务器的自定义指标检查。比如定义一个如下所示的指标模板:
apiVersion: flagger.app/v1beta1
kind: MetricTemplate
metadata:
name: not-found-percentage
namespace: istio-system
spec:
provider:
type: prometheus
address: http://prometheus.istio-system:9090
query: |
100 - sum(
rate(
istio_requests_total{
reporter="destination",
destination_workload_namespace="{{ namespace }}",
destination_workload="{{ target }}",
response_code!="404"
}[{{ interval }}]
)
)
/
sum(
rate(
istio_requests_total{
reporter="destination",
destination_workload_namespace="{{ namespace }}",
destination_workload="{{ target }}"
}[{{ interval }}]
)
) * 100
然后在 Canary
对象中引用这个指标模板即可:
analysis:
metrics:
- name: "404s percentage"
templateRef:
name: not-found-percentage
namespace: istio-system
thresholdRange:
max: 5
interval: 1m
上述配置通过检查 HTTP 404 请求/秒百分比是否低于总流量的 5% 来验证金丝雀,如果 404 率达到 5% 阈值,则金丝雀失败。