1. 为什么叫“k8s - 2”:从一个看似随意的命名说起
起这个标题的时候,我其实在整理自己的第二份 Kubernetes 实操笔记。上一次折腾 K8s 还是大半年以前,当时跟着教程把集群搭起来,跑了一个 demo,之后就再没深入。这次重新上手,我给自己定的目标很明确:不再满足于“能用”,而是要把整个环境的搭建逻辑、组件职责、网络方案、工作负载部署、故障排查从头到尾吃透。所以这个“- 2”不光是第二遍的意思,更代表一种“从零开始但比上次更深”的实战状态。
如果你也处在差不多的阶段——听说过 K8s 很火,看过一些概念图,但真正上手时被 apiserver、etcd、CNI、kubelet 这些名词砸得头晕,那这篇笔记就是给你准备的。我会用一套完整的实操过程,把 K8s 从初始化到跑通业务服务的全过程拆开揉碎,每步都讲讲我为什么要这么选,以及实际执行时踩过的坑。想要快速复现一套可用集群的同学,可以直接照着一路敲下去。
2. 环境规划与选型:第二轮部署的正确姿势
2.1 节点规划:三个机器还是单机模拟
K8s 的最小完整集群,逻辑上至少需要一个控制平面节点(control-plane)和一个工作节点(work node)。生产环境里控制平面一般会做高可用,但个人学习或者测试环境,主流做法是用三台机器:一台做控制面,两台做工作节点。这样既能体验 kubeadm join 的真实流程,也方便测试 Pod 跨节点调度。
我没有额外的物理服务器,所以选择用虚拟化方式在本地模拟三台虚拟机。这里有个关键点:虚拟机的网络模式必须选“桥接”或者“仅主机”模式,确保三台机器的 IP 能互通。我踩过一次坑,最开始用的是 NAT 模式,结果控制平面初始化没问题,工作节点 join 时怎么都连不上 apiserver,排查了半天发现是网络模式导致 IP 段不一致。
节点规划建议如下:
| 角色 | 主机名 | 配置建议 | IP 段示例 |
|---|---|---|---|
| 控制平面 | k8s-master | 2 核 4G,磁盘 40G | 192.168.10.10 |
| 工作节点 1 | k8s-node1 | 2 核 4G,磁盘 40G | 192.168.10.11 |
| 工作节点 2 | k8s-node2 | 2 核 4G,磁盘 40G | 192.168.10.12 |
内存方面建议每个节点至少 2G,实际上 4G 更稳。如果机器总内存不够,可以先只起两台虚拟机,一台控制面一台工作节点,K8s 完全支持单工作节点集群,Pod 都会调度到这一台上面去,学习阶段完全够用。
2.2 版本选型:别再去追最新的坑
我这次选的是 Kubernetes 1.28 系列(具体小版本选择 1.28.x 的某个稳定补丁版)。为什么不定到最新版本?K8s 的版本迭代非常快,大概每三个月发一个大版本,而很多周边生态组件(比如 CNI 插件、Ingress Controller、监控套件)对新版本的支持会有滞后。生产环境选型一般会采用“当前最新稳定版往回退一到两个大版本”的策略,给生态组件留出适配时间。
另外还要注意一个细节:kubeadm 工具、kubelet、kubectl 的版本号必须保持在同一大版本内。我见过有人控制面装的是 1.29,节点上 kubelet 还是 1.27,导致节点一直 NotReady,kubelet 日志里报版本不匹配的警告。版本差异在一个小版本以内通常没问题,超过一个大版本就容易出幺蛾子。
2.3 部署工具选型:kubeadm 为什么是默认选择
K8s 的部署方式大致分三类:一键托管型(云厂商的托管集群)、半自动型(kubeadm)、完全手动型(二进制部署)。我的建议是学习阶段务必用 kubeadm。它在帮你完成组件部署的同时,保留了足够的透明性——你可以通过kubeadm init看到控制平面组件是怎么以静态 Pod 方式生成的,通过kubeadm join理解节点注册的凭证机制。
完全手动的二进制部署我不推荐新手做,因为涉及证书生成、apiserver 与 etcd 的 TLS 配置、systemd 服务单元编写等大量细节,很容易在某个环节卡死,而且排错成本高。托管型集群虽然最省心,但屏蔽了太多底层实现,不利于建立对 K8s 的整体认知。
3. 核心概念拆解:控制面、工作节点和容器运行时
3.1 控制平面的五个组件都在干什么
很多人对 K8s 的恐惧,源于一上来就要面对一大堆以字母 k 开头的名词。我换个方式帮你理解:把控制平面想象成一个公司的管理层。
apiserver 是唯一的入口,相当于前台接待处,所有外部请求(kubectl 命令、节点注册、组件间的通信)都必须经过它。它负责身份认证、权限校验,然后把合法请求写入 etcd。etcd 是集群的数据库,保存了所有对象的状态,好比公司的档案室,记录着每个人在哪、每个项目进行到哪一步。scheduler(调度器)决定一个新 Pod 该放在哪台机器上,相当于人事部门分派任务。controller manager(控制器管理器)负责让实际状态不断靠近期望状态,比如 Deployment 期望 3 个副本,现在只有 2 个,它就会触发补副本的流程,相当于监督执行的角色。
还有一个 cloud-controller-manager,在自建集群中通常不需要启用,只有对接云厂商时才用到,这里不展开。
3.2 工作节点上的三个核心角色
控制平面负责决策,真正干活的是工作节点。每个工作节点上有三个关键组件:kubelet、kube-proxy、容器运行时。
kubelet 是节点的代理人,负责接收 apiserver 下发的 Pod 清单,然后调用容器运行时去创建或销毁容器,并持续上报节点和 Pod 的状态。kube-proxy 主要负责把 Service 的虚拟 IP(ClusterIP)的流量转发到具体的 Pod 上,它实现的是负载均衡和转发规则。容器运行时的职责最好理解——真正把容器跑起来的东西,比如 containerd。
3.3 容器运行时:为什么选 containerd 而不是 Docker
这里有一个很常见的困惑:K8s 不是依赖 Docker 吗?现在的版本里还要不要装 Docker?
实际上,K8s 从 1.24 版本开始就不再直接支持以 Docker 作为运行时,而是通过 CRI(Container Runtime Interface,容器运行时接口)与 containerd 这类 runtime 通信。Docker 本身包含的 dockershim 组件已经被移除,keepalived 式的“Docker 套娃”时代已经过去了。
有人说那我还是想用 Docker 怎么办?可以用 cri-dockerd 做适配层,但日常学习和生产部署,直接装 containerd 是更简洁、更符合官方推荐的做法。我这次就是用 containerd 作为运行时。
安装 containerd 之后有个关键配置:默认配置里SystemdCgroup通常是 false,必须改成 true,否则 kubelet 在检查节点状态时会报 cgroup 驱动不一致的错误。这个配置项在/etc/containerd/config.toml里修改,改完重启 containerd 服务即可。判断是否生效,可以用containerd config dump | grep SystemdCgroup检查。
4. 实操部署:从初始化到集群就绪
4.1 基础环境配置:三台机器必须做的统一动作
以下操作要在所有节点上执行。我按顺序列一下,顺便解释每条命令的意义。
# 设置主机名(每台机器改成自己的) sudo hostnamectl set-hostname k8s-master # 关闭交换分区,否则 kubelet 会报错 sudo swapoff -a # 永久关闭需要注释掉 /etc/fstab 里的 swap 行 # 加载内核模块 cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter关闭 swap 这个操作,很多第一次接触 K8s 的同学会忽略注释 fstab 这步,结果重启之后 swap 又回来了。K8s 在 kubelet 启动时会检查 swap 状态,开启 swap 会导致节点无法通过 kubelet 的健康检查。至于为什么要关 swap,官方给出的理由是:保证 CPU 和内存的资源配额可以被精确执行。如果 Pod 的内存使用被 swap 挡住了,limit 实际不起作用,OOM 的判断和回收都会出问题。
接下来是网络配置:
cat <<EOF | sudo tee /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 sudo sysctl --system这里的net.bridge.bridge-nf-call-iptables是让 iptables 规则对桥接流量也生效,K8s 的网络插件(比如 Calico 的 felix)依赖 iptables 来转发流量,不开启的话跨节点 Pod 通信会有问题。
装 kubeadm、kubelet、kubectl 的过程各家教程都大同小异,重点说一下版本锁定。安装时指定版本号,避免 apt 或 yum 自动升级。
# 以 apt 系为例,先添加 K8s 软件源 sudo apt-get update sudo apt-get install -y kubelet=1.28.* kubeadm=1.28.* kubectl=1.28.*装上之后立刻执行sudo apt-mark hold kubelet kubeadm kubectl,防止系统更新时被误升。我这次就因为这个吃了亏,第一次部署时没执行 hold,第二次实操发现 kubelet 被自动升级了,只能全部降级重来,非常浪费时间。
4.2 初始化控制平面
在控制平面节点上执行初始化命令。这里我不是直接裸跑kubeadm init,而是先把配置导出来看一眼:
kubeadm config print init-defaults > kubeadm-init.yaml打开文件之后,主要改两个地方:一个是localAPIEndpoint.advertiseAddress,改成控制平面节点的 IP;另一个是nodeRegistration.name,改成控制面的主机名。还有一个很关键的配置:criSocket,默认写的是unix:///var/run/cri-dockerd.sock,而我们用的是 containerd,所以要改成unix:///var/run/containerd/containerd.sock,否则初始化时会报找不到 socket。
设置好之后再指定 Pod 网段:
kubeadm init --config kubeadm-init.yaml --pod-network-cidr=10.244.0.0/1610.244.0.0/16是 Flannel 默认的网段。如果后面你打算用 Calico,可以改成192.168.0.0/16。这个网段一旦定下来后面不好改,所以要提前想好。我建议初始化之前就把网络插件定下来,避免因为网段冲突导致后面 Flannel 和 Calico 之间切换麻烦。
初始化成功后,末尾会输出一段命令,照着执行即可配置 kubectl:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config还有一个很重要的输出是kubeadm join命令的完整形式,它包含一个 token 和一段证书哈希,工作节点加入集群时要用。记得把这条命令保存下来。如果丢了也没关系,可以在控制面上重新生成:
kubeadm token create --print-join-command4.3 安装 CNI 网络插件
初始化成功之后,控制面节点会显示 NotReady,这很正常。核心原因是集群里缺少网络插件,Pod 与 Pod 之间没法通信。检查一下 coredns Pod,大概率是 Pending 状态,因为没有可用网络,调度器无法为它分配有效 IP。
我选择装 Calico,因为它功能更强,支持 NetworkPolicy。安装方式很简单:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/...用 kubectl apply 之前,我建议先把 yaml 下载到本地看一眼,至少确认里面引用的镜像版本能正常拉取。在国内环境经常出现拉不到镜像的情况,这种情况可以先把镜像拉到本地,给镜像打个 tag 改成对应 registry 的地址,实体急躁的时候这招很管用。
安装完成后等一会儿,用kubectl get pods -n kube-system查看。重点看 calico-node 是否 Running,coredns 是否从 Pending 变成 Running。如果一直是 Initializing 或者 CrashLoopBackOff,看日志一般能定位问题,常见的有以下几类,我后面专门开一节讲。
4.4 工作节点加入集群
把之前保存的kubeadm join命令拿到工作节点上执行。注意:必须在工作节点上安装好 containerd、kubelet、kubeadm,并且完成基础环境配置之后才能 join。安装过程跟控制面节点一模一样,只是不需要装 kubectl。
kubeadm join 192.168.10.10:6443 --token <token> \ --discovery-token-ca-cert-hash sha256:<hash>join 本质上是节点向 apiserver 发起注册请求,通过 token 做身份验证。执行成功之后,回到控制面上运行:
kubectl get nodes如果看到两个工作节点都显示 Ready,恭喜你,集群搭建完成了。这个过程最快大概几分钟,前提是镜像拉取顺利。如果卡住不动,第一步永远是journalctl -u kubelet -f看 kubelet 日志,不要盲目重启服务。
4.5 集群完整验证:不只是看节点状态
节点 Ready 只是第一步,我还会做一轮完整的验证,避免后面用的时候才发现问题。
先看所有系统组件的状态:
kubectl get pods -n kube-system -o wide然后跑一个测试 Pod 验证调度和网络:
kubectl run nginx-test --image=nginx:1.25 kubectl get pod nginx-test -o wide执行kubectl exec -it nginx-test -- curl <另一个Pod的IP>,能通说明 Pod 间网络正常。把容器删除后,再验证一下 DNS 解析:进入 Pod 内部去 ping 一下 coredns 的 Service 名称,比如nslookup kubernetes.default.svc.cluster.local。
5. 跑起第一个真实工作负载:从 Pod 到对外访问
5.1 创建 Deployment:让控制器帮你管理 Pod
手动用kubectl run创建的 Pod,挂了不会被自动拉起,这不是 K8s 的正常用法。真实场景都是用 Deployment 这类工作负载资源来管理 Pod。我写了一个最简单的编排文件:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo labels: app: nginx-demo spec: replicas: 3 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80这里有三个关键概念需要理解:replicas 声明期望副本数;selector 决定控制器管理哪些 Pod;template 是创建 Pod 的模板。三者配合,就构成了控制器的闭环逻辑——你只需要声明期望状态,控制器会不断调整实际状态去逼近它。
为了验证控制器的作用,我故意删掉一个 Pod:
kubectl delete pod nginx-demo-xxxxx几秒钟后一个新的 Pod 会自动创建,名字不同但前缀相同。这就是 Deployment 最基础也最核心的价值。
5.2 用 Service 提供稳定的访问入口
Pod 的 IP 是漂移的,每次重建都会变。我们总不能每次去查 IP 再连服务。Service 提供了稳定的虚拟 IP 和负载均衡。最常用的是 ClusterIP 类型:
kubectl expose deployment nginx-demo --port=80 --target-port=80--port是 Service 对外暴露的端口,--target-port是 Pod 上的容器端口。Service 选定 Pod 的方式就是上面 Deployment 里的 label,所以 label 的命名规范从一开始就要想好,后面 Service 和 Ingress 都要靠它关联。
验证 Service 和 Pod 的映射关系:
kubectl get endpoints能看到 endpoints 里列出的三个 Pod IP,这才算真正确认了 Service 到 Pod 的链路。
5.3 用 Ingress 做 7 层路由:按域名访问服务
ClusterIP 只能在集群内部访问。想从集群外面访问服务,有三种典型方式:NodePort、LoadBalancer、Ingress。自建集群没有云负载均衡器,LoadBalancer 用不了,直接 NodePort 虽然简单,但端口管理混乱,不支持按域名转发。所以 Igress 是更合适的方案。
安装一个 Ingress Controller,我用的是社区主流的 Nginx Ingress Controller。它本质上是一堆带有hostNetwork: true或 NodePort 的 Pod,负责监听 Ingress 资源并生成 Nginx 配置,再配合转发规则把流量带到对应 Service。
然后定义 Ingress:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx-demo-ingress spec: rules: - host: demo.local http: paths: - path: / pathType: Prefix backend: service: name: nginx-demo port: number: 80执行之后,在客户端机器的 hosts 文件里把demo.local解析到任意工作节点的 IP,访问http://demo.local就能看到 Nginx 默认页面。这里一个容易踩的坑:不同 Ingress Controller 对pathType的行为有细微差异,Prefix和Exact的匹配规则要提前测试。另外,如果多个 Ingress 规则配置了相同的域名但路径不同,小心别把路由规则写重叠了,Nginx 会按规则顺序匹配,容易跑到错误的 service 上。
5.4 让数据活起来:PV 和 PVC 的配合
Pod 是临时性的,容器内的数据随 Pod 删除而消失。想让数据持久化,需要引入 PV(PersistentVolume)和 PVC(PersistentVolumeClaim)。PV 相当于一块已经准备好的存储资源,PVC 则是使用方提出的存储申请,两者匹配成功后,Pod 才能挂载使用。
我用本地目录模拟一个 PV:
apiVersion: v1 kind: PersistentVolume metadata: name: local-pv spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain local: path: /data/k8s-local注意local类型的 PV 需要配合节点亲和性使用,否则调度器不知道这个目录在哪台节点上。这里不展开 HostPath 或者 local 的完整用法,但原理要清楚:PV 是静态资源,PVC 是动态申请,StorageClass 是动态供给的桥梁。学习阶段可以先手动创建 PV 体验一下,真正做持久化存储时,一般会部署一个 StorageClass 提供方(比如 NFS 或者云硬盘驱动)来动态创建 PV。
6. 高频问题与排查思路实录
6.1 kubeadm init 失败,kubelet 一直报错
这是我身边同学遇到最多的一个问题。初始化失败后,第一反看 kubelet 状态:
systemctl status kubelet journalctl -u kubelet -f --no-pager日志里常见报错之一是:
failed to run Kubelet: failed to create kubelet: misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"这个就是我之前提到过的 cgroup 驱动不一致问题。解决方法就是把 containerd 的SystemdCgroup改成 true。改完之后先别急着重新 init,最好清掉之前的残留状态:
kubeadm reset -f rm -rf /etc/kubernetes /var/lib/etcd /var/lib/kubelet然后再重新初始化。这个干净的环境非常重要,很多奇怪问题都是因为上一次初始化残留导致的。
6.2 Pod 一直 Pending,调度器说资源不足
kubectl describe pod <name>是排查 Pending 的第一命令。如果事件里出现0/3 nodes are available,一般分两种情况:一种是节点资源不足(CPU、内存不够满足 request),另一种是节点有污点(taint),Pod 没有对应的容忍度。
资源问题的话,可以把 Deployment 里的replicas先调小,或者检查节点上是不是堆积了一堆不用的 Pod,用kubectl top nodes查看资源使用率。污点问题则要先看节点上的污点:
kubectl describe node k8s-node1 | grep Taints控制面节点默认带node-role.kubernetes.io/control-plane:NoSchedule污点,普通 Pod 不会调度上去。如果你只有一个单节点集群,想在控制面节点上跑业务 Pod,需要给 Pod 加对应的 toleration,或者把污点去掉:
kubectl taint nodes --all node-role.kubernetes.io/control-plane-注意这个操作在生产环境要谨慎,去掉控制面污点等于允许业务负载和系统组件混部。
6.3 跨节点 Pod 网络不通,但同节点正常
这个问题几乎都出在 CNI 插件配置上。先看几个关键状态:
kubectl get pods -n kube-system | grep calico kubectl logs -n kube-system <calico-node-pod>我在实际操作中遇到过一个很隐蔽的问题:Calico 要求的net.ipv4.ip_forward没有在某个节点上开启,导致该节点上的 Pod 能接收数据,却无法转发出去。重新执行 sysctl 配置并重启网络服务就能解决。
如果你之前换过 CNI 插件,比如从 Flannel 切到 Calico,还要注意清理残留的 Flannel 配置和 iptables 规则。这种情况下最省心的做法是kubeadm reset整个节点重新加入,否则旧路由表会一直干扰新插件正常工作。
6.4 证书过期:集群跑久了的人都会遇到
K8s 集群里的组件证书默认有效期是一年。很多集群跑着跑着突然 apiserver 不可用,kubectl 报证书过期错误,就是这个原因。检查证书有效期:
kubeadm certs check-expiration如果剩余时间不长,就提前续期:
kubeadm certs renew all续期之后要重启 kubelet 和静态 Pod(控制平面组件以静态 Pod 方式运行,重启 kubelet 会自动拉起):
systemctl restart kubelet如果 kubeconfig 文件引用了旧证书,也要一并更新,把/etc/kubernetes/admin.conf重新复制到~/.kube/config。证书这块属于“你永远不会注意到,直到它出问题”的冷知识,建议大家建好集群以后就把检查证书过期加进监控告警里。
6.5 镜像拉取失败:一个容易被忽视的部署前置问题
国内环境部署 K8s,新手遇到最多的就是镜像拉不了,卡在ContainerCreating。我用的是 containerd,它的镜像配置跟 Docker 不太一样,配置文件是/etc/containerd/config.toml,里面没有 Docker daemon 那样的 registry mirror 概念,但可以通过配置为 containerd 设置镜像加速。
实际操作起来比较简单的方案是:在有网络条件的机器上先拉好镜像,导出成 tar 包,再导入到目标节点。但这只适合解决单个镜像问题。如果是批量镜像,更建议在 containerd 配置里配置镜像仓库(registry)的 mirror 或用中转镜像源,然后重启 containerd。我用的方式是先把镜像 pull 下来,打 tag 改成能推送到本地镜像仓库的地址,再从仓库拉取,虽然多了一步,但胜在可控、可重复。
7. 关于多集群、监控和后续扩展的几句补充
集群搭好、业务跑通,这只是第一步。后续的运维才是决定集群能不能长期稳定跑下去的关键。我建议下一步做三件事。
第一,部署一个监控告警体系。最经典的是 Prometheus + Grafana 组合,kube-prometheus-stack 这个 Helm chart 已经打包好了大部分组件,装上就能看到节点资源、Pod 状态、容器指标。硬盘告警一定要配,K8s 节点磁盘满会导致很多诡异行为,比如 Pod 无法创建、etcd 写入失败。
第二,资源配额一定要从一开始就配上。用 ResourceQuota 限制每个命名空间的 CPU 和内存总量,用 LimitRange 限制单个 Pod 的最小和最大资源申请。否则团队里有人创建了一个没有 limit 的 Pod,内存占用无限上涨,会拖垮整个节点。
第三,日志收集提前规划。K8s 里 Pod 是可以随时漂移的,日志分散在各个节点上,排查问题如果不集中收集日志会非常痛苦。轻量方案是 Loki,重一点用 Elasticsearch。无论选哪种,都建议在集群规模变大之前把链路设计好。
扩展高可用的话,控制平面可以拆成三节点,etcd 也要做三副本,这需要额外的机器和配置成本。但在做这步之前,先想清楚:你现阶段是否真的需要高可用?很多团队在业务量还没上来时,单控制面完全够用,盲目追求高可用反而增加运维复杂度。把基础监控做好,比多配几台机器更有价值。
我个人在实际操作中的体会是,K8s 的难点不在“搭起来”,而在“出了问题怎么办”。如果你只是照着教程敲一遍命令,那学到的东西很有限;真正让你理解 K8s 架构的,是它报错时你去翻阅日志、查资料、对照组件关系定位问题的那段时间。你试着把集群搞烂两台节点,再一条命令一条命令把它救回来,你会发现对控制面、kubelet、网络插件之间的协作关系突然就通了。
最后再分享一个小技巧:给常用命令设置好别名,比如alias k='kubectl',然后熟练使用kubectl get events --sort-by='.lastTimestamp'来看集群事件流。很多时候你以为的问题根本不是问题,只是排错了方向,多看看事件,能少走很多弯路。