K8s 集群偶发 5 秒延迟的 3 天复盘:ndots:5 + glibc 双栈 + conntrack race 五因素叠加 + NodeLocal DNSCache 落地

一套 180 节点 K8s 集群偶发服务间调用 5 秒延迟,P99 从 80ms 飙到 1.2s。3 天定位根因:ndots:5 + glibc 同源端口并发 + IPv4/IPv6 双栈 + iptables conntrack race + UDP 5 秒超时,5 个因素叠加触发稳定的 5 秒 hang。本文完整复盘 K8s DNS 解析链路、NodeLocal DNSCache 部署、6 种语言级 DNS 优化模板,落地 9 条 DNS 治理纪律。

2026 年 3 月一个周四下午 16:18,运维同事在 K8s 平台值班群里贴了一张监控截图:"过去 1 小时,集群内部服务之间的 P99 调用延迟从正常的 80ms 飙到 1.2s,但目标服务自身 P99 还是 60ms,网络流量正常,没有任何 Pod 重启或扩缩容动作。"这种"中间发生了什么我也不知道"的现象在 K8s 网络层面太常见了。我盯着这条消息看了 5 秒,凭着第六感打了 5 个字:"是不是 DNS?"

2 小时后答案揭晓:确实是 DNS。但根因比"DNS 慢"复杂得多——一条看似无害的 K8s 服务调用,在 glibc 的 resolver 库 + ndots:5 默认配置 + IPv4/IPv6 双栈串行查询 + iptables conntrack race 这五个因素叠加下,会以稳定概率 hang 5 秒(UDP 重传超时),导致 P99 灾难性变差。这篇是完整复盘,涵盖 Linux DNS 解析的内部机制、K8s 默认 ndots 的"原罪"、conntrack race 的根本原因、NodeLocal DNSCache 的工作原理,以及落地的《K8s DNS 优化清单》。

服务背景:这个被 DNS 玩死的微服务集群

维度 数值
集群规模 K8s 1.28,180 个 Node,约 2400 个 Pod
CNI Calico 3.27 + IPv4/IPv6 双栈
DNS CoreDNS 1.11,3 副本
服务网格 无(裸 K8s Service + Endpoint)
主要业务 SaaS B 端 ERP,Java/Python/Go 多语言并存
事故现象 服务间调用偶发 5 秒延迟,影响 ~5% 请求,主要是 Java + Go 服务
P99 影响 从 80ms → 1.2s,持续 1-2 小时后又自动好,无规律

这个集群从 2024 年底搭建,跑了一年多,DNS 问题一直零星冒出但都被归因为"网络抖动"。这次因为大客户业务高峰加剧,5% 的 5 秒延迟变成显眼的 P99 拉高,才被认真追究。

说到"网络抖动"这个词,它在我们团队复盘后被列入了"禁用归因清单"。所谓"网络抖动"通常是排查不下去的托词,真实根因往往是某个具体的协议层细节(如 conntrack 满、TCP retrans、DNS race、TLS 握手抖动)。我们规定任何监控告警分析,不允许出现"原因:网络抖动"这种模糊归因——必须给到协议层 + 具体配置 + 复现方式,否则视为未结案。这条纪律让我们后续半年至少避免了 3 次类似的"长期偶发"事故。

另外补充一句:这个集群里跑的 Java 服务是大头(占 60% Pod),其次是 Go(25%)和 Python(15%)。Java 的影响最大是因为 JVM 默认会启用 IPv6 双栈查询,且 InetAddress 没有应用层缓存(默认 networkaddress.cache.ttl=-1 是"永久缓存"而不是"不缓存",理解错了反而是坑),所以每次新建 HttpClient 连接都会触发完整的 DNS 解析链路,踩 race 概率最高。

事故复盘后我们做了一次"全量 Pod DNS 配置体检",发现集群里 78% 的 Deployment 完全没显式配 dnsConfig,11% 配了但参数不全,只有 11% 是按团队规范配置的。这个比例和事故影响范围(主要集中在 Java 服务)高度吻合——配置不全的 Deployment 几乎都是 Java。这个体检后来被脚本化,纳入 CI 检查项,任何新 Deployment yaml 必须满足 dnsConfig 规范才能合并。

事故时间线:从"是不是 DNS"到根因的 2 天

时刻 事件
03-12 16:18 P99 飙升告警,我猜 DNS
03-12 16:30 进一个客户端 Pod,跑 time dig service.namespace.svc.cluster.local,正常 1-3ms;但跑 10 次有 1 次 hang 5 秒
03-12 17:00 tcpdump -i any port 53 抓 DNS 包,看到 5 秒延迟的查询是 IPv6 (AAAA) 查询超时,5 秒后客户端重试改用 TCP 才成功
03-12 17:30 同时跑 cat /etc/resolv.conf,看到 ndots:5(K8s 默认)
03-13 上午 读 glibc resolver 源码 + 翻 Calico + iptables conntrack 文档,理解 conntrack race
03-13 下午 本地集群复现:写一个高并发解析脚本,稳定触发 5 秒 hang
03-14 上午 方案:Pod 级 ndots:1 + 部署 NodeLocal DNSCache + 应用层 DNS 缓存
03-14 下午 预发跑 24 小时压测,5 秒 hang 完全消失
03-15 分批 rollout 到生产,P99 稳定在 80ms

第一反应:"DNS 应该挺快的吧"

大多数应用开发者对 DNS 的认知停留在"它是个查域名拿 IP 的服务,毫秒级"。这个理解在大多数场景对,但在 K8s 容器化环境下,DNS 是性能问题的高发区,而且性能问题的形式经常不是"慢一点",而是"卡 5 秒"或者"卡 10 秒"这种数量级的崩塌。原因都和 Linux 的 UDP DNS 客户端的"超时"机制有关——glibc resolver 默认 timeout 5 秒,query 不返回就死等。

第一个关键证据是 dig 偶发 5 秒:

# 在客户端 Pod 里跑 100 次
for i in $(seq 1 100); do
    /usr/bin/time -f "%e" dig +short user-service.default.svc.cluster.local 2>&1
done | sort -rn | head -10

# 输出 (节选)
5.04
5.03
5.02
0.012
0.011
0.010
...

100 次里有 6 次正好 5.0X 秒,其余都是毫秒级。5 秒这个魔法数字直接说明是 DNS 客户端的 UDP 超时——能稳定踩到这个数字,意味着 query 包发出去后没收到响应,5 秒后重试(改 TCP 或换 server)才成功。

5 个因素叠加的因果链

把这次事故的根因画成因果图,是 5 个机制串起来的复合事件:

这条链有意思的地方是每一环单看都"合理":ndots:5 是为方便,glibc 双栈并查是为延迟,Calico + iptables 是标准 CNI,UDP 5 秒超时是 POSIX 默认。但在 K8s 大规模容器化环境下,这 5 个"合理"叠加就是稳定的 P99 杀手。

真凶 1:ndots:5 + 不带 . 的域名 = 6 次 DNS 查询

K8s 默认给每个 Pod 的 /etc/resolv.conf 是这样:

nameserver 10.96.0.10                              # CoreDNS Service IP
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

关键是 ndots:5。它告诉 glibc resolver:"如果你要查的域名里点的数量小于 5,先尝试加上每个 search 域,最后才作为完整域名查"。看上去合理(对 K8s 内服务调用方便),但代价巨大。

举个例子,当 Java 应用调用 customer-service.default.svc.cluster.local 时(看起来是 FQDN):

步骤 查询的域名 是否会被查
1 customer-service.default.svc.cluster.local.default.svc.cluster.local ✅(因为点数 4 < 5)
2 customer-service.default.svc.cluster.local.svc.cluster.local
3 customer-service.default.svc.cluster.local.cluster.local
4 customer-service.default.svc.cluster.local(原始 FQDN)

而且每个域名又会被分别查 A 和 AAAA(IPv4 / IPv6)——所以一次"我以为是直接查 FQDN"的调用,实际会触发8 个 DNS 查询包(4 域名 × 2 record type)。任何一个包丢失或对端 race,都会触发 5 秒重传。

对于不带任何点的短域名(比如 customer-service),glibc 会先尝试 3 个 search 域,只有都失败才查 FQDN——好的方面是命中 search 域很快(就是用户想要的同 namespace 调用),坏的方面是每次都至少跑完所有 search 域才结束,网络包数翻倍。

真凶 2:glibc 双栈串行 + 同源端口冲突

更深的坑在 glibc resolver 的实现。glibc 在解析时:

  1. 同一个查询的 A 和 AAAA 用同一个 UDP socket 并行发出(同源端口)
  2. 等待两个响应都回来再返回结果
  3. 响应没回来就走 timeout(默认 5 秒)

"同源端口"这个细节是灾难的根源。在 K8s 网络栈里,Pod 发出的 UDP 包要经过 iptables/netfilter 的 conntrack(连接跟踪)做 NAT。conntrack 表里以 (源 IP, 源端口, 目的 IP, 目的端口, 协议) 五元组为 key——同源端口的两个并发包(A 和 AAAA 各一个)会被 conntrack 视为同一连接

这就引出了著名的 conntrack race condition:

顺序 事件
T1 glibc 同时发出 A 查询和 AAAA 查询,源端口都是 X
T2 第一个包到达 netfilter,创建 conntrack entry
T3 第二个包几乎同时到达,看到 conntrack entry 不存在(还没插入完),尝试创建一个
T4 两个包都在尝试创建相同五元组的 conntrack entry → 内核检测到冲突,丢弃其中一个
T5 被丢弃的包对应的查询永远收不到响应
T6 glibc 等 5 秒,timeout,重试(通常用 TCP)成功

这就是为什么正好 5 秒 hang——5 秒是 glibc 默认 timeout,然后重试 TCP 才成功。在 Calico CNI + IPv4/IPv6 双栈的环境下,这个 race 的概率约 1%-5%,QPS 高的话能稳定触发。

这个 race 在内核态可以通过 conntrack -S 看到丢包计数:

# 在 K8s Node 上看 conntrack 状态
sudo conntrack -S
# 关键字段:
# insert_failed: 多少次插入冲突被丢弃
# search_restart: race 检测后的重启搜索次数
# drop: 因 conntrack 满或冲突丢的包数

# 我们事故期间这个 Node 的数据:
# insert_failed=18742 (24h)
# 平均每个 Node 每小时丢 780 个包

这些 insert_failed 数字平时根本没人看,但事故复盘后它成了"K8s Node 健康分"的关键指标——任何 Node 这个值持续增长就说明有 conntrack race 在发生。我们的告警阈值是 insert_failed 24h 增长超过 1000 即告警。

顺便说一下,这个 race 并不是 Linux 内核的 bug,而是"同源端口并发"和"conntrack 五元组唯一性"的语义冲突——内核选择"宁可丢一个包也不能让 conntrack 表错乱",这是正确决策,只是没考虑到 glibc resolver 会刻意制造这种并发。glibc 自己也有意识到这个问题,所以才提供了 single-request-reopen 和 single-request 两个 option 给用户选——但默认不开,留给应用自己决定。

真凶 3:CoreDNS 容量 + 缓存策略

除了上面两个根因,我们的 CoreDNS 部署也有几个加剧问题的因素:

问题 表现
CoreDNS 只 3 副本 整个集群 2400 Pod 共用 3 个 CoreDNS,QPS 高时 CPU 100%
没启用 cache 插件优化 同样的查询反复打到 CoreDNS,放大 ndots:5 的伤害
客户端无应用层 DNS 缓存 每次 HTTP 调用都重新解析
没部署 NodeLocal DNSCache 每次解析都走集群 Service IP → kube-proxy → CoreDNS Pod,链路长

这些不是根因但是放大器——前两个根因让 DNS 容易丢包,后面几个让丢包发生的频率更高、影响更大。

修法:四层组合优化

修法 1:dnsConfig 调小 ndots

K8s 允许 Pod 级别覆盖 DNS 配置。对所有"主要做集群内 FQDN 调用"的服务,把 ndots 改成 1 或 2:

# Deployment spec
spec:
  template:
    spec:
      dnsConfig:
        options:
          - name: ndots
            value: "2"           # K8s 内服务调用 FQDN(4 个点)直接走原域名, 不再 search
          - name: single-request-reopen
            value: ""            # 关键: 让 IPv4/IPv6 用不同源端口, 避免 conntrack race
          - name: timeout
            value: "1"           # 单次查询超时缩短到 1 秒
          - name: attempts
            value: "2"           # 重试 2 次
      dnsPolicy: ClusterFirst

关键的 single-request-reopen:它告诉 glibc resolver "每个查询包关闭 socket 重新打开",这样 IPv4 和 IPv6 查询会用不同源端口,从根本上避免 conntrack race。还有一个更激进的 single-request 选项,直接禁用并行查询,改成串行——延迟略升但完全无 race。

对短域名调用为主的服务(很多 Python / 内部脚本喜欢写短名),ndots 保持 5(K8s 默认),但配 single-request-reopen。

修法 2:NodeLocal DNSCache

K8s 官方提供的 NodeLocal DNSCache 是 daemonset,每个 Node 上跑一个 169.254.20.10 监听的 DNS 缓存。Pod 的 DNS 请求先走 NodeLocal,缓存命中直接返回,miss 才转发到 CoreDNS。

# 部署 NodeLocal DNSCache (官方 yaml, 略)
kubectl apply -f https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml

# 部署后, Pod 的 /etc/resolv.conf 自动指向 NodeLocal:
nameserver 169.254.20.10

NodeLocal 用的是 TCP 转发到 CoreDNS,避开了 UDP conntrack race;同时本地缓存让大量重复查询不出 Node,延迟从毫秒级压到微秒级。

装完 NodeLocal DNSCache 后,我们 CoreDNS 的 QPS 从 80k 直接降到 8k——90% 的查询被 NodeLocal 缓存吃掉。CoreDNS 副本可以缩到 2 个反而绰绰有余。

修法 3:Java 服务关掉 IPv6

Java 默认会尝试 IPv6,且 InetAddress.getByName 会触发 A + AAAA 查询。如果业务不需要 IPv6,加 JVM 参数:

-Djava.net.preferIPv4Stack=true     # 完全禁用 IPv6, 只查 A
-Dsun.net.inetaddr.ttl=60           # JVM 层 DNS 缓存 60 秒(默认是 forever, 反而是坑)
-Dsun.net.inetaddr.negative.ttl=10  # 失败结果缓存 10 秒

JVM 的 DNS 缓存默认是永久缓存(networkaddress.cache.ttl=-1),这本身就是 K8s 环境的反模式(参考 026 那篇 HttpClient 文章)。一定要显式设。

修法 4:Go 服务用 PureGo Resolver

Go 默认混用 cgo resolver (用 glibc) 和 Go 原生 resolver。glibc 有上面所有问题,Go 原生 resolver 自己实现 DNS,行为更可控。强制用原生:

// 在 main 里
import _ "net"

func init() {
    // 强制使用 Go 原生 resolver, 绕开 glibc
    os.Setenv("GODEBUG", "netdns=go")
}

// 或者编译时
GODEBUG=netdns=go go build ...

Go 原生 resolver 默认不会做 AAAA 查询(除非显式开 IPv6),也用不同源端口发查询,直接避免 conntrack race。

验证:24 小时混沌测试

指标 修复前 修复后
DNS 查询 P99 120ms(其中 5% 是 5 秒) 0.4ms
DNS 查询 P99.9 5042ms 1.2ms
服务间调用 P99 1.2s 78ms
CoreDNS QPS 80k 8k
CoreDNS Pod CPU 3 副本各 80%+ 2 副本各 15%
5 秒 hang 出现频率 5% 请求 0%

决策树:K8s DNS 优化怎么选

修复过程中,我们整理出一张 "DNS 优化优先级图",做容量评估时直接照着选:

这张图最有价值的判断是 "NodeLocal DNSCache 优先",原因是它一招把 conntrack race 问题彻底绕开(改 TCP),还顺便缓存了 90% 的查询。其他修法都是补丁,NodeLocal 是结构性解。

横向对比:其他云原生方案怎么处理 DNS

这次复盘前我们对 K8s DNS 的认知是 "默认就行",事后做了横向对比:

方案 DNS 处理方式 是否有 conntrack race 典型 P99
裸 K8s + CoreDNS UDP 走 kube-proxy + iptables 80ms ~ 5s
K8s + NodeLocal DNSCache 本地 169.254 缓存 + TCP 转发 无(TCP) 0.5ms ~ 5ms
K8s + Cilium eBPF 用 eBPF map 替代 iptables conntrack 1ms ~ 8ms
Istio sidecar Envoy 拦截 DNS, 应用层缓存 无(sidecar 缓存) 0.3ms ~ 3ms
Nomad + Consul DNS 每节点 Consul agent 2ms ~ 10ms
裸物理机 + systemd-resolved 本地缓存 + 直连 upstream 无(无 conntrack) 0.5ms ~ 5ms

结论:"应用 + Pod + iptables conntrack + UDP DNS" 这条链路是 K8s 默认架构的脆弱点,任何打破其中一环的方案都能消除问题。NodeLocal DNSCache 通过用 TCP 转发打破"UDP + conntrack",Cilium eBPF 通过替换 conntrack 实现打破,Istio 通过 sidecar 拦截 DNS 打破——三种思路殊途同归。

5 天里被否决的"看似合理"方案

方案 否决原因
把 CoreDNS 副本加到 20 个 问题不在 CoreDNS 容量, 是 conntrack 丢包, 加副本不解决
所有应用统一用 IP 直连绕过 DNS 失去 Service 发现机制, Pod IP 变化时全挂, 工程上不可接受
统一升级到 systemd-resolved 大多数容器镜像没装 systemd, 工作量太大且改变基础镜像
关闭 IPv6 模块 关掉宿主机 IPv6 模块影响其他系统调用, 风险面太大
把 conntrack table 调到无限大 问题是 race 不是容量, 调大表不能消除并发插入冲突
把 timeout:5 改成 timeout:1 缓解症状但不解根因, 而且短超时会增加重试压力, 风险更高

顺手发现的其他几个 K8s 网络坑

问题 表现 修法
kube-proxy iptables 模式延迟高 大集群 Service 数量多时, iptables 链路长 切 IPVS 或者 Cilium eBPF
Pod 内 DNS resolv.conf 不会随 ConfigMap 更新 修改 ndots 需要重建 Pod 滚动重启
HostNetwork Pod 的 DNS 配置和普通 Pod 不同 访问集群 Service 失败 显式 dnsPolicy: ClusterFirstWithHostNet
跨命名空间调用没用 FQDN 触发更多 search 域查询 统一用 service.namespace.svc.cluster.local
大流量服务用 ClusterIP 模式 kube-proxy 转发成瓶颈 用 Headless Service 或 LoadBalancer

修法 5:Python / Node.js 服务的应用层优化

除了 Java 和 Go,Python 和 Node.js 也有专属坑。Python 的 requests 库默认不缓存 DNS,每次 HTTP 都解析一次。我们对高频调用的内部 HTTP client 加了 cachetools 缓存层:

from cachetools import TTLCache
import socket
import threading

_dns_cache = TTLCache(maxsize=10000, ttl=60)
_dns_lock = threading.Lock()
_orig_getaddrinfo = socket.getaddrinfo

def cached_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
    key = (host, port, family, type, proto, flags)
    with _dns_lock:
        if key in _dns_cache:
            return _dns_cache[key]
        res = _orig_getaddrinfo(host, port, family, type, proto, flags)
        _dns_cache[key] = res
        return res

socket.getaddrinfo = cached_getaddrinfo

这个 monkey patch 让 Python 进程内所有 DNS 解析自动走 60 秒缓存,对高 QPS 的内部 RPC 非常有效。我们的 Python 网关服务接入后,DNS 调用数从 12k/s 降到 200/s。

Node.js 默认也不缓存 DNS,且 Node 的 DNS 模块有 dns.lookup(走 glibc, 同步阻塞 event loop)和 dns.resolve(走 c-ares, 异步)两种 API。生产环境必须强制用 c-ares,且加缓存:

// 用 cacheable-lookup 包做透明缓存
const CacheableLookup = require('cacheable-lookup');
const cacheable = new CacheableLookup({
  maxTtl: 60,           // 缓存 60 秒
  errorTtl: 5,          // 失败缓存 5 秒, 防止穿透
});
cacheable.install(http.globalAgent);
cacheable.install(https.globalAgent);

装完这个,Node.js 服务的 DNS 查询数下降 95%,5 秒 hang 完全消失。这两个语言级修法配合 NodeLocal DNSCache,是真正的"应用 + 平台"双重保险。

额外一个 Python 的注意点是 requests 库的 connection pool 默认不会复用,每次新建 Session 都重新 DNS 解析。建议改用 requests.Session() 全局实例,配合上面的 monkey patch,DNS 调用还能再减一个数量级。Node.js 这边,Axios 默认也会创建新的 http.Agent,记得显式传入复用的 globalAgent,否则前面的 cacheable-lookup 安装不生效。

对于 Rust(我们也跑了几个 Rust 服务)的 hyper / reqwest,默认用的是 tokio 的 trust-dns-resolver,行为更可控,不会触发 glibc 那一套坑,但也建议显式设缓存 TTL 60 秒,避免 DNS 抖动放大成应用层抖动。这部分是事故后我们做技术栈横扫时整理的,顺便把所有语言的 HTTP client DNS 配置规范化了。

整个"语言级 DNS 优化"花了我们一周时间梳理出 6 套语言模板(Java / Go / Python / Node.js / Rust / .NET),后续新业务上线时直接套用,从根上避免了应用层 DNS 配置不一致的问题。这套模板和 NodeLocal DNSCache 的组合是我们目前 K8s DNS 架构的最终形态,稳定运行 3 个月零事故,可以说彻底把 DNS 这个老大难从我们的告警列表里抹掉了,腾出的运维注意力转去搞 Service Mesh 升级和成本优化,这种"修一次永远不用再修"的体验真是难得,毕竟运维大多数时候是按下葫芦浮起瓢的循环游戏。

立的《K8s DNS 优化清单》

  • 所有 K8s 集群必须部署 NodeLocal DNSCache,无例外。这是性价比最高的单项优化。
  • 所有 Java 服务必须设 java.net.preferIPv4Stack=true(除非确实用 IPv6)+ sun.net.inetaddr.ttl=60。
  • 所有 Go 服务必须设 GODEBUG=netdns=go
  • 所有 Pod 的 dnsConfig 必须显式配 single-request-reopen,这是修 conntrack race 的根本解。
  • 主要做 K8s 内 FQDN 调用的服务,ndots 改成 2;调用习惯不规范的服务保持 ndots:5 但靠 NodeLocal 兜底。
  • CoreDNS 副本数 ≥ Node 数 / 100,最少 2 个,且配 PodAntiAffinity 分散在不同 Node。
  • 必须监控 DNS 查询延迟和错误率:CoreDNS 自带 metrics,P99 > 100ms 告警,SERVFAIL 比例 > 0.1% 告警。
  • 禁止在生产环境用 hostNetwork: true 同时 dnsPolicy: Default——会拿到节点的 DNS 配置,集群内服务调用全部失效。
  • 跨命名空间调用必须用完整 FQDN(service.namespace.svc.cluster.local),避免每次都走 search 域。

给读者的几条自查清单

  1. 进任一 Pod 跑 cat /etc/resolv.conf,看 ndots 是几。是 5 但你的应用大量调 FQDN,改 2。
  2. 跑 100 次 time dig +short service.namespace.svc.cluster.local,看有没有 5.0X 秒的。有就是 conntrack race。
  3. tcpdump -i any port 53 抓 60 秒,看有没有"发出去没回响"的查询。
  4. 检查 CoreDNS 副本数和 CPU 使用率。Pod 数 / CoreDNS 副本数 > 1000 是高风险。
  5. 看你的集群有没有 NodeLocal DNSCache:kubectl get ds -n kube-system | grep node-local-dns。没有的话部署它。
  6. Java / Go / Python 应用层的 DNS 缓存策略:Java 默认 forever 是坑,Python requests 默认不缓存。
  7. 如果用了 IPv6 双栈,且业务不需要 IPv6,在所有应用层强制 IPv4(节省一半 DNS 查询)。

事故后这 3 个月的长期收益

指标 事故前 修复后 3 个月
服务间调用 P99 走势 偶发 1.2s 毛刺 稳定 70-80ms
客服收到"系统卡"投诉 15/周 1-2/周
CoreDNS 集群 CPU 总和 240% (3 副本各 80%) 30% (2 副本各 15%)
DNS 相关 P3+ 告警 每周 4-6 次 近 3 个月 0 次
5 秒 hang 报告数量 每天 1000+ 次 0
新业务上线时 DNS 配置问题 每次都要踩坑 统一模板, 零配置上线

更意外的收益是整个团队对 K8s 网络栈的理解上了一个台阶。这次复盘后我组织了两场内部分享,把 ndots、conntrack、glibc resolver、NodeLocal 这些概念讲透,后续团队在排查任何 K8s 网络问题(不止 DNS)时,第一步都会先想"我的请求会经过哪些层、每层有什么默认行为"。这种"反操作系统直觉"的能力,是 K8s 工程师的核心竞争力。

这次复盘让我们更新的 K8s 工程认知

(1) "默认配置 = 反优化" 同样适用于 K8s 网络:K8s 的默认 DNS 配置(ndots:5、UDP)、kube-proxy 默认 iptables 模式、CoreDNS 默认副本数、Pod 默认 dnsPolicy——每一项都是为"通用易用"设计的,对大规模生产环境都需要调。我们把 "K8s 网络治理 checklist" 加进了集群上线 SOP,新集群必装 NodeLocal + Cilium 评估 + dnsConfig 模板。

(2) "应用层 + 平台层"边界要重新画:之前我们把 DNS 问题当作 "平台问题",应用同学不需要懂。这次事故后我们把 "K8s DNS 行为" 列为后端工程师必学知识,因为修法 (Java preferIPv4Stack、Go GODEBUG、dnsConfig) 必须从应用侧配合,不是平台单方面能搞定。

(3) "偶发性问题"必须比"稳定问题"更重视:稳定的 P99 高大家会盯,但"偶发 5 秒"很容易被归因为"网络抖动"过滤掉。我们的教训是 偶发性 5 秒延迟在 K8s 里几乎都是 DNS 问题,以后再遇到不能拖。监控加了"P99 突增告警"——只要 P99 比前 1 小时平均上涨 50%,就触发自动诊断脚本(包括 dig 测试、tcpdump、CoreDNS 状态)。

(4) "可观测性必须涵盖网络栈每一层":我们之前的 APM 只看应用层调用链,DNS 这一层是黑盒。修复后接入了 CoreDNS metrics + NodeLocal metrics + conntrack 表满率 + iptables drop 计数,这些指标和应用 P99 关联起来分析,任何 DNS 异常都能秒级定位。

这次事故让我对 K8s 网络又多了一层"敬畏感":它的复杂度大头不在 CNI 路由,而在那些"看似无关的默认配置"——ndots:5、glibc 同端口并发、UDP timeout 5 秒、conntrack race,这些在物理机时代根本不存在的问题,在容器化网络栈下会以惊人的方式叠加。如果你的 K8s 集群偶发"5 秒延迟"或者"P99 偶发飙升",不要去找应用层,先看 DNS——这是最高概率的命中。

另一个感悟是K8s 默认配置的某些选择,在大规模生产环境下不太合理。ndots:5 是为了让"短域名 + 同 namespace 调用"方便,但代价是放大了所有 FQDN 调用的开销。这种"为开发方便而牺牲性能"的默认值,在大集群下必须显式覆盖。这也是为什么生产 K8s 必须有"网络治理"这个角色——不只是搭起来,还要把这些默认值一一调对。

最后给所有还没部署 NodeLocal DNSCache 的团队一个建议:今天就装,装完一晚上的 P99 改善会让你后悔为什么没早装。这是 K8s 网络优化里 ROI 最高的单项工作,部署成本一个下午,后续 3 个月可以解决 80% 的偶发"慢"问题。我们的实际数据:服务间调用 P99 从 1.2s 降到 78ms,CoreDNS 副本从 3 缩到 2,客服投诉减少 90%——这些就是一个 daemonset 带来的全部收益。

这次事故还让我重新审视了一个工程价值观:"我们一直这么用都没事"不是稳定性的论据,而是"运气好还没爆"的别名。K8s 默认配置在小集群、低 QPS 时确实没事,但规模一旦上来,所有"理论上的小问题"都会变成"生产事故"。预防的成本是几小时调研 + 几天测试,事故的成本是几天加班 + 客户信任损失,后者贵 100 倍。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

PostgreSQL 高频 UPDATE 表 bloat 到 1.4TB 拖垮 P99 的 5 天复盘:autovacuum 调参 + pg_repack 在线重建实战

2026-5-26 11:51:33

技术教程

K8s 滚动更新每次发布 30 秒 5xx 毛刺持续 18 个月的 3 天复盘:readinessProbe + preStop + minReadySeconds 四因素叠加 + 零停机发布完整模板

2026-5-26 12:03:23

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索