基于istio 的traffic mirror为预发布环境导入线上真实流量

2021-03-07 21:16:55 浏览数 (1)

背景

流量镜像,也叫影子流量(Traffic shadowing),是一种通过复制生产环境的流量到非生产环境(一般是staging环境)进行测试开发的工作模式。

影子流量常用场景:

  • 线上流量模拟和测试,比如要用新系统替换掉老旧系统或者系统经历了大规模改造的时候,可以将线上流量导入新系统试运行;一些实验性的架构调整,也可以通过线上流量进行模拟测试。
  • 由于是全样本的模拟,影子流量可以应用于新服务的预上线演练,由于传统的手工测试本身是一种样本化的行为,通过导入真实流量形态,可以完整的模拟线上的所有情况,比如异常的特殊字符,带恶意攻击的token,可以探测预发布服务最真实的处理能力和对异常的处理能力。
  • 用于线上问题排查和临时的数据采集,比如对于一些线上突发性问题,在线下流量总是无法复现,这时候可以临时开启一个分支服务,导入影子流量进行调试和排查,而不比影响线上服务。
  • 用于日志行为采集,对于推荐系统和算法来说,样本和数据是非常核心的,传统的自动化测试在算法类的应用所面对的最大的挑战就是无法构建真实环境的用户行为数据,通过影子流量可以将用户行为以日志的形式保存起来,既可以为推荐系统和算法模型构建模拟测试样本数据,也可以作为后续大数据分析用户画像的数据来源再应用到推荐服务中。

这里给大家介绍基于 istio 服务网格做网络流量镜像的方法。

Envoy实现流量镜像的原理

envoy 会将流量复制一份影子流量发到分支服务,和正常流量的区别是对于分支服务发送影子流量后不会处理其返回响应。同时在区分分支服务的影子流量和正常服务流量, envoy 是通过对请求头中的host值标识,envoy 会在原来流量的 host 上加上-shadow的后缀进行标识。

以上图为例,镜像流量的 host 是 http://myservice-test.mycompany.com,其将被修改为myservice-backend.company.com-shadow。(如果服务中有对请求头的host进行处理需要注意这点

案例

我们知道 istio 的数据面板是基于 envoy 构建的,包括网关部分的 ingressgateway 和服务部分的 sidecar,这样我们就可以通过 istio 做网关层流量镜像和服务层的流量镜像。 这里以一个 grpc 的应用为例分别讲述 istio 在网关层和服务层做流量镜像的应用。

本文案例代码:https://github.com/shikanon/privatecode/tree/master/traffic-shadowing

PS:基于http协议的可以参考istio官方流量镜像案例:https://istio.io/latest/zh/docs/tasks/traffic-management/mirroring/

基于服务层做流量镜像

在同一service发布分支服务为其引入影子流量,这里实现了一个 grpc程序。

首先构建正常服务和分支服务(分支服务放在 testing 命名空间):

代码语言:javascript复制
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: grpc-hello-world-v1
  name: grpc-hello-world-v1
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/instance: grpc-hello-world-v1
      app.kubernetes.io/name: grpc-hello-world
      version: v1
  template:
    metadata:
      labels:
        app.kubernetes.io/instance: grpc-hello-world-v1
        app.kubernetes.io/name: grpc-hello-world
        version: v1
    spec:
      containers:
      - image: docker.io/shikanon096/grpc-helloworld
        imagePullPolicy: Always
        name: grpc-hello-world
        ports:
        - containerPort: 8000
        resources:
          limits:
            cpu: 50m
            memory: 128Mi
          requests:
            cpu: 50m
            memory: 128Mi
        env:
          - name: PODNAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: PODIP
            valueFrom:
              fieldRef:
                fieldPath: status.podIP

安装sidecar:

代码语言:javascript复制
$ istioctl kube-inject -f deploy-v1.yaml | kubectl apply -f -
$ istioctl kube-inject -f deploy-v2.yaml | kubectl apply -f -

构建istio virtualservice:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: grpc-hello-world
spec:
  hosts:
    - 'grpc-hello-world'
  http:
  - route:
    - destination:
        host: grpc-hello-world.default.svc.cluster.local
        subset: v1
      weight: 100
    mirror:
      host: grpc-hello-world.default.svc.cluster.local
      subset: v2
    mirror_percent: 100
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: grpc-hello-world
spec:
  host: grpc-hello-world.default.svc.cluster.local
  subsets:
  - name: v1
    labels:
      version: v1 #通过pod的label来区分
  - name: v2
    labels:
      version: v2
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: grpc-hello-world
  name: grpc-hello-world
  namespace: default
spec:
  ports:
  - name: grpc
    port: 8000
    targetPort: 8000
  selector:
    app.kubernetes.io/name: grpc-hello-world # 共用一个selector
  type: ClusterIP

mirror 参数说明:

  • host: istio的Destination,目标host地址
  • mirror_percent: 镜像流量百分比

用grpcurl工具测试:

代码语言:javascript复制
$ grpcurl --plaintext -d '{"name":"test01"}' grpc-hello-world:8000 helloworld.Greeter.SayHello
{
  "message": "Hello test01 ! n Pod name is grpc-hello-world-v1-548f845bf6-mwj48 n Pod IP is 10.0.2.65 n"
}

查看两个 pod 的日志:

代码语言:javascript复制
$ kubectl logs -f grpc-hello-world-v1-548f845bf6-mwj48 grpc-hello-world
...
Received: Hello test01 !
Pod name is grpc-hello-world-v1-548f845bf6-mwj48
Pod IP is 10.0.2.65
代码语言:javascript复制
$ kubectl logs -f grpc-hello-world-v2-6b9fc86c5d-sfwht grpc-hello-world
...
Received: Hello test01 !
Pod name is grpc-hello-world-v2-6b9fc86c5d-sfwht
Pod IP is 10.0.2.67

可以看到两个 pod 都收到请求了,但只有 v1 的 response 被接收了

基于网格层做跨集群流量镜像

基于网关层做流量镜像一般多是用于为预发布环境导入线上真实流量,所以多是跨集群中使用到。 这里以 staging 集群(clusterA)和 test 集群(clusterB)命名,主体请求在 clusterA,由 clusterA 网关将流量镜像拷贝 clusterB,如下图:

在 clusterA 我们需要创建 virtualservice 实现路由策略和流量镜像配置,这里和集群内调用是类似的:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: grpc-hello-world
spec:
  gateways:
  - istio-system/internal-gateway
  hosts:
    - 'grpc-hello-shikanon.cn-bj.rcmd-staging.skyengine.net.cn'
  http:
  - route:
    - destination:
        host: grpc-hello-world.default.svc.cluster.local
        port:
          number: 8000
    mirror:
      host: grpc-hello-mirror.cn-bj.rcmd-testing.skyengine.net.cn
      port:
        number: 8000
    mirror_percent: 100

我们的 mirror host 是一个外部域名,所以我们这里需要添加一个 ServiceEntry 对 hosts 的 DNS 解析方式进行指定:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: httpbin-cluster-b
spec:
  hosts:
  - grpc-hello-mirror.cn-bj.rcmd-testing.skyengine.net.cn
  location: MESH_EXTERNAL
  ports:
  - number: 8000
    name: http80
    protocol: HTTP
  resolution: DNS

这里的解析方式 resolution 可以使用外部 DNS,也可以直接指定,可以参考官方设定: https://istio.io/latest/zh/docs/reference/config/networking/service-entry/

设置好 clusterA 的路由策略,我们可以设置 clusterB 路由接受影子流量,这里需要注意 clusterB 的路由规则设置并不是grpc-hello-mirror.cn-bj.rcmd-testing.skyengine.net.cn,如果我们设置成mirror的目标路由是无法匹配的,日志如下:

代码语言:javascript复制
$ kubectl logs  -nistio-system -f istio-ingressgateway-xxxxx
...
2021-03-03T10:44:32.588499Z	debug	envoy http	[external/envoy/source/common/http/conn_manager_impl.cc:782] [C543840][S3999381319208092814] request headers complete (end_stream=true):
':authority', 'grpc-hello-shikanon.cn-bj.rcmd-staging.skyengine.net.cn-shadow:8000'
':path', '/'
':method', 'GET'
'user-agent', 'curl/7.29.0'
'accept', '*/*'
'x-forwarded-for', 'xxxxx'
'x-forwarded-proto', 'http'
'x-envoy-external-address', 'xxxxx'
'x-request-id', 'd75bea03-c046-43a3-a49e-a6b1fcfb8eff'
'x-envoy-decorator-operation', 'httpbin.default.svc.cluster.local:8000/*'
'x-envoy-peer-metadata', 'ChoKCkNMVVNURVJfSUQSDBoKS3ViZXJuZXRlcwodCgxJTlNUQU5DRV9JUFMSDRoLMTcyLjI2LjIuNTQKlQIKBkxBQkVMUxKKAiqHAgodCgNhcHASFhoUaXN0aW8taW5ncmVzc2dhdGV3YXkKEwoFY2hhcnQSChoIZ2F0ZXdheXMKFAoIaGVyaXRhZ2USCBoGVGlsbGVyChkKBWlzdGlvEhAaDmluZ3Jlc3NnYXRld2F5CiAKEXBvZC10ZW1wbGF0ZS1oYXNoEgsaCWY3NjRmOTZjNQoSCgdyZWxlYXNlEgcaBWlzdGlvCjkKH3NlcnZpY2UuaXN0aW8uaW8vY2Fub25pY2FsLW5hbWUSFhoUaXN0aW8taW5ncmVzc2dhdGV3YXkKLwojc2VydmljZS5pc3Rpby5pby9jYW5vbmljYWwtcmV2aXNpb24SCBoGbGF0ZXN0ChoKB01FU0hfSUQSDxoNY2x1c3Rlci5sb2NhbAouCgROQU1FEiYaJGlzdGlvLWluZ3Jlc3NnYXRld2F5LWY3NjRmOTZjNS05eHNtOAobCglOQU1FU1BBQ0USDhoMaXN0aW8tc3lzdGVtCl0KBU9XTkVSElQaUmt1YmVybmV0ZXM6Ly9hcGlzL2FwcHMvdjEvbmFtZXNwYWNlcy9pc3Rpby1zeXN0ZW0vZGVwbG95bWVudHMvaXN0aW8taW5ncmVzc2dhdGV3YXkKOQoPU0VSVklDRV9BQ0NPVU5UEiYaJGlzdGlvLWluZ3Jlc3NnYXRld2F5LXNlcnZpY2UtYWNjb3VudAonCg1XT1JLTE9BRF9OQU1FEhYaFGlzdGlvLWluZ3Jlc3NnYXRld2F5'
'x-envoy-peer-metadata-id', 'router~xxxxx~istio-ingressgateway-xxxxx.istio-system~istio-system.svc.cluster.local'
'x-b3-traceid', '3d9b3060d620e2bc37c7f60957d91f28'
'x-b3-spanid', 'b6f30f3edd63ee7e'
'x-b3-parentspanid', '37c7f60957d91f28'
'x-b3-sampled', '0'
'x-envoy-internal', 'true'
'content-length', '0'

2021-03-03T10:44:32.588508Z	debug	envoy http	[external/envoy/source/common/http/conn_manager_impl.cc:1337] [C543840][S3999381319208092814] request end stream
2021-03-03T10:44:32.588598Z	debug	envoy router	[external/envoy/source/common/router/router.cc:415] [C543840][S3999381319208092814] no cluster match for URL '/'

这里的:authority:path:method就是 http 协议的 hosts, path, method,其影子流量使用的是clusterA 的 host 后面加-shadow,而不是目标host地址,比如上面的影子流量网关接受到的是grpc-hello-shikanon.cn-bj.rcmd-staging.skyengine.net.cn-shadow,而不是grpc-hello-mirror.cn-bj.rcmd-testing.skyengine.net.cn

这里主要是因为 istio-ingressgateway 的 envoy 对目标请求做了转换,所以在设置cluster B 的路由策略时应该设置为grpc-hello-shikanon.cn-bj.rcmd-staging.skyengine.net.cn-shadow

代码语言:javascript复制
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: grpc-hello-world-clusterb
  namespace: default
spec:
  gateways:
  - istio-system/internal-gateway
  hosts:
  - grpc-hello-shikanon.cn-bj.rcmd-staging.skyengine.net.cn-shadow
  http:
  - route:
    - destination:
        host: grpc-hello-world.default.svc.cluster.local
        port:
          number: 8000

测试:

代码语言:javascript复制
grpcurl --plaintext -d '{"name":"shikanon"}' grpc-hello-shikanon.cn-bj.rcmd-staging.skyengine.net.cn:8000 helloworld.Greeter.SayHello
{
  "message": "Hello shikanon ! n Pod name is grpc-hello-world-cluster-a-b79b794d4-kdq2n n; Pod IP is 172.26.1.174 n;"
}

我们接到的是clusterA 的请求,同时查看 clusterB 中的服务日志,可以看到请求已经到达,示例代码:https://github.com/shikanon/privatecode/tree/master/traffic-shadowing/k8sconfig/cross-cluster

总结

isito 提供了一个基于七层负载的影子流量,不管是在集群内创建镜像副本,还是跨集群实现流量复制都可以轻松创建。 通过流量镜像我们可以创建一个更接近真实的实验环境,在这个环境下可以进行真实流量下的调试,测试,数据采集和流量回放,这让线上工作作业变成一件更可控的事情,不管是服务迁移还是新旧服务升级都可以提前验证。而且通过 istio 来统一管理网格策略可以统一技术栈,将团队从复杂的技术栈解放出来,极大地降低团队心智负担。

参考文献

  • https://istio.io/latest/docs/tasks/traffic-management/mirroring/
  • https://www.servicemesher.com/istio-handbook/practice/traffic-shadow.html
  • https://blog.markvincze.com/shadow-mirroring-with-envoy/

0 人点赞