K8s 流量複製方案

  • 2020 年 2 月 14 日
  • 筆記

背景

測試環境沒有真實的數據, 會導致很多測試工作難以展開, 尤其是一些測試任務需要使用生產環境來做時, 會極大影響現網的穩定性。

我們需要一個流量複製方案, 將現網流量複製到預發布/測試環境

流量複製示意

期望

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

方案

Kubernetes 流量複製方案
  • 承載入口流量的 Pod 新增一個 Nginx 容器 接管流量
  • Nginx Mirror 模組會將流量複製一份並 proxy 到指定 URL (測試環境)
  • Nginx mirror 複製流量不會影響正常請求處理流程, 鏡像請求的 Resp 會被 Nginx 丟棄
  • K8s Service 按照 Label Selector 去選擇請求分發的 Pod, 意味著不同Pod, 只要有相同 Label, 就可以協同處理請求
  • 通過控制有 Mirror 功能的 Pod正常的 Pod 的比例, 便可以配置流量複製的比例

我們的部署環境為 騰訊雲容器服務, 不過所述方案是普適於 Kubernetes 環境的.

實現

PS: 下文假定讀者了解

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 在主機間均勻分布
  • 使用了 tcpSocketexec.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/

內網負載均衡

流量複製到測試環境時, 盡量使用內網負載均衡, 為了成本, 安全及性能方面的考慮

LB-inner-config

總結

通過下面幾個步驟, 便可以實現流量複製啦

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