云原生应用负载均衡系列 (2): 入口流量分发、容错与高可用调度

引言

在云原生应用负载均衡系列第一篇文章《云原生应用负载均衡选型指南》介绍了云原生容器环境的入口流量管理使用场景与解决方案,用 Envoy 作为数据面代理,搭配 Istio 作控制面的 Istio Ingress Gateway,在多集群流量分发、安全、可观测性、异构平台支持等方面的综合对比中,是云原生应用流量管理的最佳方案。

在接入层,我们需要配置路由规则、流量行为(超时、重定向、重写、重试等)、负载均衡算法、断路器、跨域等流量管理规则,本文将基于 Istio Ingress Gateway 面向入口流量分发、容错与高可用调度介绍上述功能的原理与演示。

组件介绍

Istio Ingress Gateway,可作为 Kubernetes 的一种 Ingress Controller 12,由数据面(Envoy 网络代理 1)与控制面 Istiod 13组成,默认以 Deployment 的形式部署 Pod 至 Kubernetes 集群。

服务发现

Istio Ingress Gateway 控制面 Istiod 可联通各种服务发现系统(Kubernetes,Consul,DNS)获取 endpoints 信息,或者我们可以使用 ServiceEntry 手动添加服务与 endpoints 对应信息。以常见的 Kubernentes 集群为例,Istiod 从 API Server 获取 Kubernetes Services 及其对应的 endpoints,对应关系实时监测与自动更新。获取服务发现信息后,Istiod 会将其转化为数据面标准数据格式,以 Envoy xDS API 的形式 push 到实际执行流量转发操作的数据面 Envoy 网络代理。

值得注意的是,Istio Ingress Gateway 的数据面 Envoy 是以单独 Pods 的形式部署于 Kubernetes 集群,只需使用 Istio 南北向流量管理能力时,无需在业务 Pods 注入 Istio 数据面 sidecar,Ingress Gateway 的 Envoy Pods 即可承载全部入口流量管理的能力,因为 Istio 入口流量管理的功能大部分是在 Envoy 网络代理的 client 端(Istio Ingress Gateway)实现的。

Istio 流量管理模型及 API 介绍

Istio 设计了自己的流量管理 API,主要通过 Gateway,VirtualService,DestinationRule 这几个 CR(Kubernetes Custom Resource 2)实现。

  • Gateway:配置监听规则。包括端口、协议、host、SSL 等。
  • VirtualService:配置路由规则。包括匹配条件、流量行为、路由目的服务/版本等。
  • DestinationRule:配置服务版本负载均衡连接池健康检查策略

我们可以与安装 Istio 控制面所在集群的 API Server 交互,提交上述 CR,配置流量管理规则。Istiod 会与该集群的 API Server 交互,获取 Istio API,将这些配置转化为 Envoy 数据面标准数据格式,通过 xDS 接口 push 至数据面(Istio Ingress Gateway),数据面即可根据相应规则转发流量。

入口流量管理实践

下面以 Istio Ingress Gateway 为例介绍入口流量分发、容错与高可用调度的实践。

环境准备

环境准备可以使用 TCM 控制台 「一键体验」功能,将自动为您准备 TCM + 2 个跨可用区 Kubernetes 集群 + TCM demo 的初始环境。

我们准备一个 Istio Ingress Gateway(使用腾讯云服务网格 TCM 演示) + Kubernetes 集群(使用腾讯云容器服务 TKE 演示)环境,首先创建一个服务网格实例,并在网格实例中创建 Istio Ingress Gateway,然后添加一个 TKE 集群作为网格的服务发现集群。

在集群部署 TCM demo 3,无需为业务 Pod 注入 envoy sidecar。该 demo 是一个电商网站,由不同语言开发的 6 个微服务组成,以下是 demo 的完整结构:

本次演示入口流量管理会使用 demo 中的 user、product、cart 三个应用,将其提供的 API 通过 istio- ingressgateway 暴露供客户端调用。

入口流量分发

应用发布

业务需要将多个后端模块提供的 API 暴露供客户端调用,需要配置网关路由规则,将请求路径 /product 的流量路由至 product 服务,将 /cart 的请求路由至 cart 服务,将 /user 的请求路由至 user 服务。

TCM demo 中,product 服务提供 /product 接口,可获取在售商品列表;user 服务提供 /user 接口,可获取用户信息;cart 服务提供 /cart 接口,可获取购物车商品列表。下面我们配置 Istio Ingress Gateway 按照请求的路径暴露上述接口。

1. 首先我们通过 kubectl 获取 Ingress Gateway 的 IP,并配置为变量 $INGRESS_HOST,方便后续直接引用。

$ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

2. 使用 Gateway 配置 Ingress Gateway 监听规则,开启 80 端口,HTTP 协议,暂不配置 SSL。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: apis-gw
spec:
  servers:
    - port:
        number: 80
        name: HTTP-80-6wsk
        protocol: HTTP
      hosts:
        - '*'
  selector:
    app: istio-ingressgateway
    istio: ingressgateway

3. 使用 VirtualService 配置路由规则,gateway 参数填写上一步创建的 default/apis-gw,http 路由匹配规则将 /product 的请求路由至 product 服务,/user 路由至 user 服务,/cart 路由至 cart 服务。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: apis-vs
spec:
  hosts:
    - '*'
  gateways:
    - default/apis-gw
  http:
    - match:
        - uri:
            exact: /product
      route:
        - destination:
            host: product.base.svc.cluster.local
    - match:
        - uri:
            exact: /user
      route:
        - destination:
            host: user.base.svc.cluster.local
    - match:
        - uri:
            exact: /cart
      route:
        - destination:
            host: cart.base.svc.cluster.local

4. 使用 curl 验证上述配置,请求 API 返回的 JSON 字串使用 jq 解析,提取出返回的 service 信息。请求已按照预设方式按路径路由至不同服务。

$ curl http://$INGRESS_HOST/product | jq '.info[0].Service'
...
"product-v1"
$ curl http://$INGRESS_HOST/cart | jq '.Info[1].Service'
...
"cart-v1"
$ curl http://$INGRESS_HOST/user | jq '.Info[0].Service'
...
"user-v1"

灰度发布

业务需要升级 product 服务,已经完成准备新版本镜像,可以开始部署新版本的 pods。希望服务端在升级时需考虑版本的平滑、无风险迭代,按百分比调整流量逐步切换至新版本服务,灰度验证新版本无问题后再下线旧版本。

以下是配置 Ingress Gateway 做 product 服务灰度发布的流程:

1. 使用 DestinationRule 定义 product 服务的版本(subsets),用标签键值匹配即可定义一个服务内 subsets 与 endpoints 对应关系。

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: product
  namespace: base
spec:
  host: product
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2
  exportTo:
    - '*'

2. 使用 VirtualService 配置灰度发布路由策略,发布最初 v2 版本为 0% 的流量,v1 版本 100%。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: apis-vs
spec:
  hosts:
    - '*'
  gateways:
    - default/apis-gw
  http:
    - match:
        - uri:
            exact: /product
      route:
        - destination:
            host: product.base.svc.cluster.local
            subset: v2
          weight: 0
        - destination:
            host: product.base.svc.cluster.local
            subset: v1
          weight: 100
    - match:
        - uri:
            exact: /user
      route:
        - destination:
            host: user.base.svc.cluster.local
    - match:
        - uri:
            exact: /cart
      route:
        - destination:
            host: cart.base.svc.cluster.local

3. 我们把 TCM demo product 服务的 v2 版本(deployment)5 部署到集群,然后模拟发起 10 次请求调用 product 服务,返回结果表明发布最初所有流量仍然是路由到 v1 版本。

$ for((i=0;i<10;i++)) do curl http://$INGRESS_HOST/product | jq '.info[0].Service'; done
"product-v1"
"product-v1"
"product-v1"
"product-v1"
"product-v1"
"product-v1"
"product-v1"
"product-v1"
"product-v1"
"product-v1"

4. 修改 VirtualService,把 product v1 和 v2 subset 的权重值分别调整为:50,50。模拟发起 10 次请求调用 product 服务,结果如预设,v2 和 v1 版本调用次数的比例接近 1:1。

$ for((i=0;i<10;i++)) do curl http://$INGRESS_HOST/product | jq '.info[0].Service'; done
"product-v1"
"product-v2"
"product-v1"
"product-v2"
"product-v1"
"product-v2"
"product-v1"
"product-v2"
"product-v1"
"product-v1"

5. 灰度验证完成后,修改灰度发布路由规则,修改 VirtualService 调整 v1 v2 subset 权重分别为:0,100,将 100% 请求 /product 的流量路由至 product v2 版本。再次模拟发起 10 次请求验证,所有请求已被路由至 product v2,灰度发布完成。

$ for((i=0;i<10;i++)) do curl http://$INGRESS_HOST/product | jq '.info[0].Service'; done
"product-v2"
"product-v2"
"product-v2"
"product-v2"
"product-v2"
"product-v2"
"product-v2"
"product-v2"
"product-v2"
"product-v2"

会话保持

同一后端服务一般由多个实例(Pod)承载,通常需要将入口流量在多个后端实例负载均衡(例如 product 服务)负载均衡,此时配置的是 round robin、random 或最小连接数等负载均衡算法,保持多个后端实例均衡处理流量。
在一些特定情况下,需要将来自同一用户的请求路由到相同后端实例,保证某些需要会话保持的服务(例如 cart 购物车服务)能够正常工作。

会话保持有两种:

  • 基于 IP 的简单会话保持:来自同一 IP 地址的请求判定为同一用户,路由至相同后端服务实例。实现简单、方便,但无法支撑多个客户端使用代理访问后端服务的场景,此时同一 IP 地址不代表同一用户。
  • 基于 cookie(或其他 7 层信息)的会话保持:用户第一次请求时,边缘网关为其插入 cookie 并返回,后续客户端使用此 cookie 访问,边缘网关根据 cookie 将流量负载至后端服务实例。

Istio Ingress Gateway 可设置基于 IP、cookie、HTTP header 的会话保持,但该策略只对 HTTP 流量生效。下面将配置 cart 服务的 IP、cookie 两种会话保持负载均衡策略。

如使用开源 Istio Ingress Gateway + CLB,配置边缘网关流量管理规则存在如下问题:

  • 手动修改关联 Service 规则:使用 Gateway 配置了 Ingress Gateway 的端口后,需要手动配置 Ingress Gateway 关联的 LoadBalancer Service 的端口规则。
  • 无法获取真实源 IP:使用默认容器网络模式时,istio-ingressgateway 通过 loadbalancer service 的方式暴露给公网访问,流量经由 CLB 到节点的 NodePort 后,kube-proxy 会将原始请求做 SNAT 和 DNAT,因此请求到达 istio-ingressgateway 时,源 IP 已不是真实 client IP。

使用 TCM Ingress Gateway + TKE 集群则可避免上述问题:

  • TCM 已实现 Gateway 端口配置自动化同步至 Ingress Gateway 相关 Kubernetes Service 及关联 CLB,我们使用 Gateway CR 即可一致化管理 Ingress Gateway 实例的端口。
  • 可以通过指定 istio-ingressgateway service 的 externalTrafficPolicy: Local 来避免流量通过 NAT 在节点之间转发,保留了真实 client IP。同时我们可以通过添加注解 service.kubernetes.io/local-svc-only-bind-node-with-pod: "true" 来指定 CLB 后端只绑定有 istio-ingressgateway Pod 的节点,避免因后端绑定了不存在 Pod 实例的节点导致健康检查失败的问题。也可以增加注解 service.cloud.tencent.com/local-svc-weighted-balance: "true" 让 CLB 根据后端节点上的 Pod 数量做加权负载均衡,避免因不同节点 Pod 实例数量不一导致的负载不均问题 6

基于 IP 的会话保持

1. Ingress Gateway 可获取真实 client IP 后,我们通过 DestinationRule 配置 cart 服务的基于 IP 的简单负载均衡:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: cart
  namespace: base
spec:
  host: cart
  trafficPolicy:
    loadBalancer:
      consistentHash:
        useSourceIp: true
  exportTo:
    - '*'

2. 获取 cart 服务当前的 pods,一共部署了 3 个 pods。

$ kubectl get deployment cart -n base
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
cart   3/3     3            3           4d23h

3. 模拟发起 10 次请求 /cart 验证,获取提供购物车服务的 pod 信息,所有请求均被路由到了同一个 pod,基于 IP 负载均衡配置成功。

$ for((i=0;i<10;i++)) do curl http://$INGRESS_HOST/cart | jq '.Info[1].Pod'; done
"cart-855f9d75ff-x47bq"
"cart-855f9d75ff-x47bq"
"cart-855f9d75ff-x47bq"
"cart-855f9d75ff-x47bq"
"cart-855f9d75ff-x47bq"
"cart-855f9d75ff-x47bq"
"cart-855f9d75ff-x47bq"
"cart-855f9d75ff-x47bq"
"cart-855f9d75ff-x47bq"
"cart-855f9d75ff-x47bq"

基于 cookie 的会话保持

1. 修改 cart 服务的 DestinationRule,配置基于 cookie 的负载均衡。cookie name 为 cookie,cookie 过期时间为 900000 ms(900 s)。

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: cart
  namespace: base
spec:
  host: cart
  trafficPolicy:
    loadBalancer:
      consistentHash:
        httpCookie:
          name: cookie
          ttl: 900000ms
  exportTo:
    - '*'

2. 发起第一次 /cart 请求,获取 Ingress Gateway 返回的 cookie ID 及 pod 信息,使用该 cookie ID 模拟发起 10 次 /cart 请求,第一次请求和后续 10 次请求,流量均被路由到了同一个 pod(在本例中是 cart-855f9d75ff-dqg6b),基于 cookie 的负载均衡配置生效。

$ curl http://$INGRESS_HOST/cart -i
...
set-cookie: cookie="bc0e96c66ff8994b"; Max-Age=900; HttpOnly
...
Pod":"cart-855f9d75ff-dqg6b"
...
$ for((i=0;i<10;i++)) do curl http://$INGRESS_HOST/cart -b 'cookie=bc0e96c66ff8994b' | jq '.Info[1].Pod'; done
"cart-855f9d75ff-dqg6b"
"cart-855f9d75ff-dqg6b"
"cart-855f9d75ff-dqg6b"
"cart-855f9d75ff-dqg6b"
"cart-855f9d75ff-dqg6b"
"cart-855f9d75ff-dqg6b"
"cart-855f9d75ff-dqg6b"
"cart-855f9d75ff-dqg6b"
"cart-855f9d75ff-dqg6b"
"cart-855f9d75ff-dqg6b"

容错

连接池管理

连接池是保持分布式系统(服务化应用)稳定的重要配置。分布式系统中其中一个服务因请求数暴增而有故障风险时,快速返回失败信息尽快将压力施加给下游服务能有效避免整个系统发生雪崩。我们可通过连接池为有需要的服务配置 TCP/HTTP 的连接数/请求数阈值,达到阈值后拒绝处理新增流量返回错误信息,能有效保护服务运行的稳定性 7

下面我们配置 user 服务的连接池:

1. 首先我们部署一组 curl pods (30 个)模拟向 user 服务发起并发请求,受各 pod 运行环境影响,实际并发量应该 < 30。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    k8s-app: curl
    qcloud-app: curl
  name: curl
  namespace: default
spec:
  replicas: 30
  selector:
    matchLabels:
      k8s-app: curl
      qcloud-app: curl
  template:
    metadata:
      labels:
        k8s-app: curl
        qcloud-app: curl
    spec:
      containers:
      - args:
        - -c
        - while true; do curl http://$INGRESS_HOST/user; done
        command:
        - /bin/sh
        image: docker.io/byrnedo/alpine-curl
        imagePullPolicy: IfNotPresent
        name: curl

2. 使用 DestinationRule 配置 user 服务的连接池,为方便观察效果,我们配置 http1 的最大请求等待数为 1,http2 的最大请求数为 1。

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: user
  namespace: base
spec:
  host: user
  trafficPolicy:
    connectionPool:
      http:
        http1MaxPendingRequests: 1
        http2MaxRequests: 1
  exportTo:
    - '*'

3. 在 Grafana Dashboard 查看 user 服务的监控,可以看到配置了连接池后,因大部分请求被返回 503 Service Unavailable 状态码,因此客户端请求成功率骤降,user 服务超载配置成功,DestinationRule 连接池配置起到了很好的保护服务端稳定性的作用。

健康检查

当后端服务实例(Pod)在处理流量过程中发生故障时(连续返回错误,成功率降低到阈值之下等),Ingress Gateway 需要可以配置将故障的 endpoints 从健康负载均衡池中剔除的策略,保证客户端调用可以由状态正常的后端服务实例处理。
另外,地域感知负载均衡功能也需要开启异常检测,感知各地 endpoint 的健康状态才能确定流量调度策略。

Ingress Gateway(envoy)的 Outlier Detection 是一种被动健康检查,当流量出现了类似连续 5xx 错误(HTTP)、连接超时/失败(TCP)等行为时,将其识别为离群值从负载均衡池中剔除一段时间 8。下面我们配置 user 服务的健康检查策略。

1. 首先我们部署一组会为请求 /user 返回 503 错误的 pods 作为 user 服务的不健康 endpoints,部署完成后查看 user 服务的 endpoint 情况,有 1 个健康 user pod,1 个不健康 user pod。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-unhealthy
  namespace: base
spec:
  replicas: 1
  selector:
    matchLabels:
      app: user
  template:
    metadata:
      labels:
        app: user
    spec:
      containers:
      - command:
        - sleep
        - "9000"
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.name
        - name: REGION
          value: shanghai-zone1
        image: docker.io/busybox
        imagePullPolicy: IfNotPresent
        name: user
        ports:
        - containerPort: 7000
$ kubectl get deployment -n base user user-unhealthy 
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
user             1/1     1            1           6d19h
user-unhealthy   1/1     1            1           3d20h

2. 当前还未配置 user 服务的 Outlier Detection(被动健康检查),不健康的 endpoint 不会被从负载均衡池中剔除,因此发起的请求部分成功(200 OK),部分失败(503 Service Unavailable)。

$ for((i=0;i<10;i++)) do curl -I http://$INGRESS_HOST/user | grep HTTP/1.1; done
HTTP/1.1 200 OK
HTTP/1.1 200 OK
HTTP/1.1 503 Service Unavailable
HTTP/1.1 200 OK
HTTP/1.1 503 Service Unavailable
HTTP/1.1 200 OK
HTTP/1.1 503 Service Unavailable
HTTP/1.1 200 OK
HTTP/1.1 503 Service Unavailable
HTTP/1.1 503 Service Unavailable

3. 我们配置 user 服务的 DestinationRule,设置 Outlier Detection。间隔 10 秒做一次统计,从负载均衡池中剔除连续错误数为 3 以上的 endpoint 30 秒,允许最大剔除比例为 100%,最小健康比例为 0%。完成配置后我们模拟请求 user 服务,所有请求均返回 200 OK。(被动健康检查,需请求返回连续错误后才会剔除不健康 endpoint)

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: user
  namespace: base
spec:
  host: user
  trafficPolicy:
    outlierDetection:
      consecutiveErrors: 3
      interval: 10000ms
      baseEjectionTime: 30000ms
      maxEjectionPercent: 100
      minHealthPercent: 0
  exportTo:
    - '*'
$ for((i=0;i<10;i++)) do curl -I http://$INGRESS_HOST/user | grep HTTP/1.1; doneHTTP/1.1 200 OKHTTP/1.1 200 OKHTTP/1.1 200 OKHTTP/1.1 200 OKHTTP/1.1 200 OKHTTP/1.1 200 OKHTTP/1.1 200 OKHTTP/1.1 200 OKHTTP/1.1 200 OKHTTP/1.1 200 OK

重定向

当应用被迁移到新的 URI,同时又需要保持原有链接可用,此时需要配置 HTTP 重定向 9。重定向可应用于以下场景:

  1. server 端维护/停机期间迁移到新的 URI
  2. 强制使用 HTTPS 协议
  3. 多域名扩大应用覆盖用户人群

为确保通过 Ingress Gateway 访问后端 user 服务的请求强制使用更安全的 HTTPS 协议,需要配置 Ingress Gateway 的 HTTP 重定向。

下面我们配置强制使用 HTTPS 协议的重定向。

1. 用 Gateway 配置 HTTP 重定向,强制使用 HTTPS 协议。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: apis-gw
spec:
  servers:
    - port:
        number: 80
        name: HTTP-80-h7pv
        protocol: HTTP
      hosts:
        - '*'
      tls:
        httpsRedirect: true
    - port:
        number: 443
        name: HTTPS-443-p1ph
        protocol: HTTPS
      hosts:
        - '*'
      tls:
        mode: SIMPLE
        credentialName: qcloud-$CERTIFICATE_ID
  selector:
    app: istio-ingressgateway
    istio: ingressgateway

2. 使用 HTTP 请求 /user,返回 301 重定向状态码。如果是在浏览器访问,收到重定向返回时,会重新发起新请求到新的 URI。

$ curl http://$INGRESS_HOST/user -I | grep HTTP/1.1
HTTP/1.1 301 Moved Permanently

重写

使用重定向,客户端可以感知访问地址的变化,并且被重定向的流量实际上发起了两次请求才能正常访问,有一定性能损耗。而重写则向客户端屏蔽了地址的变动,完全由服务端进行重写操作,使客户端请求地址与服务端管理解藕。

TCM demo 的 cart 服务提供的 API 资源发生了变化,实现了 /clear 清空购物车的 API,希望在客户端无感知的情况下,/cart 请求实际调用的是 /clear API。

下面我们配置 /cart 的请求重写为 /clear。

1. 使用 VirtualService 配置 /cart 请求在进行实际调用前,将 URI 重写为 /clear。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: apis-vs
  namespace: default
spec:
  hosts:
    - 121.4.8.11
  gateways:
    - default/apis-gw
  http:
    - match:
        - uri:
            exact: /cart
      route:
        - destination:
            host: cart.base.svc.cluster.local
      rewrite:
        uri: /clear

2. 发起 /cart 请求,客户端不感知重写操作,由服务端执行,实际调用的是 /clear API,cart 服务的 /clear API 成功返回调用清空购物车成功的信息。

$ curl http://$INGRESS_HOST/cart -H 'UserID: 1'
{"Success":"true"}

高可用调度

随着业务规模的增加,或对跨可用区/地域容灾、数据合规性、业务之间隔离要求的提升,业务会考虑与实施部署多个 Kubernetes 集群,把同一个服务部署在跨可用区/地域的多个集群,做高可用调度。主要有两种诉求:

  1. 地域&错误感知自动 failover:根据服务的地域信息与 endpoint 健康信息确定流量的可用区/地域分发策略,当 endpoint 健康度高于阈值时,流量 100% 在本地路由,低于阈值时,视 endpoint 健康度自动 failover 一定比例流量至其他可用区/地域,直至 endpoint 全部不健康时 100% 流量自动 failover 至其他可用区/地域。endpoint 健康信息感知依赖于健康检查的能力。
  2. 地域感知流量分发 distribute :不按照地域与错误信息自动 failover 流量,管理员自定义配置跨可用区/地域多集群流量分发策略,例如配置来自上海一区的流量在上海一区和上海二区按照 80% 和 20% 的比例分发。无须感知 endpoint 健康,不依赖健康检查的能力。

地域&错误感知自动 failover

随着 TCM demo 网站后台规模增加,对后台服务容灾的诉求也提上日程,希望实现 user 服务的跨集群(本次以集群跨可用区为例)容灾,在上海二区新增一个业务集群部署 user 备份服务,流量仍然是从上海一区的 ingress gateway 访问 user。希望在上海一区 user 服务 endpoints 都健康时,就近访问本可用区的 user,当上海一区 user endpoints 健康比例下降到一定程度时(例如 71.4%),开始视健康程度转移一定比例的流量到上海二区的 user endpoints,直至上海一区 user endpoints 健康程度完全下降为 0% 时将流量完全切到上海二区 user 备份。

环境准备:

1. 添加一个上海二区的 TKE 集群(如使用 TCM 一键体验功能搭建环境,可跳过,TCM 一键体验已经准备了第二个可用区的服务发现集群),并在此集群部署 user 服务(replicas: 14)作为上海一区 user 服务的容灾备份服务。

apiVersion: v1
kind: Namespace
metadata:
  name: base
spec:
  finalizers:
    - kubernetes
---
apiVersion: v1
kind: Service
metadata:
  name: user
  namespace: base
  labels:
    app: user
spec:
  ports:
    - port: 7000
      name: http
  selector:
    app: user
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user
  namespace: base
  labels:
    app: user
spec:
  replicas: 14
  selector:
    matchLabels:
      app: user
  template:
    metadata:
      labels:
        app: user
    spec:
      containers:
        - name: user
          image: ccr.ccs.tencentyun.com/zhulei/testuser:v1
          imagePullPolicy: Always
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: REGION
              value: "shanghai-zone2"
          ports:
            - containerPort: 7000

2. 调整原有上海一区集群 user 服务的 healthy 和 unhealthy pods 数量分别为 10 和 4。调整完成后一区和二区的 user 服务 endpoints 数量情况如下:

$ kubectl get deployment user user-unhealthy -n base
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
user             10/10   10           10          8d
user-unhealthy   4/4     4            4           5d2h
$ kubectl get deployment user -n base
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
user   14/14   14           14          5d2h

下面我们准备配置开启与测试 Istio Ingress Gateway 的地域感知负载均衡功能。

1. TCM 的地域感知功能默认开启,我们只需要配置 user 服务的 Outlier Detection,地域感知负载均衡即可生效。且默认当上海一区 user endpoints 健康比例下降至 10/14 (71.4%)后,会开始按比例转移一区的流量到二区。

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: user
  namespace: base
spec:
  host: user
  trafficPolicy:
    outlierDetection:
      consecutiveErrors: 3
      interval: 10000ms
      baseEjectionTime: 30000ms
      maxEjectionPercent: 100
      minHealthPercent: 0
  exportTo:
    - '*'

2. 当前从 Ingress Gateway(上海一区)访问 user 服务的流量,当前上海一区 user 服务健康比例还未小于 10/14 的临界值,因此访问 user 服务的流量全部由上海一区的 user endpoints 提供。发起一组请求验证,所有流量均被路由到了上海一区的 endpoints。

$ for((i=0;i<10;i++)) do curl http://$INGRESS_HOST/user | jq '.Info[0].Region'; done
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"

3. 调整上海一区集群内 user 服务健康与非健康 endpoints 的比例,调整健康 endpoints 为 5,不健康endpoints 为 9。此时健康上海一区 user 服务的健康比例 5/14 已经小于 10/14,应当有部分流量被切至上海二区,发起一组请求验证,/user 流量部分被路由至上海二区,路由至上海一区/二区的比例大致均衡为 1:1。

$ for((i=0;i<10;i++)) do curl http://$INGRESS_HOST/user | jq '.Info[0].Region'; done
"shanghai-zone2"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone1"
"shanghai-zone2"

4. 继续调整上海一区集群内 user 服务健康与非健康 endpoints 的比例,健康 endpoints 为 0,不健康 endpoints 为 14,此时上海一区 user 服务健康比例为 0%,不具备提供后端服务的能力,应当将所有 /user 请求路由到上海二区。发起一组请求验证,所有流量均被路由至上海二区。

$ for((i=0;i<10;i++)) do curl http://$INGRESS_HOST/user | jq '.Info[0].Region'; done
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"

Istio 默认策略为当流量需要 failover 时,会在下一同等地域优先级的所有地域内全局负载均衡,同一个服务若有超过 2 个地域的部署,则需考虑配置 failover 优先级,例如 user 服务在广州、上海、北京均有部署时,需要配置当广州的 endpoints 健康比例下降到阈值之下时,流量 failover 的地域是最近的上海而不是更远的北京或在上海和北京全局负载均衡,使用 localityLbSetting 的 failover 参数即可配置。当 user 服务在广州、上海、北京均有部署时我们可以如下配置地域间 failover 策略,配置方式与 distribute 策略相同。

failover:
   - from: gz
     to: sh
   - from: sh
     to: gz
   - from: bj
     to: sh

TL;DR. 以下内容是地域感知负载均衡获取 endpoints 健康程度、地域信息、确定流量转移比例的背景知识补充。

实现跨可用区/地域多集群地域感知负载均衡,视服务 endpoints 地域信息及健康程度按比例 failover 流量,需要具备以下能力:

1. 联通多集群网络并发现所有集群的服务:

跨集群网络连通后,Istiod 可以从多个集群的 API Server 中获取服务与 endpoint 信息,并推送给数据面代理 Envoy Pod。

2. 获取服务的地理位置信息:

实现就近访问与容灾,需要服务的地理位置信息,在服务网格中,地理位置由有如下三个层级的信息:region,zone,subzone。其中 region 和 zone 的信息分别来自集群节点的 topology.kubernetes.io/region 标签和 topology.kubernetes.io/region 标签。这两个标签在 TKE 集群中已经提供 10,例如上海一区节点的标签为:topology.kubernetes.io/region: shtopology.kubernetes.io/zone: "2000400001";上海二区节点的标签为:topology.kubernetes.io/region: shtopology.kubernetes.io/zone: "200002"

Kubernetes 不存在 subzone 的概念,Istio 引入了 topology.istio.io/subzone 的标签定义 subzone,可根据需要配置。

在 Istio 默认使用的地域优先负载均衡策略中,优先级如下:

  • Priority 0 最高优先级,同地域同可用区;
  • Priority 1,同地域不同可用区;
  • Priority 2 最低优先级,不同地域。

3. 获取服务 endpoints 的健康信息:

endpoints 健康信息获取依赖开启 Istio 的健康检查: Outlier Detection。地域&错误自动 failover 功能依赖健康检查,未开启时,数据面无法得知服务 endpoints 的健康状况,默认按照全局的方式进行流量负载均衡。地域感知流量分发 distribute 不依赖开启健康检查。

4. 判定服务健康状态&确定流量转移比例:

一个服务会部署多个副本,服务的健康状态不是绝对的 0 和 1 的状态,流量的转移是一个逐步转移的过程,不健康 endpoints 超过一定比例时,再开始按比例逐步进行流量转移。Istio 的地域负载均衡默认使用地域优先的策略,即控制面告诉数据面在健康状态下,优先考虑将请求发送到地理位置最近的实例,地理优先级最高可用区/地域 endpoints 健康度 100% 时,所有的请求都会路由到这个地域,不会做流量转移,endpoints 不健康比例超过某阈值,流量将开始按比例逐步转移。

这个阈值由 envoy 的 overprovisioning factor 控制,默认为 1.4,根据该 factor 及服务 endpints 健康比例可确定不同地理 Priority Level 的流量比例。例如,假设目前某服务在广州和上海两个地域均有 endpoints,流量入口 Ingress Gateway 部署在广州。通过广州 Ingress Gateway 访问该服务的流量,按照优先级广州的服务为 P0 优先级,上海的服务为 P1 优先级。假设上海作为容灾的地域,,endpoints 健康比例一直为 100%,假设广州上海两个地域的权重相等,overprovisioning factor 为默认值 1.4。两个地域流量负载比例计算过程如下:

  • 广州服务 endpoints 健康比例:P0_health = 广州服务健康 endpoints 数量 / 广州服务 endpoints 总数;
  • 广州服务流量负载比例:P0_traffic = min(1, P0_health * 1.4);
  • 上海服务流量负载比例:P1_traffic = 1 - P0_traffic = max(0, 1 - P0_health * 1.4)。

按照该计算规则,overprovisioning factor 为 1.4 时:

  • 广州服务 endpoints 健康比例 P0_health 低于 71.4% 时,该地域访问该服务的流量才会开始切换至上海地域;
  • 当广州地域的 endpoints 健康比例为 50% 时,会有 1 - 50% * 1.4 = 30% 的流量转移到上海地域的服务;
  • 当广州地域 endpoints 完全不可用 P0_health = 0% 时,流量才会被完全切换到上海地域。

PX_traffic = min(100, PX_health * 1.4 * 100) 反映某地域某服务当前的流量承载能力,Envoy 社区称为健康评分。当所有地域的健康评分总和低于 100 时,Envoy 则认为当前健康状态没有完全处理请求的的能力,此时 Envoy 会根据健康评分的比例分配请求。例如广州和上海的健康评分分别为 20 和 30 时,分别会承担 40% 和 60% 的负载 11

地域感知流量分发 distribute

业务不希望流量根据地域和健康信息自动 failover,而是自定义流量分发策略,来自 Istio Ingress Gateway(上海一区)的 /user 请求在一区和二区按照 1:1 的比例均衡分发,而不是应用 Istio 默认的自动 failover 策略:100% 健康时,来自上海一区的请求 100% 负载均衡至上海一区 user endpoints。

可通过 meshconfig(配置网格全局)或 DestinationRule(配置单个服务)的 distribute 参数来配置自定义地域感知流量分发策略。

1. 恢复上海一区和二区 user 服务 健康/不健康 endpoints 数量至最初状态,一区一共 14 个endpoints 全部健康,二区一共 14 个endpoints 全部健康。按照 Istio Ingress Gateway 默认的地域感知策略,从 Ingress Gateway(上海一区)访问 /user 的流量会全部路由至上海一区的 endpoints。

2. 配置 user 服务的 DestinationRule,自定义流量调度规则,来自上海一区的流量,均匀路由至上海一区和二区的 endpoints。

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: user
  namespace: base
spec:
  host: user
  trafficPolicy:
    loadBalancer:
      localityLbSetting:
        distribute:
          - from: sh/2000400001/*
            to:
              sh/2000400001/*: 50
              sh/200002/*: 50
        enabled: true
    outlierDetection:
      consecutiveErrors: 3
      interval: 10000ms
      baseEjectionTime: 30000ms
      maxEjectionPercent: 100
      minHealthPercent: 0
  exportTo:
    - '*'

3. 发起一组 /user 请求验证,流量被比较均衡的路由到了一区和二区的 endpoints,而不是 Istio Ingress Gateway 默认地域/错误感知自动 failover(100% 流量路由到上海一区)。

$ for((i=0;i<10;i++)) do curl http://$INGRESS_HOST/user | jq '.Info[0].Region'; done
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone2"
"shanghai-zone1"
"shanghai-zone2"
"shanghai-zone1"
"shanghai-zone1"
"shanghai-zone2"
"shanghai-zone2"

结语

本文介绍了 Istio Ingress Gateway 流量管理的技术原理及流量管理模型。并从入口流量分发、容错与高可用调度三个方面实操演示了内容路由、权重路由、负载均衡、断路器、地域&错误感知自动 failover、地域感知流量分发等功能。

除基本的入口流量管理外,南北流量管理还有安全、可观测性、异构服务支持等场景,将在后续系列文章中继续讨论。

posted @ 2021-07-02 16:16  腾讯云原生  阅读(1234)  评论(0编辑  收藏  举报