课程目录(第 28 章 / 共 33 章)
节点与内核调优:让 Pod 跑得更稳
容器共享同一个内核,节点上的 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_syncookies、ip_unprivileged_port_start | 是(netns) | Pod 的 securityContext.sysctls,属安全集合 |
net.core.somaxconn | 是(netns),但属 unsafe | 先给 kubelet 开白名单,再写进 Pod |
vm.swappiness、vm.max_map_count | 否,节点级 | /etc/sysctl.d/、Tuned 或特权 DaemonSet |
kernel.pid_max、fs.file-max、透明大页、swap | 否,节点级 | 节点初始化脚本、GRUB 参数或 Tuned |
调优的三层结构,以及每层的生效范围:
一台节点(所有 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_forced、net.ipv4.ip_local_port_range、net.ipv4.tcp_syncookies、net.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_time 与 tcp_keepalive_intvl、tcp_keepalive_probes、tcp_fin_timeout(1.29+)、net.ipv4.tcp_rmem 与 tcp_wmem(1.32+)。
两个例外要注意:Pod 用了 hostNetwork: true 时所有 net.* 都不允许;net.ipv4.tcp_syncookies 在 4.5 及更早的内核上不是 namespaced 的。
不在安全集合里的 sysctl 默认被拒绝,管理员可以在逐节点的基础上用 kubelet 参数放开:
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 端口。
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"]第二步,应用并进容器读一遍参数:
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 = 1 和 net.ipv4.ip_unprivileged_port_start = 0。注意 sysctl -a 在容器里只能看到本 namespace 的参数,看不到 vm.* 这类节点级的值——这正好是「Pod 级」的直观证明。
第三步,亲手制造一次拒绝:
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 模式):
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.yamlkubectl -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"ds 的 DESIRED 等于节点数且全部 READY,Corefile 里能看到 bind 169.254.20.10 与 forward . <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 才生效。
验证解析与缓存命中:
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。
落地时的三个真实坑
上线后最常见的问题不是「没生效」,而是「生效了但解析行为变了」。三条来自实际运维记录:
- 同一个域名配了多个 IP,容器里 `ping` 永远只拿到第一个。 用
hosts插件在 Corefile 里写死一批机房内网域名时,CoreDNS 默认按顺序返回记录,而客户端只看第一条。必须显式打开loadbalance插件才会轮询:
.: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 必须对齐的那个本地地址。
- 本地监听地址必须和 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对一遍。
- 集群域名和内网域名的解析路径要分开。
cluster.local继续交给 CoreDNS,机房内网域名直接走内网 DNS(forward或hosts),否则一次内网 DNS 抖动会连带拖慢集群内部的服务发现。kubespray 里对应的开关是nodelocaldns_external_zones与dns_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 这类组件都依赖它。
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/charts 再 helm 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:
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.27NFD 与 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 用的就是它:
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.conf 与 sysctl -a | grep -E "somaxconn|swappiness" 确认。
做法二:Tuned Profile(CRD 声明式),OpenShift 的 Node Tuning Operator 自带,Kubernetes 上可用 redhat-cop 的 tuned-operator。它以 CRD 描述 Profile 与「哪些节点用哪个 Profile」:
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/workerkubectl apply -f tuned-worker.yaml
kubectl get tuned -Akubectl 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 把「满足某组特性」的节点自动打上你自己的标签:
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 越大越优先,多条规则同时命中时高优先级的生效:
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 的状态:
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 active和sysctl -a | grep复核。 - NFD 是很多 Operator 的前置依赖:先
kubectl wait等 NFD 的 PodReady,再装 GPU Operator、SR-IOV Operator。
参考:reference/k8s-in-action/base/nfd/README.md、reference/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 | 硬驱逐阈值,无宽限期,直接杀 Pod | memory.available<1Gi,nodefs.available<10% |
--eviction-soft | 软驱逐阈值,配合 --eviction-soft-grace-period | memory.available<1.5Gi / 1m30s |
kubelet 的默认硬驱逐阈值是 memory.available<100Mi、nodefs.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 引起的毛刺,习惯设成 never 或 madvise(Tuned 里写 [vm] transparent_hugepages=never,或用内核参数 transparent_hugepage=never)。swap 默认是关闭的:Linux 节点启用 swap 时 kubelet 默认拒绝启动(fail-swap-on)。Kubernetes 后来支持了 memorySwap.swapBehavior(NoSwap 为默认、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 race | conntrack -S 看 insert_failed,看 CoreDNS 延迟 | 部署 NodeLocal DNS Cache,或调大 nf_conntrack_max |
| Pod 起不来,事件报 forbidden sysctl | 该参数不在安全集合且节点未开白名单 | kubectl describe pod 看 forbidden 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 node 看 MemoryPressure,看 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-policy从none改成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.somaxconn、kernel.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 与网络链路排查看集群网络与排障手册。
练习
- 在集群里找出所有
Pending的 Pod,看有没有一个是因为forbidden sysctl起不来的;如果有,判断它需要的参数是 Pod 级还是节点级。 - 给某个 Deployment 加上
net.ipv4.ip_unprivileged_port_start: "0",让它以非 root 用户监听 80 端口,并用kubectl exec确认参数生效。 - 用 NFD 标签做一次调度实验:找出带
feature.node.kubernetes.io/cpu-cpuid.AVX512F的节点,写一个只调度到它的 Pod,再用kubectl describe pod看结果;顺手想想你所在环境里有哪些内核参数是「某个人 SSH 上去手工改过」的,把它们整理成一份 Tuned Profile 或 kube-node-tuning 的 ConfigMap 配置。
下一章我们看平台化的另一块拼图:把共享文件与对象存储接进集群。