1. 为什么我选择在裸机上直接部署K8s与KubeSphere
1.1 从“能跑就行”到“跑得稳”的认知转变
刚接触容器编排那会儿,我和很多人一样,觉得用kubeadm敲几行命令把集群拉起来就算完事了。直到有一次线上环境的一个节点突然失联,排查了半天才发现是containerd的socket路径和kubelet配置对不上,导致整个节点上的Pod全部漂移失败。那次事故让我意识到,部署K8s不是终点,而是一个需要精细化设计的起点。
后来我开始系统性地研究生产级部署方案,发现大多数教程要么停留在“单机MiniKube”的玩具阶段,要么直接跳到云厂商的托管服务,中间那块自建高可用集群的空白地带反而很少有人讲透。而KubeSphere的出现,恰好填补了K8s原生Dashboard功能孱弱、可观测性配置繁琐的痛点。它把Prometheus、Grafana、Jenkins、Istio这些组件做了开箱即用的集成,对于中小团队来说,能省下至少两周的脚手架搭建时间。
这篇文章我想聊的,就是如何在裸金属服务器或虚拟机上,从零搭建一套带KubeSphere的K8s集群。内容会覆盖系统初始化、容器运行时选型、高可用拓扑设计、网络插件对比、存储方案选型,以及KubeSphere的安装避坑指南。适合那些手头有3台以上机器、想自己掌控集群全生命周期、又不愿意在重复造轮子上浪费时间的运维或后端开发者。
1.2 部署前的核心决策:版本、拓扑与组件选型
在动手之前,有几个决策点必须先定下来,否则装到一半发现版本不兼容,回滚的成本极高。
K8s版本的选择。截至我写这篇内容时,K8s 1.26到1.28是相对稳定的区间。1.26开始移除了对Docker Shim的直接支持,这意味着如果你坚持用Docker作为运行时,需要额外安装cri-dockerd。我的建议是直接上containerd,性能更好,调用链更短,而且KubeSphere 3.4以上版本对containerd的支持已经很成熟了。如果你用的是KubeSphere 4.0,它底层对应的是K8s 1.28,这个组合我实测下来最稳。
集群拓扑的设计。生产环境至少需要3台Master节点来实现etcd和控制平面的高可用。Worker节点的数量取决于你的业务负载,但建议至少2台起步,方便做Pod反亲和性调度。如果资源紧张,可以用3台机器做混合部署(Master和Worker复用),但要在kubelet启动参数里预留足够的资源给系统组件,否则业务Pod一压测,API Server就可能因为CPU饥饿而响应超时。
网络插件的取舍。Calico和Flannel是两大主流。Flannel配置简单,但缺乏网络策略支持;Calico功能全面,支持BGP路由反射和细粒度的NetworkPolicy,但配置复杂度高。如果你的团队有网络工程师背景,选Calico;如果只是想快速跑起来,Flannel的VXLAN模式足够用。我个人的习惯是用Calico的IPIP模式,在跨子网通信时性能损耗比VXLAN小,而且排查路由问题时有calicoctl工具辅助,比Flannel的flanneld日志清晰得多。
存储方案的考量。裸机环境没有云厂商的块存储,所以必须自己搭一套。NFS是最简单的选择,但性能受限于网络带宽和单点故障。如果追求性能,可以考虑Longhorn或OpenEBS,它们能把本地磁盘聚合成分布式块存储。我试过在3节点集群上用Longhorn跑MySQL,随机写IOPS大概在3000左右,对于中小型业务够用了。但要注意,Longhorn的副本机制会占用额外磁盘空间,规划容量时记得乘以副本数。
2. 系统初始化与容器运行时配置的实操细节
2.1 操作系统层面的调优与内核参数
拿到机器后的第一件事不是急着装Docker或containerd,而是把系统基础环境打磨到位。我踩过最深的坑是:集群跑了一周后,某个节点突然NotReady,查日志发现是kubelet报too many open files。这就是典型的ulimit没调优导致的。
先看主机名和Hosts解析。K8s的节点发现依赖主机名,所以每台机器的hostname必须唯一且能互相解析。我习惯用k8s-master-01、k8s-worker-01这种命名规范,然后在每台机器的/etc/hosts里写全所有节点的映射。虽然可以用DNS,但裸机环境里Hosts文件是最可靠的兜底方案。
# 设置主机名(以master-01为例) hostnamectl set-hostname k8s-master-01 # 编辑/etc/hosts,加入所有节点 cat >> /etc/hosts << EOF 192.168.1.10 k8s-master-01 192.168.1.11 k8s-master-02 192.168.1.12 k8s-master-03 192.168.1.20 k8s-worker-01 192.168.1.21 k8s-worker-02 EOF接下来是关闭Swap。K8s的设计假设是节点内存充足,Swap会导致Pod的内存限制失效,引发OOM Killer误判。虽然可以通过--fail-swap-on=false强制忽略,但生产环境不建议这么做。
swapoff -a sed -i '/swap/s/^/#/' /etc/fstab然后是内核模块加载。overlay和br_netfilter是containerd和Calico依赖的模块,必须确保开机自动加载。
cat > /etc/modules-load.d/k8s.conf << EOF overlay br_netfilter EOF modprobe overlay modprobe br_netfilter网络参数的调整同样关键。net.bridge.bridge-nf-call-iptables必须设为1,否则Service的ClusterIP无法正常转发。net.ipv4.ip_forward也要开启,这是Pod跨节点通信的基础。
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注意:有些发行版默认的
fs.file-max只有65535,对于跑大量Pod的节点来说远远不够。建议在/etc/security/limits.conf里把nofile调到1048576,并确保systemd的DefaultLimitNOFILE也同步修改,否则重启后kubelet还是会继承旧值。
2.2 containerd的安装与关键配置项解读
为什么不用Docker?因为K8s 1.24之后,Docker Shim被移出了主仓库,继续用Docker需要额外维护cri-dockerd这个中间层,增加了故障点。containerd本身就是CNCF的毕业项目,调用链更短,资源占用更低。
安装containerd最稳妥的方式是从官方仓库拉取二进制包。我一般用containerd-1.7.x这个版本,它对K8s 1.28的兼容性最好。
# 下载并解压 wget https://github.com/containerd/containerd/releases/download/v1.7.13/containerd-1.7.13-linux-amd64.tar.gz tar Cxzvf /usr/local containerd-1.7.13-linux-amd64.tar.gz # 生成systemd服务文件 wget https://raw.githubusercontent.com/containerd/containerd/main/containerd.service -O /etc/systemd/system/containerd.service systemctl daemon-reload systemctl enable --now containerd接下来是生成默认配置并修改。containerd config default会输出一份完整的配置,但有几个关键项必须改。
mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml打开/etc/containerd/config.toml,找到SystemdCgroup这一项,把它从false改成true。这一步非常关键,因为K8s的kubelet默认使用systemd cgroup驱动,如果containerd用cgroupfs,两者会冲突,导致Pod启动时反复报failed to create containerd task。
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true还有一个容易忽略的点:sandbox镜像的地址。默认配置里用的是registry.k8s.io/pause:3.8,国内环境可能拉不下来。可以换成你私有仓库的地址,或者提前把镜像导入到本地。
[plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.9"改完后重启containerd,用ctr version验证一下。
systemctl restart containerd ctr version2.3 kubelet、kubeadm、kubectl的版本对齐策略
这三个组件的版本必须严格一致,否则kubeadm init时会直接报错退出。我习惯用apt或yum的版本锁定功能,避免不小心升级到不兼容的版本。
# 添加K8s仓库 curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' > /etc/apt/sources.list.d/kubernetes.list # 安装指定版本 apt update apt install -y kubelet=1.28.2-1.1 kubeadm=1.28.2-1.1 kubectl=1.28.2-1.1 apt-mark hold kubelet kubeadm kubectlapt-mark hold这一步很重要,防止后续apt upgrade时把K8s组件意外升级。我见过有人因为自动升级导致kubelet版本跳到1.29,而kubeadm还是1.28,结果整个集群的API Server起不来。
实操心得:如果你的机器无法访问外网,可以提前在有网的机器上用
apt download把deb包下载下来,然后拷贝到目标机器上用dpkg -i安装。注意依赖关系,kubelet依赖conntrack和ebtables,这些也要一并下载。
3. 高可用集群的初始化与网络插件落地
3.1 kubeadm init的配置文件与高可用策略
kubeadm init是集群诞生的时刻,但直接敲命令行参数容易漏掉关键配置。我习惯写一个kubeadm-config.yaml,把所有定制项固化下来,方便后续审计和复现。
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 bindPort: 6443 nodeRegistration: criSocket: unix:///run/containerd/containerd.sock kubeletExtraArgs: cgroup-driver: systemd --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 controlPlaneEndpoint: "k8s-api.example.com:6443" networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/12" apiServer: certSANs: - "k8s-api.example.com" - "192.168.1.10" - "192.168.1.11" - "192.168.1.12"这里有几个关键点。controlPlaneEndpoint必须是一个统一的访问入口,可以是VIP(用Keepalived+HAProxy实现),也可以是DNS轮询。我推荐用HAProxy做四层负载,Keepalived做VIP漂移,这样任何一个Master节点宕机,API Server的访问都不会中断。
certSANs里要把所有Master节点的IP和VIP的域名都加进去,否则kubectl从其他节点访问时会报证书不匹配。
初始化命令如下:
kubeadm init --config kubeadm-config.yaml --upload-certs--upload-certs会把证书加密后上传到etcd,这样后续加入Master节点时可以用--control-plane和--certificate-key自动拉取证书,不用手动拷贝。
初始化完成后,按照提示配置kubectl:
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config3.2 Calico的安装与BGP模式调优
网络插件我选Calico,因为它的IPIP模式在跨子网通信时性能损耗比VXLAN小,而且支持NetworkPolicy。安装方式用Tigera Operator,比直接apply manifest更优雅。
# 安装Operator kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml # 配置Calico cat > custom-resources.yaml << EOF apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: calicoNetwork: ipPools: - blockSize: 26 cidr: 10.244.0.0/16 encapsulation: IPIP natOutgoing: Enabled nodeSelector: all() EOF kubectl create -f custom-resources.yamlblockSize: 26意味着每个节点分配一个/26的地址块,也就是64个IP。对于大多数节点来说够用了,如果节点上Pod数量超过64,可以调成/25。
安装完成后,用kubectl get pods -n calico-system检查Pod状态。如果calico-node一直CrashLoopBackOff,大概率是IP_AUTODETECTION_METHOD没配对。Calico默认会自动探测网卡,但在多网卡环境下可能选错。可以在tigera-operator的DaemonSet里加环境变量:
env: - name: IP_AUTODETECTION_METHOD value: "interface=eth0"注意:如果你的节点在不同的子网,IPIP模式需要确保
ipip内核模块已加载。用lsmod | grep ipip检查,如果没有就modprobe ipip。
3.3 Worker节点的加入与标签管理
Worker节点的加入相对简单,在Master上生成token:
kubeadm token create --print-join-command然后在Worker节点上执行输出的命令。加入后,用kubectl get nodes确认状态为Ready。
接下来是打标签。K8s的调度器依赖标签来做亲和性调度,我习惯给节点打上角色标签:
kubectl label node k8s-worker-01 node-role.kubernetes.io/worker= kubectl label node k8s-worker-01 topology.kubernetes.io/zone=zone-atopology.kubernetes.io/zone这个标签在跨机架部署时特别有用,可以配合Pod反亲和性,让副本分散在不同的物理区域。
如果某个节点是SSD,可以打上disktype=ssd的标签,然后在PVC的StorageClass里用nodeSelector指定,确保高性能存储落在SSD节点上。
4. KubeSphere的安装与核心功能验证
4.1 基于Helm的KubeSphere最小化安装
KubeSphere的安装方式有ks-installer和Helm两种。ks-installer适合全新集群,Helm适合已有集群的增量安装。我选Helm,因为可控性更强,升级和回滚都方便。
先添加Helm仓库:
helm repo add kubesphere https://charts.kubesphere.io/main helm repo update然后创建命名空间并安装:
kubectl create ns kubesphere-system helm install ks-core kubesphere/ks-core -n kubesphere-system安装完成后,用kubectl get pods -n kubesphere-system查看Pod状态。ks-apiserver和ks-console是核心组件,必须Running。ks-controller-manager负责CRD的协调,如果它起不来,后续启用可观测性插件会失败。
默认的NodePort是30880,用http://<任意节点IP>:30880访问。初始账号是admin,密码是P@88w0rd。第一次登录会强制修改密码。
实操心得:KubeSphere的安装过程中,
ks-apiserver可能会因为Redis连接超时而反复重启。这是因为ks-installer默认部署的Redis是单副本,如果节点资源紧张,Redis启动慢,ks-apiserver就会探活失败。解决办法是在ks-core的values里把Redis的livenessProbe的initialDelaySeconds调大,比如从10秒改成30秒。
4.2 可观测性组件的启用与资源规划
KubeSphere最吸引我的地方是开箱即用的可观测性。但默认安装只启用了基础功能,Prometheus、Grafana、Alertmanager这些需要手动开启。
在ks-installer的ConfigMap里,把monitoring的enabled改成true:
kubectl edit cm ks-installer -n kubesphere-system找到monitoring部分:
monitoring: enabled: true prometheusReplicas: 1 prometheusMemoryRequest: 400Mi prometheusVolumeSize: 20Gi这里有个资源规划的坑。Prometheus默认只给400Mi内存,如果集群里Pod数量超过200个,Prometheus很容易OOM。我建议至少给2Gi,prometheusVolumeSize至少50Gi,否则历史数据几天就写满了。
存储方面,如果之前没配StorageClass,Prometheus的PVC会一直Pending。可以用NFS做动态供给:
helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ --set nfs.server=192.168.1.100 \ --set nfs.path=/data/k8s \ -n kube-system安装完Provisioner后,创建一个默认的StorageClass:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client annotations: storageclass.kubernetes.io/is-default-class: "true" provisioner: k8s-sigs.io/nfs-subdir-external-provisioner4.3 多租户与DevOps流水线的初步配置
KubeSphere的多租户模型是基于Workspace、Namespace和User的三层结构。Workspace是租户的边界,Namespace是资源隔离的单位,User可以绑定到不同的Workspace并分配角色。
创建Workspace后,进入“企业空间”->“项目管理”,创建一个项目(本质是Namespace)。然后在“应用负载”里部署一个测试应用,验证网络和存储是否正常。
DevOps流水线需要单独启用。在ks-installer里把devops的enabled改成true,然后重启ks-installer的Pod。Jenkins会以Pod的形式在kubesphere-devops-system命名空间里启动。
注意:Jenkins的Pod默认需要2Gi内存,如果节点资源不足,它会一直Pending。可以提前给节点打上
node-role.kubernetes.io/devops的标签,然后用nodeSelector把Jenkins调度到资源充足的节点上。
流水线的配置界面比较直观,但Jenkinsfile的语法需要熟悉。我建议先用“图形化编辑”生成一个简单的流水线,然后切换到“Jenkinsfile”模式,看看它生成的代码结构,再基于此修改。
5. 部署后的稳定性验证与常见故障排查
5.1 集群健康检查的必查项
集群装完后,别急着上业务,先做一轮健康检查。我整理了一个检查清单,每次部署新集群都会过一遍。
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 节点状态 | kubectl get nodes | 所有节点Ready |
| 系统Pod | kubectl get pods -n kube-system | 全部Running |
| 核心组件 | kubectl get componentstatuses | scheduler/controller-manager/etcd Healthy |
| DNS解析 | kubectl run test --image=busybox -- nslookup kubernetes | 返回ClusterIP |
| 网络连通 | kubectl exec test -- ping <另一节点PodIP> | 能通 |
| 存储供给 | kubectl get sc | 有默认StorageClass |
| API延迟 | kubectl get --raw /metrics | grep apiserver_request_duration | P99小于1秒 |
DNS解析是最容易出问题的环节。CoreDNS的Pod如果起不来,所有Service的域名解析都会失败。常见原因是kubelet的resolvConf配置指向了错误的/etc/resolv.conf,导致CoreDNS自己无法解析上游DNS。可以在kubelet的启动参数里加--resolv-conf=/run/systemd/resolve/resolv.conf,指向正确的文件。
5.2 节点NotReady的排查路径
节点NotReady是运维中最常见的问题。排查思路可以总结为从下往上:先看kubelet服务状态,再看容器运行时,最后看网络。
# 1. 检查kubelet服务 systemctl status kubelet # 2. 查看kubelet日志 journalctl -u kubelet -n 100 --no-pager # 3. 检查containerd systemctl status containerd crictl ps # 4. 检查网络插件 kubectl get pods -n calico-system -o wide如果kubelet日志里出现failed to get node ip,通常是--node-ip没指定,或者节点有多个网卡,kubelet选错了。可以在kubelet的extraArgs里显式指定--node-ip=192.168.1.20。
如果crictl ps报connection refused,说明containerd的socket路径不对。检查/etc/containerd/config.toml里的address是否和kubelet的--container-runtime-endpoint一致。
5.3 KubeSphere控制台无法访问的应急处理
KubeSphere控制台打不开,先别急着重装。按以下顺序排查:
检查ks-console的Pod状态。如果是CrashLoopBackOff,看日志里有没有
connection refused到ks-apiserver。这通常是ks-apiserver没起来导致的连锁反应。检查ks-apiserver的日志。常见错误是
failed to connect to redis。KubeSphere用Redis做会话缓存,如果Redis的Service名称或端口配错了,ks-apiserver就会启动失败。检查NodePort是否被占用。
netstat -tlnp | grep 30880,如果端口被其他进程占了,改一下ks-console的Service类型为ClusterIP,然后用kubectl port-forward临时访问。检查Ingress Controller。如果用的是域名访问,确认Ingress的规则是否正确,以及
ingress-nginx的Pod是否正常。
避坑技巧:KubeSphere的安装过程中,
ks-installer会创建一个Job来执行初始化任务。如果这个Job失败了,整个安装流程会卡住。可以用kubectl get jobs -n kubesphere-system查看Job状态,如果失败,用kubectl logs job/ks-installer看具体报错。常见原因是镜像拉取失败,可以提前把需要的镜像导入到本地仓库。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| Pod一直Pending | 资源不足或没有匹配的节点 | kubectl describe pod看Events |
| Pod一直ContainerCreating | 网络插件未就绪或镜像拉取失败 | 检查CNI Pod状态和镜像仓库连通性 |
| Service无法访问 | kube-proxy未正常工作 | 检查kube-proxy日志和iptables规则 |
| PVC一直Pending | 没有默认StorageClass | 创建StorageClass或手动指定 |
| KubeSphere登录后白屏 | 浏览器缓存或ks-apiserver异常 | 清缓存,检查ks-apiserver日志 |
| 节点磁盘压力 | 镜像和日志占满磁盘 | 配置kubelet的imageGCHighThresholdPercent |
6. 一些关于长期维护的个人体会
集群跑起来只是第一步,长期维护才是真正的考验。我经历过几次因为证书过期导致集群不可用的情况,K8s的证书默认有效期是一年,kubeadm安装的证书到期后,kubelet会拒绝连接API Server。建议在集群上线后,立刻配置证书自动轮换,或者用kubeadm certs renew定期手动续期。
另一个体会是监控告警的优先级。很多人装完KubeSphere就放着不管了,直到业务出问题才去看监控。我的做法是,集群上线第一天就把Alertmanager的告警通道配好,至少把NodeDown、DiskPressure、APIServerLatency这三个告警规则启用。这样节点一有问题,手机就能收到通知,不用等用户投诉才发现。
最后说一个关于备份的细节。etcd的数据是集群的“大脑”,丢了就全完了。我习惯用etcdctl snapshot save每天凌晨做一次快照,然后传到对象存储里。恢复的时候用etcdctl snapshot restore,注意要停掉所有Master节点的kube-apiserver,恢复完再逐个启动。这个过程我演练过三次,每次都能在15分钟内把集群恢复到前一天的状态。
这套部署方案我在不同的环境里复现过五六次,从3节点的测试集群到10节点的生产集群都跑过。KubeSphere的版本迭代很快,每次升级前记得先看官方的Release Notes,特别是CRD的变更,有些版本会废弃旧的API版本,直接升级会导致自定义资源无法识别。如果拿不准,先在测试环境里用helm upgrade --dry-run跑一遍,确认没有报错再上生产。