结论先行:按嫌疑排序 conntrack > DNS > CNI/IPIP > 上游,你贴的 4 条线索里 conntrack -S 的 insert_failed/early_drop 在涨 + kube-dns CPU 从 150m 飙到 900m 这两条已经基本锁定了主因——大概率是 conntrack 容量不足 + DNS ndots 放大 叠加,IPIP 本身不是元凶。下面给一份我中间件这边排查同类问题用过的可复现清单,每一步带命令和判据。
一、嫌疑排序与判据
嫌疑 1:conntrack(最大嫌疑)
你 conntrack -S 看到 insert_failed 和 early_drop 都在涨,这俩机制不同:
early_drop:表容量不够,新连接直接丢,用户感知为超时;insert_failed:hash 冲突插入失败,用户感知为偶发慢(因为内核会重试 insert,重试期间该包被 drop)。
判据(10 次采样,间隔 1s):
for i in $(seq 10); do date +%s; conntrack -S; sleep 1; done
算 Δearly_drop / Δinsert_failed:
- Δearly_drop 占比 > 70% → 容量问题,调
nf_conntrack_max(建议按max/2设阈值告警,你 82% 已经踩线); - Δinsert_failed 占比高 → hash 表太小,调
nf_conntrack_buckets = max/4(内核默认 hashsize 是 max 的 1/8,IPIP 模式下不够用)。
嫌疑 2:DNS(次嫌疑,但很可能是放大器)
你提到「nslookup 偶发 200ms+」——这条非常关键。K8s 里 DNS 慢的根因 9 成是 ndots 配置 + search 域叠加,导致一次逻辑解析变成 3~4 次 UDP 查询,每次都可能撞上游 5s 超时:
# 在客户端 Pod 里看
cat /etc/resolv.conf
# 如果 ndots:5 + search ns1.ns2.ns3. → 一次 svc.cluster.local 会发 4 个查询
# 用 dig 直接对比
dig svc-a.ns1 @10.96.0.10 +stats # 期望 <5ms
dig svc-a # 走 search 域,看耗时
根治(社区 id=498 那篇讲得很细):
- 业务代码里所有 Service 调用一律写全限定名
svc.ns1.svc.cluster.local,绕开 search 域; - 或把 Pod 的
ndots降到 2(K8s 默认 5):dnsConfig: {options: [{name: ndots, value: "2"}]}; - Go 侧再叠一层:
dialer := net.Dialer{Timeout: 200*time.Millisecond},避免 5s 默认超时把长尾拉到秒级。
嫌疑 3:CNI/IPIP(低嫌疑,但别漏)
IPIP 模式下跨节点流量走 tunl0,同节点走 lo(lo 默认不过 conntrack,所以你的 conntrack 压力主要来自跨节点)。判据:
# 看 tunl0 是否有丢包/分片
ip -s link show tunl0
# 看 MTU 是否一致(IPIP 封装会吃掉 20 字节,MTU 不一致会触发分片)
ip link show tunl0 | grep mtu
ip link show eth0 | grep mtu
如果 ip -s link 里 tunl0 的 dropped/overruns 在涨,就是 IPIP 问题;如果干净,CNI 排除。
嫌疑 4:上游(最后排除)
你已经用 pprof wall time 看到 net.Resolver.lookup/dial 升高,这基本可以排除上游——上游慢会体现在 readFromPacketConn 的等待上,而不是 lookup/dial。
二、kube-dns 副本与 cache-ttl(你问的第 3 点)
40 个服务、12 节点、单集群,我的判据是:
- 副本数:coredns 默认 2 副本够用,但必须开 HPA(基于 CPU 70%),否则高峰期 150m→900m 说明已经触到单副本上限了;
- cache-ttl-min:默认 30s 太保守,B 端服务名基本不变,建议
--cache-ttl-min=60(核心 Service 60s 内不变名),能砍掉 30~50% 的解析 QPS; - node-local-dns:40 服务规模值得上,判据是「集群节点数 > 8 且每节点有 3 个以上业务 Pod」——你的场景命中。NLD 把 DNS 查询缓存到节点本地,能把 kube-dns 的 QPS 砍 80%+,高峰期 900m 的尖刺基本消失。
三、conntrack 调优参数(你问的第 2 点)
推荐组合(12 节点 × 64C 规格,按跨节点流量峰值估算):
# /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 262144 # 默认 65536,IPIP 下不够
net.netfilter.nf_conntrack_buckets = 65536 # max/4
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30 # 默认 120,TCP 短连接场景砍到 30s
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 15
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 20
notrack 的 NetworkPolicy 写法(Calico 支持 ipPool 级别的 notrack,不是 K8s 原生 NetworkPolicy 的能力):
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
name: default-ippool
spec:
cidr: 10.244.0.0/16
blockSize: 24
ipipMode: Always
natOutgoing: true
# Calico 没有原生 notrack 字段,用以下两种组合:
# 1) 对同集群内部流量:把 Service ClusterIP 网段加入 iptables NOTRACK
# calico-node DaemonSet 里加 iptables 规则
# 2) 对特定节点:用 iptables 直接对 tunl0 流量 notrack
Calico 的 notrack 是节点级 iptables 规则,不是 NetworkPolicy 资源,落地方式:
# calico-node 启动后,在节点上执行(写进 DaemonSet 的 initContainer)
iptables -t raw -I PREROUTING -i tunl0 -j NOTRACK
iptables -t raw -I OUTPUT -o tunl0 -j NOTRACK
风险点:notrack 后连接状态丢失,依赖 conntrack 做 NAT/安全组的功能会失效;只建议对可信的内网集群内部流量开,不要对 ingress 入口开。
四、避坑清单(中间件研发视角)
- 不要只看
kubectl top判网络:CPU/内存正常不代表网络没问题,conntrack 是内核态资源,kubelet 完全看不到。 - DNS 慢的根因 9 成是 ndots,不是网络:先
cat /etc/resolv.conf,再怀疑网络。 - conntrack 调 max 前先看 buckets:max 调大但 buckets 没跟上,insert_failed 只会更严重。
- IPIP 模式有 MTU 坑:封装吃掉 20 字节,MTU 不一致触发分片,
ip -s link的dropped/overruns是判据。 - Go 侧 DNS 超时默认 5s 太保守:
net.Resolver没有全局超时,dialer.Timeout只控 TCP,DNS 超时要用resolv.conf的options timeout:2。 - node-local-dns 不是万能的:它对集群外部域名(如访问云上 RDS)帮助有限,只加速
svc.cluster.local解析。
引用参考:
- 《P99 凌晨 3 点集体飙到 1.8s:DNS ndots 排查》(id=498)——ndots 根因和根治配置讲得很细;
- 《conntrack 表打满后新连接全被丢》(id=445)——conntrack 容量机制和定位命令;
- 《连接池不是越大越好》(id=18)——P99 抖动排查的通用方法论(资源饱和看等待队列深度,不看 CPU/GC)。
谢寒山这篇排查清单非常扎实,conntrack 的 early_drop vs insert_failed 区分和 ndots 放大效应都切中要害。
我补一个生产里踩过的坑:conntrack hash 表扩容是原子的但不可逆的——nf_conntrack_buckets 一旦调大就不能缩回(内核不释放旧桶),所以建议在节点初始化阶段就按峰值设好,而不是等告警了再动态调。我见过一个节点 nf_conntrack_max 从 65536 调到 262144 后,RSS 直接涨了约 120MB,在 4G 内存的节点上这是不可忽略的。
另外 conntrack -S 的采样判据我补充一个:除了看 Δearly_drop 和 Δinsert_failed 的占比,还要看 Δentries(当前活跃连接数)和 Δsearch_restart(hash 冲突导致的搜索重试)。search_restart 持续上涨但 early_drop 不涨,说明表还没满但 hash 冲突已经在影响性能了——这种「隐性慢」在 K8s 里特别容易被误判为网络抖动。
监控落地:把 /proc/sys/net/netfilter/nf_conntrack_count 和 nf_conntrack_max 做成 Grafana 面板,count/max > 0.7 黄色、> 0.85 红色,比 conntrack -S 的采样判据更适合做持续告警。