Repository Reading Site
系统准备 — 每一步都有原因
在安装 K8s 之前,操作系统需要满足一些前置条件。这不是"照着做",每一步都有明确的技术原因。 | 角色 | IP | 主机名 | 位置 | 配置 | |------|-----|--------|------|------| | Master | 107.148.176.193 | us480851516617a | LA | 16C/16G/50G |
系统准备 — 每一步都有原因
在安装 K8s 之前,操作系统需要满足一些前置条件。这不是"照着做",每一步都有明确的技术原因。
我们的 5 台机器
| 角色 | IP | 主机名 | 位置 | 配置 |
|---|---|---|---|---|
| Master | 107.148.176.193 | us480851516617a | LA | 16C/16G/50G |
| Worker-1 | 107.148.164.118 | us590068728056 | LA | 16C/16G/50G |
| Worker-2 | 154.9.27.60 | cp-3 | LA | 16C/16G/50G |
| Worker-3 | 38.76.221.17 | hk652699382121 | HK | 4C/8G/80G |
| Worker-4 | 154.219.104.66 | wk-1 | HK | 16C/16G/150G |
以下操作需要在所有 5 台机器上执行。
1. 关闭 Swap
swapoff -a
sed -i '/swap/d' /etc/fstab
这两条命令做了什么?
swapoff -a:立即关闭所有 swap 分区(-a= all)sed -i '/swap/d' /etc/fstab:从/etc/fstab(开机自动挂载配置)中删除 swap 行,防止重启后 swap 恢复
为什么 K8s 要求关闭 Swap?
核心原因:kubelet 的内存管理假设没有 swap。
Linux 的 swap 机制是:当物理内存不够时,把不常用的内存页写到磁盘(swap 分区),腾出内存给其他进程。这对普通服务器是有用的——宁可慢一点也比 OOM(Out Of Memory)杀进程好。
但在 K8s 中:
-
资源调度依赖精确的内存计量。 你在 Pod 中声明
requests.memory: 256Mi,Scheduler 会据此判断节点是否有足够内存。如果有 swap,一个 Pod 可能实际占了 2GB 内存(大部分在 swap 里),但 Scheduler 不知道,还会往这个节点塞更多 Pod。 -
性能不可预测。 swap 的 I/O 速度比内存慢 1000 倍以上。一个 Pod 的延迟可能突然从 1ms 飙到 100ms,因为它的内存页被 swap 到了磁盘。这对容器化应用是不可接受的。
-
OOM 反而是期望行为。 K8s 的设计哲学是:如果 Pod 超出内存限制(
limits.memory),直接 OOMKill 然后重启,而不是用 swap 苟活。快速失败 + 自动恢复比"慢慢死"要好。
面试加分点: K8s 1.22 开始实验性支持 swap(需要开启 feature gate
NodeSwap),但默认仍然要求关闭。面试时可以提到这个进展,说明你关注社区动态。
验证结果
# 应该无输出,表示没有 swap
swapon --show
# 或者用 free 查看,Swap 行应该全是 0
free -h
我们 5 台机器的实际验证结果:所有节点 swap 均为 0,确认关闭成功。
2. 加载内核模块
cat <<EOF > /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter
这些命令做了什么?
cat <<EOF > /etc/modules-load.d/k8s.conf:创建配置文件,让系统启动时自动加载这两个内核模块modprobe overlay:立即加载 overlay 模块modprobe br_netfilter:立即加载 br_netfilter 模块
overlay 模块是什么?为什么需要?
overlay 是容器使用的文件系统驱动。
容器镜像由多个"层"(layer)组成。比如一个 Node.js 应用的镜像:
Layer 3: COPY app.js /app/ (你的代码,很小)
Layer 2: RUN npm install (依赖包,较大)
Layer 1: FROM node:18-slim (基础OS+Node,很大)
OverlayFS 让这些只读层可以"叠加"起来,再加一个可写层在最上面:
┌────────────────────┐
│ 可写层(容器运行时) │ ← 容器写的文件放这里
├────────────────────┤
│ Layer 3: app.js │ ← 只读
├────────────────────┤
│ Layer 2: node_mods │ ← 只读
├────────────────────┤
│ Layer 1: node:18 │ ← 只读
└────────────────────┘
好处:
- 多个容器可以共享相同的只读层,节省磁盘和内存
- 只有修改的文件才会被"复制上来"(Copy-on-Write),效率很高
- containerd 默认使用 overlayfs 作为存储驱动
如果不加载 overlay 模块,containerd 就无法创建容器的文件系统。
br_netfilter 模块是什么?为什么需要?
br_netfilter 让 Linux 网桥上的流量经过 iptables 规则。
这需要理解 K8s 的网络模型:
- 每个 Pod 有自己的 IP 地址
- 同一节点上的 Pod 通过 Linux 网桥(bridge)通信
- 跨节点的 Pod 通过 overlay 网络(如 Calico/Flannel)通信
- Service 的流量转发靠 iptables 规则
问题来了:默认情况下,经过 Linux 网桥的流量不会被 iptables 处理。也就是说,同一节点上的 Pod 之间的流量,不会经过 kube-proxy 设置的 Service iptables 规则。
br_netfilter 模块的作用就是:让桥接流量也走 netfilter/iptables,确保 Service 的网络规则在节点内部也能生效。
面试深度题: "为什么 Pod A 访问 Service ClusterIP 时,如果 Pod B(后端)恰好在同一个节点上,流量也能正确路由?" 答案就是 br_netfilter。
验证结果
# 确认模块已加载
lsmod | grep br_netfilter
lsmod | grep overlay
我们 5 台机器的实际验证结果:所有节点 br_netfilter 和 overlay 模块均已加载。
3. 配置内核网络参数
cat <<EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system
这些参数分别是什么意思?
net.bridge.bridge-nf-call-iptables = 1
"桥接的 IPv4 流量要经过 iptables 规则"。这是 br_netfilter 模块的配套开关——加载了模块还不够,还需要显式开启这个行为。
不设置的后果: 同节点 Pod 之间访问 Service ClusterIP 可能不通,因为 iptables 的 DNAT 规则没有被应用。
net.bridge.bridge-nf-call-ip6tables = 1
同上,但针对 IPv6。即使你目前不用 IPv6,也建议开启——未来万一有 IPv6 的 Pod,不会因为这个被坑。
net.ipv4.ip_forward = 1
"允许本机转发 IP 数据包"。
默认情况下,Linux 收到一个目的地不是自己的数据包,会直接丢弃。但在 K8s 中,节点需要充当 Pod 的"路由器":
Pod A (10.244.1.5) → Node 1 → [IP forward] → Node 2 → Pod B (10.244.2.3)
Node 1 收到 Pod A 发给 10.244.2.3 的包,这个 IP 不是 Node 1 的,如果不开启 ip_forward,包就被丢了。
不设置的后果: 跨节点的 Pod 通信完全不通。
sysctl --system
重新加载所有 /etc/sysctl.d/ 下的配置文件,让设置立即生效(不需要重启)。
验证结果
sysctl net.bridge.bridge-nf-call-iptables
sysctl net.bridge.bridge-nf-call-ip6tables
sysctl net.ipv4.ip_forward
我们 5 台机器的实际验证结果:所有节点 ip_forward=1,内核网络参数均已正确设置。
4. 安装容器运行时(containerd)
# 安装 containerd
apt-get update
apt-get install -y containerd
# 生成默认配置
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
# 修改关键配置:使用 systemd cgroup 驱动
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# 重启并设置开机启动
systemctl restart containerd
systemctl enable containerd
为什么选 containerd?
参见 01-k8s-architecture.md 中的解释。简单回顾:
- K8s 1.24+ 不再支持 Docker 作为运行时
- containerd 是 Docker 的核心组件之一,被独立出来
- 更轻量、更直接、没有多余的中间层
SystemdCgroup = true 是什么?为什么必须设置?
这是一个非常容易踩的坑,也是面试常问的点。
背景知识 — cgroup(Control Groups):
cgroup 是 Linux 内核的资源隔离机制。它限制和追踪进程的 CPU、内存、I/O 使用。容器的资源限制(limits.cpu: 500m)底层就是靠 cgroup 实现的。
cgroup 有两种驱动:
| 驱动 | 说明 |
|---|---|
| cgroupfs | 直接操作 cgroup 文件系统(/sys/fs/cgroup/) |
| systemd | 通过 systemd 管理 cgroup |
问题:kubelet 和 containerd 必须使用同一种 cgroup 驱动。
现代 Ubuntu(包括我们用的 24.04)使用 systemd 作为 init 系统,systemd 默认管理 cgroup。如果 containerd 用 cgroupfs 驱动,而 kubelet 用 systemd 驱动,两者会打架——各自创建不同的 cgroup 层级,导致资源统计混乱,严重时节点会变成 NotReady。
所以:两边都用 systemd 驱动,设置 SystemdCgroup = true。
面试回答模板: "containerd 和 kubelet 必须使用相同的 cgroup 驱动。在使用 systemd 的发行版上(绝大多数现代 Linux),两者都应设为 systemd 驱动。如果不一致,会导致资源管理冲突,节点不稳定。"
验证结果
# 确认 containerd 运行正常
systemctl is-active containerd
# 确认 SystemdCgroup 设置
grep SystemdCgroup /etc/containerd/config.toml
我们 5 台机器的实际验证结果:
| 节点 | containerd | SystemdCgroup |
|---|---|---|
| 107.148.176.193 (Master) | active, v2.2.1 | true |
| 107.148.164.118 (W1) | active, v2.2.1 | true |
| 154.9.27.60 (W2) | active, v2.2.1 | true |
| 38.76.221.17 (W3) | active, v2.2.1 | true |
| 154.219.104.66 (W4) | active, v1.7.24 | true |
注意:W4 的 containerd 版本较旧(1.7.24),这是因为该机器之前安装过。不影响 K8s 1.30 的使用。
5. 安装 kubeadm、kubelet、kubectl
# 安装依赖
apt-get install -y apt-transport-https ca-certificates curl gpg
# 添加 Kubernetes 官方 GPG 密钥
mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key \
| gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
# 添加 Kubernetes apt 仓库
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' \
> /etc/apt/sources.list.d/kubernetes.list
# 安装
apt-get update
apt-get install -y kubelet kubeadm kubectl
# 锁定版本,防止 apt upgrade 意外升级
apt-mark hold kubelet kubeadm kubectl
这三个组件分别是什么?
| 组件 | 角色 | 在哪运行 | 类比 |
|---|---|---|---|
| kubeadm | 集群安装工具 | 安装时用,装完基本不用了 | "装修队"——房子装好就走了 |
| kubelet | 节点代理 | 每个节点,作为 systemd 服务常驻运行 | "物业管家"——一直在,管理本节点的容器 |
| kubectl | 命令行客户端 | 你的操作终端(可以在任何机器上) | "遥控器"——远程操控集群 |
为什么用 apt-mark hold?
K8s 集群升级是一个严格的流程(先升 Master,再逐个升 Worker),不能让 apt upgrade 随机把某个节点的 kubelet 升到新版本。版本不一致会导致 API 不兼容。
K8s 版本偏差策略:
- kubelet 可以比 API Server 低最多 2 个次版本(如 API Server 1.30,kubelet 可以是 1.28-1.30)
- kubelet 不能高于 API Server
- kubectl 可以比 API Server 高或低 1 个次版本
我们选择 v1.30 的原因: 这是一个稳定的版本,后续可以练习升级到 v1.31。
为什么用 pkgs.k8s.io 而不是 apt.kubernetes.io?
K8s 社区从 2023 年开始将包仓库从 Google 托管的 apt.kubernetes.io 迁移到社区托管的 pkgs.k8s.io。旧仓库已停止更新新版本。
验证结果
kubeadm version -o short
kubelet --version
kubectl version --client -o short
我们 5 台机器的实际验证结果:所有节点均为 kubeadm v1.30.14。
6. VPN 安全检查
在我们的两台 VPN 节点上,确认以上操作没有影响 VPN 服务:
# 检查 Xray 进程
systemctl is-active web-gateway
# 检查端口
ss -tlnp | grep -E ':443|:40000'
实际验证结果:
| VPN 节点 | web-gateway | 443 端口 | 40000 端口 |
|---|---|---|---|
| 107.148.176.193 | active | 正常监听 | 正常监听 |
| 107.148.164.118 | active | 正常监听 | 正常监听 |
OS 准备工作全部完成,VPN 未受影响。
总结:为什么这些前置条件缺一不可
| 操作 | 不做的后果 |
|---|---|
| 关闭 swap | kubelet 拒绝启动 / 内存调度不准确 |
| 加载 overlay 模块 | containerd 无法创建容器文件系统 |
| 加载 br_netfilter | 同节点 Pod 之间的 Service 访问不通 |
| ip_forward=1 | 跨节点 Pod 通信不通 |
| bridge-nf-call-iptables=1 | 桥接流量跳过 iptables,Service 规则失效 |
| containerd SystemdCgroup | cgroup 驱动冲突,节点不稳定 |
| apt-mark hold | 意外升级导致版本不兼容,集群崩溃 |
接下来
操作系统准备就绪,下一步是初始化 K8s 集群。
→ 03-container-runtime.md (已合并到本文档第 4 节) → 04-cluster-init.md