云原生入口流量与网关:Ingress、DNS/Anycast、CDN 与 DSR

2022-02-11T10:00:00+08:00 | 19分钟阅读 | 更新于 2022-02-11T10:00:00+08:00

@

云原生入口流量与网关:Ingress、DNS/Anycast、CDN 与 DSR

面试定位:本章聚焦"电商大促千万级并发入口流量接入"这一真实场景。面试官想听的不是你会不会配 YAML,而是你能不能在大流量、多活、容灾、降级的综合约束下,把流量这条"主动脉"稳住。下面所有内容都围绕一个核心问题展开:当每秒百万级请求砸向入口,网关、DNS、CDN、四层负载如何各司其职、互不拖垮?


一、基于 Kubernetes 的 Ingress-NGINX 性能调优

(一)白话直觉:Ingress-NGINX 是一个"收费站"

把 Ingress-NGINX 想象成高速路上的主收费站:每辆车(请求)都要先在这里被"抬杆"(TLS 卸载、路由匹配、限流),再放行到后端服务。收费站有几个关键资源:

  • 收费员数量(worker-processes):一个进程一个收费员,太少则排队。
  • 每个收费员手里的车道数(worker-connections):一个收费员同时能抬几根杆。
  • 待抬杆的缓冲区(listen 队列 / accept 队列):车太多时先停在缓冲区,溢出就丢车。

单 Pod 的 QPS 上限,本质上就是"收费员数 × 每人能并发处理的车道数 × 每车道平均处理速度"。调优不是玄学,而是把这三个旋钮拧到与机器资源匹配。

(二)worker-processes 与 worker-connections 怎么设

Ingress-NGINX 的 worker 由 ConfigMap 与 nginx.ingress.kubernetes.io 注解控制,但底层终究是 NGINX。核心配置在 nginx.confevents 块:

# 这一段由 ingress-nginx 的 controller 自动生成,但我们可以通过
# values.yaml / command 参数覆盖。这里展示"最终生效"的样子。
worker_processes  auto;          # 关键1:设成 auto,让 NGINX 按 CPU 核数起进程
worker_rlimit_nofile 1048576;    # 关键2:每个 worker 能打开的最大文件句柄(连接≈fd)

events {
    worker_connections  65535;   # 关键3:单 worker 最大并发连接数
    use epoll;                   # Linux 下必用 epoll 多路复用
    multi_accept on;             # 一次事件循环尽可能多 accept
}

面试要点(如何最大化单 Pod QPS):

  1. worker_processes auto:必须等于 Pod 能用的 CPU 核数,不能超配(超了反而争抢)。在 K8s 里给 Ingress Pod 设 resources.limits.cpu 为整数核(如 8),并让 auto 取到 8。
  2. worker_connectionsnofile 的匹配关系:单 Pod 最大并发连接 ≈ worker_processes × worker_connections。但每个连接至少要一个 fd,所以 worker_rlimit_nofile 必须 ≥ worker_processes × worker_connections(再留余量给日志、临时文件)。
  3. 内核侧配合(initContainer 里 sysctl):
# 让 listen 队列能容纳突发连接,避免握手阶段就丢包
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
# 允许进程突破默认 1024 文件句柄限制(与 worker_rlimit_nofile 呼应)
ulimit -n 1048576
  1. 反向直觉:QPS 不是连接数的平方。短连接(HTTP/1.1 每次建连)受 worker_connections 和建连速率双重制约;开 HTTP/2 后多路复用让单连接承载多请求,此时瓶颈从"连接数"转移到"worker 的 CPU 与内存"。

(三)开启 HTTP/2 后大量 502 的定位与解决

现象:开了 nginx.ingress.kubernetes.io/backend-protocol: "GRPC" 或启用了 HTTP/2 到后端,突然大量 502。

根因(面试常考点),按出现频率排序:

  1. 后端不支持 / 错误协商 HTTP/2:Ingress 用 HTTP/2 连后端,但后端(如老版本 Spring Boot、某些 sidecar)只认 HTTP/1.1,握手即断 → 502。
  2. keepalive 超时不一致:NGINX upstream 的 keepalive_timeout 比后端长,NGINX 复用一个已被后端关掉的空闲连接发请求 → 502(recv() failed (104: Connection reset by peer))。
  3. 每连接最大请求数(keepalive_requests)耗尽:HTTP/2 多路复用单连接跑很久,超过后端 max_requests 被强制断开,NGINX 复用旧连接即 502。

定位三板斧

# 1. 看 ingress-nginx 的错误日志,关键词 502 与 upstream
kubectl exec -n ingress-nginx <controller-pod> -- \
  tail -f /var/log/nginx/error.log | grep -i '502\|upstream\|reset'

# 2. 抓到具体错误后,临时把后端协议改回 HTTP/1.1 验证(排除协商问题)
kubectl annotate ingress <name> nginx.ingress.kubernetes.io/backend-protocol=HTTP

# 3. 观察后端真实响应:用 kubectl 直接 port-forward 打后端,确认它是否吐 502
kubectl port-forward svc/<backend> 8080:8080 &
curl -v http://127.0.0.1:8080/healthz

解决配置(ConfigMap 层面统一收敛 keepalive,避免"半开连接"):

# ingress-nginx 的 ConfigMap,统一 upstream keepalive 行为
apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  # upstream 空闲连接最大存活,必须 <= 后端 keepalive 超时,留 5s 余量
  keep-alive: "55"
  # 单连接最大请求数,避免后端主动断开后 NGINX 复用
  keep-alive-requests: "1000"
  # 开启 proxy_next_upstream 自动重试(对非幂等要谨慎)
  proxy-next-upstream: "error timeout http_502 http_503"
  # 启用 upstream 连接池
  upstream-keepalive-connections: "512"
  upstream-keepalive-timeout: "55"
  upstream-keepalive-keepalive-requests: "1000"

经验值:后端(Tomcat/Jetty)keepalive 常是 60s,Ingress 侧设 55s;后端 maxKeepAliveRequests 默认常是 100,这里设 1000 反而不能大于后端,需对齐后端。面试时说"要对齐上下游超时与请求数"比背数字更得分。

(四)通过 eBPF 采集 Ingress-NGINX 连接队列长度的完整命令链

为什么用 eBPFss -lnt 只能看到"瞬时值",大促时队列在毫秒级抖动,用户态采样抓不到。eBPF 在内核态直接读 struct socksk_ack_backlog(已入 accept 队列、等待 accept() 的连接数),零拷贝、低开销。

完整命令链(假设已装 bpftracebcc):

# 步骤1:确认用户态已经出现 listen 队列溢出迹象(先快速体检)
ss -lnt | grep -E ':80|:443'        # Recv-Q 持续逼近 Send-Q 即积压
netstat -s | grep -i 'listen queue of a socket overflowed'  # 有数值=已丢 SYN

# 步骤2:用 bpftrace 在内核态实时采样 accept 队列长度
# tcp_v4_syn_recv_sock 的第一个参数就是"监听 socket"本身
sudo bpftrace -e '
#include <linux/tcp.h>
#include <net/sock.h>
kprobe:tcp_v4_syn_recv_sock {
    $sk = (struct sock *)arg0;
    @ack  = $sk->sk_ack_backlog;     // 当前 accept 队列长度
    @max  = $sk->sk_max_ack_backlog; // 队列上限(= listen backlog)
}
interval:s:5 {
    printf("accept_queue=%d / max=%d\n", @ack, @max);
    clear(@ack); clear(@max);
}'

# 步骤3(可选):统计 SYN 被丢弃的次数,定位是否真溢出
sudo bpftrace -e '
kprobe:tcp_drop {
    $skb = (struct sk_buff *)arg0;
    @drop = count();
}
interval:s:10 { print(@drop); clear(@drop); }'

读法:步骤2 中若 accept_queue 长期贴近 max,且步骤3 的 drop 持续增长,说明 Ingress Pod 的 accept 速度跟不上入队速度(典型是 worker 被 CPU 限流或业务 accept() 慢),应扩容 Ingress Pod 或调大 somaxconn

(五)一图总览 Ingress-NGINX 调优关系

flowchart LR
    A[客户端 HTTPS] --> B[Ingress-NGINX Pod]
    B -->|worker_processes=CPU核数| C{多 worker 进程}
    C -->|每个 worker| D[worker_connections=65535]
    D --> E[epoll 多路复用]
    E -->|accept 队列| F[net.core.somaxconn 兜底]
    F --> G[后端 Service]
    G -->|HTTP/2 多路复用| H[keepalive 对齐防 502]
    style B fill:#fef3c7,stroke:#d97706
    style H fill:#dbeafe,stroke:#2563eb

二、全球多活 DNS 与 Anycast IP 流量调度

(一)白话直觉:DNS 是"全球交通警察",Anycast 是"就近入口"

  • DNS 权重 + 健康检查 = 交警按"各路口车流量配比 + 哪个路口堵了就改道"来分流。
  • BGP Anycast = 同一个 IP 地址在全球多个机房同时广播,运营商路由自然把用户引到物理最近的那个入口。就像"同一个电话号,你打时自动接到离你最近的营业厅"。

(二)阿里云云解析 + AWS Route53 混合:权重 + 健康检查实现秒级容灾

场景:国内用户走阿里云,海外走 Route53,但两者都要能在对方故障时"接管"。

设计要点

  1. 主域名交给 Route53(顶级容灾兜底),用 Failover 策略;国内子域 cn.example.com 用阿里云"线路类型=默认/国内"做权重调度。
  2. 健康检查 ≠ DNS 轮询:DNS 本身 TTL 长,秒级切换靠的是"权威 DNS 在被探活失败后立即收回该记录",而不是客户端重新解析。

阿里云云解析(权重 + 健康检查)示例思路

# 阿里云云解析控制台 / OpenAPI 配置(伪配置,表达意图)
记录类型: A
主机记录: cn
线路类型: 默认
记录值:   10.0.1.10   权重: 100   # 主 AZ
记录值:   10.0.2.10   权重: 0     # 备 AZ,平时 0 权重不接流量
健康检查: 开启,URL=/healthz,间隔 2s,连续 3 次失败则"暂停"该记录
# 当主 AZ 探活失败 -> 自动把备 AZ 权重调为 100(秒级生效,因 TTL 设 1s)

Route53 侧 Failover(容灾兜底)

{
  "Name": "example.com",
  "Type": "A",
  "SetIdentifier": "primary-aliyun",
  "Failover": "PRIMARY",
  "HealthCheckId": "hc-aliyun-cn",
  "TTL": 1,
  "ResourceRecords": [{ "Value": "10.0.1.10" }]
},
{
  "Name": "example.com",
  "Type": "A",
  "SetIdentifier": "secondary-route53",
  "Failover": "SECONDARY",
  "TTL": 1,
  "ResourceRecords": [{ "Value": "52.1.2.3" }]
}

关键点:把 TTL 压到 1 秒,Health Check 间隔 2s + 连续失败 3 次 ≈ 6s 内完成切换,满足"秒级容灾"的面试叙事。但面试官可能反问"TTL=1s 会不会把权威 DNS 打爆"——答:用递归 DNS 的缓存 + 健康检查驱动记录启停,真实生效的是权威侧,递归侧 TTL 短只是配合。

(三)BGP Anycast 防 TCP 长连接被重置

问题:Anycast 同一个 IP 在多节点广播。某节点故障重启,BGP 收敛期间,原本连到它的 TCP 长连接(如 WebSocket、gRPC 流)会被路由到另一个节点,而新节点没有这条连接的会话状态 → 对端收到 RST,长连接断开。

解法(面试三层回答)

  1. BGP 优雅撤退(Graceful Shutdown):节点下线前先 withdraw 路由前缀,等已建立的连接自然 drained 再重启,避免"路由突变"。
  2. 会话同步 / 无状态入口:让 Anycast 节点只做 L4 透传,真正的有状态连接在后端集群(共享会话存储),任一 Anycast 节点挂掉,连接重路由到另一节点也能从后端恢复上下文。
  3. 连接保持(Connection Draining):入口层在 withdraw 后维持 FIN 优雅关闭,配合客户端自动重连 + 断点续传逻辑,把"连接重置"转化为"可恢复的瞬时抖动"。

(四)CoreDNS Split-Horizon 实现国内/海外分流示例 YAML

Split-Horizon(分视界)让"同一域名、不同来源客户端解析到不同 IP"。CoreDNS 原生 forward 不支持按客户端分流,可用 template 插件的 policy client_ip_range 实现,或用 view 插件。下面给出可直接落地的 template 方案:

apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        # 国内段客户端(Pod CIDR 10.0.0.0/8)走阿里云内网 DNS,解析到内网地址
        template ANY ANY {
            policy client_ip_range 10.0.0.0/8
            upstream 100.100.2.136 100.100.2.138
        }
        # 其余(海外/办公网段)走公共解析,命中 Route53 / 公网 VIP
        template ANY ANY {
            policy client_ip_range 0.0.0.0/0
            upstream 8.8.8.8 1.1.1.1
        }
        # 兜底:集群内部 Service 域名仍由 kubernetes 插件解析
        kubernetes cluster.local in-addr.arpa ip6.arpa {
            pods insecure
        }
        cache 30
        errors
        health
    }    

注意:template 插件主职是"合成记录",用它做分流转发在生产中常用于简单场景;更标准的 Split-Horizon 是部署两套 CoreDNS(分别 bind 不同接口 / 不同 forward),或用社区 view 插件。面试时说出"两种实现路径 + 各自取舍"更显深度。

(五)容灾切换时序图

sequenceDiagram
    participant U as 用户
    participant R as 递归 DNS
    participant A as 阿里云解析(主)
    participant B as Route53(备)
    participant H as 健康检查
    H->>A: 每2s探 /healthz
    Note over A: 连续3次失败
    A-->>R: 暂停主记录(权重置0)
    A-->>R: 启用备记录(权重100)
    U->>R: 解析 example.com
    R->>B: TTL=1s 命中备
    B-->>R: 52.1.2.3
    R-->>U: 返回备 IP
    Note over U,B: 秒级切换完成,用户无感

三、CDN 边缘缓存预热与回源策略

(一)白话直觉:CDN 是"把货提前铺到小区便利店"

大促前,用户会集中点击"爆款商品图"。如果等用户来才去源头仓库(源站)取,仓库瞬间被挤爆。预热就是大促前把热门图片提前"铺货"到全国边缘节点;回源就是边缘没货时回仓库取,取完顺便自己存一份。

(二)千万级商品图片预热:Kubernetes Job + SLS 批量提交不触发 QPS 限速

难点:直接循环调 CDN 预热 API 会被限流(如 100 QPS);千万 URL 提交要分批、可重试、可观测。

方案:用 K8s Job 起多个 worker,每个 worker 从对象存储/SLS(日志服务,这里指"URL 清单存储")拉一批 URL,按令牌桶匀速调用 CDN 预热接口。

apiVersion: batch/v1
kind: Job
metadata:
  name: cdn-prewarm
spec:
  parallelism: 20          # 20 个 Pod 并行,总吞吐 = 20 × 单Pod限速
  completions: 20
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: prewarm
        image: registry.example.com/cdn-prewarm:1.0
        env:
        - name: SLS_ENDPOINT          # URL 清单所在(日志服务/对象存储)
          value: "https://sls.example.com/urls/shard-${POD_INDEX}"
        - name: CDN_API_QPS           # 单 Pod 限速,远小于全局阈值,避免触发风控
          value: "5"
        - name: BATCH_SIZE
          value: "200"                # 每批 200 个 URL 合并一次预热请求
        command: ["/bin/sh","-c"]
        args:
        - |
          # 1. 从 SLS 拉取本分片 URL 清单
          # 2. 令牌桶限速调用 CDN 预热 API(伪代码逻辑)
          python3 prewarm.py \
            --qps $CDN_API_QPS \
            --batch $BATCH_SIZE \
            --source $SLS_ENDPOINT          

不触发限速的关键:每个 Pod 把 QPS 压到单账号阈值的 1/总并发以下(如全局 1000 QPS ÷ 20 Pod = 50,这里再保守取 5),且预热是"提前离线"操作,不与大促实时流量争抢 API 配额。

(三)源站返回 Cache-Control: private 时强制 CDN 缓存

问题:源站(如某些框架默认)吐 Cache-Control: private, max-age=0,语义上"禁止共享缓存(CDN)存储"。但商品图明明可缓存,被这个头卡住 → 每次回源。

解法:在 CDN 边缘(或回源层 OpenResty)重写响应头,去掉 private 并补 public + 长 max-age

# 在 CDN 回源节点 / 边缘的 OpenResty 中
location ~* \.(jpg|png|webp)$ {
    # 强制忽略源站的 private 指令,改为可公共缓存 1 天
    proxy_ignore_headers Cache-Control Expires Set-Cookie;
    add_header Cache-Control "public, max-age=86400, immutable";
    proxy_cache my_static;
    proxy_cache_valid 200 86400s;
    proxy_pass http://origin;
}

注意:对含用户隐私的图片(如头像)不能这么做,否则会跨用户泄露。面试强调"只对静态、可公共缓存资源强制覆盖,敏感资源保持 private"。

(四)OpenResty + Lua 回源失败降级到对象存储

场景:边缘回源源站 5xx/超时,与其让用户看到裂图,不如直接从对象存储(OSS/S3)取一份兜底图片。

# OpenResty 边缘节点配置:回源失败 -> Lua 兜底对象存储
location /img/ {
    proxy_pass http://origin-backend;
    proxy_intercept_errors on;     # 拦截后端错误,交给 error_page 处理
    error_page 502 503 504 = @oss_fallback;
}

location @oss_fallback {
    # 用 lua-resty-http 从 OSS 拉兜底图,避免再走一次失败回源
    content_by_lua_block {
        local http = require "resty.http"
        local ngx  = ngx

        local path = ngx.var.uri                -- /img/xxx.jpg
        local oss_host = "https://oss-fallback.example.com"
        local httpc = http.new()
        httpc:set_timeout(2000)                 -- 2s 超时,快速失败

        local res, err = httpc:request_uri(oss_host .. path, {
            method = "GET",
            ssl_verify = false,
        })

        if not res or res.status ~= 200 then
            ngx.status = 502
            ngx.say("image unavailable")
            return
        end

        ngx.status = 200
        ngx.header["Content-Type"] = res.headers["Content-Type"] or "image/jpeg"
        ngx.header["X-Served-By"]  = "oss-fallback"   -- 标记降级来源,便于监控
        ngx.print(res.body)
    }
}

(五)CDN 预热与降级流程

flowchart TD
    A[大促前 Job 批预热] -->|匀速令牌桶| B[CDN 边缘节点]
    B -->|命中| C[直接返回商品图]
    B -->|未命中| D[回源源站]
    D -->|200| E[缓存并返回]
    D -->|502/503/504| F[Lua 兜底 OSS]
    F -->|200| G[返回兜底图 标记 X-Served-By]
    F -->|失败| H[返回 502 裂图告警]
    style A fill:#dcfce7,stroke:#16a34a
    style F fill:#fef9c3,stroke:#ca8a04

四、四层负载均衡 DSR(Direct Server Return)

(一)白话直觉:DSR 是"去的时候经过调度器,回来的时候抄近路"

普通 L4 负载(如 NAT 模式):请求和响应经过负载均衡器,响应往往比请求大得多(图片、视频),均衡器成了带宽瓶颈。

DSR(直接返回):请求经负载均衡器转发到后端 Pod,但响应由后端 Pod 直接发给客户端,不再绕回均衡器。就像你网购:快递员(请求)把包裹送到你家(后端),你退换货(响应)直接寄回商家(客户端),不用再经过快递中转站。

代价:后端 Pod 必须"假装"自己就是 VIP(因为客户端源目 IP 是 客户端→VIP),否则客户端收到来自"真实 Pod IP"的包会丢弃。

(二)Kubernetes 中为 Pod 配置 VIP 与 loopback 使 DSR 闭环

DSR 要求后端 Pod 在 loopback 上绑定 VIP,这样它发出的响应源 IP = VIP,客户端才认。

# 思路:通过 initContainer + 主机网络/特权,在 Pod 的 lo 上添加 VIP
# 实际生产多用 IPVS + DSR 模式的 Service,这里给出"手动闭环"演示
apiVersion: v1
kind: Pod
metadata:
  name: dsr-backend
  annotations:
    # 告知 L4 调度器(如 LVS/IPVS)该 Pod 参与 DSR
    l4dsr.vip: "192.168.100.100"
spec:
  # DSR 通常需要 hostNetwork 或在 lo 上配 VIP;演示用 hostNetwork 简化
  hostNetwork: true
  initContainers:
  - name: bind-vip
    image: busybox
    command: ["/bin/sh","-c"]
    args:
    - |
      # 在 loopback 上绑定 VIP,使本机发出的响应源 IP 为 VIP
      ip addr add 192.168.100.100/32 dev lo
      # 关键:抑制 ARP 应答,避免本 Pod 抢答 VIP 的 ARP 导致同网段冲突
      echo 1 > /proc/sys/net/ipv4/conf/lo/arp_ignore
      echo 2 > /proc/sys/net/ipv4/conf/lo/arp_announce
      echo 1 > /proc/sys/net/ipv4/conf/all/arp_ignore
      echo 2 > /proc/sys/net/ipv4/conf/all/arp_announce      
    securityContext:
      capabilities:
        add: ["NET_ADMIN"]
  containers:
  - name: app
    image: nginx
    ports:
    - containerPort: 80
      hostPort: 80

生产环境更推荐用 Cilium / kube-proxy IPVS 模式 + DSR(--ipvs-scheduler + --ipvs-dr),让 kube-proxy 自动完成 VIP 与 loopback 闭环,而不是手写 initContainer。手写方案用于讲清原理。

(三)IPVS 与 DPDK 在 DSR 性能差异的根本原因

一句话根因:IPVS 跑在内核协议栈里,数据包要过内核网络栈(软中断、skb 分配、路由查找);DPDK 是用户态轮询驱动(PMD),绕开内核栈,直接把包从网卡 DMA 到用户态内存,靠"忙等轮询"换掉中断开销。

维度IPVS (内核态)DPDK (用户态)
数据路径网卡 → 内核栈 → IPVS → 转发网卡 → 用户态 PMD 直接处理
中断模型中断驱动,高 PPS 下软中断打满 CPU轮询驱动,无中断抖动
典型瓶颈内核 skb 分配 / 锁竞争需要独占核,CPU 利用率"看起来空"实则忙轮询
适用通用 K8s Service 负载超高性能网关(千万 PPS)

根本原因:差异不在"DSR 算法"本身,而在转发路径是否经过内核协议栈。DSR 只是改了响应走向,IPVS 仍需内核处理每个包;DPDK 直接绕开内核,所以同硬件下 DPDK 的 PPS/时延上限显著更高,代价是占用整核、生态重。

(四)DSR 防后端 Pod 看到客户端真实 IP 导致反向路由不对称

问题:DSR 下,请求包目的 IP 是 VIP,但源 IP 是客户端真实 IP。后端 Pod 收到后,若它想"主动回包给客户端"(实际应走 DSR 直返),或者它的反向路由把去往客户端真实 IP 的包发给了默认网关而非原路返回,就会出现非对称路由——响应没走 DSR 直返,反而绕回调度器或被丢弃,客户端收不到回包。

防护三招

  1. 关闭后端对客户端网段的独立路由:后端 Pod 不学习客户端真实 IP 的明细路由,所有"回客户端"的流量统一由 DSR 机制(源 IP=VIP + lo 绑定)直发,避免反向路由表指错路。
  2. rp_filter 严格模式net.ipv4.conf.all.rp_filter=1,防止后端因"收到源 IP 不在本地路由"的包而触发反向路径过滤丢包。
  3. 源 IP 保留但响应强制走 lo/VIP 出口:确保响应包的出口接口与入包一致,配合上面 initContainer 的 ARP 抑制,彻底闭环。

(五)DSR 流量闭环图

flowchart LR
    C[客户端] -->|目的IP=VIP| L[L4 调度器 IPVS/DPDK]
    L -->|改写MAC,目的IP仍为VIP| P[后端 Pod]
    P -->|响应源IP=VIP 直发| C
    subgraph Pod内部
      P -->|lo 绑定 VIP| V[(loopback: VIP)]
    end
    style L fill:#fee2e2,stroke:#dc2626
    style C fill:#dbeafe,stroke:#2563eb

五、入口流量镜像与灰度审计

(一)白话直觉:流量镜像是"平行世界的影子测试"

你想验证新版本网关/风控规则,又不敢真拿用户流量试。于是把一份一模一样的副本悄悄发给影子环境,真实用户走原链路,影子环境随便折腾。但影子不是免费的——镜像放大会让下游(数据库、第三方计费 API)收到双倍甚至多倍请求。

(二)Istio TrafficMirror 避免镜像流量放大导致下游计费飙升

陷阱TrafficMirror 若配置成"按比例镜像 + 多层级联",流量可能在多个服务间被反复镜像,呈指数放大,打到按调用量计费的第三方(如短信、支付风控)直接爆账单。

正确姿势

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: product-mirror
spec:
  hosts: ["product.example.svc.cluster.local"]
  http:
  - route:
    - destination:
        host: product.example.svc.cluster.local   # 100% 真实流量走原服务
      weight: 100
    mirror:
      host: product-shadow.example.svc.cluster.local  # 镜像到影子服务
    mirrorPercentage:
      value: 10                                     # 只镜像 10%,控制放大倍数
    # 关键:给镜像流量打标,下游据此短路"真实计费"逻辑
    headers:
      request:
        set:
          x-shadow: "true"

防计费飙升的三道闸

  1. mirrorPercentage 控制在低位(如 5%~10%),放大倍数 = 1 + 比例,可预算。
  2. 影子链路不调用真实计费/短信:下游服务读 x-shadow: true 头后,走 mock 或只写影子库,绝不触达计费 API。
  3. 镜像流量不二次镜像:影子服务禁用 TrafficMirror,防止级联放大。

(三)基于 eBPF + pcap 收集镜像流量并实时脱敏的架构图

目标:在入口网卡用 eBPF 抓包(比 tcpdump 用户态更轻),pcap 拿到原始流后,在用户态用 eBPF map / 程序做敏感字段(手机号、身份证、token)脱敏,再落盘供审计,避免明文敏感数据落库。

flowchart TD
    A[入口网卡] -->|eBPF tc 挂载| B[eBPF 抓包程序]
    B -->|perf/ringbuf| C[pcap 消费者]
    C --> D{脱敏引擎}
    D -->|识别手机号/身份证/token| E[掩码替换]
    E --> F[(审计存储 已脱敏)]
    D -->|命中敏感字段计数| G[metrics: 脱敏命中率]
    style B fill:#ede9fe,stroke:#7c3aed
    style E fill:#fef9c3,stroke:#ca8a04
# 用 bpftool + tc 加载 eBPF 抓包(架构图中"eBPF 抓包程序"的落地)
# 1. 在入口网卡 ingress 挂载 eBPF 程序(假设已编译 mirror_capture.o)
sudo tc qdisc add dev eth0 clsact
sudo tc filter add dev eth0 ingress bpf da obj mirror_capture.o sec ingress

# 2. 用户态用 pcap 从 perf buffer 读包并脱敏(伪代码调用)
sudo ./pcap-desensitize \
  --ebpf-map /sys/fs/bpf/tc/eth0/ingress/mirror_map \
  --rules phone,idcard,token \
  --out /audit/mirrored.pcap

(四)镜像流量延迟 > 5 秒:快速定位是 Sidecar 还是网络设备

决策树思路

flowchart TD
    Q[镜像延迟>5s] --> A{真实链路延迟正常?}
    A -->|正常,仅镜像慢| B{Sidecar CPU/队列}
    A -->|真实也慢| C[先查网络设备/链路]
    B -->|Sidecar CPU 打满| D[扩容 Sidecar / 降 mirrorPercentage]
    B -->|Sidecar 队列堆积| E[检查 envoy 的 mirror cluster 连接池]
    C -->|交换机/专线抖动| F[查网络设备丢包与时延]
    C -->|DNAT/iptables 规则多| G[迁 Cilium eBPF 替代 kube-proxy]

落地命令

# 1. 先看真实与镜像两路延迟差(在 Pod 内)
kubectl exec <pod> -c istio-proxy -- \
  curl -s http://localhost:15000/stats | grep -E 'mirror|upstream_rq_time'

# 2. 看 Sidecar 是否被 CPU 限流(延迟>5s 常见元凶)
kubectl top pod <pod> -c istio-proxy
kubectl exec <pod> -c istio-proxy -- \
  curl -s localhost:15000/stats | grep 'listener_manager.workers'

# 3. 排除网络设备:在节点上测到镜像目标的裸 RTT
ping -c 20 <shadow-service-ip>
# 若裸 RTT 就 > 5s,说明是网络/链路,不是 Sidecar

面试总结句:延迟>5s 先二分——真实链路正常则锅在 Sidecar(CPU/连接池),真实链路也慢则锅在网络设备


自测题与动手练习

自测题(口述)

  1. worker_processes auto 配合 K8s 的 limits.cpu 为什么要设成整数核?小数核会导致什么问题?
  2. 开 HTTP/2 后大量 502,请按"协商 / keepalive 超时 / keepalive_requests"三条线分别给出定位命令与修复配置。
  3. BGP Anycast 节点故障重启,为什么已有 TCP 长连接会被 RST?给出三层防御手段。
  4. DSR 下后端 Pod 为什么要在 loopback 绑 VIP?如果不抑制 ARP 会怎样?
  5. Istio TrafficMirror 的"计费飙升"风险根因是什么?三道闸分别挡住什么?

动手练习

  1. 在测试集群用 bpftrace 采样一次 Ingress-NGINX 的 sk_ack_backlog,记录峰值并判断是否需要扩容。
  2. 手写一个 CoreDNS template 的 Split-Horizon ConfigMap,让 10.0.0.0/8 走内网 DNS,其余走公网,并 kubectl applydig 验证。
  3. 用 OpenResty + Lua 写一个最小回源降级脚本,故意让 proxy_pass 指向不存在的地址,确认 @oss_fallback 能返回兜底内容且带 X-Served-By 头。
  4. 在本地用 ipvsadm 建一条 DR 模式虚拟服务,观察响应是否不经调度器直返客户端(tcpdump 抓调度器网卡验证)。

本章小结

本章围绕"千万级并发入口流量接入"这一主线,串起四道关口:

  • Ingress-NGINX:调优本质是 worker 数 × 连接数 × 单连接效率,HTTP/2 的 502 多源于上下游 keepalive 超时/请求数不对齐,eBPF 能在内核态精准采样 accept 队列。
  • DNS / Anycast:权重 + 健康检查 + TTL=1s 实现秒级容灾;Anycast 靠优雅撤退与无状态入口化解 TCP 长连接重置;CoreDNS Split-Horizon 按客户端网段分流。
  • CDN:预热用 Job + 令牌桶匀速避限速;private 头可在边缘强制覆盖(敏感资源除外);回源失败用 Lua 降级对象存储。
  • DSR:响应直返破解带宽瓶颈,代价是后端需 lo 绑 VIP + 抑制 ARP;IPVS 与 DPDK 的性能差根本在"是否过内核栈";反向路由不对称靠 rp_filter 与闭环出口规避。
  • 流量镜像:影子测试要控比例、禁级联、下游短路计费;延迟>5s 用"真实 vs 镜像"二分法定 Sidecar 还是网络设备。

面试时记住一个贯穿全局的视角:**入口层的所有技术,都是在"性能、成本、可用性、安全"四角之间做权衡",能说清"为什么这么选、代价是什么",比背配置更值钱。

复习提示:
  • Ingress-NGINX 调优核心公式:单 Pod QPS ≈ worker_processes × worker_connections × 单连接吞吐量;HTTP/2 多路复用后瓶颈从连接数转到 CPU。
  • Anycast 优雅撤退:TCP 长连接在 BGP 撤销时强制断开,靠"渐进式权重下调 + 健康检查"避免瞬时丢包。
  • CDN 预热防限流:大文件用 Job + 令牌桶匀速预热,避免一次性大量回源触发源站限速。
  • DSR 反路由不对称:响应不经调度器直返客户端,需要后端节点在 lo 绑定 VIP 并抑制 ARP 通告。
  • 下一篇讲未来架构趋势——Serverless、边云协同、AI Native 等前沿方向。
面试官
Ingress-NGINX 开启 HTTP/2 后大量 502,为什么是 upstream keepalive 超时导致的?怎么排查和修复?
候选人
HTTP/2 下单个 TCP 连接承载多个请求,如果 upstream(后端服务)的 keepalive 超时时间短于 Ingress-NGINX 的连接超时时间,就会出现:Ingress 认为连接还活着尝试复用,但后端已经关闭,返回 502。

排查步骤
1. 查看 Ingress-NGINX 日志中的 upstream_timeout 错误
2. 对比 upstream_keepalive_timeout 和后端服务的 keepalive 配置

修复方案:确保上游 keepalive 超时 ≥ Ingress 的超时设置,或在 Ingress-NGINX 中设置 proxy-nextProtoNegotiation 等参数。

面试加分点:提到可以用 bpftrace 在内核态采样 sk_ack_backlog,看 accept 队列是否真的满了,而不是盲目加 worker 数量。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!