前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Istio 结合 Flagger 实现 A/B 测试

Istio 结合 Flagger 实现 A/B 测试

作者头像
我是阳明
发布2024-01-02 16:52:18
2420
发布2024-01-02 16:52:18
举报
文章被收录于专栏:k8s技术圈k8s技术圈

会话亲和性

虽然 Flagger 可以单独执行加权路由和 A/B 测试,但通过 Istio,它可以将两者结合起来,从而形成具有会话关联性的 Canary 版本。这种部署策略将金丝雀发布与 A/B 测试相结合,当我们尝试逐步向用户推出新功能时,金丝雀发布是很有帮助的,但由于其路由的特性(基于权重),即使用户之前已经被路由到新版本,他们仍然还有路由到应用程序的旧版本上,这种情况可能不符合我们的预期。

由于 A/B 测试对于需要会话关联的应用程序特别有用,因此我们将基于 cookie 的路由与常规的基于权重的路由集成在一起,这意味着一旦用户接触到我们应用程序的新版本(基于流量权重),他们总是会被路由到该版本,不会被路由回我们应用程序的旧版本。

我们可以通过在 Canary 对象中指定 .spec.anasyis.sessionAffinity 来启用此功能:

代码语言:javascript
复制
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 版本的请求的响应头将如下所示:

代码语言:javascript
复制
Set-Cookie: flagger-cookie=LpsIaLdoNZ; Max-Age=21600

Canary 运行结束并且所有流量都转移回主应用后,所有响应都将具有以下 Header:

代码语言:javascript
复制
Set-Cookie: flagger-cookie=LpsIaLdoNZ; Max-Age=-1

这告诉客户端删除 cookie,确保用户系统中没有垃圾 cookie。

如果触发新的 Canary 运行,响应标头将被路由到 Canary 版本的所有请求设置一个新的 cookie:

代码语言:javascript
复制
Set-Cookie: flagger-cookie=McxKdLQoIN; Max-Age=21600

比如我们这里的 podinfo 这个金丝雀对象如果想要启用会话亲和性,我们可以这样配置:

代码语言:javascript
复制
# 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 对象:

代码语言:javascript
复制
kubectl apply -f podinfo-canary-session-affinity.yaml

更新后我们可以重新触发金丝雀发布:

代码语言:javascript
复制
kubectl -n test set image deployment/podinfo podinfod=ghcr.io/stefanprodan/podinfo:6.0.0

当在金丝雀的过程中,如果前端应用被路由到了 6.0.0 版本那么就会始终被路由到 6.0.0 版本,直到金丝雀发布结束,在请求中我们也可以看到对应的 cookie 信息:

cookie

这个时候我们查看 VirtualService 对象可以发现里面就包含了会话亲和性的相关配置:

代码语言:javascript
复制
$ 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 来启用流量镜像:

代码语言:javascript
复制
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 将通过以下步骤运行金丝雀版本:

  • 检测新版本
  • 金丝雀版本从零开始扩展
  • 等待 HPA 设置金丝雀最小副本数
  • 检查 Canary pod 的健康状况
  • 运行验收测试
  • 如果测试失败则中止金丝雀发布
  • 开始负载测试
  • 将 100% 的流量从主版本镜像到金丝雀版本
  • 每分钟检查请求成功率和请求持续时间
  • 如果达到指标检查失败阈值,则中止金丝雀发布
  • 达到迭代次数后停止流量镜像
  • 将实时流量路由到 Canary Pod
  • 升级金丝雀
  • 等待主要部署完成
  • 等待 HPA 设置主最小副本数
  • 检查主 Pod 的运行状况
  • 将实时流量切换回主版本
  • 将金丝雀规模归零

上述过程我们还可以通过自定义指标检查、webhook、手动升级批准以及 Slack 或 MS Teams 通知进行扩展。

A/B 测试

接下来我们了解下如何使用 Istio 和 Flagger 进行 A/B 测试。除了加权路由之外,Flagger 还可以配置为根据 HTTP 匹配条件将流量路由到金丝雀版本,在 A/B 测试场景中,我们会使用 HTTP Header 或 cookie 来定位特定的用户群体,这对于需要会话关联的前端应用程序特别有用。

A/B 测试实验一般有 2 个目的:

  • 判断哪个更好:比如在 APP 界面上做了一个新的修改,究竟效果会不会更新,需要数据来判定
  • 计算收益:例如最近新上线了一个直播功能,那么直播功能究竟给平台带了来多少额外的 DAU,多少额外的使用时长,多少直播以外的视频观看时长等

我们一般比较熟知的是上述第 1 个目的,对于第 2 个目的,对于收益的量化,计算 ROI,往往对数据分析师和管理者非常重要。对于一般的 A/B 测试,其实本质上就是把平台的流量均匀分为几个组,每个组添加不同的策略,然后根据这几个组的用户数据指标,例如:留存、人均观看时长、基础互动率等等核心指标,最终选择一个最好的组上线。

所以 A/B 测试其实没有一个固定的标准,一般都是根据业务场景来定制的,比如我们可以根据用户的地域、设备、版本、渠道、用户行为等等来进行分组,然后针对不同的分组进行不同的策略,最后根据不同的指标来选择最好的组。

Istio A/B 测试

同样这里我们对上面的 podinfo 应用来进行 A/B 测试,创建一个如下所示的 Canary 对象:

代码语言:javascript
复制
# 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 对象:

代码语言:javascript
复制
kubectl apply -f podinfo-ab.yaml

更新后我们可以重新触发金丝雀发布:

代码语言:javascript
复制
kubectl -n test set image deployment/podinfo podinfod=ghcr.io/stefanprodan/podinfo:6.0.1

然后就会开始金丝雀发布过程了:

代码语言:javascript
复制
$ 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 请求成功率和持续时间。

代码语言:javascript
复制
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 语句如下所示:

代码语言:javascript
复制
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 语句为:

代码语言:javascript
复制
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 服务器的自定义指标检查。比如定义一个如下所示的指标模板:

代码语言:javascript
复制
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 对象中引用这个指标模板即可:

代码语言:javascript
复制
analysis:
  metrics:
    - name: "404s percentage"
      templateRef:
        name: not-found-percentage
        namespace: istio-system
      thresholdRange:
        max: 5
      interval: 1m

上述配置通过检查 HTTP 404 请求/秒百分比是否低于总流量的 5% 来验证金丝雀,如果 404 率达到 5% 阈值,则金丝雀失败。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-01-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 k8s技术圈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 会话亲和性
  • 流量镜像
  • A/B 测试
  • 自定义指标
相关产品与服务
腾讯云服务器利旧
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档