基于Nginx Ingress Controller网关实现推理服务的灰度发布

Raw Deployment部署模式下,应用的灰度发布需要基于网关实现。本文以Nginx Ingress Controller网关为例,介绍如何实现推理服务的灰度发布,并最终平稳地完成从v1v2版本推理服务的升级。

前提条件

操作说明

本文将部署两个推理服务,分别为canaryv1版本和v2版本。基于这两个服务来演示两种灰度策略场景,并最终完成版本切换:

  • 基于客户端请求的流量切分。

    将带有请求头foo: bar的请求定向model-v2-svc的服务为例,其他请求默认指向model-svc

  • 基于服务权重的流量切分。

    20%的流量被导向名为model-v2-svc的服务,剩余流量继续流向model-svc

关于通过Nginx Ingress实现灰度发布的背景信息及原理说明,请参见通过Nginx Ingress实现灰度发布和蓝绿发布

步骤一:部署并验证推理服务

v1版本

  1. 部署模型名称为canaryv1版本推理服务。

    arena serve kserve \
        --name=model-v1 \
        --image=kube-ai-registry.cn-shanghai.cr.aliyuncs.com/ai-sample/kserve-canary:1.0.0 \
        --cpu=1 \
        --memory=2Gi \
        "python app.py --model_name=canary"
  2. 创建v1版本推理服务的Service。

    1. 创建model-svc.yaml。

      apiVersion: v1
      kind: Service
      metadata:
        name: model-svc
      spec:
        ports:
        - port: 80
          protocol: TCP
          targetPort: 8080
        selector:
          serving.kserve.io/inferenceservice: model-v1
        type: ClusterIP
    2. 创建Service。

      kubectl apply -f model-svc.yaml
  3. 通过Nginx Ingress访问一个名为model-v1Inference Service,以验证model-v1是否正确部署。

    curl -H "Host: $(kubectl get inferenceservice model-v1 -o jsonpath='{.status.url}' | cut -d "/" -f 3)" \
         -H "Content-Type: application/json" \
         http://$(kubectl -n kube-system get svc nginx-ingress-lb -ojsonpath='{.status.loadBalancer.ingress[0].ip}'):80/v1/models/canary:predict -X POST \
         -d '{"data": "test"}'

v2版本

  1. 部署模型名称为canaryv2版本推理服务。

    arena serve kserve \
        --name=model-v2 \
        --image=kube-ai-registry.cn-shanghai.cr.aliyuncs.com/ai-sample/kserve-canary:1.0.0 \
        --cpu=1 \
        --memory=2Gi \
        "python app-v2.py --model_name=canary"
  2. 创建v2版本推理服务的Service。

    1. 创建model-v2-svc.yaml。

      apiVersion: v1
      kind: Service
      metadata:
        name: model-v2-svc
      spec:
        ports:
        - port: 80
          protocol: TCP
          targetPort: 8080
        selector:
          serving.kserve.io/inferenceservice: model-v2
        type: ClusterIP
    2. 创建Service。

      kubectl apply -f model-v2-svc.yaml
  3. 通过Nginx Ingress访问一个名为model-v2Inference Service,以验证model-v2是否正确部署。

    curl -H "Host: $(kubectl get inferenceservice model-v2 -o jsonpath='{.status.url}' | cut -d "/" -f 3)" \
         -H "Content-Type: application/json" \
         http://$(kubectl -n kube-system get svc nginx-ingress-lb -ojsonpath='{.status.loadBalancer.ingress[0].ip}'):80/v1/models/canary:predict -X POST \
         -d '{"data": "test"}'

步骤二:创建Ingress

  1. 创建model-ingress.yaml。

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: model-ingress
    spec:
      rules:
      - host: model.example.com # 替换为您实际业务的Host。
        http:
          paths:
          # 老版本服务。
          - path: /
            backend:
              service: 
                name: model-svc
                port:
                  number: 80
            pathType: ImplementationSpecific
  2. 创建Ingress。

    kubectl apply -f model-ingress.yaml

步骤三:创建并验证灰度策略

场景一:基于客户端请求的流量切分场景

  1. 创建gray-release-canary.yaml。

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: gray-release-canary
      annotations:
        # 开启Canary。
        nginx.ingress.kubernetes.io/canary: "true"
        # 请求头为foo。
        nginx.ingress.kubernetes.io/canary-by-header: "foo"
        # 请求头foo的值为bar时,请求才会被路由到新版本服务model-v2中。
        nginx.ingress.kubernetes.io/canary-by-header-value: "bar"
    spec:
      rules:
      - host: model.example.com
        http:
          paths:
          # 新版本服务。
          - path: /
            backend:
              service: 
                name: model-v2-svc
                port:
                  number: 80
            pathType: ImplementationSpecific
  2. 部署灰度发布策略。

    kubectl apply -f gray-release-canary.yaml
  3. 验证无特定请求头(默认版本访问)的服务响应。

    # 以下代码的HostIngress中定义的业务Host。
    curl -H "Host: model.example.com" -H "Content-Type: application/json" \
         http://$(kubectl -n kube-system get svc nginx-ingress-lb -ojsonpath='{.status.loadBalancer.ingress[0].ip}'):80/v1/models/canary:predict -X POST \
         -d '{"data": "test"}'

    预期输出:

    {"id":"4d8c110d-c291-4670-ad0a-1a30bf8e314c","model_name":"canary","model_version":null,"outputs":[{"name":"output-0","shape":[1,1],"datatype":"STR","data":["model-v1"]}]}%  

    输出结果返回了model-v1的结果,表明默认情况下服务能够正确提供model-v1的预测结果。即流量流向了model-v1。

  4. 执行以下命令,验证带有"foo: bar"请求头(期望访问金丝雀版本)的客户端请求。

    curl -H "Host: model.example.com" -H "Content-Type: application/json" \
         -H "foo: bar" \
         http://$(kubectl -n kube-system get svc nginx-ingress-lb -ojsonpath='{.status.loadBalancer.ingress[0].ip}'):80/v1/models/canary:predict -X POST \
         -d '{"data": "test"}'

    预期输出:

    {"id":"4d3efc12-c8bd-40f8-898f-7983377db7bd","model_name":"canary","model_version":null,"outputs":[{"name":"output-0","shape":[1,1],"datatype":"STR","data":["model-v2"]}]}%   

    输出结果返回了model-v2的结果,表明带有特定请求头的流量被正确地导向了灰度版本,即灰度发布策略已生效。

场景二:基于服务权重的流量切分场景

  1. 创建gray-release-canary.yaml。

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: gray-release-canary
      annotations:
        # 开启Canary。
        nginx.ingress.kubernetes.io/canary: "true"
        # 仅允许20%的流量会被路由到新版本服务model-v2中。
        # 默认总值为100。
        nginx.ingress.kubernetes.io/canary-weight: "20"
    spec:
      rules:
      - host: model.example.com
        http:
          paths:
          # 新版本服务。
          - path: /
            backend:
              service: 
                name: model-v2-svc
                port:
                  number: 80
            pathType: ImplementationSpecific
  2. 部署灰度发布策略。

    kubectl apply -f gray-release-canary.yaml
  3. 验证流量的流向。

    curl -H "Host: model.example.com" -H "Content-Type: application/json" \
         http://$(kubectl -n kube-system get svc nginx-ingress-lb -ojsonpath='{.status.loadBalancer.ingress[0].ip}'):80/v1/models/canary:predict -X POST \
         -d '{"data": "test"}'

    重复执行以上命令,您可以观察到大约有20%的流量被路由到新版本(model-v2)的服务上,其他流量则流向稳定版本(model-v1)的服务,两种服务比例大约为1:4。即灰度策略已生效。

步骤四:版本切换

当新版本服务已经稳定并且符合预期后,需要下线老版本的服务 ,仅保留新版本服务在线上运行。

  1. 更新model-svc.yaml,将Service指向新版本model-v2服务。

    apiVersion: v1
    kind: Service
    metadata:
      name: model-svc
    spec:
      ports:
      - port: 80
        protocol: TCP
        targetPort: 8080
      selector:
        serving.kserve.io/inferenceservice: model-v2 # 将model-v1修改为model-v2。
      type: ClusterIP
  2. 执行以下命令,重新部署Service。

    kubectl apply -f model-svc.yaml 
  3. 查看路由访问情况。

    curl -H "Host: model.example.com" -H "Content-Type: application/json" \
         http://$(kubectl -n kube-system get svc nginx-ingress-lb -ojsonpath='{.status.loadBalancer.ingress[0].ip}'):80/v1/models/canary:predict -X POST \
         -d '{"data": "test"}'

    预期输出:

    {"id":"a13f2089-73ce-41e3-989e-e58457d14fed","model_name":"canary","model_version":null,"outputs":[{"name":"output-0","shape":[1,1],"datatype":"STR","data":["model-v2"]}]}%  

    重复执行以上命令后,您可以观察到100%的请求(流量)被路由到新版本(model-v2)的服务上。

  4. 执行以下命令,删除旧版本服务。

    kubectl delete ingress gray-release-canary
    arena serve delete model-v1
    kubectl delete svc model-v2-svc

    至此,完成从旧版本到新版本过渡过程中的清理工作。