Skip to content

Pod 网络基准测试 — 同节点、同一 AZ、跨 AZ 与 DNS ndots ​

支持版本: Kubernetes 1.36 (Amazon EKS), Amazon VPC CNI v1.21.1, kube-proxy iptables mode 最后更新: September 2, 2026

当 EKS 上的两个 Pod 位于同一节点、同一 AZ 的不同节点,或不同 AZ 时,实际会发生什么变化?这个问题常伴随着两个误解。第一个是,跨越 AZ “会让速度变慢并降低带宽”——在本次运行中,AZ 边界改变了延迟和账单,但完全没有改变带宽。第二个与 DNS 有关:每个 EKS Pod 获得的 ndots:5 和四项搜索列表,会悄然将一次外部查找——对于任何点数少于五个的名称——从 2 次 DNS 查询变为 10 次。本页汇集了在 September 2, 2026、使用“如何复现”中所述装置,对 fsi-demo-cluster(首尔)测得的 RTT、HTTP/gRPC 延迟、iperf3 吞吐量、区域内数据传输成本和 DNS 查询次数。每个数字均为 Pod IP 到 Pod IP(不经 ClusterIP);原因见“注意事项”。

节点 A(ap-northeast-2a)上的客户端 Pod 访问位于同一节点、同一 AZ 节点 B 和 ap-northeast-2b 节点 C 的服务器 Pod — RTT 为 0.040 / 0.339 / 0.544 ms,单流为 29.97 / 4.96 / 4.96 Gbps。

节点 A(ap-northeast-2a)上的客户端 Pod 访问位于同一节点、同一 AZ 节点 B 和 ap-northeast-2b 节点 C 的服务器 Pod — RTT 为 0.040 / 0.339 / 0.544 ms,单流为 29.97 / 4.96 / 4.96 Gbps。全屏打开 ↗

TL;DR — 我们测得了什么 ​

  1. RTT 阶梯:同节点 0.040 ms → 同一 AZ 0.339 ms → 跨 AZ 0.544 ms(ping,200 个探测的平均值)。一次 AZ 跳转增加 +0.21 ms,相比同一节点则增加 +0.50 ms。
  2. HTTP p50 / p99(fortio,100 qps,4 个连接,keepalive,60 s):0.259 / 0.350 ms → 0.461 / 0.667 ms → 0.704 / 0.812 ms —— 从应用层看到相同的阶梯。
  3. 带宽:无论流量留在 AZ 内还是跨越 AZ,单个 TCP 流最高均为 4.96 Gbps(EC2 5 Gbps 单流限制)。八个流达到 9.94 Gbps = m5.xlarge 10 Gbps 峰值。跨越 AZ 不会降低吞吐量。
  4. 同节点 Pod 到 Pod:单个流为 29.97 Gbps(受 CPU 限制——客户端使用了一个核心的 99.8 %),8 个流为 48.15 Gbps。流量经过一对 veth,完全不会接触 NIC。
  5. 账单:以线速跨 AZ 运行 3 分钟 = 223.4 GB = 约 $4.47 的区域数据传输费用(每个方向 $0.01/GB)。在 180 s 内未观察到向 1.25 Gbps 基线的突发积分降速。
  6. DNS:使用默认的 ndots:5 时,glibc Pod 一次解析 sts.ap-northeast-2.amazonaws.com 会发送 10 次查询(其中 8 次回答 NXDOMAIN),预热中位数为 3.78 ms。末尾点将其降至 2 次查询(A+AAAA)/ 0.80 ms;ndots:1 为 2 次查询 / 0.54 ms。
  7. 每个请求的新连接多花一个 RTT:关闭 keepalive 时,p50 从 0.259 → 0.664、0.461 → 1.079 和 0.704 → 1.517 ms。跨 AZ 时,每请求延迟增加超过一倍。

测试环境 ​

项目值
集群Amazon EKS fsi-demo-cluster、ap-northeast-2(首尔)、控制平面 v1.36.2-eks-bca9cf6、使用两个 AZ(2a、2b)
节点3 × m5.xlarge,由 Karpenter system NodePool 为本测试新启动——2a 中的一个客户端节点、一个服务器节点,以及 2b 中的一个服务器节点。4 vCPU,Intel Xeon Platinum 8175M @ 2.50GHz
节点 OSAmazon Linux 2023.12.20260817、内核 6.18.41-94.142.amzn2023.x86_64、containerd 2.2.5、kubelet v1.36.3-eks-cb19647
CNIAmazon VPC CNI v1.21.1-eksbuild.8(+ network-policy-agent v1.3.4);ENABLE_PREFIX_DELEGATION=false、ENABLE_POD_ENI=false、AWS_VPC_K8S_CNI_EXTERNALSNAT=false、NETWORK_POLICY_ENFORCING_MODE=standard、WARM_ENI_TARGET=1、WARM_IP_TARGET=3
kube-proxyv1.35.3-eksbuild.5、mode: "iptables"
CoreDNSv1.14.2-eksbuild.4、2 个副本——每个 AZ 一个(10.0.2.106 / 2a、10.0.3.14 / 2b);Service kube-dns ClusterIP 172.20.0.10;Corefile kubernetes cluster.local … { pods insecure }、forward . /etc/resolv.conf、cache 30、loadbalance;没有 NodeLocal DNSCache,没有 autopath 插件
Pod resolv.conf(默认)search bench-net.svc.cluster.local svc.cluster.local cluster.local ap-northeast-2.compute.internal / nameserver 172.20.0.10 / options ndots:5
Pod NICeth0 MTU 9001(巨型帧)、TCP 拥塞控制 cubic、iperf3 tcp_mss_default: 8949
EC2 网络规格m5.xlarge “Up to 10 Gigabit”——基线 1.25 Gbps、峰值 10 Gbps、4 vCPU(对比:m5.large 基线 0.75 Gbps、峰值 10 Gbps、2 vCPU)。使用 aws ec2 describe-instance-types 验证;需要 ENA
定价usagetype APN2-DataTransfer-Regional-Bytes、“Regional Data Transfer - in/out/between AZs or when using public IP or Elastic IP addresses”、$0.01/GB(aws pricing get-products --region us-east-1,查询于 2026-09)
工具nicolaka/netshoot:v0.14 —— iperf 3.19、fortio 1.69.5、iputils ping 20250605、tcpdump 4.99.5;DNS 客户端 python:3.12-slim(Debian 13、glibc 2.41、Python 3.12.14)
测试时段2026-09-02 07:58–08:40 UTC(首个 Pod 于 07:58:22Z,DNS Pod 于 08:16:24Z)

“Up to”表示可突发网络:实例拥有网络 I/O 积分时可以使用峰值带宽,积分耗尽后会向基线限速(AWS EC2 User Guide,“Amazon EC2 instance network bandwidth”)。测量 2 中的持续运行仅表明该限制未在 180 s 内触发(未观察到向基线降速,且没有测试更长时间)。

本次运行期间的装置放置:

PodIP节点区域角色 / requests
cli10.0.2.109ip-10-0-2-128(nodeclaim system-76r87)ap-northeast-2a客户端;2500m / 1Gi
srv-same10.0.2.72ip-10-0-2-128 — 与 cli 同一节点(required podAffinity)ap-northeast-2a服务器;200m / 256Mi
srv-a10.0.2.37ip-10-0-2-20(nodeclaim system-ksrbg,与 cli podAntiAffinity)ap-northeast-2a服务器;2800m / 1Gi
srv-b10.0.3.65ip-10-0-3-32(nodeclaim system-svdvk)ap-northeast-2b服务器;2500m / 1Gi
dns-default10.0.2.5ip-10-0-2-20(与 srv-a podAffinity)ap-northeast-2aglibc 解析器,默认 ndots:5
dns-ndots110.0.2.143ip-10-0-2-20ap-northeast-2aglibc 解析器,dnsConfig.options ndots=1

服务器 Pod 运行 sh -c "iperf3 -s -p 5201 & exec fortio server -http-port 8080 -grpc-port 8079 -tcp-port 8078",每个基准 Pod 均带有 karpenter.sh/do-not-disrupt: "true"。srv-a 最初请求 m5.large / 1500m,但 Karpenter 报告 no instance type has enough resources——DaemonSet 开销占用了 m5.large 的 1930m 可分配资源中的 821m——因此改为 m5.xlarge / 2800m。

装置 manifest ​

仅移除了较长的英文注释头;nodeSelector、affinity、requests、command 和 annotations 与实际运行的内容完全一致。由于该装置只使用 Pod IP,因此没有 Service 对象(原因见“注意事项”)。

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: bench-net
  labels:
    bench: net
---
# client — fresh m5.xlarge in ap-northeast-2a
apiVersion: v1
kind: Pod
metadata:
  name: cli
  namespace: bench-net
  labels: { app: cli, role: client }
  annotations: { karpenter.sh/do-not-disrupt: "true" }
spec:
  nodeSelector:
    topology.kubernetes.io/zone: ap-northeast-2a
    node.kubernetes.io/instance-type: m5.xlarge
    karpenter.sh/nodepool: system
  terminationGracePeriodSeconds: 5
  containers:
    - name: netshoot
      image: nicolaka/netshoot:v0.14
      command: ["sleep", "infinity"]
      resources:
        requests: { cpu: "2500m", memory: "1Gi" }
---
# same-node — co-located with cli through required podAffinity
apiVersion: v1
kind: Pod
metadata:
  name: srv-same
  namespace: bench-net
  labels: { app: srv-same, role: server, zone: a }
  annotations: { karpenter.sh/do-not-disrupt: "true" }
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector: { matchLabels: { app: cli } }
          topologyKey: kubernetes.io/hostname
  terminationGracePeriodSeconds: 5
  containers:
    - name: netshoot
      image: nicolaka/netshoot:v0.14
      command: ["sh", "-c", "iperf3 -s -p 5201 & exec fortio server -http-port 8080 -grpc-port 8079 -tcp-port 8078"]
      ports: [{ containerPort: 8080 }, { containerPort: 5201 }]
      resources:
        requests: { cpu: "200m", memory: "256Mi" }
---
# same-AZ — same AZ as cli, different node (podAntiAffinity). m5.large did not fit because of DaemonSet overhead, hence m5.xlarge
apiVersion: v1
kind: Pod
metadata:
  name: srv-a
  namespace: bench-net
  labels: { app: srv-a, role: server, zone: a }
  annotations: { karpenter.sh/do-not-disrupt: "true" }
spec:
  nodeSelector:
    topology.kubernetes.io/zone: ap-northeast-2a
    node.kubernetes.io/instance-type: m5.xlarge
    karpenter.sh/nodepool: system
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector: { matchLabels: { app: cli } }
          topologyKey: kubernetes.io/hostname
  terminationGracePeriodSeconds: 5
  containers:
    - name: netshoot
      image: nicolaka/netshoot:v0.14
      command: ["sh", "-c", "iperf3 -s -p 5201 & exec fortio server -http-port 8080 -grpc-port 8079 -tcp-port 8078"]
      ports: [{ containerPort: 8080 }, { containerPort: 5201 }]
      resources:
        requests: { cpu: "2800m", memory: "1Gi" }
---
# cross-AZ — fresh m5.xlarge in ap-northeast-2b
apiVersion: v1
kind: Pod
metadata:
  name: srv-b
  namespace: bench-net
  labels: { app: srv-b, role: server, zone: b }
  annotations: { karpenter.sh/do-not-disrupt: "true" }
spec:
  nodeSelector:
    topology.kubernetes.io/zone: ap-northeast-2b
    node.kubernetes.io/instance-type: m5.xlarge
    karpenter.sh/nodepool: system
  terminationGracePeriodSeconds: 5
  containers:
    - name: netshoot
      image: nicolaka/netshoot:v0.14
      command: ["sh", "-c", "iperf3 -s -p 5201 & exec fortio server -http-port 8080 -grpc-port 8079 -tcp-port 8078"]
      ports: [{ containerPort: 8080 }, { containerPort: 5201 }]
      resources:
        requests: { cpu: "2500m", memory: "1Gi" }

两个 DNS Pod 被放置在与 srv-a 相同的节点上。app 容器使用 glibc(python:3.12-slim、Debian 13、glibc 2.41):本页测量 glibc 解析器,未测量其他解析器(musl/alpine)的结果;sniffer(netshoot)共享 Pod 的网络命名空间,因此其 tcpdump 能看到 app 发送的每一项查询。这两个 Pod 唯一的差异是 dnsConfig。

yaml
apiVersion: v1
kind: Pod
metadata:
  name: dns-default          # the second Pod is name: dns-ndots1 plus the dnsConfig block below
  namespace: bench-net
  labels: { app: dns-default, role: dns }
  annotations: { karpenter.sh/do-not-disrupt: "true" }
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector: { matchLabels: { app: srv-a } }
          topologyKey: kubernetes.io/hostname
  # present only in dns-ndots1:
  # dnsConfig:
  #   options:
  #     - name: ndots
  #       value: "1"
  terminationGracePeriodSeconds: 5
  containers:
    - name: app
      image: python:3.12-slim
      command: ["sleep", "infinity"]
      resources: { requests: { cpu: "50m", memory: "64Mi" } }
    - name: sniffer
      image: nicolaka/netshoot:v0.14
      command: ["sleep", "infinity"]
      resources: { requests: { cpu: "50m", memory: "64Mi" } }

测量 1 — RTT 和 HTTP 延迟:同节点 → 同一 AZ → 跨 AZ ​

先使用 ICMP 测量纯网络路径(ping -c 200 -i 0.05 -q),然后使用 fortio 将相同的三条路径作为 HTTP/1.1 和 gRPC 请求进行测试。列出单个冷 curl 请求的连接 / 总时间供参考。

路径RTT 最小 / 平均 / 最大 / mdev (ms)丢失curl,1 个冷请求:连接 / 总计
同节点 → 10.0.2.720.021 / 0.040 / 0.089 / 0.0070/2000.194 ms / 0.497 ms
同一 AZ → 10.0.2.370.300 / 0.339 / 0.450 / 0.0170/2000.497 ms / 2.333 ms
跨 AZ → 10.0.3.650.504 / 0.544 / 0.625 / 0.0150/2000.694 ms / 4.038 ms

差值:同一 AZ − 同节点 = +0.30 ms,跨 AZ − 同一 AZ = +0.21 ms,跨 AZ − 同节点 = +0.50 ms。三条路径都非常稳定——mdev 均为 0.017 ms 或更低。curl 的“总计”是包含进程启动的一次冷运行,因此仅作指示用途;请从下面的 fortio 表中读取延迟。

HTTP/1.1 — 100 qps、4 个连接、keepalive、60 s(6,000 个请求)、ms ​

路径平均p50p90p99p99.9最大最小
同节点0.2600.2590.2990.3501.2672.0800.111
同一 AZ0.4680.4610.5600.6670.7832.8230.336
跨 AZ0.7060.7040.7820.8121.1504.5810.551

gRPC ping — 100 qps、4 个连接、30 s(3,000 个请求)、ms ​

路径平均p50p90p99p99.9最大最小
同节点0.4100.3970.4490.8691.1871.3140.241
同一 AZ0.6010.5920.6870.8891.0521.1050.448
跨 AZ0.8780.8650.9671.2092.5822.8260.692

响应体约为 75 字节(fortio echo,空 payload),且每次运行均以 0 错误(200 / SERVING)完成。

**如何解读。**HTTP p50 是 ping 平均值加上 0.12–0.22 ms(0.259 − 0.040 ≈ 0.22、0.461 − 0.339 ≈ 0.12、0.704 − 0.544 ≈ 0.16)——该余量来自客户端和服务器用户空间栈。AZ 跳转在 p50 上增加 +0.24 ms(0.461 → 0.704),与 ping 的 +0.21 ms 大小相同,也接近节点跳转(+0.20 ms,0.259 → 0.461)。换言之,“同节点 → 不同节点”和“同一 AZ → 不同 AZ”各自都是约 0.2 ms 的恒定阶跃。每条路径上 gRPC ping p50 均比 HTTP/1.1 高约 0.13–0.16 ms(0.397 / 0.592 / 0.865 对比 0.259 / 0.461 / 0.704)——这是 fortio ping 中 HTTP/2 framing 加 protobuf 所致。路径差异最大的地方在尾部:HTTP p99 为 0.350 → 0.667 → 0.812 ms,而 gRPC p99.9 为 1.187 → 1.052 → 2.582 ms,只有跨 AZ 路径超过 2 ms。

**一个对比点。**在本仓库的 Istio sidecar 与 ambient 测量中,单个 sidecar 在 p50 上增加 +1.29 ms。此处一次 AZ 跳转的成本为 +0.21–0.24 ms——**一次 mesh 跳转的成本高于一次 AZ 跳转。**在将慢请求归咎于“另一个 AZ”之前,先数一数其路径中的代理。

新连接的成本 — keepalive=false、100 qps、4 个连接、30 s(3,000 个请求)、ms ​

当每个请求都建立新的 TCP 连接(fortio -keepalive=false)时,延迟会怎样?

路径平均p50p90p99p99.9最大最小相比 keepalive p50
同节点0.6720.6640.7820.9571.2531.3060.364+0.405 ms
同一 AZ1.0661.0791.1851.3691.5821.7950.769+0.618 ms
跨 AZ1.5301.5171.6781.7961.9812.0091.300+0.813 ms

一个新连接大约需要一个 RTT(TCP 握手)加上约 0.3 ms 的 socket 建立和拆除时间。路径的 RTT 越大,附加成本越高:跨 AZ 时,单个请求的 p50 从 0.704 变为 1.517 ms——超过两倍。连接池(HTTP keepalive、gRPC channel 重用、数据库连接池)不是性能优化;它是所有跨 AZ 调用的前提条件(并且和任何每请求一个连接的客户端一样,每个新连接还会留下一个 TIME_WAIT socket——此处未测量)。

固定连接池的最大 qps — 延迟就是吞吐量(闭环、16 个连接、20 s) ​

使用 -qps 0(无限制、闭环)时,16 个连接可持续的最大请求速率会将延迟差异转变为吞吐量差异。

路径请求数达到的 qps平均 msp50p90p99p99.9最大
同节点899,82744,9910.3550.2490.7331.6953.38913.593
同一 AZ770,15638,5070.4150.3960.5370.7281.1474.502
跨 AZ512,06025,6020.6240.5970.7700.9491.2934.725

Little 定律(推导:吞吐量 = 并发数 ÷ 延迟)几乎完全成立——16 ÷ 0.000355 s = 45,070(实测 44,991)、16 ÷ 0.000415 = 38,554(38,507)、16 ÷ 0.000624 = 25,641(25,602)。对于固定大小的池,AZ 跳转的 +0.2 ms 将可达到的吞吐量降低 34 %(38.5k → 25.6k qps)。对于请求/响应服务,另一个 AZ 昂贵的原因是此延迟而非带宽。同节点的 p99/最大值比同一 AZ 更差也并非网络问题:在 45k qps 下,客户端和服务器共享一个节点的 4 vCPU 并争用 CPU。

测量 2 — 吞吐量:5 Gbps 单流上限和 10 Gbps 实例上限 ​

iperf3 3.19、TCP、每次运行 20 s、-J、客户端 cli。CPU 列为 iperf3 自身的每进程数值,其中 100 % = 一个 vCPU。

路径流数 (-P)发送 Gbps接收 Gbps重传已发送字节客户端 CPU服务器 CPU发送方 TCP 平均 RTT(流 1)最大 snd_cwnd
同节点 (cli→srv-same)129.9729.971374,921,541,63299.8 %80.9 %34 µs1,861,392 B
同节点848.1548.0814,567120,375,083,008179.0 %186.9 %201 µs / 767 µs(流 1、2)5,888,442 B
同一 AZ (cli→srv-a, 2a→2a)14.964.96412,411,731,96819.5 %15.4 %5,641 µs4,349,214 B
同一 AZ89.949.935,87424,846,139,39236.3 %159.3 %2,720 µs / 1,626 µs1,163,370 B
跨 AZ (cli→srv-b, 2a→2b)14.964.96212,411,994,11220.0 %22.5 %5,420 µs4,304,469 B
跨 AZ89.949.935,97924,845,090,81636.7 %138.2 %3,671 µs / 3,237 µs1,226,013 B

这里有四点需要解读。

  1. **同节点是内存复制速度。**单个流达到 29.97 Gbps 时,客户端 iperf3 使用了一个核心的 99.8 %;8 个流将其推至 48.15 Gbps。通过一对 veth 的数据包不会经过 NIC 或 ENA shaper,因此这些是此实例的 CPU 数据,在其他实例系列上会不同。
  2. **节点之间的单个 TCP 流在 4.96 Gbps 停止——同一 AZ 与跨 AZ 精确到小数点后两位都相同。**AWS 文档说明,在 cluster placement group 外部,单个流被限制为 5 Gbps(“Amazon EC2 instance network bandwidth”),测得的正是此限制。该流仅使用约一个核心的 20 %,因此 CPU 不是瓶颈。
  3. 八个流达到 9.94 Gbps = m5.xlarge 10 Gbps 峰值,同样在两条路径上完全一致。跨越 AZ 边界不会降低带宽。仅当达到实例上限时才出现重传(8 个流为 5,874 / 5,979,而 1 个流为 2–13)——这是符合 ENA allowance shaping 在上限处丢包的间接信号;本次运行没有收集 ENA *_allowance_exceeded 计数器(注意事项)。
  4. **流饱和时,搭乘它的每个请求都会等待。**单个流固定在上限时,发送方 TCP RTT 从空闲 ping RTT 的 0.34 ms(同一 AZ)/ 0.54 ms(跨 AZ),增长为拥塞窗口约 4.3 MB 下的 5.6 ms / 5.4 ms。该延迟是 shaper 中的排队,因此与批量传输复用在同一 TCP 连接上的请求/响应交换会损失约 5 ms。

MSS 8949 源自 9001 字节 MTU(巨型帧),已发送字节列是下一节成本计算的基础。

**所以呢:**一个 gRPC stream、一个 Kafka replica fetcher、一次 volume copy——位于不同节点的两个 Pod 之间的“一条连接”无论承载什么,均不会超过约 5 Gbps。要使用实例的 10 Gbps,必须将工作拆分到并行连接(num.replica.fetchers、multipart uploads、并行 rsync 等);反之,“保留在一个 AZ 内会使带宽翻倍”的预期也不受这些测量支持。

3 分钟持续运行与突发积分 ​

“Up to 10 Gigabit”的基线是 1.25 Gbps。积分耗尽后,实例应从峰值降向基线,因此以 10 s 间隔监控了一个 4 流跨 AZ 运行 180 s(iperf3 -c 10.0.3.65 -p 5201 -t 180 -P 4 -i 10 -J)。

项目值
每个 10 s 间隔的 Gbps(18 个间隔)9.94、9.93 ×12、9.92、9.93 ×4 —— 最小 9.92、最大 9.94
总发送量223,376,179,200 B = 180.0 s 内 223.4 GB(9.93 Gbps)
重传44,842(≈ 249/s;每个 10 s 间隔 2,273–2,669)
CPU客户端 30.7 %(系统 30.1 %),服务器 54.2 %(系统 52.2 %)

**在 180 s 内没有观察到向 1.25 Gbps 基线的下降。**这并不表示突发积分不存在。AWS 记录,在“Up to”实例上,较长时间的持续传输可能被限速至基线;此处没有测试超过 180 s 的情况,因此本次运行无法说明何时——或者对于新节点是否——达到该点。如果正在为 m5.xlarge 上持续数小时的备份、重新平衡或重放做计划,请按 1.25 Gbps 基线预算(其他规格有各自的基线),并将 10 Gbps 视为额外收益。

测量 3 — 真正的跨 AZ 成本是账单 ​

测量 1 和 2 显示,AZ 跳转增加 +0.2 ms 延迟,但带宽不受影响。那么不同 AZ 之间真正的差异在哪里?在账单上。

在一个区域内,AWS 对 AZ 跨越两侧均收取 $0.01/GB——发送 AZ 的“out”和接收 AZ 的“in”(EC2 On-Demand pricing page,“Data Transfer within the same AWS Region”)。此账户的 Pricing API 条目为 APN2-DataTransfer-Regional-Bytes,“Regional Data Transfer - in/out/between AZs or when using public IP or Elastic IP addresses”,$0.0100000000 USD/GB。对于单向批量传输,发送 AZ 的“out”按 $0.01/GB 计费,接收 AZ 的“in”也按 $0.01/GB 计费,因此在一个账户内,实际为每 GB 跨越 AZ 边界 $0.02(推导:$0.01 × 2)。

场景跨越 AZ 边界的字节数成本(推导:GB × $0.01 × 2)
本页的 180 s 持续运行(实测)223.4 GB223.4 × $0.01 = 每个方向 $2.23,合计 $4.47
测量 2 中所有跨 AZ 传输(实测:12.41 + 24.85 + 223.38 GB)260.6 GB≈ 每个方向 $2.61,合计 ≈ $5.21(fortio 和 ping 流量低于 0.2 GB,忽略)
跨 AZ 平均 1 Gbps 持续 30 天(假设)0.125 GB/s × 86,400 s × 30 天 = 324,000 GB ≈ 324 TB324,000 × $0.02 ≈ $6,480 / 月
分布在 3 个 AZ 的 RF3 StatefulSet,leader ingest 为 100 MiB/s(假设,仅复制流量)两个 follower 各位于另一个 AZ → 2 × 100 MiB/s = 209,715,200 B/s × 2,592,000 s ≈ 543,600 GB ≈ 544 TB / 月543,600 × $0.02 ≈ $10,870 / 月

最下面两行不是测量值,而是按该单价得出的估算,且忽略了生产者/消费者流量和 AZ 放置。关键在于量级:三节点基准测试在三分钟内花费了 $4.47,全天持续运行则会变成每月数千美元。带宽不受损地跨越边界,但每个字节都带有价格标签。

运维人员应该做什么。

  • 将流量保留在 AZ 内。Service.spec.trafficDistribution: PreferClose——Kubernetes 1.31 中为 beta、1.33 中为 GA(Kubernetes 文档,“Traffic Distribution”)——使 kube-proxy 优先选择同一区域的 endpoint。本次运行未测量它——无法创建 Service(注意事项)——因此本页没有其效果的数值。
  • **按区域对齐有状态工作负载。**具有大量复制 fan-out 的 StatefulSet(Kafka RF3、分布式数据库)会将大部分复制字节跨 AZ 发送,如上表第四行所示。区域级放置和 AZ failover 设计见 Zonal Cluster Operations guide。
  • **了解批量传输的方向和体量。**记录 AZ 备份、重新平衡和重放的流向及流量,并记住在同一账户中,“in”和“out”作为两个独立的账单项计费。

测量 4 — DNS:ndots:5 的查询放大 ​

在 ndots:5 下,一次 glibc 查找会以 A+AAAA 成对遍历四个搜索后缀(8 个 NXDOMAIN、10 次查询),然后绝对名称才得到响应;相比之下,末尾点查找在 2 次查询后结束。

在 ndots:5 下,一次 glibc 查找会以 A+AAAA 成对遍历四个搜索后缀(8 个 NXDOMAIN、10 次查询),然后绝对名称才得到响应;相比之下,末尾点查找在 2 次查询后结束。全屏打开 ↗

EKS Pod 的 /etc/resolv.conf 有四个搜索域(bench-net.svc.cluster.local svc.cluster.local cluster.local ap-northeast-2.compute.internal)和 options ndots:5。glibc 解析器会将点数少于 ndots 的任何名称先尝试附加每个搜索后缀,并为每个候选并行发送 A 和 AAAA(默认关闭 single-request)。因此有 3 个点的 sts.ap-northeast-2.amazonaws.com,必须先为全部四个候选收集 NXDOMAIN,才会查询绝对名称。方法:在 dns-default / dns-ndots1 的 app 容器中,执行一次冷 socket.getaddrinfo(name, 80, AF_UNSPEC, SOCK_STREAM) 调用,同时 sniffer sidecar 中的 tcpdump(-i eth0 -nn udp port 53)捕获并计数该次解析的 DNS 数据包;随后再解析相同名称 20 次并在进程内计时——这就是预热延迟。

单次解析的发送查询数与预热延迟(20 次重复)、ms ​

Pod / ndots名称(点数)发送查询数NXDOMAIN 响应数预热最小中位数p90最大
默认 / 5kubernetes.default (1)420.871.711.972.61
默认 / 5kubernetes.default.svc.cluster.local (4)1081.533.634.456.41
默认 / 5kubernetes.default.svc.cluster.local.(末尾点)200.330.461.091.58
默认 / 5sts.ap-northeast-2.amazonaws.com (3)1083.083.784.664.84
默认 / 5sts.ap-northeast-2.amazonaws.com.(末尾点)200.420.801.252.17
默认 / 5www.amazon.com (2)1082.513.463.745.86
ndots1 / 1kubernetes.default (1)641.162.042.804.54
ndots1 / 1kubernetes.default.svc.cluster.local (4)200.350.971.081.35
ndots1 / 1kubernetes.default.svc.cluster.local.200.340.400.971.17
ndots1 / 1sts.ap-northeast-2.amazonaws.com (3)200.450.541.221.42
ndots1 / 1sts.ap-northeast-2.amazonaws.com.200.470.751.201.30
ndots1 / 1www.amazon.com (2)200.630.901.272.74

冷的首次解析(包括 glibc NSS 初始化;仅作指示用途)为:默认 / sts 6.22 ms、默认 / sts. 2.87 ms、默认 / www.amazon.com 9.58 ms、默认 / kubernetes.default.svc.cluster.local 7.40 ms、ndots1 / kubernetes.default 10.52 ms、ndots1 / sts 2.84 ms。

如何解读。在默认 ndots:5 下,两个外部名称(sts.…、www.amazon.com)以及——或许令人意外的——没有末尾点的集群 FQDN(kubernetes.default.svc.cluster.local:4 个点,少于 5)均需要10 次查询、8 个 NXDOMAIN,中位数 3.5–3.8 ms。给相同名称加一个点(….com.),则变为 2 次查询 / 0.46–0.80 ms——查询数仅为五分之一,且 sts 的预热中位数从 3.78 降至 0.80 ms,集群 FQDN 从 3.63 降至 0.46 ms。短名称 kubernetes.default 在第二个候选(svc.cluster.local)匹配并停止在 4 次查询 / 1.71 ms。CoreDNS 的 cache 30 也最多缓存 NXDOMAIN 30 s,因此预热状态下昂贵的不是上游查找,而是等待 5 次顺序的 Pod↔CoreDNS 往返。

实际遍历过程 — 一次 sts.ap-northeast-2.amazonaws.com 的冷解析(ndots:5、tcpdump、从第一个数据包起算 ms) ​

t (ms)发送至 172.20.0.10 的候选(并行 A + AAAA)响应
0.00sts.ap-northeast-2.amazonaws.com.bench-net.svc.cluster.local.NXDomain(权威,CoreDNS kubernetes plugin)于 0.92 / 1.14
1.21sts.ap-northeast-2.amazonaws.com.svc.cluster.local.NXDomain 于 2.01 / 2.26
2.32sts.ap-northeast-2.amazonaws.com.cluster.local.NXDomain 于 3.15 / 3.41
3.47sts.ap-northeast-2.amazonaws.com.ap-northeast-2.compute.internal.NXDomain(转发至 VPC resolver——非权威)于 3.68 / 3.93
3.99sts.ap-northeast-2.amazonaws.com.A 10.0.3.84, A 10.0.2.129 于 4.37(AAAA:无数据)

10 次查询、8 个 NXDOMAIN、5 次顺序往返、端到端 4.37 ms——有用的响应在最后 0.38 ms 到达。每个候选的 Pod→CoreDNS→Pod 往返为 0.8–1.1 ms,其中包含 CoreDNS 处理时间及测量 1 中的 RTT 阶梯。172.20.0.10 通过 iptables 随机选择分配到两个 CoreDNS Pod,且其中一个位于另一个 AZ,因此约一半 DNS 查询跨越 AZ 边界。sts.ap-northeast-2.amazonaws.com 解析为两个私有 IP(10.0.2.x / 10.0.3.x),因为该 VPC 有一个 STS interface endpoint,每个 AZ 一个 ENI。没有末尾点的 kubernetes.default.svc.cluster.local 走过相同的路径;其 .ap-northeast-2.compute.internal 候选耗时 2.2 ms,因为 CoreDNS 将其向上游转发;整个遍历冷状态为 5.6 ms,而带末尾点时为 0.4–0.5 ms。

ndots:1 的作用及其副作用 ​

  • 外部名称:10 → 2 次查询,中位数 3.5–3.8 → 0.5–0.9 ms(约快 4–7 倍,查询数为五分之一)。
  • 短集群名称变差。kubernetes.default(1 个点,≥ ndots 1)首先作为绝对名称 kubernetes.default. 尝试;CoreDNS 没有其 zone,因而将它转发至 VPC resolver(1.6 ms 后 NXDomain),然后遍历 bench-net.svc.cluster.local(NXDOMAIN),最后从 svc.cluster.local 获取 172.20.0.1——6 次查询、4 个 NXDOMAIN、中位数 2.04 ms,比 ndots:5 下的 1.71 ms 更慢。集群内部名称也会泄漏到上游 resolver。若使用 ndots:1,请通过 FQDN(name.namespace.svc.cluster.local)访问集群内 Service。
  • 无论 ndots 如何,末尾点均有效——所有情况下均为 2 次查询、0.4–0.8 ms。

放大计算(推导) ​

每个请求解析一个外部名称的应用,在 ndots:5 下会发送 10 次而不是 2 次 DNS 查询,并为每次解析花费约 +3 ms(推导:sts 为 3.78 − 0.80 = 2.98 ms,未带末尾点的 FQDN 为 3.63 − 0.46 = 3.17 ms)。假设全集群每秒 1,000 次解析,CoreDNS 将接收 10,000 次而不是 2,000 次查询,其中 8,000 次回答 NXDOMAIN。两个 CoreDNS 副本上五分之四的负载用于生成“不存在”,而其中约一半数据包还会以区域数据传输费率跨越 AZ 边界(体量小,但并非零)。

所以呢——四种修复方法。(1) 在配置中的外部 endpoint 加上末尾点(sts.ap-northeast-2.amazonaws.com.)——立即变为 2 次查询,无需改代码。(2) 对发起大量外部调用的 Pod 设置 dnsConfig: {options: [{name: ndots, value: "1"}]}——但随后要通过 FQDN 访问集群名称。(3) NodeLocal DNSCache——本集群不存在它;有了它,Pod↔CoreDNS 往返(其中一半跨 AZ)将变为节点本地缓存命中(未测量)。(4) CoreDNS autopath 插件会代表 Pod 在服务器端遍历搜索路径;它不在此 Corefile 中(未测量)。

如何复现 ​

  1. 将上述 manifest 保存为 bench-net.yaml,应用它,并检查放置是否符合预期。每次运行的 Pod IP 都不同,因此请从 -o wide 中读取并替换到下面的命令中。

    bash
    kubectl apply -f bench-net.yaml
    kubectl -n bench-net get pods -o wide   # cli and srv-same on one node (2a), srv-a on another 2a node, srv-b in 2b
    kubectl -n bench-net exec -it cli -- bash
  2. RTT——每条路径以 50 ms 间隔进行 200 次探测:

    bash
    ping -c 200 -i 0.05 -q 10.0.2.72   # same-node
    ping -c 200 -i 0.05 -q 10.0.2.37   # same-AZ
    ping -c 200 -i 0.05 -q 10.0.3.65   # cross-AZ
    curl -s -o /dev/null -w 'connect=%{time_connect} total=%{time_total}\n' http://10.0.3.65:8080/   # one cold request, for reference
  3. 吞吐量——iperf3,20 s、1 个流和 8 个流、JSON 输出。持续运行是跨 AZ 的 180 s / 4 个流 / 10 s 间隔:

    bash
    for SRV in 10.0.2.72 10.0.2.37 10.0.3.65; do
      iperf3 -c $SRV -p 5201 -t 20 -P 1 -J > t1-$SRV-P1.json
      iperf3 -c $SRV -p 5201 -t 20 -P 8 -J > t1-$SRV-P8.json
    done
    iperf3 -c 10.0.3.65 -p 5201 -t 180 -P 4 -i 10 -J > t1-b-sustained180-P4.json

    表格列来自 JSON:end.sum_sent.bits_per_second、retransmits、end.cpu_utilization_percent.host_total / remote_total,以及每个流的 sender.mean_rtt 和 max_snd_cwnd。

  4. 请求延迟——fortio。每次运行均使用 -quiet -r 0.00001 -json -:

    bash
    SRV=10.0.3.65   # repeat per path
    fortio load -quiet -r 0.00001 -json - -qps 100 -c 4 -t 60s http://$SRV:8080/                    # HTTP keepalive
    fortio load -quiet -r 0.00001 -json - -qps 100 -c 4 -t 30s -keepalive=false http://$SRV:8080/   # new connection per request
    fortio load -quiet -r 0.00001 -json - -qps 0 -c 16 -t 20s http://$SRV:8080/                     # qps 0 = unlimited, closed loop
    fortio load -quiet -r 0.00001 -json - -grpc -ping -qps 100 -c 4 -t 30s $SRV:8079                # gRPC ping

    **不要去掉 -r 0.00001。**fortio 的默认直方图分辨率为 -r 0.001,即 1 ms 的 bucket。本页的每个延迟都低于 1 ms,因此使用默认值时,每个请求都会落入第一个 bucket,p50/p99 会变为该 bucket 内的线性插值——任何低于 1 ms 的值 p50 = 0.5 ms。首次 T2 运行正是得到了该结果;其百分位数被丢弃(平均值有效),而上面的表为 10 µs 分辨率下的重新运行。任何使用 fortio 测量亚毫秒延迟的人都会遇到这一问题。

  5. DNS——从 bench-dns.yaml 部署两个 Pod;一个终端中通过 sniffer sidecar 捕获,同时另一个终端中 app 容器冷解析该名称一次、预热解析 20 次:

    bash
    kubectl apply -f bench-dns.yaml
    kubectl -n bench-net exec dns-default -c app -- cat /etc/resolv.conf        # confirm 4 search domains + ndots:5
    kubectl -n bench-net exec dns-ndots1  -c app -- grep ndots /etc/resolv.conf  # options ndots:1
    # terminal 1 — every DNS packet this Pod sends or receives
    kubectl -n bench-net exec dns-default -c sniffer -- tcpdump -i eth0 -nn udp port 53
    # terminal 2 — one cold resolution (count queries and NXDOMAIN in the capture above) + 20 warm timings
    kubectl -n bench-net exec dns-default -c app -- python3 - <<'PY'
    import socket, statistics, time
    name = "sts.ap-northeast-2.amazonaws.com"       # trailing-dot variant: name + "."
    def one():
        t = time.perf_counter()
        socket.getaddrinfo(name, 80, socket.AF_UNSPEC, socket.SOCK_STREAM)
        return (time.perf_counter() - t) * 1000
    print("cold ms", round(one(), 2))
    xs = sorted(one() for _ in range(20))
    print("warm min/median/p90/max", round(xs[0], 2), round(statistics.median(xs), 2), round(xs[int(len(xs)*0.9)-1], 2), round(xs[-1], 2))
    PY

    对 dns-ndots1 重复相同过程,以获得表格的最后六行。该表测量 glibc 解析器(python:3.12-slim);未测量其他解析器(musl/alpine)的结果,因此请使用 glibc 镜像复现这些数字。

  6. 完成后删除 namespace——kubectl delete ns bench-net。Karpenter 会移除空节点。

注意事项 ​

  • **节点是新建的,但并非完全独占。**Karpenter 为此测试启动三个 m5.xlarge 节点后不久,consolidation 将其他 namespace 中少量小 Pod 移至这些节点(cli 节点一个,srv-b 节点三个——与基准流量无关的小型内部服务和 controller)。运行期间它们空闲或流量很低,负载被限制为最长 180 s 的突发。cli 节点显示 CPU requests 为 3901m / 3920m(99 %),这并未说明实际利用率。
  • **单次运行(每个单元 n = 1、仅一天)。**未做重复以估计方差。请将这些数字视为量级锚点,而非 SLA,并基于比例和模式得出结论(RTT 阶梯、5 Gbps 流上限、10 Gbps 实例上限、10 对 2 次查询)。
  • **无法测量 ClusterIP(kube-proxy iptables 跳转)和 trafficDistribution: PreferClose。**集群中的每次 Service kubectl apply 均被拒绝,错误为 Internal error occurred: failed calling webhook "mservice.elbv2.k8s.aws": … no endpoints available for service "aws-load-balancer-webhook-service"。只读诊断结果:aws-load-balancer-controller 已持续数周处于 CrashLoopBackOff,因此其后的 failurePolicy: Fail webhook 有零个就绪 endpoint——在 controller 恢复之前,集群中任何位置都无法创建 Service。本基准没有绕过该 webhook;装置只使用 Pod IP。症状 → 诊断 → 修复已写入 Troubleshooting Playbook,第 11 项“无法创建 Service:failed calling webhook” (English)。
  • 未收集 ENA allowance 计数器。ethtool -S eth0 | grep allowance_exceeded(bw_in_allowance_exceeded、bw_out_allowance_exceeded、pps_allowance_exceeded、conntrack_allowance_exceeded、linklocal_allowance_exceeded)需要节点上的 hostNetwork Pod,此处未运行。重传计数是间接信号。
  • **仅是在 180 s 内未观察到突发积分耗尽。**在“Up to”实例上,较长的持续传输可能被限速至基线(1.25 Gbps)。未测试超过 180 s 的情况。
  • **DNS 延迟包含 CoreDNS 缓存效应。**冷首次解析和 20 次预热重复不同(cache 30 也会缓存 NXDOMAIN),外部名称经过 VPC resolver。预热值之间的比较有效;绝对值取决于缓存状态。
  • **同节点 iperf3 受一个客户端核心限制(99.8 %)。**29.97 / 48.15 Gbps 是此实例系列的 CPU 数值,在其他系列上会不同。
  • **未比较其他 CNI 模式。**关闭 prefix delegation、关闭 Security Groups for Pods、network policy enforcing mode 为 standard(eBPF agent 存在,但 namespace 没有 policy)。这些设置改变时会发生什么不在本页范围内。

相关阅读 ​

向文档提问

按 Enter 发送,按 Shift+Enter 换行。