一、从 Docker Compose 到 Kubernetes:为什么需要编排
在接触 K8s 之前,很多人已经用过 Docker Compose 来管理多容器应用。Compose 用一个 docker-compose.yml 就能定义服务、网络、数据卷,一条 up -d 拉起整套环境------这在单机开发场景下非常方便。
services:
web1:
image: nginx:1.23
networks:
- mynet1
volumes:
- "webdata1:/usr/share/nginx/html"
web2:
image: nginx:1.23
networks:
- mynet1
volumes:
- "webdata2:/usr/share/nginx/html"
haproxy:
image: haproxy:2.3
ports:
- "80:80"
volumes:
- "/conf/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg"
但 Compose 有明显的天花板:
| 维度 | Docker Compose | Kubernetes |
|---|---|---|
| 规模 | 单机多容器 | 跨节点集群 |
| 自愈能力 | 无自动重启策略 | ReplicaSet/Deployment 自动补副本 |
| 负载均衡 | 手动配 haproxy | Service 原生支持 |
| 滚动更新 | 无 | 声明式,零停机 |
| 资源调度 | 无 | Scheduler 按资源/亲和性调度 |
我的理解 :Compose 是"写死在哪台机器上跑什么",而 K8s 是"声明我要什么状态,系统自己决定怎么跑"。这就是命令式 vs 声明式的本质区别。
二、Kubernetes 集群架构与核心组件原理
2.1 整体架构一览

图为k8s集群集体架构
一个完整的 K8s 集群由两大部分组成:
2.2 控制平面(Control Plane)
控制平面是集群的"大脑",负责全局决策和事件响应。
| 组件 | 职责 | 我的理解 |
|---|---|---|
| kube-apiserver | 暴露 HTTP API,是唯一入口 | 所有组件都通过它通信;做鉴权、准入校验 |
| etcd | 高可用键值数据库 | 存储集群全部状态(Pod、Service、ConfigMap...);是唯一的"真相源" |
| kube-scheduler | 为未调度的 Pod 选择运行节点 | 综合考虑 CPU/内存/亲和性/污点等 |
| kube-controller-manager | 运行各种控制器循环 | 维持期望态:副本数不够就创建、Node 宕机就迁移 |
关键点 :控制器的工作模式是控制循环(Control Loop)------不断监听 apiserver 的实际态,与期望态比较,有偏差就修正。这是 K8s 自愈能力的根源。
2.3 节点组件(Node Components)
每个工作节点运行的组件:
| 组件 | 职责 |
|---|---|
| kubelet | 在本节点启动 Pod/容器,向 apiserver 报告状态 |
| kube-proxy | 维护 iptables/ipvs 规则,实现 Service 的负载均衡 |
| 容器运行时 | 真正运行容器的引擎(Docker/containerd/CRI-O) |
2.4 为什么需要 cri-dockerd?
这是一个非常重要的历史转折点:
K8s v1.24 之前,kubelet 内置了 dockershim 直接调用 Docker Engine。但从 1.24 起,K8s 要求所有容器运行时必须实现 CRI(Container Runtime Interface) 标准。Docker Engine 本身不实现 CRI,所以需要中间层适配器 ------ cri-dockerd。
kubelet → (CRI 协议) → cri-dockerd.sock → cri-dockerd → Docker Engine
这就是我们在部署时必须安装 cri-dockerd 的原因。它本质上是把被移除的 dockershim 以独立进程形式复活了。
三、主流集群部署方式对比

图 2:kubeadm 五步部署流程及每步的核心原理
| 方式 | 适用场景 | 复杂度 | 原理说明 |
|---|---|---|---|
| kubeadm | 生产环境首选 | ⭐⭐⭐ | 官方推荐工具,用静态 Pod 启动控制面,标准化程度最高 |
| 二进制手动部署 | 深度学习原理 | ⭐⭐⭐⭐⭐ | 手动生成证书、配置 systemd unit,理解最深但最繁琐 |
| kubespray(Ansible) | 批量生产部署 | ⭐⭐⭐⭐ | 基于 Ansible playbook,适合一次部署几十个节点 |
| 云托管(EKS/GKE/ACK) | 企业上云 | ⭐ | 托管控制面,只管 Worker 节点 |
| minikube / kind | 本地学习测试 | ⭐ | 单机模拟集群,快速验证 |
我的选择 :学习和中小型生产环境,kubeadm 是最佳平衡点------既不是黑盒(能看到每一步),又不会像二进制部署那样让人崩溃。下面我们以 kubeadm 为主线展开实战。
四、实战:基于 kubeadm 的集群部署全流程
4.1 环境规划
本次实验使用以下拓扑(来自实际操作记录):
| 主机名 | IP 地址 | 配置 | 角色 |
|---|---|---|---|
| harbor.timinglee.org | 172.25.254.254 | 1C/1G | Harbor 私有镜像仓库 |
| master | 172.25.254.100 | 4C/>4G | 控制平面 |
| node1 | 172.25.254.10 | 2C/2G | 工作节点 |
| node2 | 172.25.254.20 | 2C/2G | 工作节点 |
我的建议:Master 至少 4 核 4G 内存,否则 etcd + apiserver 会 OOM。生产环境建议 Master 8C/16G+ 且高可用(3 个 master)。
4.2 第一步:所有主机前置准备
关闭 Swap(必须!)
systemctl disable --now swap.target
systemctl mask swap.target
sed '/swap/s/^/#/g' -i /etc/fstab
原理 :Kubelet 默认检测到 swap 就拒绝启动。因为 K8s 的 QoS 和资源保证机制依赖 cgroup 对内存的精确控制,swap 会破坏这种保证。官方文档明确要求禁用或配置 failSwapOn: false。
安装 Docker 并配置私有仓库访问
# 安装 docker-ce(所有节点)
dnf install docker-ce -y
# 内核参数(桥接流量走 iptables)
cat > /etc/sysctl.d/docker.conf <<EOF
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system
# 分发 Harbor 证书到所有节点
mkdir -p /etc/docker/certs.d/reg.timinglee.org/
scp harbor:/data/certs/timinglee.org.crt \
/etc/docker/certs.d/reg.timinglee.org/ca.crt
# 配置镜像加速器指向 Harbor
cat > /etc/docker/daemon.json <<EOF
{
"registry-mirrors":["https://reg.timinglee.org"]
}
EOF
systemctl restart docker && systemctl enable docker
为什么要私有仓库? 国内拉取 registry.k8s.io 或 gcr.io 经常超时。Harbor 作为本地缓存代理,不仅加速 K8s 组件镜像下载,也是企业存放业务镜像的标准方案。
配置主机名解析 & K8s yum 源
# 所有节点的 /etc/hosts
172.25.254.100 master
172.25.254.10 node1
172.25.254.20 node2
172.25.254.200 reg.timinglee.org
# K8s 安装源(阿里云镜像)
cat > /etc/yum.repos.d/kubernetes.repo <<EOF
[kubernetes]
name = kubernetes
baseurl = https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.35/rpm/
gpgcheck = 0
EOF
4.3 第二步:安装 cri-dockerd(所有节点)
这是 K8s 1.24+ 使用 Docker 运行时的关键步骤:
# 方式 A:RPM 包安装(版本较旧)
rpm -ivh cri-dockerd-0.3.14-3.el8.x86_64.rpm libcgroup-0.41-19.el8.x86_64.rpm
# 方式 B:二进制包安装(推荐,版本更新)
wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.4.4/cri-dockerd-0.4.4.amd64.tgz
tar zxf cri-dockerd-0.4.4.amd64.tgz
install -o root -g root -m 0755 cri-dockerd/cri-dockerd /usr/local/bin/cri-dockerd
cp cri-dockerd/cri-docker.service /lib/systemd/system/
cp cri-dockerd/cri-docker.socket /lib/systemd/system/
chmod +x /lib/systemd/system/cri-docker.s*
修改 service 文件指定 pause 镜像和 CRI socket:
[Service]
Type=notify
ExecStart=/usr/local/bin/cri-dockerd \
--network-plugin=cni \
--pod-infra-container-image=reg.timinglee.org/k8s/pause:3.10.1 \
--container-runtime-endpoint fd://
Restart=always
systemctl daemon-reload
systemctl enable --now cri-docker.service
ll /var/run/cri-dockerd.sock # 确认 socket 存在
pause 镜像是做什么的? 它是每个 Pod 的"基础设施容器"(也叫 sandbox container),负责持有 Pod 的网络命名空间。所有同一 Pod 的容器共享它的网络栈。这个镜像必须提前准备好,否则 Pod 创建会卡在 ContainerCreating。
4.4 第三步:安装 kubelet / kubeadm / kubectl
# Master 节点(需要 kubectl 管理)
dnf install kubelet kubeadm kubectl -y
systemctl enable --now kubelet.service
# Node 节点(不需要 kubectl)
dnf install kubelet kubeadm -y
systemctl enable --now kubelet.service
# Master 补齐 bash 自动补全
echo "source <(kubectl completion bash)" >> ~/.bashrc
echo "source <(kubeadm completion bash)" >> ~/.bashrc
source ~/.bashrc
注意 :kubeadm 不帮你管理 kubelet 和 kubectl,你需要自己确保三者版本匹配。官方建议 kubelet 版本不超过 apiserver 一个小版本。
4.5 第四步:拉取并推送镜像到 Harbor
# 从阿里云镜像站拉取 K8s 组件镜像
kubeadm config images pull \
--image-repository registry.aliyuncs.com/google_containers \
--kubernetes-version v1.35.3 \
--cri-socket=unix:///var/run/cri-dockerd.sock
# 输出:
# Pulled registry.aliyuncs.com/google_containers/kube-apiserver:v1.35.3
# Pulled registry.aliyuncs.com/google_containers/kube-controller-manager:v1.35.3
# Pulled registry.aliyuncs.com/google_containers/kube-scheduler:v1.35.3
# Pulled registry.aliyuncs.com/google_containers/kube-proxy:v1.35.3
# Pulled registry.aliyuncs.com/google_containers/coredns:v1.13.1
# Pulled registry.aliyuncs.com/google_containers/pause:3.10.1
# Pulled registry.aliyuncs.com/google_containers/etcd:3.6.6-0
推送到本地 Harbor(这样其他节点也能快速拉取):
docker login reg.timinglee.org -u admin
# 批量 tag 并 push
docker images --format "{{.Repository}}:{{.Tag}}" \
| awk -F "/" '/google/{system("docker tag "$0" reg.timinglee.org/k8s/"$3)}'
docker images --format "{{.Repository}}:{{.Tag}}" \
| awk -F "/" '/timinglee/{system("docker push "$0)}'
这 7 个镜像就是 K8s 控制平面和工作节点的全部依赖。
4.6 第五步:kubeadm init 初始化控制平面
kubeadm init \
--pod-network-cidr=10.244.0.0/16 \
--image-repository reg.timinglee.org/k8s \
--kubernetes-version v1.35.3 \
--cri-socket=unix:///var/run/cri-dockerd.sock
参数解读(我的说明):
| 参数 | 含义 | 为什么重要 |
|---|---|---|
--pod-network-cidr |
Pod 网络地址段 | 必须与后续 CNI 插件配置一致,Flannel 默认就是 10.244.0.0/16 |
--image-repository |
镜像仓库地址 | 默认去 registry.k8s.io 拉,国内基本连不上,改为我们的 Harbor |
--cri-socket |
CRI socket 路径 | 告诉 kubeadm 用哪个容器运行时 |
init 成功后输出关键信息:
Your Kubernetes control-plane has initialized successfully!
# 加入集群的凭证(保存好!)
kubeadm join 172.25.254.100:6443 \
--token tdwjoc.8d1yw3wl4r4tm6c4 \
--discovery-token-ca-cert-hash sha256:6b59...
配置 kubectl 访问集群:
export KUBECONFIG=/etc/kubernetes/admin.conf
echo "export KUBECONFIG=/etc/kubernetes/admin.conf" >> ~/.bash_profile
source ~/.bash_profile
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# master NotReady control-plane 102s v1.35.3
注意 :此时节点状态是 NotReady,这是正常的!因为我们还没装网络插件(CNI),Pod 网络还没打通。
4.7 第六步:Worker 节点加入集群
在每个 worker 节点上执行 init 时输出的 join 命令:
# node1 和 node2 分别执行
kubeadm join 172.25.254.100:6443 \
--token jl4ztx.cax3iysvu7onsh5s \
--discovery-token-ca-cert-hash sha256:6b59... \
--cri-socket=unix:///var/run/cri-dockerd.sock
Token 原理 :Token 是一张短期有效的引导凭据(默认 24 小时),包含 join 所需的基本信息。ca-cert-hash 是控制平面 CA 证书的 SHA256 摘要,用于防止你手滑 join 到别人的集群。
如果忘了 token:
kubeadm token create --print-join-command
# 会重新生成并打印完整 join 命令
如果初始化出了严重问题想重来:
kubeadm reset --cri-socket=unix:///var/run/cri-dockerd.sock
# 这会清理本机的 K8s 配置,但不影响其他节点
加入后在 master 上查看:
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# master NotReady control-plane 5m16s v1.35.3
# node1 NotReady <none> 66s v1.35.3
# node2 NotReady <none> 58s v1.35.3
三个节点都在了,但都是 NotReady ------ 等待网络插件。
4.8 第七步:部署 Flannel 网络插件
Flannel 是最常用的 CNI(Container Network Interface)插件之一,为每个 Pod 分配独立的 IP 地址,并通过 VXLAN/UDP 封装实现跨节点互通。
# 加载 flannel 镜像到本地
docker load -i flannel-0.28.1.tar
# tag 并推送到 Harbor(确保所有节点能拉取)
docker tag ghcr.io/flannel-io/flannel-cni-plugin:v1.9.0-flannel1 \
reg.timinglee.org/flannel-io/flannel-cni-plugin:v1.9.0-flannel1
docker push reg.timinglee.org/flannel-io/flannel-cni-plugin:v1.9.0-flannel1
docker tag ghcr.io/flannel-io/flannel:v0.28.1 \
reg.timinglee.org/flannel-io/flannel:v0.28.1
docker push reg.timinglee.org/flannel-io/flannel:v0.28.1
修改 kube-flannel.yml 中的镜像地址为我们的 Harbor,然后 apply:
kubectl apply -f kube-flannel.yml
# namespace/kube-flannel created
# serviceaccount/flannel created
# clusterrole.rbac.authorization.k8s.io/flannel created
# clusterrolebinding.rbac.authorization.k8s.io/flannel created
# configmap/kube-flannel-cfg created
# daemonset.apps/kube-flannel-ds created
验证集群 Ready:
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# master Ready control-plane 13m v1.35.3
# node1 Ready <none> 9m44s v1.35.3
# node2 Ready <none> 9m36s v1.35.3
🎉 三个节点全部 Ready!集群部署完成。
五、部署后的 Pod 管理与应用发布
5.1 基础对象管理
# 查看命名空间
kubectl get namespaces
# default / kube-system / kube-flannel / kube-public / kube-node-lease
# 创建 Pod(命令式)
kubectl run lee --image nginx:latest
kubectl get pods -o wide
# NAME READY STATUS RESTARTS AGE IP NODE
# lee 1/1 Running 0 25s 10.244.1.10 k8s-node1
# 排查问题 Pod
kubectl describe pods error-pod # 查看 Events 定位原因
kubectl logs pods/my-pod # 查看容器日志
kubectl exec -it pods/testpod -- /bin/bash # 进入容器
kubectl cp testpod:/path/file ./local # 从 Pod 拷贝文件
5.2 Deployment 控制器:声明式管理的核心
Deployment 是生产中最常用的控制器,提供副本管理、滚动更新、回滚等能力。
apiVersion: apps/v1
kind: Deployment
metadata:
name: webcluster
labels:
app: webcluster
spec:
replicas: 2 # 期望副本数
selector:
matchLabels:
app: webcluster
template:
metadata:
labels:
app: webcluster
spec:
containers:
- image: myapp:v1
name: myapp
ports:
- containerPort: 80
kubectl apply -f webcluster.yml
kubectl expose deployment webcluster --port 80 --target-port 80 --type NodePort
kubectl get svc
# NAME TYPE CLUSTER-IP PORT(S) AGE
# webcluster NodePort 10.99.3.32 80:30713/TCP 2m54s
此时可以通过 http://<任意节点IP>:30713 访问应用。
5.3 滚动更新与回滚

图 3:Deployment 滚动更新与回滚原理 --- 新旧 ReplicaSet 同时保留
# 更新镜像版本(触发滚动更新)
kubectl set image deployments/webcluster myapp=myapp:v2
kubectl annotate deployment/webcluster kubernetes.io/change-cause="升级到v2"
# 查看更新进度
kubectl rollout status deployment/webcluster
# deployment "webcluster" successfully rolled out
# 查看历史版本
kubectl rollout history deployment/webcluster
# REVISION CHANGE-CAUSE
# 1 <none>
# 2 升级到v2
# 回滚到上一版
kubectl rollout undo deployment/webcluster
# 回滚到指定版本
kubectl rollout undo deployment/webcluster --to-revision=1
原理说明 :Deployment 底层维护多个 ReplicaSet(RS),每个 RS 对应一个版本。滚动更新时,新 RS 逐步扩容、旧 RS 逐步缩容,始终保持可用副本数不低于阈值。回滚时只需切换活跃 RS 即可,秒级完成。
5.4 其他常用 kubectl 操作速查
| 操作 | 命令 | 说明 |
|---|---|---|
| 弹性伸缩 | kubectl scale deploy webcluster --replicas=4 |
动态调整副本数 |
| 编辑资源 | kubectl edit deploy webcluster |
打开编辑器直接改 YAML |
| 打补丁 | kubectl patch deploy webcluster -p '{"spec":{"replicas":1}}' |
局部更新 |
| 重启 | kubectl rollout restart deployment webcluster |
滚动重启所有 Pod |
| 标签管理 | kubectl label pods xxx app=newlabel |
添加/修改标签 |
| 端口暴露 | kubectl expose deploy webcluster --port 80 |
创建 Service |
六、用 Ansible 实现部署自动化

图 4:Ansible 无代理批量自动化 --- 控制节点通过 SSH 编排所有受控主机
前面我们手动在 4 台机器上执行了几十条命令。如果是 50 台节点呢?这就需要 Ansible 了。
6.1 Ansible 是什么?
Ansible 是一个 IT 自动化工具,以**无代理(agentless)**方式管理机器,使用 OpenSSH 进行传输。
执行流程:
- 加载配置文件和 inventory 清单
- 将任务编译成临时 Python 脚本
- 通过 SSH 传送到受控节点执行
- 收集结果返回给控制节点
- 删除远端临时文件
这意味着受控节点只需要 Python 和 SSH,不需要安装任何 Agent。
6.2 Ansible 自动化 K8s 部署的关键结构
# inventory 文件 --- 定义受控主机分组
[master]
192.168.168.10
[workers]
192.168.168.21
192.168.168.22
[harbor]
192.168.168.200
[k8s:children]
master
workers
harbor
# playbook 示例 --- k8s_cluster_init.yml
---
- name: 初始化 K8s 集群
hosts: k8s
become: yes
tasks:
- name: 关闭 swap
ansible.builtin.command: swapoff -a
# ... 更多 tasks
- name: 部署 Harbor
hosts: harbor
roles:
- harbor
- name: 初始化 Master
hosts: master
roles:
- k8s-master
- name: 加入 Worker 节点
hosts: workers
roles:
- k8s-worker
我的实践体会:对于 K8s 集群部署这种"多机多步骤"的任务,Ansible 的价值在于:
- 幂等性:重复执行不会出错(已完成的跳过)
- 角色化(Roles):将 Harbor 部署、Master 初始化、Worker Join 拆成独立 role,可复用
- 变量驱动:不同环境的 IP、版本号通过变量文件切换
进阶选择 :如果你不想自己写 playbook,社区有成熟的 kubespray 项目,它本身就是一套完整的 Ansible playbook 集合,支持高可用集群部署。
七、总结与踩坑建议
7.1 核心原理回顾

图 5:控制平面组件协作 --- 用户声明期望态,控制器让实际态逼近期望态
回顾整个部署流程,我认为有几个最关键的原理值得记住:
- CRI 是必经之路:K8s 1.24 移除 dockershim 后,Docker 必须通过 cri-dockerd 适配。这不是可选的。
- 静态 Pod 是 kubeadm 的魔法:控制平面组件(apiserver/etcd/scheduler/controller-manager)本质上是由 kubelet 以静态 Pod 形式拉起的,所以它们也受 kubelet 管理。
- NotReady 是正常的:没装 CNI 前节点一定是 NotReady,这不代表部署失败。
- Token 有时效 :join token 默认 24 小时过期,忘记的话用
kubeadm token create重建。 - 声明式 > 命令式 :尽量用 YAML +
kubectl apply,而不是kubectl run/create,前者可追溯、可版本化管理。
7.2 常见坑与解决方案
| 问题 | 原因 | 解决 |
|---|---|---|
ImagePullBackOff |
镜像不存在或仓库不可达 | 检查 image 字段和 docker login 状态 |
Init:CrashLoopBackOff |
Init 容器反复崩溃 | kubectl describe pod 看 Event 日志 |
| 节点一直 NotReady | CNI 未部署或 CNI 配置错误 | 检查 kube-system 下 daemonset 是否正常 |
kubeadm join 报错 x509 |
ca-cert-hash 不匹配 | 重新获取 hash:`openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt |
| kubelet 启动失败 | swap 未关闭 | swapoff -a 并注释 /etc/fstab 中 swap 行 |
| cri-dockerd 连接不上 | socket 路径不对 | 确认 --cri-socket 与实际 socket 路径一致 |