天天看點

基于istio的流量鏡像建構真實流量的staging環境

背景

流量鏡像,也叫影子流量(Traffic shadowing),是一種通過複制生産環境的流量到非生産環境(一般是staging環境)進行測試開發的工作模式。

影子流量常用場景:

  • 線上流量模拟和測試,比如要用新系統替換掉老舊系統或者系統經曆了大規模改造的時候,可以将線上流量導入新系統試運作;一些實驗性的架構調整,也可以通過線上流量進行模拟測試。
  • 由于是全樣本的模拟,影子流量可以應用于新服務的預上線演練,由于傳統的手工測試本身是一種樣本化的行為,通過導入真實流量形态,可以完整的模拟線上的所有情況,比如異常的特殊字元,帶惡意攻擊的token,可以探測預釋出服務最真實的處理能力和對異常的處理能力。
  • 用于線上問題排查和臨時的資料采集,比如對于一些線上突發性問題,線上下流量總是無法複現,這時候可以臨時開啟一個分支服務,導入影子流量進行調試和排查,而不比影響線上服務。
  • 用于日志行為采集,對于推薦系統和算法來說,樣本和資料是非常核心的,傳統的自動化測試在算法類的應用所面對的最大的挑戰就是無法建構真實環境的使用者行為資料,通過影子流量可以将使用者行為以日志的形式儲存起來,既可以為推薦系統和算法模型構模組化拟測試樣本資料,也可以作為後續大資料分析使用者畫像的資料來源再應用到推薦服務中。

這裡給大家介紹基于 istio 服務網格做網絡流量鏡像的方法。

Envoy實作流量鏡像的原理

基于istio的流量鏡像建構真實流量的staging環境

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釋出分支服務為其引入影子流量,首先建構正常服務和分支服務(分支服務放在 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 參數說明:

  • host: istio的Destination,目标host位址
  • mirror_percent: 鏡像流量百分比
基于istio的流量鏡像建構真實流量的staging環境

用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,如下圖:

基于istio的流量鏡像建構真實流量的staging環境

在 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 來統一管理網格政策可以統一技術棧,将團隊從複雜的技術棧解放出來,極大地降低團隊心智負擔。

參考文獻

繼續閱讀