课程目录(第 28 章 / 共 33 章)
课程/平台化与规模化

节点与内核调优:让 Pod 跑得更稳

28 章 / 共 33·20 分钟·进阶节点调优sysctlNodeLocalDNSNFDTuned

容器共享同一个内核,节点上的 sysctl、DNS 缓存、硬件特性标签与 kubelet 预留参数,决定了 Pod 到底跑得稳不稳。

学完这一章,你将能够

  • 分得清哪些内核参数属于 Pod 级、哪些必须改节点级,并能写出可执行的 securityContext.sysctls
  • 用 NodeLocal DNS Cache 缓解集群内 DNS 偶发超时,并用 metrics 确认缓存命中
  • 用 NFD 把 CPU 指令集、GPU、网卡等硬件特性变成节点标签,供调度与 Device Plugin 使用
  • 用 kube-node-tuning / Tuned 把内核参数声明式下发到节点,替代手工改 /etc/sysctl.d

为什么要做节点级调优

前面章节里,我们习惯把 Pod 看成一个独立的小盒子:给它 CPU、内存、探针,它就应该跑好。但 Pod 最终落在某台 Linux 节点上,和这台机器上的其它 Pod、kubelet、容器运行时、systemd、日志采集器共享同一个内核。内核里的全局开关——TCP 连接回收时间、内存回收策略、透明大页、进程数上限——一旦调错,影响的是整台机器,而不是某一个 Pod。

所以调优前必须先回答一个问题:这个参数能不能按 Pod 隔离? Linux 内核参数分两类。一类在 network、ipc、uts 等 namespace 里有独立副本(namespaced),可以为每个 Pod 单独设置;另一类只有一份(node-level),改了就是改整台机器。搞混这两类,是新手最常犯的错误。

参数能否按 Pod 隔离正确的下发位置
net.ipv4.tcp_syncookiesip_unprivileged_port_start是(netns)Pod 的 securityContext.sysctls,属安全集合
net.core.somaxconn是(netns),但属 unsafe先给 kubelet 开白名单,再写进 Pod
vm.swappinessvm.max_map_count否,节点级/etc/sysctl.d/、Tuned 或特权 DaemonSet
kernel.pid_maxfs.file-max、透明大页、swap否,节点级节点初始化脚本、GRUB 参数或 Tuned

调优的三层结构,以及每层的生效范围:

text
一台节点(所有 Pod 共享同一个内核)
 ┌────────────────────────────────────────────────────┐
 │ ③ Pod 级   securityContext.sysctls                 │
 │    只作用于本 Pod 的 net / ipc / uts namespace     │
 ├────────────────────────────────────────────────────┤
 │ ② kubelet 级   --system-reserved / --kube-reserved │
 │    --eviction-hard / --allowed-unsafe-sysctls      │
 │    --cluster-dns:决定 Pod 里 resolv.conf 指向谁   │
 ├────────────────────────────────────────────────────┤
 │ ① 内核 / 节点级   sysctl、THP、swap、HugePages     │
 │    下发方式:/etc/sysctl.d/、GRUB、Tuned、DaemonSet│
 └────────────────────────────────────────────────────┘
   越往下影响面越大:能 Pod 级解决的,就不要动节点级
一条实用原则:优先 Pod 级,必须节点级时走声明式下发(Tuned / DaemonSet / 节点初始化),不要 SSH 上去手敲 sysctl -w——手敲的改动重启就没了,而且没人知道它存在过。

Pod 级 sysctl:安全集合与 unsafe 白名单

Kubernetes 通过 spec.securityContext.sysctls 设置 namespaced 内核参数。它是 Pod 级字段,作用于该 Pod 的所有容器,字段本身不区分安全与否,能不能生效由 kubelet 校验决定。

Kubernetes 把 sysctl 分成安全(safe)与不安全(unsafe)两类,安全集合默认允许,当前包括 kernel.shm_rmid_forcednet.ipv4.ip_local_port_rangenet.ipv4.tcp_syncookiesnet.ipv4.ping_group_range(1.18+)、net.ipv4.ip_unprivileged_port_start(1.22+)、net.ipv4.ip_local_reserved_ports(1.27+)、net.ipv4.tcp_keepalive_timetcp_keepalive_intvltcp_keepalive_probestcp_fin_timeout(1.29+)、net.ipv4.tcp_rmemtcp_wmem(1.32+)。

两个例外要注意:Pod 用了 hostNetwork: true 时所有 net.* 都不允许;net.ipv4.tcp_syncookies 在 4.5 及更早的内核上不是 namespaced 的。

不在安全集合里的 sysctl 默认被拒绝,管理员可以在逐节点的基础上用 kubelet 参数放开:

bash
kubelet --allowed-unsafe-sysctls 'kernel.msg*,net.core.somaxconn' ...

只有 namespaced 的 sysctl 能通过这个白名单放开;vm.swappiness 这类节点级参数写进 Pod 也没用。被拒绝的 Pod 会正常被调度,但启动失败,事件里写得很清楚:forbidden sysctl: "net.core.somaxconn" not allowlisted

动手练习:写一个设置 sysctl 的 Pod,并验证它真的生效

第一步,创建 Pod。这里用两个安全集合里的参数:tcp_syncookies 防 SYN flood,ip_unprivileged_port_start 让容器里的普通用户也能监听 80 端口。

yaml
apiVersion: v1
kind: Pod
metadata:
  name: sysctl-demo
spec:
  securityContext:
    sysctls:
      - name: net.ipv4.tcp_syncookies
        value: "1"
      - name: net.ipv4.ip_unprivileged_port_start
        value: "0"
  containers:
    - name: shell
      image: busybox:1.36
      command: ["sleep", "3600"]

第二步,应用并进容器读一遍参数:

bash
kubectl apply -f sysctl-demo.yaml
kubectl exec sysctl-demo -- sysctl net.ipv4.tcp_syncookies net.ipv4.ip_unprivileged_port_start

两条命令都应该输出 net.ipv4.tcp_syncookies = 1net.ipv4.ip_unprivileged_port_start = 0。注意 sysctl -a 在容器里只能看到本 namespace 的参数,看不到 vm.* 这类节点级的值——这正好是「Pod 级」的直观证明。

第三步,亲手制造一次拒绝:

bash
kubectl delete pod sysctl-demo
kubectl run unsafe-sysctl --image=busybox:1.36 --restart=Never -- sleep 3600 \
  --overrides='{"spec":{"securityContext":{"sysctls":[{"name":"net.core.somaxconn","value":"1024"}]}}}'
kubectl describe pod unsafe-sysctl | grep -A3 Events

事件里出现 forbidden sysctl: "net.core.somaxconn" not allowlisted。想让它跑起来,必须先在节点的 kubelet 上加 --allowed-unsafe-sysctls=net.core.somaxconn 并重启 kubelet——kubeadm 集群的参数在 /var/lib/kubelet/config.yaml/etc/default/kubelet(Debian 系)、/etc/sysconfig/kubelet(RHEL 系)里,托管集群走节点池配置,详见生产集群部署。清理:kubectl delete pod unsafe-sysctl --ignore-not-found

NodeLocal DNS Cache:把 DNS 查询留在本机

集群 DNS 的默认链路是:Pod 查域名 → /etc/resolv.conf 里的 CoreDNS ClusterIP → kube-proxy 做 DNAT → 可能是另一台节点上的某个 CoreDNS 副本。这条链路有四个隐患:

  • 跨节点查询:DNS QPS 高的 Pod 可能每次都把请求发到别的节点。
  • conntrack 竞争:UDP 没有连接状态,每条 DNS 查询都会在 conntrack 表里留条目,默认 nf_conntrack_udp_timeout 是 30 秒才回收。查询量大时表被填满或发生 conntrack race,表现就是偶发解析超时。
  • 超时被放大:UDP 丢包时 DNS 客户端通常是「3 次重试 + 10 秒超时」,一次抖动就可能让应用等 30 秒。
  • CoreDNS 压力:所有节点的所有 Pod 都打同一组 CoreDNS 副本。

NodeLocal DNS Cache 的做法很直接:在每个节点上跑一个本地 DNS 缓存(DaemonSet + hostNetwork),监听链路本地地址 169.254.20.10(示例值,kubespray 默认是 169.254.25.10,只要不与集群冲突、且与 kubelet 的 --cluster-dns 一致即可),Pod 的查询落在本机、命中就直接返回;未命中时由本地代理以 TCP 转发给 CoreDNS 的 ClusterIP。它绕开了 iptables DNAT 与 conntrack,同时把重复查询挡在节点内,因此它不替代 CoreDNS:cluster.local 未命中仍然回源,CoreDNS 副本该保留的还要保留。

部署思路就是「DaemonSet + 本地监听地址 + kubelet 指向它」。注意 CoreDNS 的 Service 名在多数集群里叫 kube-dns(历史原因),但有的发行版直接叫 coredns,先用 kubectl -n kube-system get svc | grep -i dns 确认。下面按官方清单来做(kube-proxy 为 iptables 模式):

bash
curl -fsSL -o nodelocaldns.yaml \
  https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml

coredns=$(kubectl get svc kube-dns -n kube-system -o jsonpath='{.spec.clusterIP}')  # 若 Service 叫 coredns 就改这里
sed -i "s/__PILLAR__LOCAL__DNS__/169.254.20.10/g; s/__PILLAR__DNS__DOMAIN__/cluster.local/g; s/__PILLAR__DNS__SERVER__/$coredns/g" nodelocaldns.yaml
kubectl apply -f nodelocaldns.yaml
bash
kubectl -n kube-system get ds | grep -i dns    # 名字随安装方式不同,kubespray 是 nodelocaldns
kubectl -n kube-system get pods -l k8s-app=node-local-dns -o wide
kubectl -n kube-system get cm node-local-dns -o jsonpath='{.data.Corefile}' | grep -E "bind|forward"

dsDESIRED 等于节点数且全部 READY,Corefile 里能看到 bind 169.254.20.10forward . <CoreDNS ClusterIP> { force_tcp },就说明本地缓存已就位。

关于 kubelet 的 --cluster-dns:在 iptables 模式下 node-local-dns 会同时监听 CoreDNS ClusterIP 和本地地址,所以不改也能工作;在 IPVS 模式下必须把 --cluster-dns 改成 169.254.20.10。无论哪种模式,显式改成 `169.254.20.10` 都更干净:Pod 的 /etc/resolv.conf 直接指向本机,彻底不经过 DNAT 与 conntrack。改完要重建 Pod 才生效。

验证解析与缓存命中:

bash
kubectl run dnstest --image=nicolaka/netshoot:v0.16 --restart=Never --rm -it -- \
  dig @169.254.20.10 kubernetes.default.svc.cluster.local +short

pod=$(kubectl -n kube-system get pods -l k8s-app=node-local-dns -o jsonpath='{.items[0].metadata.name}')
kubectl -n kube-system port-forward pod/$pod 9253:9253
# 另开终端:反复查几次后再看命中数
curl -s localhost:9253/metrics | grep -E "coredns_cache_(hits|requests)_total"

dig 返回的是 kubernetes Service 的 ClusterIP;coredns_cache_hits_total 随查询增长、除以 coredns_cache_requests_total 就是命中率。

关于「看日志确认命中率」

官方清单的 Corefile 默认没有开 `log` 插件,所以 kubectl -n kube-system logs -l k8s-app=node-local-dns 里看不到逐条查询,别以为它没工作。要观察命中情况请用上面的 :9253/metrics;确实需要日志时,给 node-local-dns ConfigMap 的 Corefile 加上 log 再滚动重启 DaemonSet。

落地时的三个真实坑

上线后最常见的问题不是「没生效」,而是「生效了但解析行为变了」。三条来自实际运维记录:

  1. 同一个域名配了多个 IP,容器里 `ping` 永远只拿到第一个。hosts 插件在 Corefile 里写死一批机房内网域名时,CoreDNS 默认按顺序返回记录,而客户端只看第一条。必须显式打开 loadbalance 插件才会轮询:
text
    .:53 {
        hosts {
          100.64.4.1 example.internal
          100.64.4.2 example.internal
        }
        loadbalance
        bind 169.254.25.10
        forward . 223.5.5.5
        prometheus :9253
    }

缺了 loadbalance 那一行,多 IP 只会返回第一个;forward 是未命中时的回源地址(集群域名要回源到 CoreDNS 的 ClusterIP),bind 就是 kubelet 必须对齐的那个本地地址。

  1. 本地监听地址必须和 kubelet 的 `--cluster-dns` 一致。 NodeLocal DNS 监听的本地地址,官方清单示例是 169.254.20.10,kubespray 默认是 169.254.25.10;kubelet 指向的地址和它实际监听的不一致时,Pod 的 /etc/resolv.conf 就指向一个没人监听的地址,表现是「DNS 全部超时」。部署后用 kubectl -n kube-system get cm node-local-dns -o jsonpath='{.data.Corefile}' | grep bind 和节点上的 grep cluster-dns /var/lib/kubelet/config.yaml 对一遍。
  1. 集群域名和内网域名的解析路径要分开。 cluster.local 继续交给 CoreDNS,机房内网域名直接走内网 DNS(forwardhosts),否则一次内网 DNS 抖动会连带拖慢集群内部的服务发现。kubespray 里对应的开关是 nodelocaldns_external_zonesdns_etchosts

参考:reference/k8s-in-action/k8s/nodelocaldns/faq.md

NFD:把硬件特性变成节点标签

调度器只认标签和资源,但「这台机器支持 AVX512」「装了两块 H100」「网卡支持 SR-IOV」这些信息默认不在任何节点标签里。没有它们,你只能靠节点名或人工维护的标签挑机器,集群一扩缩容就失效。

Node Feature Discovery(NFD)在每个节点上探测 CPU 指令集、内核版本、PCI 设备、存储与网络特性,把结果写成 feature.node.kubernetes.io/ 前缀的节点标签。GPU Operator、SR-IOV Network Operator 这类组件都依赖它。

bash
helm install -n node-feature-discovery --create-namespace nfd \
  oci://registry.k8s.io/nfd/charts/node-feature-discovery --version 0.19.0

版本号按最新 release 替换;也可以加 Helm 仓库 https://kubernetes-sigs.github.io/node-feature-discovery/chartshelm install nfd nfd/node-feature-discovery。验证用 kubectl -n node-feature-discovery get ds,deploy(命名空间随安装方式不同,kubespray 里是 nfd)。

你会看到 feature.node.kubernetes.io/cpu-cpuid.AVX512F: "true"kernel-version.major: "5"pci-10de.present: "true"10de 是 NVIDIA 的 PCI 厂商 ID)这类标签。有了它们,挑机器就变成普通的 nodeSelector

yaml
apiVersion: v1
kind: Pod
metadata:
  name: feature-dependent-pod
spec:
  nodeSelector:
    feature.node.kubernetes.io/cpu-cpuid.AVX512F: "true"
  containers:
    - name: app
      image: nginx:1.27

NFD 与 Device Plugin 是互补的两层:NFD 负责描述节点有什么(标签),Device Plugin(如 NVIDIA GPU Operator、SR-IOV)负责把设备上报为可调度资源nvidia.com/gpu: 1)并真正挂进容器。典型组合是:NFD 打标签 → Operator 在这些节点上部署驱动与 Device Plugin → 你的 Pod 用 nodeSelector 挑节点、用 resources.limits 申请设备。

kube-node-tuning 与 Tuned:声明式下发内核参数

Pod 级 sysctl 解决不了节点级参数,而 SSH 上去改 /etc/sysctl.d/ 不可审计、不可回滚,新扩容的节点还得再改一次。社区有两种主流做法。

做法一:kube-node-tuning(ConfigMap + DaemonSet),本地 cookbook 用的就是它:

bash
helm repo add kube-node-tuning https://kubean-io.github.io/kube-node-tuning/
helm repo update
helm install -n kube-node-tuning kube-node-tuning kube-node-tuning/kube-node-tuning \
  --version 0.3.1 --create-namespace

# 参数在 ConfigMap 里,改完滚动重启 DaemonSet
kubectl -n kube-node-tuning edit cm/kube-node-tuning-config
kubectl -n kube-node-tuning rollout restart ds kube-node-tuning

它会把这些参数落到节点的 /etc/sysctl.d/99-kube-node-tuning.conf,SSH 上去用 cat /etc/sysctl.d/99-kube-node-tuning.confsysctl -a | grep -E "somaxconn|swappiness" 确认。

做法二:Tuned Profile(CRD 声明式),OpenShift 的 Node Tuning Operator 自带,Kubernetes 上可用 redhat-cop 的 tuned-operator。它以 CRD 描述 Profile 与「哪些节点用哪个 Profile」:

yaml
apiVersion: tuned.openshift.io/v1
kind: Tuned
metadata:
  name: kube101-worker
  namespace: openshift-cluster-node-tuning-operator
spec:
  profile:
    - name: kube101-worker-profile
      data: |
        [main]
        summary=kube101 worker node tuning
        [sysctl]
        net.core.somaxconn=4096
        net.ipv4.tcp_fin_timeout=15
        vm.max_map_count=262144
  recommend:
    - priority: 20
      profile: kube101-worker-profile
      match:
        - label: node-role.kubernetes.io/worker
bash
kubectl apply -f tuned-worker.yaml
kubectl get tuned -A

kubectl get tuned -A 里 Profile 状态正常后,再 SSH 到节点用 tuned-adm active 确认真正生效的是 kube101-worker-profile——CR 显示正常不等于内核参数已经改了。

对比项手工改 /etc/sysctl.d/kube-node-tuning / Tuned
是否可审计只有节点上的文件,改了什么没人知道配置在 ConfigMap / CR 里,可走 GitOps review
是否可回滚靠人记得改回什么改回旧版本再滚动重启即可
新节点扩容必须重新手工执行一遍DaemonSet / Operator 自动落到新节点
失败可见性只能逐台 SSH 检查kubectl get ds、Operator 状态与事件

生产用法:把标签和 Profile 串成一条链

单用 NFD 只是多了些标签,单用 Tuned 只是多了份参数;真正的价值在于串起来:NFD 探测硬件 → 自动打业务标签 → Tuned 按标签选 Profile → 新扩容的节点自动获得正确的内核参数,不用人工介入。

第一步,用 NodeFeatureRule 把「满足某组特性」的节点自动打上你自己的标签:

yaml
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeatureRule
metadata:
  name: kube101-instance-type
spec:
  rules:
    - name: kube101.gpu-node
      matchFeatures:
        - feature: pci.device
          matchExpressions: { vendor: { op: In, value: ["10de"] } }
      labels: { node.kube101.io/gpu: "true" }

第二步,让 Tuned 的 recommend.match 按这个标签选 Profile。priority 越大越优先,多条规则同时命中时高优先级的生效:

yaml
  recommend:
    - priority: 30
      profile: kube101-gpu-profile
      match: [{ label: node.kube101.io/gpu }]
    - priority: 10
      profile: kube101-worker-profile
      match: [{ label: node-role.kubernetes.io/worker }]

第三步,验收要落在节点上,而不是只看 CR 的状态:

bash
kubectl get nodes -l node.kube101.io/gpu --show-labels
# SSH 到节点:tuned-adm active 看生效的 Profile,sysctl 看参数实际值
tuned-adm active

三条生产经验:

  • Profile 按节点角色分,不要一套打全集群:控制面、CPU worker、GPU worker、存储节点关心的参数完全不同,混成一套等于谁都不满足。
  • 改动要落在声明式配置里(自己的 Profile 或 kube-node-tuning 的 ConfigMap,改完 rollout restart ds kube-node-tuning),再到节点上用 tuned-adm activesysctl -a | grep 复核。
  • NFD 是很多 Operator 的前置依赖:先 kubectl wait 等 NFD 的 Pod Ready,再装 GPU Operator、SR-IOV Operator。

参考:reference/k8s-in-action/base/nfd/README.mdreference/k8s-in-action/base/kube-node-tuning/README.md

其他必须知道的节点参数

kubelet 预留与驱逐阈值。 kubelet 默认按整机容量做调度计算,不管系统开销节点就会被 Pod 挤到 OOM。三个关键参数:

参数作用示例
--system-reserved给 OS 与系统守护进程预留cpu=1,memory=2Gi
--kube-reserved给 kubelet、容器运行时等组件预留cpu=200m,memory=512Mi
--eviction-hard硬驱逐阈值,无宽限期,直接杀 Podmemory.available<1Gi,nodefs.available<10%
--eviction-soft软驱逐阈值,配合 --eviction-soft-grace-periodmemory.available<1.5Gi / 1m30s

kubelet 的默认硬驱逐阈值是 memory.available<100Minodefs.available<10%imagefs.available<15%nodefs.inodesFree<5%。有个容易踩的坑:只要改动其中任意一项,其它项不再继承默认值(会被设成 0),除非把 MergeDefaultEvictionSettings 设为 true。驱逐顺序按 QoS:先 BestEffort,再超出 requests 的 Burstable,最后才轮到 Guaranteed——这也是探针与资源管理里强调「requests/limits 要写对」的原因。

THP 与 swap。 透明大页(THP)让内核自动合并大页,对多数 Web 服务无害,但 Redis、MongoDB 这类延迟敏感的应用常遇到 THP 引起的毛刺,习惯设成 nevermadvise(Tuned 里写 [vm] transparent_hugepages=never,或用内核参数 transparent_hugepage=never)。swap 默认是关闭的:Linux 节点启用 swap 时 kubelet 默认拒绝启动(fail-swap-on)。Kubernetes 后来支持了 memorySwap.swapBehaviorNoSwap 为默认、LimitedSwap 允许 Burstable Pod 用一部分 swap),但生产上普遍仍然关闭——swap 会让「内存够不够」变得不可预测,让 requests/limits、驱逐与延迟全部失真。

CPU 管理策略(一句话)。 想让延迟敏感的业务拿到独占核心,把 kubelet 设成 --cpu-manager-policy=static 并用 --reserved-cpus--kube-reserved 预留核心:只有 Guaranteed QoS 且 CPU requests 为整数的容器才会被分配独占核心,其余 Pod 继续共享剩余 CPU。

HugePages(大页)。 大页要在节点上预先分配(内核参数如 hugepagesz=1G hugepages=2),之后节点作为扩展资源上报(hugepages-1Gi: 2Gi);Pod 里 requests 必须等于 limits,挂载卷时用 medium: HugePages-1Gi。注意 THP 与大页不是一回事:前者是内核自动合并,后者是显式预留。

常见坑与排错

节点调优的问题往往表现为「偶发」「只在某几台机器上」,所以排查要先定位到节点,再定位到层。

现象原因怎么确认怎么办
集群内 DNS 偶发解析超时UDP DNS 撑满 conntrack 或发生 conntrack raceconntrack -Sinsert_failed,看 CoreDNS 延迟部署 NodeLocal DNS Cache,或调大 nf_conntrack_max
Pod 起不来,事件报 forbidden sysctl该参数不在安全集合且节点未开白名单kubectl describe podforbidden sysctl: ... not allowlisted节点 kubelet 加 --allowed-unsafe-sysctls 后重启
改了 sysctl 但值没变节点级参数写进了 Pod,或没重载容器内 sysctl -a 看不到它;节点上 cat /etc/sysctl.d/*.conf节点级参数走 Tuned / DaemonSet,并 sysctl --system
看不到 feature.node.* 标签NFD 未装、worker 未就绪、或标签被过滤kubectl -n node-feature-discovery get pods 与 worker 日志修好 NFD DaemonSet,检查 NodeFeatureRule 与白名单
Pod 频繁被驱逐,显示 Evicted节点内存/磁盘触发硬驱逐阈值kubectl describe nodeMemoryPressure,看 Evicted 事件调大 requests、设 --system-reserved、上调阈值
CPU 打满但 kubectl top 利用率不高容器被 CFS 限流(throttling)rate(container_cpu_cfs_throttled_periods_total[5m])container_cpu_cfs_periods_total 对比调大 CPU limit 或 requests,必要时用 static 策略
节点重启后调优「消失」手工 sysctl -w 没落盘节点上 cat /etc/sysctl.d/99-*.conf 无对应项改用声明式下发

三类最容易搞错的细节

  • *`hostNetwork` 会让所有 `net. sysctl 失效**:Pod 一旦共享主机网络命名空间,securityContext.sysctls 里的网络参数会被直接拒绝,事件提示 not allowed with host net enabled`。
  • unsafe 是逐节点的:只有部分节点加了白名单时,同一个 Pod 会在没开的节点上启动失败、在开了的节点上正常。建议配合 taint/toleration 把这类 Pod 固定到调优过的节点上。
  • 换 CPU 管理策略要 drain:直接把 --cpu-manager-policynone 改成 static 会遇到 could not restore state from checkpoint 报错,必须 drain 节点并删除 /var/lib/kubelet/cpu_manager_state

自测题

自测:为什么在容器里改 sysctl 有时会报 forbidden?(点击展开答案)

校验发生在 kubelet 创建容器时,而不是内核层面。安全集合(net.ipv4.tcp_syncookies 等)默认允许,因为它们在 network/ipc 等 namespace 里有独立副本,一个 Pod 改了不会影响同节点其它 Pod;而 net.core.somaxconnkernel.msg* 这类参数虽然也是 namespaced,但可能影响节点稳定性或其它 Pod,因此默认关闭,必须由管理员在每个节点的 kubelet 上用 --allowed-unsafe-sysctls 显式放行。

还有两个边界:只有 namespaced 的参数才能通过白名单放开,vm.swappiness 这类节点级参数写进 Pod 也不会生效;Pod 开了 hostNetwork 后所有 net.* 都会被拒绝。所以看到 forbidden 时,正确动作是先判断「这个参数是 Pod 级还是节点级」,再决定是申请白名单还是改成节点级下发。

自测:NodeLocal DNS 为什么能缓解 conntrack 竞争?(点击展开答案)

关键在于把 DNS 请求留在节点内部,并让本地代理用 TCP 访问 CoreDNS。默认架构下 Pod 发 UDP 到 CoreDNS 的 ClusterIP,kube-proxy 做 DNAT 转发到某台节点上的副本;UDP 没有连接状态,每条查询都会在 conntrack 表里留条目,要等 nf_conntrack_udp_timeout(默认 30 秒)才回收,查询量大时表被填满或插入失败,表现就是偶发解析超时。

启用 NodeLocal DNS 后,Pod 打到本机的 169.254.20.10,不经过 DNAT,也基本不产生新的 conntrack 条目;只有缓存未命中时本地代理才以 TCP 连到 CoreDNS,而 TCP 连接关闭后条目会立即释放,不会像 UDP 那样堆积。再加上每节点一份缓存,重复查询根本不出节点,CoreDNS 压力同步下降。

自测:为什么生产环境普遍要求关闭 swap?(点击展开答案)

因为 swap 会让「内存」这个资源变得不可预测。调度、QoS、驱逐、requests/limits 全都建立在「Pod 用到的物理内存」这一假设上:内存不够就触发驱逐,超限就 OOMKilled。一旦允许换页,节点可以在物理内存已满时继续「看起来很健康」地运行,实际延迟却因换页抖动大幅上升,kubelet 的 memory.available 与驱逐判断也随之失真。所以默认行为是:Linux 节点启用 swap 时 kubelet 直接拒绝启动(fail-swap-on);虽然 Kubernetes 后来支持了 memorySwap.swapBehavior(默认 NoSwap,可设 LimitedSwap),但绝大多数生产集群仍然关闭 swap,把内存不足暴露成明确的 OOM 与驱逐信号。

小结

  • 容器共享内核,所以调优前先分清参数是 Pod 级(namespaced) 还是 节点级;能 Pod 级解决的别动节点级。
  • securityContext.sysctls 只能设 namespaced 参数,安全集合默认放行;unsafe 需要逐节点用 --allowed-unsafe-sysctls 批准,否则 Pod 会以 forbidden sysctl 启动失败。
  • NodeLocal DNS Cache 用「每节点 DaemonSet + 169.254.20.10 + TCP 回源」绕开 DNAT 与 conntrack,缓解 DNS 偶发超时,但它不替代 CoreDNS。
  • NFD 把 CPU 指令集、PCI 设备等硬件特性写成 feature.node.kubernetes.io/ 标签,供 nodeSelector 与 Device Plugin 配合使用。
  • 节点级内核参数应当声明式下发(kube-node-tuning / Tuned),而不是 SSH 手工 sysctl -w;同时别忘 kubelet 的 --system-reserved / --kube-reserved / --eviction-hard

相关章节:节点怎么规划与部署见生产集群部署;调度规则见第 14 章 调度入门;requests/limits 与驱逐的关系见第 11 章 探针与资源管理;调优效果要靠可观测性验证,DNS 与网络链路排查看集群网络排障手册

练习

  1. 在集群里找出所有 Pending 的 Pod,看有没有一个是因为 forbidden sysctl 起不来的;如果有,判断它需要的参数是 Pod 级还是节点级。
  2. 给某个 Deployment 加上 net.ipv4.ip_unprivileged_port_start: "0",让它以非 root 用户监听 80 端口,并用 kubectl exec 确认参数生效。
  3. 用 NFD 标签做一次调度实验:找出带 feature.node.kubernetes.io/cpu-cpuid.AVX512F 的节点,写一个只调度到它的 Pod,再用 kubectl describe pod 看结果;顺手想想你所在环境里有哪些内核参数是「某个人 SSH 上去手工改过」的,把它们整理成一份 Tuned Profile 或 kube-node-tuning 的 ConfigMap 配置。

下一章我们看平台化的另一块拼图:把共享文件与对象存储接进集群。