API 网关 K8s 生产部署终极指南:万字总结 12 个避坑点 + 3 次线上事故复盘,从开发到上线一步到位!
8/30/2026

文章链接:https://blog.csdn.net/qq_43201350/article/details/163477946

从 Docker Compose 开发环境到 K8s 生产集群,这条路上坑比代码多。 本文是我在 3 个项目中将网关从开发→测试→预发→生产的完整记录,涵盖 12 个避坑点、5 套完整 YAML、3 次线上事故还原。 读完这篇文章,你能省下至少两周的踩坑时间。


目录

  1. 生产部署的基本盘

  2. 避坑 1:多副本 etcd 的正确姿势

  3. 避坑 2:滚动更新零中断

  4. 避坑 3:TLS 证书自动管理

  5. 避坑 4:配置管理优雅方案

  6. 避坑 5:HPA 自动扩缩容

  7. 避坑 6:健康检查配置

  8. 避坑 7:资源限制

  9. 避坑 8:日志与监控集成

  10. 避坑 9:网络策略与安全

  11. 避坑 10:灰度与金丝雀发布

  12. 事故复盘 x3

  13. 完整部署架构总览


一、生产部署的基本盘

1.1 从 Docker Compose 到K8s的思维转变

维度Docker ComposeK8s 生产
存储本地目录挂载PVC / 云盘
网络容器间直连Service + Ingress
配置文件映射ConfigMap / Secret
扩容手动改副本数HPA 自动
证书手动生成cert-manager 自动
日志docker logs集中采集

1.2 生产环境拓扑

                    ┌──────────────────┐
                    │  External LB     │  (云厂商 CLB/ALB)
                    │  port: 443       │
                    └────────┬─────────┘
                             │
              ┌──────────────┴──────────────┐
              │     K8s Ingress (Nginx)     │
              │   TLS 终结,转发到 Service   │
              └──────────────┬──────────────┘
                             │
              ┌──────────────┴──────────────┐
              │   Service: api-gateway-svc  │
              │   ClusterIP, port: 9080     │
              └──────────────┬──────────────┘
                             │
         ┌───────────────────┼───────────────────┐
         ▼                   ▼                   ▼
   ┌──────────┐       ┌──────────┐       ┌──────────┐
   │Gateway-1 │       │Gateway-2 │       │Gateway-3 │
   │ (Pod)    │       │ (Pod)    │       │ (Pod)    │
   │ Admin:   │       │ Admin:   │       │ Admin:   │
   │ 9180     │       │ 9180     │       │ 9180     │
   └────┬─────┘       └────┬─────┘       └────┬─────┘
        │                  │                  │
        └──────────────────┼──────────────────┘
                           │
              ┌────────────┴────────────┐
              │  Service: etcd-headless │
              │  指向 etcd StatefulSet   │
              └────────────┬────────────┘
                           │
         ┌─────────────────┼─────────────────┐
         ▼                 ▼                   ▼
   ┌──────────┐     ┌──────────┐       ┌──────────┐
   │ etcd-0   │◄───►│ etcd-1   │◄─────►│ etcd-2   │
   │ PVC:10Gi │     │ PVC:10Gi │       │ PVC:10Gi │
   └──────────┘     └──────────┘       └──────────┘

二、避坑 1:高可用 etcd 集群

坑:单节点 etcd 是最大单点故障

很多人生产环境仍然 docker run etcd 单节点。一旦 etcd 挂了,网关无法读取路由配置、无法热更新,全站 503。

这个坑踩过的代价:全站宕机 45 分钟。

方案:StatefulSet 三节点 etcd

# k8s/etcd-statefulset.yaml
apiVersion: v1
kind: Service
metadata:
  name: etcd-headless
  namespace: demo-gateway
  labels:
    app: etcd
spec:
  clusterIP: None
  ports:
    - name: client
      port: 2379
      targetPort: 2379
    - name: peer
      port: 2380
      targetPort: 2380
  selector:
    app: etcd
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: etcd
  namespace: demo-gateway
spec:
  serviceName: etcd-headless
  replicas: 3
  selector:
    matchLabels:
      app: etcd
  podManagementPolicy: Parallel
  template:
    metadata:
      labels:
        app: etcd
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - etcd
              topologyKey: kubernetes.io/hostname
      containers:
        - name: etcd
          image: bitnami/etcd:3.5
          env:
            - name: ALLOW_NONE_AUTHENTICATION
              value: "yes"
            - name: ETCD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: ETCD_ADVERTISE_CLIENT_URLS
              value: "http://$(ETCD_NAME).etcd-headless.demo-gateway.svc.cluster.local:2379"
            - name: ETCD_LISTEN_CLIENT_URLS
              value: "http://0.0.0.0:2379"
            - name: ETCD_INITIAL_ADVERTISE_PEER_URLS
              value: "http://$(ETCD_NAME).etcd-headless.demo-gateway.svc.cluster.local:2380"
            - name: ETCD_LISTEN_PEER_URLS
              value: "http://0.0.0.0:2380"
            - name: ETCD_INITIAL_CLUSTER_TOKEN
              value: "demo-etcd-cluster"
            - name: ETCD_INITIAL_CLUSTER
              value: "etcd-0=http://etcd-0.etcd-headless.demo-gateway.svc.cluster.local:2380,etcd-1=http://etcd-1.etcd-headless.demo-gateway.svc.cluster.local:2380,etcd-2=http://etcd-2.etcd-headless.demo-gateway.svc.cluster.local:2380"
            - name: ETCD_INITIAL_CLUSTER_STATE
              value: "new"
          ports:
            - containerPort: 2379
              name: client
            - containerPort: 2380
              name: peer
          livenessProbe:
            exec:
              command:
                - /bin/sh
                - -c
                - etcdctl endpoint health
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            exec:
              command:
                - /bin/sh
                - -c
                - etcdctl endpoint health
            initialDelaySeconds: 10
            periodSeconds: 5
          volumeMounts:
            - name: data
              mountPath: /bitnami/etcd
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
            limits:
              cpu: 1000m
              memory: 1Gi
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

关键点

配置项作用
podAntiAffinity确保三副本分布在不同物理节点
Parallel并行启动,加速集群初始化
volumeClaimTemplates每个节点独立存储,避免脑裂时数据冲突
ETCD_INITIAL_CLUSTER预定义所有成员,启动时自动建集群

验证

# 检查集群健康
kubectl exec -n demo-gateway etcd-0 -- etcdctl endpoint health --cluster
# 预期: 三个节点都是 healthy
 
# 查看集群成员
kubectl exec -n demo-gateway etcd-0 -- etcdctl member list

三、避坑 2:滚动更新零中断

坑:默认滚动更新会导致连接断开

很多人以为 K8s 的 RollingUpdate 策略天然零中断。实际上, 如果不配 preStop hook + terminationGracePeriodSeconds,新 Pod 还没就绪旧 Pod 就被杀了,导致流量黑洞。

方案:三要素保证零中断

# k8s/gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gateway
  namespace: demo-gateway
  labels:
    app: api-gateway
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # 最多多创建 1 个 Pod
      maxUnavailable: 0    # 保证至少 3 个 Pod 始终可用
  selector:
    matchLabels:
      app: api-gateway
  template:
    metadata:
      labels:
        app: api-gateway
    spec:
      # ⚠️ 关键1:给足优雅终止时间
      terminationGracePeriodSeconds: 60
 
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - api-gateway
                topologyKey: kubernetes.io/hostname
 
      containers:
        - name: gateway
          image: apache/apisix:3.10.0-centos
          ports:
            - containerPort: 9080
              name: proxy
              protocol: TCP
            - containerPort: 9180
              name: admin
              protocol: TCP
            - containerPort: 9443
              name: tls
              protocol: TCP
 
          env:
            - name: APISIX_STAND_ALONE
              value: "false"
            - name: APISIX_DEPLOYMENT_ETCD_HOST
              value: "http://etcd-headless.demo-gateway.svc.cluster.local:2379"
 
          # ⚠️ 关键2:生命周期钩子
          lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sh
                  - -c
                  - |
                    echo "=== Preparing for graceful shutdown ==="
                    # 1. 将自己从负载均衡摘除(等 5 秒让 Service 端点更新)
                    sleep 5
                    # 2. 停止接收新请求(Nginx graceful shutdown)
                    apisix quit || true
                    # 3. 等待存量请求处理完毕
                    sleep 10
                    echo "=== Graceful shutdown completed ==="
 
          # ⚠️ 关键3:就绪探针——旧 Pod 启动后再杀
          readinessProbe:
            httpGet:
              path: /apisix/status
              port: 9080
            initialDelaySeconds: 15
            periodSeconds: 5
            failureThreshold: 3
 
          livenessProbe:
            httpGet:
              path: /apisix/status
              port: 9080
            initialDelaySeconds: 30
            periodSeconds: 15
            failureThreshold: 5
 
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: 2000m
              memory: 2Gi
 
          volumeMounts:
            - name: gateway-config
              mountPath: /usr/local/apisix/conf/config.yaml
              subPath: config.yaml
 
      volumes:
        - name: gateway-config
          configMap:
            name: gateway-config

滚动更新全过程时间线

T+0s:  kubectl apply 触发更新
T+0s:  创建新 Pod-1 (maxSurge=1,现 4 个 Pod)
T+15s: 新 Pod-1 就绪探针通过 → Service 端点加入
T+20s: 准备杀旧 Pod-A
T+20s: 旧 Pod-A 收到 SIGTERM
T+20s: preStop hook 执行: sleep 5 → apisix quit → sleep 10
T+35s: 旧 Pod-A 容器退出
T+35s: 创建新 Pod-2
... 重复
T+90s: 全部更新完成,全程 0 个 502

验证零中断

# 开一个终端持续压测
while true; do
  curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
    http://api-gateway-svc.demo-gateway.svc.cluster.local:9080/apisix/status
  sleep 0.1
done
 
# 另一个终端触发更新
kubectl rollout restart deployment/api-gateway -n demo-gateway
 
# 观察终端 A:全程不应出现 5xx

四、避坑 3:TLS 证书自动管理

坑:手动管证书,过期了都不知道

手写 openssl 命令生成证书 → 文档里记到期日 → 忘了 → 某天一早用户反馈"网站被标不安全"。

方案:cert-manager 全自动

# 安装 cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
# k8s/cert-issuer.yaml
# 使用云厂商 DNS 验证的 ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: demo-admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          ingress:
            class: nginx
---
# 为网关域名申请证书
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: gateway-tls-cert
  namespace: demo-gateway
spec:
  secretName: gateway-tls-secret
  duration: 2160h  # 90 天
  renewBefore: 360h # 提前 15 天续期
  dnsNames:
    - api.example.com
    - admin-api.example.com
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
---
# Ingress 配置 TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: gateway-ingress
  namespace: demo-gateway
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: gateway-tls-secret
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-gateway-svc
                port:
                  number: 9080

cert-manager 自动做的事情

步骤说明
1向 Let’s Encrypt 申请证书
2通过 HTTP-01 验证域名归属
3证书写入 Secret gateway-tls-secret
4证书过期前 15 天自动续期
5Ingress 自动挂载最新证书

五、避坑 4:配置管理(不要硬编码)

坑:把配置写在启动命令或镜像里

改一个路由超时时间 → 重新打镜像 → 走 CI/CD → 部署 → 半小时过去了。

方案:ConfigMap + 热加载

# k8s/gateway-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: gateway-config
  namespace: demo-gateway
data:
  config.yaml: |
    apisix:
      node_listen: 9080
      enable_admin: true
      enable_admin_cors: true
 
      # etcd 集群地址
      deployment:
        role: traditional
        role_traditional:
          config_provider: etcd
        etcd:
          host:
            - "http://etcd-0.etcd-headless.demo-gateway.svc.cluster.local:2379"
            - "http://etcd-1.etcd-headless.demo-gateway.svc.cluster.local:2379"
            - "http://etcd-2.etcd-headless.demo-gateway.svc.cluster.local:2379"
          prefix: "/apisix"
          timeout: 30
 
      # Admin API 配置
      admin_key:
        - name: admin
          key: xxxxxxxxxxxxxx   # 生产环境用 Secret!
          role: admin
 
      # Prometheus 暴露
      plugin_attr:
        prometheus:
          export_uri: /apisix/prometheus/metrics
          enable_export_server: true
          export_addr:
            ip: 0.0.0.0
            port: 9091
 
    # 日志配置
    nginx_config:
      error_log: "/usr/local/apisix/logs/error.log"
      error_log_level: "warn"
 
      http_configuration_snippet: |
        # 真实客户端 IP——网关前面有 LB/Ingress
        real_ip_header X-Forwarded-For;
        real_ip_recursive on;
        set_real_ip_from 0.0.0.0/0;
 
        # 日志格式——结构化 JSON,方便采集
        log_format main escape=json
          '{'
            '"timestamp":"$time_iso8601",'
            '"remote_addr":"$remote_addr",'
            '"x_forwarded_for":"$http_x_forwarded_for",'
            '"method":"$request_method",'
            '"uri":"$uri",'
            '"status":$status,'
            '"body_bytes_sent":$body_bytes_sent,'
            '"request_time":$request_time,'
            '"upstream_response_time":"$upstream_response_time",'
            '"upstream_addr":"$upstream_addr",'
            '"upstream_status":"$upstream_status",'
            '"http_host":"$http_host",'
            '"http_user_agent":"$http_user_agent",'
            '"route_id":"$http_x_apisix_route_id"'
          '}';
 
        access_log /usr/local/apisix/logs/access.log main buffer=16k flush=3s;
# k8s/gateway-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: gateway-admin-secret
  namespace: demo-gateway
type: Opaque
stringData:
  admin-key: "实际的生产密钥请用随机字符串生成"

关键点 :敏感信息走 Secret,通用配置走 ConfigMap。


六、避坑 5:HPA 自动扩缩容(切忌拍脑袋)

坑:只配了CPU阈值

“CPU 超过 80% 就扩容”——然后某天突发流量,CPU 还没上来,内存先 OOM 了。

方案:多指标 HPA

# k8s/gateway-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-gateway-hpa
  namespace: demo-gateway
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-gateway
  minReplicas: 3
  maxReplicas: 20
  behavior:
    # 扩容策略:激进
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Pods
          value: 4           # 每 30s 最多扩容 4 个
          periodSeconds: 30
        - type: Percent
          value: 50          # 或 50%
          periodSeconds: 30
      selectPolicy: Max
    # 缩容策略:保守
    scaleDown:
      stabilizationWindowSeconds: 300   # 等 5 分钟再缩,防止抖动
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60
  metrics:
    # 指标 1:CPU
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
 
    # 指标 2:内存
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
 
    # 指标 3:自定义——QPS(需要 Prometheus Adapter)
    - type: Pods
      pods:
        metric:
          name: apisix_http_requests_per_second
        target:
          type: AverageValue
          averageValue: "150"    # 单 Pod 超过 150 QPS 就扩容

Prometheus Adapter 安装(用于自定义指标)

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus-adapter prometheus-community/prometheus-adapter \
  --namespace demo-gateway \
  --set prometheus.url=http://prometheus-server.demo-gateway.svc:9090

压测验证 HPA

# 使用 hey 压测
hey -z 120s -c 100 -q 200 \
  http://api-gateway-svc.demo-gateway.svc.cluster.local:9080/demo/echo/hello
 
# 观察扩容
kubectl get hpa -n demo-gateway -w
# 预期: 副本数从 3 → 5 → 8 → ...
 
kubectl get pods -n demo-gateway -w

七、避坑 6:健康检查(配错等于没配)

坑:用 / 做健康检查路径

很多网关的 / 返回 404 ,这时 K8s 认为 Pod 不健康,一直重启——死循环。

# ❌ 错误配置
readinessProbe:
  httpGet:
    path: /        # 这个路径可能 404!
    port: 9080
 
# ✅ 正确配置
readinessProbe:
  httpGet:
    path: /apisix/status   # 网关专用健康检查端点
    port: 9080

三层探针的正确姿势

探针作用失败后果典型配置
startupProbePod 启动时专用重启容器initialDelay: 10s, failure: 30
readinessProbe是否接收流量从 Service 摘除period: 5s, failure: 3
livenessProbe是否活着重启容器period: 15s, failure: 5
startupProbe:
  httpGet:
    path: /apisix/status
    port: 9080
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 30    # 给足 2.5 分钟启动时间
 
readinessProbe:
  httpGet:
    path: /apisix/status
    port: 9080
  initialDelaySeconds: 5
  periodSeconds: 5
  failureThreshold: 3     # 15 秒没恢复就摘除
 
livenessProbe:
  httpGet:
    path: /apisix/status
    port: 9080
  initialDelaySeconds: 30
  periodSeconds: 15
  failureThreshold: 5     # 75 秒没恢复就重启

关键公式

startupProbe 总容忍 = 30 × 5 = 150s(给 Pod 充足的启动时间)
readiness 总容忍   = 3 × 5 = 15s(快速摘除不健康的)
liveness 总容忍    = 5 × 15 = 75s(不到万不得已不重启)

八、避坑 7:资源限制——不要裸奔

坑:不设 limits

某个 Pod 的 Worker 进程悄悄泄漏内存,最后吃掉整个节点 90% 内存 → 节点 OOM → 驱逐所有 Pod → 雪崩。

方案:requests = limits = 稳定

resources:
  requests:
    cpu: "500m"
    memory: "512Mi"
  limits:
    cpu: "2000m"
    memory: "2Gi"

生产环境的经验值:

场景requests.cpulimits.cpurequests.memorylimits.memory
轻流量 (< 500 QPS)250m1000m256Mi1Gi
中流量 (500-5000 QPS)500m2000m512Mi2Gi
高流量 (> 5000 QPS)1000m4000m1Gi4Gi

为什么不设 limits = 很大

因为 K8s 的 QoS 机制:

  • requests == limits → Guaranteed(最高优先级,最后被杀)

  • requests < limits → Burstable(中优先级)

  • 没设 → BestEffort(最低优先级,最先被驱逐)

网关是链路入口,必须 Guaranteed


九、避坑 8:日志(进入生产就要换方案)

坑:仍然用http-logger 推日志

http-logger在开发环境够用,但生产环境有 3 个问题:

  1. 丢失 :网关崩溃时未推送的日志消失

  2. 阻塞 :Loki 慢了会阻塞网关线程

  3. 耦合 :日志和业务共享一个 HTTP 连接池

方案:DaemonSet 采集器 + stdout 输出

# 配置网关日志输出到 stdout/文件
# 在 ConfigMap 中:
nginx_config:
  access_log: "/usr/local/apisix/logs/access.log main buffer=64k flush=5s"
  error_log: "/usr/local/apisix/logs/error.log warn"
# k8s/gateway-deployment.yaml 追加 sidecar
spec:
  containers:
    - name: gateway
      # ... 主容器配置 ...
      volumeMounts:
        - name: shared-logs
          mountPath: /usr/local/apisix/logs
 
    # Sidecar——用 Filebeat/Promtail 采集日志
    - name: log-shipper
      image: grafana/promtail:2.9.0
      args:
        - -config.file=/etc/promtail/promtail.yaml
      volumeMounts:
        - name: shared-logs
          mountPath: /var/log/gateway
        - name: promtail-config
          mountPath: /etc/promtail
      resources:
        requests:
          cpu: 50m
          memory: 64Mi
        limits:
          cpu: 200m
          memory: 128Mi
 
  volumes:
    - name: shared-logs
      emptyDir: {}         # Pod 内共享的日志目录
    - name: promtail-config
      configMap:
        name: promtail-config
# k8s/promtail-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: promtail-config
  namespace: demo-gateway
data:
  promtail.yaml: |
    server:
      http_listen_port: 9080
      grpc_listen_port: 0
 
    clients:
      - url: http://loki.demo-gateway.svc.cluster.local:3100/loki/api/v1/push
 
    positions:
      filename: /tmp/positions.yaml
 
    scrape_configs:
      - job_name: gateway-access-log
        static_configs:
          - targets:
              - localhost
            labels:
              app: api-gateway
              log_type: access
              __path__: /var/log/gateway/access.log
        pipeline_stages:
          - json:
              expressions:
                timestamp: timestamp
                status: status
                request_time: request_time
                upstream_addr: upstream_addr
          - labels:
              status:
              upstream_addr:
 
      - job_name: gateway-error-log
        static_configs:
          - targets:
              - localhost
            labels:
              app: api-gateway
              log_type: error
              __path__: /var/log/gateway/error.log

好处 :日志采集和网关进程完全解耦,网关挂了日志也已写入磁盘并推送完毕。


十、避坑 9:网络策略

坑:AdminAPI直接暴露

网关的 Admin API(端口 9180)默认监听 0.0.0.0 ,如果不限制网络策略,任何人只要知道 IP 就能增删路由。

方案:NetworkPolicy 最小权限

# k8s/gateway-networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: gateway-network-policy
  namespace: demo-gateway
spec:
  podSelector:
    matchLabels:
      app: api-gateway
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # 允许外部流量访问网关代理端口
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
      ports:
        - protocol: TCP
          port: 9080
        - protocol: TCP
          port: 9443
 
    # 只允许同 namespace 的监控组件访问 Admin API
    - from:
        - podSelector:
            matchLabels:
              app: prometheus
      ports:
        - protocol: TCP
          port: 9180
 
    # 只允许同 namespace 访问 metrics
    - from:
        - podSelector:
            matchLabels:
              app: prometheus
      ports:
        - protocol: TCP
          port: 9091
 
  egress:
    # 网关需要访问 etcd
    - to:
        - podSelector:
            matchLabels:
              app: etcd
      ports:
        - protocol: TCP
          port: 2379
        - protocol: TCP
          port: 2380
 
    # 网关需要访问上游服务(按需放开)
    - to:
        - namespaceSelector: {}
          podSelector: {}
      ports:
        - protocol: TCP
          port: 8080

验证安全

# 从非允许的 Pod 访问 Admin API(应被拒绝)
kubectl run test-pod --rm -it --image=alpine/curl -- sh
curl http://api-gateway-svc.demo-gateway.svc.cluster.local:9180/admin-api/routes
# 预期: timeout / connection refused

十一、避坑 10:灰度发布(不是等比例切)

坑:用权重直接切流量

“新版本上线,先切 10% 流量试试”——但这 10% 里包含了一个超级 VIP 客户,出问题后投诉最猛。

方案:按请求头/用户粒度灰度

在网关层做灰度,而不是在 K8s Ingress 层:

# 在网关 Admin API 中配置金丝雀路由
 
# 1. 创建两个上游:stable 和 canary
curl -s http://127.0.0.1:9180/admin-api/upstreams/demo-svc-stable -X PUT -d '
{
  "type": "roundrobin",
  "nodes": {"demo-svc-stable.demo-gateway.svc.cluster.local:8080": 100}
}'
 
curl -s http://127.0.0.1:9180/admin-api/upstreams/demo-svc-canary -X PUT -d '
{
  "type": "roundrobin",
  "nodes": {"demo-svc-canary.demo-gateway.svc.cluster.local:8080": 100}
}'
 
# 2. 创建两条路由:canary 优先匹配
# Canary 路由——带特定 header
curl -s http://127.0.0.1:9180/admin-api/routes/demo-svc-canary -X PUT -d '
{
  "uri": "/demo/*",
  "name": "demo-svc-canary",
  "priority": 100,
  "vars": [
    ["http_x_canary", "==", "v2"]
  ],
  "upstream_id": "demo-svc-canary",
  "plugins": {
    "traffic-split": {
      "rules": [{
        "weighted_upstreams": [{
          "upstream_id": "demo-svc-canary",
          "weight": 100
        }]
      }]
    }
  }
}'
 
# 主路由——默认
curl -s http://127.0.0.1:9180/admin-api/routes/demo-svc-stable -X PUT -d '
{
  "uri": "/demo/*",
  "name": "demo-svc-stable",
  "priority": 10,
  "upstream_id": "demo-svc-stable"
}'

灰度策略选择

策略网关配置方式适用场景
Header 匹配vars: [["http_x_canary", "==", "v2"]]内部测试人员验证
用户 ID 哈希traffic-split + hash on userId按百分比切,但同用户一致
地域灰度vars: [["http_x_region", "==", "cn-south"]]先在边缘地区验证
Agent 匹配vars: [["http_user_agent", "~~", "内部测试客户端"]]App 内测

十二、三起线上事故复盘

事故 1:etcd 磁盘满,网关全站 503

时间 :某周五凌晨 2:00

现象 :监控告警——所有路由 503,Prometheus 显示 etcd 连接失败。

原因 :etcd 的 PVC 用完了所有磁盘空间( quota-backend-bytes 未设置上限)。

修复

# 临时:手动压缩 etcd
kubectl exec etcd-0 -- etcdctl compact $(etcdctl endpoint status --write-out=json | jq '.[0].Status.header.revision')
kubectl exec etcd-0 -- etcdctl defrag
 
# 长期:配置自动压缩和配额
# 在 etcd StatefulSet 环境变量中加入
- name: ETCD_AUTO_COMPACTION_RETENTION
  value: "1"          # 保留 1 小时历史
- name: ETCD_QUOTA_BACKEND_BYTES
  value: "8589934592" # 8GB 硬限制

教训

  • etcd 磁盘必须监控(保留 30% 余量)

  • 开启自动压缩( auto-compaction-retention

  • PVC 配够大(生产建议 50Gi+)

事故 2:滚动更新导致 10 秒流量黑洞

时间 :某周三下午 3:00

现象 :发布新版本后,监控显示有 10 秒的 502 高峰。

原因maxUnavailable: 1 + 没有 preStop hook → 旧 Pod 被直接 kill,新 Pod 还没就绪。

修复 :改为本文第三章的配置( maxUnavailable: 0 + preStop + 就绪探针)。

教训

  • maxUnavailable 生产环境必须设 0

  • preStop hook 的 sleep 时间要大于 Service 端点更新延迟(通常 5-10s)

事故 3:HPA 反复扩缩——“颤抖效应”

时间 :某大促活动当天

现象 :网关副本数在 3 ↔ 10 之间反复跳动,每 2 分钟一次。每次扩容都需要 30 秒,缩容后再来一波流量又不够,陷入死循环。

原因scaleDown.stabilizationWindowSeconds 设成了 0(立即缩容)。

修复

scaleDown:
  stabilizationWindowSeconds: 300   # 等 5 分钟
  policies:
    - type: Percent
      value: 10
      periodSeconds: 60

把缩容窗口从 0 改为 300 秒后,"颤抖效应"消失。

教训

  • 扩容要快,缩容要慢

  • stabilizationWindowSeconds 至少 300 秒

  • 大促前提前扩容避免冷启动延迟


十三、完整部署架构

13.1 最终部署清单

# 1. 创建 namespace
kubectl create namespace demo-gateway
 
# 2. 部署 etcd 集群
kubectl apply -f k8s/etcd-statefulset.yaml
 
# 3. 部署网关 + 配置
kubectl apply -f k8s/gateway-config.yaml
kubectl apply -f k8s/gateway-secret.yaml
kubectl apply -f k8s/gateway-deployment.yaml
 
# 4. 暴露服务
kubectl apply -f k8s/gateway-service.yaml
 
# 5. 配置 HPA
kubectl apply -f k8s/gateway-hpa.yaml
 
# 6. 配置 Ingress + TLS
kubectl apply -f k8s/cert-issuer.yaml
kubectl apply -f k8s/gateway-certificate.yaml
kubectl apply -f k8s/gateway-ingress.yaml
 
# 7. 部署监控
kubectl apply -f k8s/promtail-config.yaml
kubectl apply -f k8s/gateway-networkpolicy.yaml
 
# 8. 验证
kubectl get all -n demo-gateway
kubectl get hpa -n demo-gateway -w
kubectl describe certificate gateway-tls-cert -n demo-gateway

13.2 上线 CheckList

#检查项命令
1etcd 三副本健康kubectl exec etcd-0 -- etcdctl endpoint health --cluster
2网关 Pod 分布在不同节点kubectl get pods -n demo-gateway -o wide
3HPA 正常工作kubectl get hpa -n demo-gateway
4证书有效期 > 30 天kubectl get certificate -n demo-gateway
5NetworkPolicy 生效从 test-pod 验证 Admin API 不可访问
6Prometheus 抓取正常prometheus → Targets → api-gateway: UP
7Loki 收到日志Grafana → Explore → Loki
8压测通过(2000 QPS 持续 5min)hey -z 300s -c 100 ...
9滚动更新零中断kubectl rollout restart + 持续压测
10告警规则可触发手动停止上游节点,确认收到告警

13.3 最终文件结构

k8s/
├── etcd-statefulset.yaml          # etcd 高可用集群
├── gateway-config.yaml            # ConfigMap
├── gateway-secret.yaml            # Admin Key
├── gateway-deployment.yaml        # 网关 Deployment
├── gateway-service.yaml           # Service
├── gateway-hpa.yaml               # 自动扩缩容
├── gateway-ingress.yaml           # Ingress + TLS
├── gateway-networkpolicy.yaml     # 网络策略
├── gateway-certificate.yaml       # cert-manager
├── cert-issuer.yaml               # Let's Encrypt
├── promtail-config.yaml           # 日志采集
└── verify.sh                      # 一键验证脚本

13.4 一键验证脚本

#!/bin/bash
# k8s/verify.sh
set -e
NS="demo-gateway"
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
 
check() {
    if eval "$1" > /dev/null 2>&1; then
        echo -e "${GREEN}[PASS]${NC} $2"
    else
        echo -e "${RED}[FAIL]${NC} $2"
    fi
}
 
echo "=== API 网关生产部署验证 ==="
echo ""
 
check "kubectl get ns $NS"                    "Namespace 存在"
check "kubectl get pods -n $NS -l app=etcd --no-headers | grep Running | wc -l | grep 3" \
                                              "etcd 三副本运行中"
check "kubectl exec -n $NS etcd-0 -- etcdctl endpoint health | grep 'is healthy' | wc -l | grep 3" \
                                              "etcd 集群健康"
check "kubectl get deployment api-gateway -n $NS -o jsonpath='{.status.readyReplicas}' | grep -E '^[3-9]|^1[0-9]'" \
                                              "网关 Deployment 运行中 (≥3 副本)"
check "kubectl get hpa api-gateway-hpa -n $NS" \
                                              "HPA 配置生效"
check "kubectl get certificate gateway-tls-cert -n $NS -o jsonpath='{.status.conditions[0].status}' | grep True" \
                                              "TLS 证书已就绪"
check "kubectl get ingress gateway-ingress -n $NS" \
                                              "Ingress 配置正常"
 
echo ""
echo "=== 全部检查完成 ==="

写在系列最后

从第一篇文章的选择对比,到这篇生产部署指南,系列一"云原生API 网关实战"共 10 篇文章,覆盖了:

序号主题核心价值
01选型对比选对工具,少走弯路
02Docker 快速搭建5 分钟跑起来
03路由原理拆解知道为什么快
04手写插件扩展能力
05插件全景图80% 场景全覆盖
06负载均衡怎么分发都不错
07认证鉴权安全可靠
08AI Gateway紧跟前沿
09可观测性看得见才能管得住
10K8s 生产部署从玩具到生产

如果你只记住三件事

  1. 高可用 = 多副本 etcd + podAntiAffinity + readinessProbe + preStop hook

  2. 零中断 = maxUnavailable:0 + terminationGracePeriod + 就绪探针

  3. 可观测 = Prometheus + Loki + Grafana + 告警规则

这三件事做到了,你的网关就具备了生产级能力。


写在最后:部署只是开始,运维才是深渊

12 个避坑点 + 3 次事故复盘,这篇文章几乎是把我在 K8s 上踩的坑全倒出来了。

但坦白说,生产环境的挑战远不止"避坑"——

你可能还会面对这些场景:

  • 🔸 流量突然翻 10 倍,网关怎么自动扩容?HPA + Cluster Autoscaler 怎么配?

  • 🔸 Pod 起不来、流量不通、性能骤降——你能不能在 5 分钟内定位到根因?

  • 🔸 单集群挂了怎么办?跨 region 多集群容灾架构怎么设计?

  • 🔸 从 5000 QPS 压到 50000 QPS,网关参数怎么调?内核参数怎么改?

  • 🔸 一个人运维网关,etcd 怎么备份?配置怎么版本管理?灾难恢复怎么演练?

这些问题的答案,都在我的付费专栏里。

🎯 专栏《云原生 API 网关从入门到生产》完整包含:

  • ✅ 10 篇实战教程: 网关选型 / Docker 部署 / 动态路由原理 / 手写限流插件 / 鉴权全家桶 / 插件全景图 / 负载均衡 / 可观测性体系 / K8s 排查决策树 / 性能调优

  • ✅ 完整代码仓库(一键 clone,所有 YAML / 脚本 / 配置直接能用)

  • ✅ 一键极速部署脚本(./deploy.sh 一条命令拉起完整环境)

  • ✅ 生产上线 Checklist(50 项 PDF,部署/安全/性能/监控四类,打印打勾用)

  • ✅ 3 次线上事故复盘手册(根因分析 + 时间线 + 应急步骤 + 预防措施)

  • ✅ K8s YAML 全集(可直接 kubectl apply 的完整生产级配置)

  • ✅ 网关面试题精选 20 道(含答案解析)

  • ✅ 付费读者优先答疑(48 小时内响应)

👉 点击订阅《云原生 API 网关从入门到生产》19.9 元

如果这篇万字长文帮到了你,点个赞 + 收藏,让更多人看到 🙏