Skip to content

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 node3

2.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 -h

2.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_forward

2.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 /data

3. 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 version

3.2 镜像加速 ​

containerd 2.x 用 certs.d,不要改 Docker 的 daemon.json。

bash
# 建 docker.io 的 hosts 配置目录
mkdir -p /etc/containerd/certs.d/docker.io
toml
# /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 --client

5. 初始化控制面 ​

这一步决定 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.1

5.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-cidrPod 网段,必须和 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 nodes

5.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 nodes

5.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 wide

6. 跨节点网络验证 ​

节点 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-node3

7. 本阶段验收 ​

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 通。

下一步:存储与元数据。