1. 为什么我还愿意折腾单master集群
先说个现实情况:现在大家一提到K8s集群,搜索出来的文章几乎全是高可用、双master起步、甚至三master五master的生产架构。我并不是说高可用不重要,但如果你和我一样,是刚接触云原生、想在自己电脑或几台廉价服务器上把K8s跑通、验证一些业务逻辑的开发者,一上来就搞三master远不如从单master集群开始来得实在。
单master集群搭建,本质上就是一台控制平面节点(Control Plane)同时充当工作节点(Worker Node)的角色。它最直接的价值在于:用最少的硬件资源,把K8s的调度、容器编排、服务发现、配置管理这一整套核心流程完整地跑起来。某个曾经只跑过Docker Compose的小团队,想迁移到K8s,先在单master上把业务容器化、把Deployment、Service、Ingress这些概念落地一遍,这个过程的性价比非常高。
这套方案适合谁?适合正在学习K8s的初学者、适合个人开发者自建实验室、适合中小企业测试环境(只要你对可用性要求没那么极端)。它不适合谁?不适合承载核心生产业务、不适合对数据安全有强要求的线上场景。我把这些话放在最前面,就是希望大家把单master集群放在一个合理的定位上,不要把它当成万能药,也不要彻底否定它的价值。它就是一个很好用的“训练场”和“轻量引擎”。
很多人会问:为什么不用Minikube或者Kind?我的看法是,那些工具把K8s封装得太好了,一键启动一键删,反而掩盖了集群内部组件的交互关系。手动用kubeadm搭一套单master集群,至少经历一遍完整的初始化流程,搞清楚etcd、API Server、Scheduler、Controller Manager、kubelet、kube-proxy这些组件各自在忙什么,以后再去看生产级集群就不会发怵。
下面我把我实际搭建的过程、踩过的坑、最后稳定运行的配置和盘托出,照着这条路走,周末花上几个小时,一套属于自己的K8s环境就可以跑起来了。
2. 搭单master之前,先把架构和硬件账算清楚
2.1 单master架构的组件流转关系
先建立整体认知。K8s集群内部可以拆成两大部分:控制平面和节点平面。控制平面负责“做决策”,节点平面负责“干实事”。
在单master架构里,这台机器上同时运行着两类角色进程:
- 控制平面组件:kube-apiserver(所有请求的入口)、kube-scheduler(决定Pod跑在哪台机器上)、kube-controller-manager(维持集群期望状态)、etcd(集群的数据库,存储所有资源对象的状态)。
- 节点组件:kubelet(管理本机容器的生命周期)、kube-proxy(维护网络规则,实现Service负载均衡)、容器运行时(负责真正创建和运行容器)、CNI插件(负责Pod网络互通)。
当你在机器上执行kubectl apply -f deployment.yaml时,这条命令实际上是在和API Server打交道。API Server把Deployment对象写入etcd,然后Scheduler观察到有新的Pod需要调度,结合节点资源情况做出决策,把Pod绑定到某个节点上。接着该节点上的kubelet通过CRI接口调用containerd创建容器,kube-proxy负责把Service的虚拟IP转发到具体Pod的IP上。
理解这个链路后你就明白了,搭建单master集群的核心动作,本质上就是把控制平面的几个进程和节点平面的几个进程,在这台机器上全部安装、配置、连接起来。
2.2 硬件资源的最低要求和推荐配置
我强烈建议不要用配置太低的机器,虽然网上有人说2核2G能跑,但那是极限压榨玩法,你还要在这台机器上跑业务Pod,很容易OOM。
我实际测试下来,比较舒服的配置是:
| 资源 | 最低要求 | 推荐配置 | 说明 |
|---|---|---|---|
| CPU | 2核 | 4核 | 控制平面组件较多,多核可以减少调度延迟 |
| 内存 | 2G | 8G | etcd、API Server都比较吃内存,系统本身也占用一部分 |
| 磁盘 | 20G | 40G SSD | 镜像、容器日志、etcd数据都要落盘,SSD性能差距明显 |
| 网络 | 千兆网卡 | 千兆 | 单机部署对网络要求不高,但后续要拉镜像,带宽很重要 |
如果你用的是云服务器,带宽建议至少5Mbps以上,不然拉镜像的时候你会体会到什么叫“坐等半小时”。如果是虚拟机,建议给宿主机留足内存,虚拟机内部把swap关掉,这点下面细说。
2.3 单master和多master的核心取舍
单master只有一个控制平面副本,意味着API Server和etcd没有热备。如果这台机器宕机了,整个集群的管控能力就没了,已经运行中的业务Pod其实仍然在跑,但你不能通过kubectl去操作它们了,也不能进行滚动更新和故障自愈。
多master架构则通过keepalived或者云厂商负载均衡器,把请求分发到多个API Server上,etcd也是集群模式,任一控制平面节点宕机,不影响集群管理面。
这个取舍看起来很明显,但别忘了成本。多master至少要求3台机器才能体现出优势(2台没有多数派投票能力),硬件成本直接翻3倍。单master适合的是“我把所有鸡蛋放在一个篮子里,但这个篮子我盯得很紧”的阶段。企业里面很多内部工具、个人博客、学习环境,单master完全够用了,没必要为了“高可用”这三个字多掏好几倍的预算。
3. 基础系统配置与容器运行时准备
3.1 操作系统选型与初始化配置
我的建议是使用Ubuntu 22.04 LTS或者CentOS 7.9。CentOS 7的生态文档多,遇到问题容易搜到答案,也稳定。Ubuntu 22.04的内核版本较新,对containerd的支持更友好,叠加网络模块也更省心。我自己的主环境用的Ubuntu 22.04,下面的命令以Ubuntu为准,CentOS用户注意把apt换成yum即可。
系统装好后,第一步不是急着装Docker,而是先把基础配置完成。
# 切换root,后续所有操作都建议用root执行 sudo su - # 设置hostname,方便后续辨识节点 hostnamectl set-hostname k8s-master # 更新系统源 apt update && apt upgrade -y # 安装基础工具 apt install -y vim curl wget net-tools # 关闭swap,kubelet的强制要求 swapoff -a sed -ri 's/.*swap.*/#&/' /etc/fstab关闭swap这一步不能省。K8s的kubelet在启动时会检查swap是否开启,如果开启,默认情况下kubelet会拒绝启动(除非在配置里强制允许,但我不推荐这么干)。原因也很简单,K8s要求内存资源是可预测的,Pod的内存申请、Limit、QoS都依赖cgroup来管控,swap一开,Pod可以直接用swap空间,内存Limit形同虚设,系统整体稳定性大打折扣。
接下来加载内核模块并调整系统参数。这一步是为了让iptables可以正确处理K8s的流量转发,同时开启IPv4转发,Pod之间的通信依赖这个。
# 加载br_netfilter模块 modprobe br_netfilter echo "br_netfilter" >> /etc/modules-load.d/k8s.conf # 调整内核参数 cat <<EOF | 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 sysctl --system如果你问这些参数到底起什么作用,可以简单理解成:K8s内部很多流量是通过iptables规则的DNAT/SNAT实现的,如果bridge层面的流量无法被iptables处理,Service和Pod的网络就会非常诡异。不配置的话,你大概率会遇到Pod间通信不通、Service访问超时这些经典问题。
3.2 containerd安装与配置避坑指南
现在K8s社区已经不再推荐Docker作为容器运行时,虽然K8s仍然支持通过dockershim的方式使用Docker,但从K8s 1.24开始,dockershim已经从源码中移除。现在官方推荐的CRI运行时是containerd,它和Docker共用底层的容器运行时技术(都是CNCF项目),但更轻量、更契合K8s的原生生态。
安装containerd的方式很简单:
# 安装containerd apt install -y containerd # 生成默认配置 mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml >/dev/null默认生成完配置后,不要急着启动。有两个地方必须调整,不然后面必踩坑。
第一个是SystemdCgroup参数。containerd默认使用cgroupfs作为cgroup驱动,但K8s的kubelet默认使用systemd作为cgroup驱动(这是从K8s 1.22版本之后通过KubeletConfiguration配置的)。两边驱动不一致的话,Pod运行过程中会随机出现内存和CPU资源统计不准、Pod被意外杀死的问题。
# 修改/etc/containerd/config.toml # 找到 SystemdCgroup 参数,改为 true sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml第二个是镜像仓库加速配置。因为在拉取k8s.gcr.io仓库的镜像时,国内服务器直连基本拉不动。我在配置文件中的[plugins."io.containerd.grpc.v1.cri".registry]部分,把默认的Docker Hub源和k8s源都加上了国内镜像加速地址。这是非常合理且普遍的做法,配置完后续初始化速度能快上好几倍。
[plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://docker.m.daocloud.io"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"] endpoint = ["https://k8s.m.daocloud.io"]修改完成后重启containerd:
systemctl daemon-reload systemctl restart containerd systemctl enable containerd3.3 kubeadm、kubelet、kubectl的版本配套问题
这里有一个我踩过的重要的坑:三个组件的版本必须严格配套。不要升一个降一个,否则kubelet向API Server注册时会因为版本不匹配被拒绝。
我使用的是当前非常稳定的1.28版本系列。按照官方推荐的版本组合:kubeadm 1.28.x、kubelet 1.28.x、kubectl 1.28.x,全部一致。
# 添加K8s官方软件源 curl -fsSL https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add - echo "deb https://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main" > /etc/apt/sources.list.d/kubernetes.list apt update # 安装指定版本(这一步会同时安装三个组件) apt install -y kubelet=1.28.2-00 kubeadm=1.28.2-00 kubectl=1.28.2-00 # 锁定版本,防止意外升级 apt-mark hold kubelet kubeadm kubectl安装完成后,kubelet当前并不能启动成功,因为还没有执行kubeadm init,它没有配置文件。这是正常的,不需要慌张。kubelet会处于不断重启等待kubeadm写配置文件的状态。
4. 控制平面初始化:kubeadm init全流程
4.1 初始化参数与配置文件
我推荐使用配置文件的方式而不是直接命令行传参,因为配置文件可重复、可追溯。先准备一个kubeadm-init.yaml:
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.31.100 bindPort: 6443 nodeRegistration: criSocket: /run/containerd/containerd.sock --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: 1.28.2 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd几个关键参数的含义我说一下:
- advertiseAddress:API Server对外通告的IP地址,必须设置成这台机器的内网IP,不能是默认的0.0.0.0。后续节点加入时会用这个IP访问API Server。
- criSocket:告诉kubeadm使用哪个容器运行时,我们用的containerd路径就是/run/containerd/containerd.sock。
- podSubnet:Pod网段,10.244.0.0/16是Flannel默认的网段。如果后面要装Calico,这里建议改为192.168.0.0/16(Calico的默认网段)。
- cgroupDriver:必须和containerd里的SystemdCgroup配置保持一致,这就是前面调整containerd配置的原因所在。
如果你不想写配置文件,直接命令执行也能通,但后续如果你要重新初始化或者给别人参考,配置文件的方式会清晰很多。我个人是文件方式的坚定支持者。
4.2 执行init并处理镜像预拉取
在执行init之前,有一个关键动作:先把需要的镜像拉取到本地。kubeadm提供了kubeadm config images pull命令来做这件事。但它默认使用的还是官方源,所以我们需要带上config参数让它读配置文件中的镜像加速配置。
kubeadm config images pull --config kubeadm-init.yaml执行这个命令后,如果看到每个镜像后面跟着“已拉取”的提示,说明镜像加速配置生效了。如果卡住不动,大概率是网络和镜像源问题。
然后是正式的初始化动作:
kubeadm init --config kubeadm-init.yaml --upload-certs执行过程耗时大约2-3分钟,日志会逐步打印控制平面组件Pod的创建情况。看到以下信息就代表成功了:
Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config You should now deploy a Pod network to the cluster. Run "kubectl apply -f [podnetwork]".这时候kubelet会自动启动并注册节点,控制平面的几个静态Pod(由kubelet直接管理的Pod)已经在系统上运行了。你可以通过crictl ps查看这些容器,或者用docker命令查看(如果装了Docker的话)。
4.3 配置kubectl并与API Server建立连接
初始化完成后,我们要让kubectl这个客户端能够访问API Server。kubeadm把生成的证书和kubeconfig文件放在/etc/kubernetes/admin.conf,现在把它复制到当前用户下:
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config配置完成后,验证一下集群状态:
kubectl get nodes这时候节点应该处于NotReady状态,因为还没有安装网络插件。不要急着往下走,先确认API Server、etcd这些组件是健康的:
kubectl get pod -n kube-system正常情况下,etcd、kube-apiserver、kube-scheduler、kube-controller-manager这几个Pod应该都是Running状态,CoreDNS会有一个CrashLoopBackOff的情况,这也是因为缺网络插件。
5. 网络插件选择:Calico与Flannel的实际对比
5.1 为什么Pod网段和Service网段不能混淆
在安装网络插件之前,先理清K8s里的两层网络。Pod网段(podSubnet)是分配给每个Pod的虚拟IP段,Pod之间的通信直接走这个网段。Service网段(serviceSubnet)是分配给Service的虚拟IP段,这个IP不是真实存在于某张网卡上的,而是通过kube-proxy维护的iptables或IPVS规则做转发。
这两个网段必须互不重叠,而且要避开你物理网络的IP段。如果你家里的局域网是192.168.1.0/24,Pod网段就不能用192.168.1.0/16,否则会出现路由冲突。这是搭建过程中比较隐蔽的坑,我见过很多人在这里栽跟头。
5.2 Calico的安装和配置参数
我最终选择的是Calico,因为它的网络策略能力比Flannel强得多。针对单master集群,Calico的性能开销虽然比Flannel大一点,但测试下来完全在可接受范围内。
安装Calico,首先到它的GitHub发布页面下载对应的manifest文件:
curl -L https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/calico.yaml -O然后检查一下里面对Pod网段的定义。默认情况下,Calico的IP池配置读取的是kubeadm传入的podSubnet,也就是kubeadm-init.yaml里的10.244.0.0/16,Calico的控制器会自动适配,不需要手工修改IP池配置。但如果你想明确指定,可以找到下面这段配置(在calico.yaml的ConfigMap部分):
- name: CALICO_IPV4POOL_CIDR value: "10.244.0.0/16"确认CIDR和你初始化时的podSubnet一致后,执行:
kubectl apply -f calico.yaml等待片刻后,查看所有Pod状态:
kubectl get pods -n kube-system -w当calico-node、calico-kube-controllers都变成Running状态,CoreDNS也不再CrashLoop时,节点状态会自动变为Ready:
kubectl get nodes看到Ready,说明网络层已经通了。这时候你可以跑几个测试Pod验证:
kubectl run nginx-test --image=nginx kubectl get pod -o wide kubectl exec -it nginx-test -- curl 127.0.0.1一个很简单的验证方法,在Pod里ping同网段的其他Pod IP,通了说明Calico的数据面正常。
6. 让Master节点承担业务调度:去除Taint与验证
6.1 理解NoSchedule污点的作用
K8s默认会给Master节点打上一个污点(Taint),值为node-role.kubernetes.io/control-plane:NoSchedule。这个污点的含义是:普通的Pod调度器不会主动把Pod调度到这个节点上。这是生产环境的安全设计,因为你不想让业务Pod和控制平面组件抢资源。
单master集群的特殊性在于,你只有这一台机器,不让Master跑业务Pod,那这套集群就没有实际用途了。所以我们要把这个Taint去掉。
kubectl taint nodes --all node-role.kubernetes.io/control-plane-注意这个命令后面的短横线表示移除污点,不是添加。执行完后,再查看节点详情:
kubectl describe node k8s-master | grep TaintsTaints字段为空,说明可以正常调度业务Pod了。这里插一句:如果你只想让特定Pod调度到Master上,而不想让所有Pod都无差别调度,可以保留这个Taint,然后在Pod上添加对应的容忍度(Toleration)。这个做法更精细,但我个人在单master场景下直接全部去掉更省事。
6.2 部署一个真实业务验证集群可用性
网络通了,调度也通了,接下来用一个稍微完整一点的应用做验证。我选择部署一个有状态的服务,这样能顺带验证存储、服务发现、外部访问这些能力。这里用一个简单的Web应用示例。
先创建Deployment和Service:
cat <<EOF | kubectl apply -f - apiVersion: apps/v1 kind: Deployment metadata: name: web-demo spec: replicas: 2 selector: matchLabels: app: web-demo template: metadata: labels: app: web-demo spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: web-demo-svc spec: selector: app: web-demo ports: - port: 80 targetPort: 80 type: NodePort EOF查看Pod调度情况:
kubectl get pods -o wide kubectl get svc web-demo-svcNodePort类型会为Service分配一个30000-32767范围内的随机端口,比如查出来是NodePort 31234,那么通过http://192.168.31.100:31234就能访问到这个Web服务。
这一步如果通了,说明从kube-apiserver到Scheduler到kubelet到containerd到Calico到kube-proxy的整个链条都是通畅的。这套集群已经具备日常使用的条件了。
6.3 有状态应用挂载存储的补充建议
如果你需要部署数据库这类有状态应用,单master集群的存储方案可以直接用hostPath或者local volume。因为只有一个节点,不需要考虑跨节点的数据同步。比如用local provisioner动态创建PV,比hostPath更规范,管理也方便。
单master场景下,数据就这么一份,备份策略就显得格外重要。我在这个集群上跑了一个数据库,每天用crontab把数据库目录打包,同步到另一台机器上。因为集群本身没有高可用能力,备份就是最后一道防线,这话一点不夸张。
7. 常见问题排查与避坑实录
7.1 问题排查前的通用套路
K8s的故障排查有个基本顺序:先从最底层看,逐层往上。具体来说,就是先看节点状态、再看系统Pod状态、再看业务Pod状态、最后看日志和事件。
# 1. 节点状态 kubectl get nodes # 2. 系统组件Pod状态 kubectl get pods -n kube-system # 3. 业务Pod状态 kubectl get pods -A # 4. 查看具体Pod的详细信息 kubectl describe pod <pod-name> -n <namespace> # 5. 查看容器日志 kubectl logs <pod-name> -n <namespace>describe里的事件(Events)信息是最有价值的,它会明确告诉你为什么Pod没有启动,比如镜像拉取失败、探针失败、端口冲突等。
7.2 镜像拉取失败
症状:Pod一直处于ImagePullBackOff状态,事件显示拉取镜像失败。
排查方向:先确认镜像地址有没有打错,然后在节点上手动拉一次相同镜像:
crictl pull nginx:1.25如果手动也失败,多半是网络问题或者镜像源不受信任。给containerd配置的mirror地址确认有没有生效,重新生成配置并重启containerd。还有一个容易被忽略的点:如果使用的是私有镜像仓库,需要在containerd配置里添加registry的auth配置,不然始终会报拉取认证失败。
7.3 kubelet一直处于等待注册状态
症状:kubelet进程存在,但kubectl get nodes看不到节点,或者节点状态一直是NotReady。
排查方向:查看kubelet日志:
journalctl -u kubelet -f我遇到过的最常见原因是kubelet的cgroupDriver和containerd不一致,报错信息类似failed to run Kubelet: failed to create kubelet component。解决办法就是前面说的,确认两边都改成systemd,然后重启containerd和kubelet。
还有一种情况是证书过期。单master搭建好以后长期不维护,过了一年证书过期了,kubelet和API Server之间的通信就断了。此时需要执行kubeadm certs renew all来更新证书,然后重启相关组件。所以证书有效期这个事,建议大家提前写在运维备忘里。
7.4 CoreDNS CrashLoopBackOff
症状:CoreDNS Pod反复重启,节点迟迟不Ready。
这个问题的原因非常明确,就是没有装CNI网络插件。CoreDNS需要监听Pod IP,Pod网络插件没到位,它起不来是正常的。装了Calico或Flannel之后它会自动恢复。
如果装了网络插件还是CrashLoopBackOff,查看日志确认是不是解析外部域名超时。这类问题通常是DNS配置不对或者上游DNS不可达,把CoreDNS的ConfigMap里的forward指向公共DNS有时候能解决:
forward . 223.5.5.57.5 端口占用导致API Server无法启动
症状:kubeadm init执行到一半失败,日志显示6443端口被占用。
这个我遇到过,原因是你之前可能装过测试性质的K8s组件,或者别的服务占用了6443。解决办法是把占用端口的进程找出来杀掉,然后清理掉之前的残留数据:
netstat -tlnp | grep 6443 kubeadm reset -f rm -rf /etc/cni/net.dkubeadm reset是很有用的清理命令,执行后可以重新走init流程。建议每次初始化失败后都先清理再重试,不要原地反复执行init,环境会越来越乱。
8. 使用小技巧与个人体会
集群搭建完成只是第一步,日常使用中的顺手程度才是让这套环境真正发挥价值的关键。
首先讲一下命名空间隔离的重要性。我在这台单master上跑了开发、测试、个人项目好几种不同用途的负载,一开始全部放在default命名空间里,后来随着资源增多,管理变得混乱。为不同项目分配不同命名空间,配合ResourceQuota限制资源用量,现在清晰了很多。虽然单master硬件资源有限,但通过命名空间划分,一套集群模拟了多个环境,边际成本几乎为零。
其次,控制平面组件和业务Pod在同一台机器上,资源竞争是不可避免的。我个人的做法是给系统关键组件(kube-apiserver、etcd)设置较高的CPU资源和内存限额,在节点资源紧张时,优先保证控制平面稳定。具体做法是在部署业务Deployment时,给Pod设置合理的requests和limits,不要不设置就盲目部署,否则kubelet在调度时有可能把节点的CPU和内存全部占满,拖垮整个集群。
还有一个小技巧,建议把k8s的自动补全加上,平时敲命令效率能提升不少:
echo "source <(kubectl completion bash)" >> ~/.bashrc source ~/.bashrc最后说一句关于监控。单master集群因为硬件有限,跑完整套Prometheus + Grafana可能会有点吃力,但至少要保证核心组件的存活监控。最轻量级的方案是给kube-apiserver和kubelet加探活脚本,发现异常就告警通知。有了这套基本保障,你的“单点”才不至于成为“盲点”。
这套集群我在用了大半年后,才逐步扩展到多节点。但直到今天,我依然觉得单master搭建的过程是一个值得认真走一遍的节点,因为配置过程中的每一个参数、每一步操作,都是在为以后理解更复杂的生产架构打地基。如果你正在犹豫要不要从零开始手搭一套,我的建议非常简单:直接开干。踩几个坑,把问题磨平,你对K8s的理解会上一个台阶。