什么是流量镜像?
微服务为我们带来了快速开发部署的优秀特性,然而如何降低快速开发部署带来的变更风险成为了一个至关重要的问题。服务网格技术提供了流量镜像(Traffic Mirroring)的功能,也称为影子流量(Traffic shadowing),会将实时流量的副本发送到镜像服务, 镜像流量发生在主服务的关键请求路径之外。这样一来, 可以将生产的流量镜像拷贝到测试集群或者新的测试版本中,在引导实时流量之前进行测试,可以有效地降低版本变更风险, 以尽可能低的风险为生产带来变化的强大的功能。
流量镜像有以下优点:
- 当流量镜像到不同的服务时,会发生在请求的关键路径之外,这样流量镜像产生的任何问题都不会影响到生产环境;
- 忽略对任何镜像流量的响应, 流量被视为“即发即忘”,镜像实例发回的任何响应都将被忽略, 这样就不会干扰到正常的生产流量的响应;
- 当流量被镜像时,请求将通过其主机或授权报头发送到镜像服务附上“–shadow”,用以区分流量从何处被镜像到何处;
- 利用实时生产用例和流量可以有更真实的测试服务环境,有效降低了部署的风险;
启用流量镜像的典型场景
下面介绍几种典型的使用场景可以发挥流量镜像的优势:
- 线上流量模拟和测试:测试集群的测试可以使用生产实例真实流量,不会影响正常生产的关键路径。比如要用新系统替换掉老旧系统或者系统经历了大规模改造的时候,可以将线上流量导入新系统试运行;一些实验性的架构调整,也可以通过线上流量进行模拟测试。
- 用于新版本校验:可以实时对比生产流量和镜像流量的输出结果。由于是全样本的模拟,影子流量可以应用于新服务的预上线演练,由于传统的手工测试本身是一种样本化的行为,通过导入真实流量形态,可以完整的模拟线上的所有情况,比如异常的特殊字符,带恶意攻击的token,可以探测预发布服务最真实的处理能力和对异常的处理能力。
- 用于协作服务版本回退:当用到镜像流量的服务需要修改协作服务时,因为镜像模式添加了-shadow标记, 所以可以正常处理镜像请求,并在提交之前回滚。不会让镜像版本的更改影响生产版本。
- 隔离测试数据库:与数据处理相关的业务,可以使用空的数据存储并加载测试数据,针对该数据进行镜像流量操作,实现测试数据的隔离。
- 用于线上问题排查和临时的数据采集,比如对于一些线上突发性问题,在线下流量总是无法复现,这时候可以临时开启一个分支服务,导入影子流量进行调试和排查,而不比影响线上服务。
- 用于日志行为采集,对于推荐系统和算法来说,样本和数据是非常核心的,传统的自动化测试在算法类的应用所面对的最大的挑战就是无法构建真实环境的用户行为数据,通过影子流量可以将用户行为以日志的形式保存起来,既可以为推荐系统和算法模型构建模拟测试样本数据,也可以作为后续大数据分析用户画像的数据来源再应用到推荐服务中。
如何启用流量镜像
下面是一个简单的 YAML 代码段,显示了如何使用 Istio 启用流量镜像。
apiVersion networking.istio.io/v1beta1 kind VirtualService metadata name myapp-traffic-mirroring spec hosts myapp httproutedestination host myapp.default.svc.cluster.local port number8000 subset v1 weight100 mirror host myapp.default.svc.cluster.local port number8000 subset v1-mirroring
上面的 VirtualService 将 100%的流量路由到 v1 子集,同时还将相同的流量镜像到该v1-mirroring子集。发送给v1子集的相同请求将被复制并触发该v1-mirroring子集。
最快看到此效果的方法是v1-mirroring在将一些请求发送到应用程序的 v1 版本时查看应用程序的日志。
调用应用程序时您将获得的响应将来自该v1子集。但是,您还将看到请求镜像到该v1-mirroring子集。
基于集群内服务层做流量镜像
下面通过实例来演示一下,先让所有流量都到v1版本,然后使用规则将流量镜像到v1-mirroring版本:
步骤一:配置并启动示例应用服务
首先部署两个版本的httpbin服务:
apiVersion v1 kind ServiceAccount metadata name httpbin ---apiVersion v1 kind Service metadata name httpbin labels app httpbin service httpbin spec portsname http port8000 targetPort80 selector app httpbin ---apiVersion apps/v1 kind Deployment metadata name httpbin-v1 spec replicas1 selector matchLabels app httpbin version v1 template metadata labels app httpbin version v1 spec serviceAccountName httpbin containersimage docker.io/kennethreitz/httpbin imagePullPolicy IfNotPresent name httpbin portscontainerPort80---apiVersion apps/v1 kind Deployment metadata name httpbin-v1-mirroring spec replicas1 selector matchLabels app httpbin version v1-mirroring template metadata labels app httpbin version v1-mirroring spec serviceAccountName httpbin containersimage docker.io/kennethreitz/httpbin imagePullPolicy IfNotPresent name httpbin portscontainerPort80
然后部署一个客户端应用sleep服务,为curl来提供负载:
apiVersion v1 kind ServiceAccount metadata name sleep ---apiVersion v1 kind Service metadata name sleep labels app sleep service sleep spec portsport80 name http selector app sleep ---apiVersion apps/v1 kind Deployment metadata name sleep spec replicas1 selector matchLabels app sleep template metadata labels app sleep spec terminationGracePeriodSeconds0 serviceAccountName sleep containersname sleep image curlimages/curl command"/bin/sleep""infinity" imagePullPolicy IfNotPresent volumeMountsmountPath /etc/sleep/tls name secret-volume volumesname secret-volume secret secretName sleep-secret optionaltrue---
步骤二:创建路由策略
通过如下控制台来创建新的目标规则,定义2个不同的版本:v1和v1-mirroring。
通过如下控制台来创建新的虚拟服务路由策略,将全部100%的流量导入到v1版本。同时, 将流量镜像到v1-mirroring服务。
通过如下YAML来创建新的路由策略,将全部100%的流量导入到v1版本。同时, 将流量镜像到v2服务。
apiVersion networking.istio.io/v1beta1 kind DestinationRule metadata name httpbin spec host httpbin subsetsname v1 labels version v1 name v1-mirroring labels version v1-mirroring ---apiVersion networking.istio.io/v1beta1 kind VirtualService metadata name httpbin-traffic-mirroring spec hosts httpbin httproutedestination host httpbin port number8000 subset v1 weight100 mirror host httpbin port number8000 subset v1-mirroring mirrorPercentage value50
步骤三:发送流量请求
等待应用服务运行正常之后, 我们通过sleep应用向httpbin服务发送一些流量:
exportSLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})kubectl exec -it$SLEEP_POD-csleep--curl http://httpbin:8000/headers
可以发现v1和v1-mirroring版本的Pod中有上述请求流量访问的记录, 说明100%的流量在发送到v1版本的同时, 也有50%的流量被镜像到v1-mirroring的pod中,这与上述定义的镜像策略相匹配。
对比两者的日志内容, 可以看到在v1的流量被镜像到了v1-mirroring, 而且日志中的v1-mirroring报文比v1大是流量被标记为影子流量所致, 并且在authority结果中影子流量自动补加了“-shadow”。
基于网格层跨集群流量镜像
基于网关层做流量镜像一般多是用于为预发布环境导入线上真实流量,所以多是跨集群中使用到。 这里以生产环境的集群(clusterA)和 测试集群(clusterB)为例,主体请求在生产环境的集群clusterA上,由它的网关将流量镜像拷贝到集群clusterB中,如下图:
步骤一:在测试集群中配置并启动示例应用服务
首先在测试集群中部署一个版本的httpbin服务:
apiVersion v1 kind ServiceAccount metadata name httpbin ---apiVersion v1 kind Service metadata name httpbin labels app httpbin service httpbin spec portsname http port8000 targetPort80 selector app httpbin ---apiVersion apps/v1 kind Deployment metadata name httpbin-v1 spec replicas1 selector matchLabels app httpbin version v1 template metadata labels app httpbin version v1 spec serviceAccountName httpbin containersimage docker.io/kennethreitz/httpbin imagePullPolicy IfNotPresent name httpbin portscontainerPort80---
步骤二:在测试集群中配置网关路由规则
apiVersion networking.istio.io/v1alpha3 kind Gateway metadata name httpbin-gateway spec selector istio ingressgateway # use istio default controller serversport number80 name http protocol HTTP hosts"*"---apiVersion networking.istio.io/v1alpha3 kind VirtualService metadata name httpbin spec hosts"*" gateways httpbin-gateway httpmatchuri prefix /headers routedestination host httpbin port number8000
然后通过访问集群B中的入口网关, 确认上述测试服务工作正常, 返回相应的请求结果。
curl http://{集群B的入口网关地址}/headers
类似结果如下:
"headers" "Accept""*/*" "Host""47.99.56.58" "User-Agent""curl/7.79.1" "X-Envoy-Attempt-Count""1" "X-Envoy-External-Address""120.244.218.105" "X-Forwarded-Client-Cert""By=spiffe://cluster.local/ns/default/sa/httpbin;Hash=158e4ef69876550c34d10e3bfbd8d43f5ab481b16ba0e90b4e38a2d53acf134f;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"
步骤三:在主集群的网格中配置外部访问规则
由于镜像的服务主机是一个外部域名,所以我们这里需要添加一个 ServiceEntry 对服务主机host的 DNS 解析方式进行指定:
apiVersion networking.istio.io/v1alpha3 kind ServiceEntry metadata name httpbin-cluster-b spec hosts httpbin.mirror.cluster-b location MESH_EXTERNAL portsnumber 80 #注意此处的端口值是指向集群cluster-b的入口网关的端口80 name http protocol HTTP resolution STATIC endpointsaddress 47.95.xx.xx #注意此处的IP值是指向集群cluster-b的入口网关地址
然后, 通过如下YAML来创建新的路由策略,将全部100%的流量导入到主集群内的v1版本http服务。同时, 将流量镜像到测试集群B中的服务, 其中通过访问域名httpbin.mirror.cluster-b指向外部的服务地址。
apiVersion networking.istio.io/v1alpha3 kind Gateway metadata name httpbin-gateway spec selector istio ingressgateway # use istio default controller serversport number80 name http protocol HTTP hosts"*"---apiVersion networking.istio.io/v1alpha3 kind VirtualService metadata name httpbin spec gateways httpbin-gateway hosts'*' httpmatchuri prefix /headers mirror host httpbin.mirror.cluster-b port number80 mirrorPercentage value50 routedestination host httpbin port number8000 subset v1
查看主集群中入口网关Pod中的Envoy config dump, 可以看到类似如下的内容:
"routes": "match" "prefix""/headers" "case_sensitive"true "route" "cluster""outbound|8000|v1|httpbin.default.svc.cluster.local" "timeout""0s" "retry_policy" "retry_on""connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes" "num_retries"2 "retry_host_predicate" "name""envoy.retry_host_predicates.previous_hosts" "typed_config" "@type""type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate" "host_selection_retry_max_attempts""5" "retriable_status_codes" 503 "request_mirror_policies" "cluster""outbound|80||httpbin.mirror.cluster-b" "runtime_fraction" "default_value" "numerator"500000 "denominator""MILLION" "trace_sampled"false
总结
流量镜像主要用于能够在不以任何方式影响最终客户端的情况下测试具有实际生产流量的服务。当我们重写现有服务时,它特别有用,我们想要验证新版本是否可以以相同的方式处理真正的各种传入请求,或者当我们想要在同一服务的两个版本之间进行比较基准测试时。它还可以用于对我们的请求进行一些额外的越界处理,这些处理可以异步完成,例如收集一些额外的统计信息,或者进行一些扩展日志记录。
总之, 在实践中,无论是在生产环境还是非生产环境中,将生产流量镜像到测试集群,是降低新部署风险的一种非常有效的方法。像大型互联网企业,多年来也一直在坚持这么做。服务网格技术提供了一个基于七层负载的影子流量,不管是在集群内创建镜像副本,还是跨集群实现流量复制都可以轻松创建。通过流量镜像我们可以创建一个更接近真实的实验环境,在这个环境下可以进行真实流量下的调试、测试、或者数据采集和流量回放,这让线上工作作业变成一件更可控的事情。而且通过服务网格技术来统一管理网格策略可以统一技术栈,将团队从复杂的技术栈解放出来,极大地降低团队心智负担。