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
比例, 便可以实现按比例流量复制