K8s 流量複製方案
- 2020 年 2 月 14 日
- 筆記
背景
測試環境沒有真實的數據, 會導致很多測試工作難以展開, 尤其是一些測試任務需要使用生產環境來做時, 會極大影響現網的穩定性。
我們需要一個流量複製方案, 將現網流量複製到預發布/測試環境

期望
- 將線上請求拷貝一份到預發布/測試環境
- 不影響現網請求
- 可配置流量複製比例, 畢竟測試環境資源有限
- 零程式碼改動
方案

- 承載入口流量的 Pod 新增一個
Nginx 容器接管流量 - Nginx Mirror 模組會將流量複製一份並 proxy 到指定 URL (測試環境)
Nginx mirror複製流量不會影響正常請求處理流程, 鏡像請求的 Resp 會被 Nginx 丟棄K8s Service按照Label Selector去選擇請求分發的 Pod, 意味著不同Pod, 只要有相同Label, 就可以協同處理請求- 通過控制有
Mirror 功能的 Pod和正常的 Pod的比例, 便可以配置流量複製的比例
我們的部署環境為 騰訊雲容器服務, 不過所述方案是普適於 Kubernetes 環境的.
實現
PS: 下文假定讀者了解
- Kubernetes 以及 YAML
- Helm
- Nginx
Nginx 鏡像
使用 Nginx 官方鏡像便已經預裝了 Mirror 插件
即: docker pull nginx
yum install nginx 安裝的版本貌似沒有 Mirror 插件的哦, 需要自己裝
Nginx ConfigMap
kind: ConfigMap metadata: name: entrance-nginx-config namespace: default apiVersion: v1 data: nginx.conf: |- worker_processes auto; error_log /data/athena/logs/entrance/nginx-error.log; events { worker_connections 1024; } http { default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { access_log /data/athena/logs/entrance/nginx-access.log; listen {{ .Values.entrance.service.nodePort }}; server_name entrance; location / { root html; index index.html index.htm; } location /entrance/ { mirror /mirror; access_log /data/athena/logs/entrance/nginx-entrance-access.log; proxy_pass http://localhost:{{ .Values.entrance.service.nodePortMirror }}/; } location /mirror { internal; access_log /data/athena/logs/entrance/nginx-mirror-access.log; proxy_pass {{ .Values.entrance.mirrorProxyPass }}; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }
其中重點部分如下:

業務方容器 + Nginx Mirror
{{- if .Values.entrance.mirrorEnable }} apiVersion: extensions/v1beta1 kind: Deployment metadata: name: entrance-mirror spec: replicas: {{ .Values.entrance.mirrorReplicaCount }} template: metadata: labels: name: entrance spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 podAffinityTerm: labelSelector: matchExpressions: - key: "name" operator: In values: - entrance topologyKey: "kubernetes.io/hostname" initContainers: - name: init-kafka image: "centos-dev" {{- if .Values.delay }} command: ['bash', '-c', 'sleep 480s; until nslookup athena-cp-kafka; do echo "waiting for athena-cp-kafka"; sleep 2; done;'] {{- else }} command: ['bash', '-c', 'until nslookup athena-cp-kafka; do echo "waiting for athena-cp-kafka"; sleep 2; done;'] {{- end }} containers: - image: "{{ .Values.entrance.image.repository }}:{{ .Values.entrance.image.tag }}" name: entrance ports: - containerPort: {{ .Values.entrance.service.nodePort }} env: - name: ATHENA_KAFKA_BOOTSTRAP value: "{{ .Values.kafka.kafkaBootstrap }}" - name: ATHENA_KAFKA_SCHEMA_REGISTRY_URL value: "{{ .Values.kafka.kafkaSchemaRegistryUrl }}" - name: ATHENA_PG_CONN value: "{{ .Values.pg.pgConn }}" - name: ATHENA_COS_CONN value: "{{ .Values.cos.cosConn }}" - name: ATHENA_DEPLOY_TYPE value: "{{ .Values.deployType }}" - name: ATHENA_TPS_SYS_ID value: "{{ .Values.tps.tpsSysId }}" - name: ATHENA_TPS_SYS_SECRET value: "{{ .Values.tps.tpsSysSecret }}" - name: ATHENA_TPS_BASE_URL value: "{{ .Values.tps.tpsBaseUrl }}" - name: ATHENA_TPS_RESOURCE_FLOW_PERIOD_SEC value: "{{ .Values.tps.tpsResourceFlowPeriodSec }}" - name: ATHENA_CLUSTER value: "{{ .Values.cluster }}" - name: ATHENA_POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: ATHENA_HOST_IP valueFrom: fieldRef: fieldPath: status.hostIP - name: ATHENA_POD_IP valueFrom: fieldRef: fieldPath: status.podIP command: ['/bin/bash', '/data/service/go_workspace/script/start-entrance.sh', '-host 0.0.0.0:{{ .Values.entrance.service.nodePortMirror }}'] volumeMounts: - mountPath: /data/athena/ name: athena readOnly: false imagePullPolicy: IfNotPresent resources: limits: cpu: 3000m memory: 800Mi requests: cpu: 100m memory: 100Mi livenessProbe: exec: command: - bash - /data/service/go_workspace/script/health-check/check-entrance.sh initialDelaySeconds: 120 periodSeconds: 60 - image: "{{ .Values.nginx.image.repository }}:{{ .Values.nginx.image.tag }}" name: entrance-mirror ports: - containerPort: {{ .Values.entrance.service.nodePort }} volumeMounts: - mountPath: /data/athena/ name: athena readOnly: false - mountPath: /etc/nginx/nginx.conf name: nginx-config subPath: nginx.conf imagePullPolicy: IfNotPresent resources: limits: cpu: 1000m memory: 500Mi requests: cpu: 100m memory: 100Mi livenessProbe: tcpSocket: port: {{ .Values.entrance.service.nodePort }} timeoutSeconds: 3 initialDelaySeconds: 60 periodSeconds: 60 terminationGracePeriodSeconds: 10 nodeSelector: entrance: "true" volumes: - name: athena hostPath: path: "/data/athena/" - name: nginx-config configMap: name: entrance-nginx-config imagePullSecrets: - name: "{{ .Values.imagePullSecrets }}" {{- end }}
上面為真實在業務中使用的 Deployment 配置, 有些地方可以參考:
valueFrom.fieldRef.fieldPath可以取到容器運行時的一些欄位, 如NodeIP,PodIP這些可以用於全鏈路監控ConfigMap直接 Mount 到文件系統, 覆蓋默認配置的例子affinity.podAntiAffinity親和性調度, 使 Pod 在主機間均勻分布- 使用了
tcpSocket和exec.command兩種健康檢查方式
Helm Values
# entrance, Athena 上報入口模組 entrance: enable: true replicaCount: 3 mirrorEnable: true mirrorReplicaCount: 1 mirrorProxyPass: "http://10.16.0.147/entrance/" image: repository: athena-go tag: v1901091026 service: nodePort: 30081 nodePortMirror: 30082
如上, replicaCount: 3 + mirrorReplicaCount: 1 = 4 個容器, 有 1/4 流量複製到 http://10.16.0.147/entrance/
內網負載均衡
流量複製到測試環境時, 盡量使用內網負載均衡, 為了成本, 安全及性能方面的考慮

總結
通過下面幾個步驟, 便可以實現流量複製啦
- 建一個內網負載均衡, 暴漏測試環境的
服務入口 Service 服務入口 Service需要有可以更換埠號的能力 (例如命令行參數/環境變數)- 線上環境, 新增一個 Deployment, Label 和之前的
服務入口 Service一樣, 只是埠號分配一個新的 - 為新增的 Deployment 增加一個 Nginx 容器, 配置 nginx.conf
- 調節有
Nginx Mirror的 Pod 和 正常的Pod比例, 便可以實現按比例流量複製
