流量镜像,也叫影子流量(Traffic shadowing),是一种通过复制生产环境的流量到非生产环境(一般是staging环境)进行测试开发的工作模式。
影子流量常用场景:
这里给大家介绍基于 istio 服务网格做网络流量镜像的方法。
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 命名空间):
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:
$ istioctl kube-inject -f deploy-v1.yaml | kubectl apply -f -
$ istioctl kube-inject -f deploy-v2.yaml | kubectl apply -f -
构建istio virtualservice:
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 参数说明:
用grpcurl工具测试:
$ 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 的日志:
$ 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
$ 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 实现路由策略和流量镜像配置,这里和集群内调用是类似的:
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 解析方式进行指定:
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的目标路由是无法匹配的,日志如下:
$ 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
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
测试:
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 来统一管理网格策略可以统一技术栈,将团队从复杂的技术栈解放出来,极大地降低团队心智负担。