Appearance
01. 节点与 Kubernetes
目标:三台机器全部 Ready,跨节点 Pod 能互通。
本页命令在 node1 执行,标注“三台”的要在每台执行。
为什么先做节点
数据平台的“玄学问题”大多能追到节点层:
- 时间不同步 → 证书校验失败、Kafka 消息顺序异常;
- swap 没关 → kubelet 拒绝启动;
- 内核模块没加载 → Pod 跨节点不通;
- 数据盘没挂 → 镜像/PVC 把系统盘写满;
- CPU 不支持 AVX2 → Doris BE 直接退出。
所以顺序是:节点体检 → 容器运行时 → K8s → CNI → 跨节点验证。
不用 k3s:本教程验证的是上游 kubeadm 的标准行为,k3s 会隐藏 control-plane、etcd、证书细节。
1. 检查机器
三台都执行。每条命令都是在确认一个前提,不满足就先处理,不要带病往下装。
bash
# 看主机名,后面 kubeadm 注册节点会用它
hostname
# 看发行版,必须是 Ubuntu 22.04;20.04/24.04 的包和内核行为不同
lsb_release -a
# 看 CPU 型号、核数,并确认有 avx2(Doris BE 的硬要求)
lscpu | grep -E 'Model name|CPU\(s\)|avx2'
# 看内存,低于 30G 不建议跑全栈
free -h
# 看块设备,找到系统盘和数据盘
lsblk
# 看磁盘余量,/data 还没挂时先看根分区
df -h预期:
lscpu输出里有avx2;- 内存 ≥ 30G;
- 有一块未挂载、容量较大的数据盘。
1.1 认盘
盘符在不同机器上可能相反,不要照抄 /dev/sdb。
bash
# 列出名字、大小、类型、挂载点,找“没有 MOUNTPOINT 且容量最大”的盘
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT找到后记下设备名,例如 /dev/vdb。后文用 <DATA_DISK> 代替。
2. 系统初始化
这些不是“装机习惯”,每条都对应 K8s 或数据组件的硬要求。
2.1 主机名与 hosts
bash
# 设置节点名;node2/node3 改成对应名字
hostnamectl set-hostname node1三台都写入 hosts,把变量替换成内网 IP。K8s 节点注册、组件互访都用这个名字,避免 IP 变化后配置全改。
bash
cat >>/etc/hosts <<EOF
$NODE1 node1
$NODE2 node2
$NODE3 node3
EOF验证解析:
bash
# 能解析到内网 IP,说明 hosts 生效
getent hosts node2
getent hosts node32.2 时间同步
bash
# 统一时区,日志和调度时间才对得上
timedatectl set-timezone Asia/Shanghai
# 安装 chrony 并立即启动
apt-get update
apt-get install -y chrony
systemctl enable --now chrony
# 看时间源是否同步;出现 ^* 表示已选中
chronyc sources -v时间不同步会导致 TLS 证书、Kafka 消息、Iceberg 快照时间出现连锁问题。
2.3 关闭 swap
bash
# 立即关闭 swap;kubelet 默认要求关闭
swapoff -a
# 注释 fstab 里的 swap 行,重启后不再启用
sed -ri '/\sswap\s/s/^#?/#/' /etc/fstab
# 确认 Swap 一行为 0
free -h2.4 内核模块与转发
bash
# overlay 给容器文件系统,br_netfilter 让桥接流量走 iptables
cat >/etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF
# 立即加载,不重启
modprobe overlay
modprobe br_netfilter
# 开启网桥的 iptables 处理和 IPv4 转发
cat >/etc/sysctl.d/k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1
net.ipv4.ip_forward=1
EOF
# 应用内核参数
sysctl --system验证:
bash
# 两个模块都应出现
lsmod | grep -E 'overlay|br_netfilter'
# 必须是 1
sysctl net.ipv4.ip_forward2.5 数据盘挂到 /data
bash
# 格式化数据盘;<DATA_DISK> 换成上一步认出的设备
mkfs.ext4 /dev/<DATA_DISK>
# 建挂载点
mkdir -p /data
# 挂载
mount /dev/<DATA_DISK> /data
# 写 fstab,重启自动挂载
echo '/dev/<DATA_DISK> /data ext4 defaults 0 2' >>/etc/fstab
# 确认 /data 有足够空间
df -h /data3. containerd
K8s 只负责“要什么容器”,真正拉镜像、解压、跑容器的是 CRI。
选 containerd 而不是 Docker:Docker 多一层 daemon,K8s 1.24 后已不再内置 dockershim。
3.1 安装
三台:
bash
# 安装 containerd
apt-get install -y containerd
# 建配置目录
mkdir -p /etc/containerd
# 生成默认配置;后面只改关键项,不手写整份
containerd config default >/etc/containerd/config.toml改两个关键配置:
bash
# kubelet 和 containerd 都用 systemd 管理 cgroup,避免双层 cgroup
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# 默认 pause 镜像在国内常拉不到,换成可达仓库
PAUSE_IMAGE="registry.aliyuncs.com/google_containers/pause:3.10.2"
sed -i \
"s#sandbox_image = .*#sandbox_image = \"$PAUSE_IMAGE\"#" \
/etc/containerd/config.toml为什么改 pause:Pod 启动前要先起 sandbox,pause 镜像拉不到就会卡在
SandboxChanged。
为什么用 SystemdCgroup:资源统计、驱逐、OOM 行为才和 kubelet 一致。
把数据目录移到 /data:
bash
# containerd 的镜像层默认在 /var/lib/containerd,系统盘会被写满
mkdir -p /data/containerd
# 改 root 目录到数据盘
sed -i 's#^root = .*#root = "/data/containerd"#' /etc/containerd/config.toml
# 重启生效
systemctl restart containerd
systemctl enable containerd验证:
bash
# active (running) 才算成功
systemctl status containerd --no-pager
# 能输出版本
ctr version3.2 镜像加速
containerd 2.x 用 certs.d,不要改 Docker 的 daemon.json。
bash
# 建 docker.io 的 hosts 配置目录
mkdir -p /etc/containerd/certs.d/docker.iotoml
# /etc/containerd/certs.d/docker.io/hosts.toml
# server 写实际镜像站,不要写 registry-1.docker.io
server = "https://<你的DockerHub镜像站>"
# 备用镜像站;主站失败时按顺序尝试
[host."https://<备用镜像站>"]
capabilities = ["pull", "resolve"]bash
# 重启 containerd 读取 hosts.toml
systemctl restart containerd验证拉取:
bash
# 能拉 busybox 说明镜像配置生效
ctr -n k8s.io images pull docker.io/library/busybox:latest如果镜像站不可用,直接跳过加速;后面所有镜像都改成可达 registry 的完整地址。
4. kubeadm / kubelet / kubectl
kubeadm 引导控制面,kubelet 管节点容器,kubectl 是操作入口。
三者版本必须一致;用apt-mark hold锁版本,避免 upgrade 把集群升到未验证版本。
三台:
bash
# 导入阿里云 K8s 仓库的 GPG key
curl -fsSL https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.37/deb/Release.key \
| gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
# 写入 v1.37 的 apt 源
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.37/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验证:
bash
# 版本必须是 v1.37.1
kubeadm version -o short
# 客户端可用
kubectl version --client5. 初始化控制面
这一步决定 API 地址、Pod 网段和证书 SAN。
--control-plane-endpoint是以后加 control-plane 的稳定入口;--pod-network-cidr必须和 Calico 的 IPPool 一致。
5.1 预拉镜像
bash
# 先用国内仓库把控制面镜像拉下来,避免 init 时卡在 registry.k8s.io
kubeadm config images pull \
--image-repository=registry.aliyuncs.com/google_containers \
--kubernetes-version=v1.37.15.2 kubeadm init
bash
kubeadm init \
--apiserver-advertise-address=$NODE1 \
--control-plane-endpoint=$NODE1 \
--pod-network-cidr=10.244.0.0/16 \
--kubernetes-version=v1.37.1 \
--image-repository=registry.aliyuncs.com/google_containers \
--cri-socket unix:///run/containerd/containerd.sock参数含义:
| 参数 | 作用 |
|---|---|
--apiserver-advertise-address | 控制面对外公布的 IP,必须是节点内网 IP |
--control-plane-endpoint | 集群 API 的稳定入口,以后加 control-plane 也用它 |
--pod-network-cidr | Pod 网段,必须和 Calico IPPool 一致 |
--kubernetes-version | 锁死版本,避免和 kubelet 不一致 |
--image-repository | 控制面镜像仓库,国内必须换 |
--cri-socket | 使用 containerd 的 CRI socket |
成功后按输出执行:
bash
# 建 kubeconfig 目录
mkdir -p $HOME/.kube
# 复制 admin.conf
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
# 改属主,普通用户才能读
chown $(id -u):$(id -g) $HOME/.kube/config验证:
bash
# 此时 node1 可能 NotReady,正常,等 CNI
kubectl get nodes5.3 安装 Calico
bash
# 部署 Calico v3.30.3;网络插件负责 Pod 跨节点通信
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.30.3/manifests/calico.yaml如果拉不到,先下载到本地再 kubectl apply -f calico.yaml。
验证:
bash
# calico-node 每个节点一个,必须 Running
kubectl -n kube-system get pods | grep calico
# node1 应变成 Ready
kubectl get nodes5.4 node2/node3 加入
bash
# 在 node1 生成 join 命令
kubeadm token create --print-join-command输出类似:
text
kubeadm join 10.0.0.1:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx在 node2/node3 执行,并加 CRI socket:
bash
# 把 <token> 和 <hash> 换成上一条命令的输出
kubeadm join $NODE1:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash> \
--cri-socket unix:///run/containerd/containerd.sock验证:
bash
# 三台都应 Ready,角色为 1 control-plane + 2 worker
kubectl get nodes -o wide6. 跨节点网络验证
节点 Ready 不代表网络通。Calico IPPool 配错、安全组拦了 IPIP/VXLAN,都会表现成“Pod Running 但跨节点 ping 不通”。
bash
# 在三个节点各起一个 busybox,指定 nodeName 确保落点
for n in node1 node2 node3; do
kubectl run ping-$n --image=busybox:1.36 --restart=Never \
--overrides="{\"spec\":{\"nodeName\":\"$n\"}}" -- sleep 3600
done
# 看 Pod IP 和所在节点
kubectl get pods -o wide拿到 IP 后互 ping:
bash
# node1 的 Pod 访问 node2 的 Pod IP
kubectl exec ping-node1 -- ping -c 3 <node2-pod-ip>
# node1 的 Pod 访问 node3 的 Pod IP
kubectl exec ping-node1 -- ping -c 3 <node3-pod-ip>预期 0% packet loss。失败先查 Calico 和 --pod-network-cidr。
清理:
bash
# 删除测试 Pod
kubectl delete pod ping-node1 ping-node2 ping-node37. 本阶段验收
bash
# 三节点 Ready
kubectl get nodes
# 系统组件全部 Running
kubectl -n kube-system get pods
# 不应该有 Pending/CrashLoopBackOff
kubectl get pods -A | grep -v Running要求:
- 三个节点 Ready;
- kube-system 全部 Running;
- 跨节点 Pod 能 ping 通。
下一步:存储与元数据。