大數據

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

背景

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

影子流量常用場景:

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

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

Envoy實現流量鏡像的原理

envoy-mirror-setup.png

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: 鏡像流量百分比

集群內同service影子流量.png

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

跨集群影子流量.png

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

參考文獻

Leave a Reply

Your email address will not be published. Required fields are marked *