运维知识
悠悠
2026年8月24日

3台机器30分钟,我用K3s撸了一套生产可用的K8s集群(附4个致命坑)

手头3台CentOS虚机,本来准备跑Kafka测试集群的,结果领导临时说要验证一下容器化部署方案。正经用kubeadm搭K8s?光etcd和各种证书问题就够折腾半天的。果断上K3s——一个二进制文件解决战斗。

但真跑起来才发现,"轻量"不等于"没坑"。从节点加入报x509证书错误、重启服务直接卡死、镜像拉取超时……记录一下完整的部署过程和4个差点让我砸键盘的问题。

环境说明

节点主机名IP角色
Masterkafka-node1192.168.198.201Server(控制平面)
Worker 1kafka-node2192.168.198.202Agent
Worker 2kafka-node3192.168.198.203Agent

操作系统是 CentOS/Rocky Linux 系列,K3s版本 v1.36.3+k3s1

部署Master节点

K3s的安装体验确实丝滑。在kafka-node1上一行搞定:

curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \
  INSTALL_K3S_MIRROR=cn \
  RANCHER_MIRROR_URL="https://rancher-mirror.rancher.cn/k3s" \
  sh -

这里用的是Rancher的国内镜像源,直接从GitHub拉脚本的话大概率超时。装完之后 systemctl status k3s 看一眼是active就行。

拿Token,后面Worker加入要用:

cat /var/lib/rancher/k3s/server/node-token

输出一长串字符,复制好备用。顺便说一句,这个token文件权限是600,root才能读,安全性上还行。

部署Worker节点

Worker节点加入集群理论上也是一行命令的事,但国内网络环境下我建议先手动下载K3s二进制文件,不然安装脚本去GitHub下载binary能卡到天荒地老。

手动放置二进制文件

在kafka-node2和kafka-node3上都执行:

mkdir -p /usr/local/bin/
curl -Lo /usr/local/bin/k3s https://rancher-mirror.rancher.cn/k3s/v1.36.3+k3s1/k3s
chmod +x /usr/local/bin/k3s

验证一下:

k3s --version
# 输出: k3s version v1.36.3+k3s1

注册加入集群

二进制文件到位后,跑安装脚本时它会自动识别本地已有的k3s文件,跳过下载直接配置systemd服务:

curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \
  K3S_URL=https://192.168.198.201:6443 \
  K3S_TOKEN=<你的Node-Token> \
  sh -

注意K3S_URL 一定要带,不带的话安装脚本会把这台机器当Server启动——这就是我踩的第一个坑,下面会讲。

打标签验证

回到Master节点,给Worker打上角色标签:

kubectl label node kafka-node2 node-role.kubernetes.io/worker=worker
kubectl label node kafka-node3 node-role.kubernetes.io/worker=worker

不打标签其实不影响调度,但 kubectl get nodes 的时候ROLES那一列会显示 <none>,看着不舒服:

kubectl get nodes

正常输出应该三个节点全部Ready:

NAME          STATUS   ROLES                  AGE   VERSION
kafka-node1   Ready    control-plane,master   10m   v1.36.3+k3s1
kafka-node2   Ready    worker                 5m    v1.36.3+k3s1
kafka-node3   Ready    worker                 5m    v1.36.3+k3s1

到这里集群就跑起来了。但实际操作中,我在kafka-node2和node3加入的过程中连续撞了4个问题。

踩坑记录:4个差点让人放弃的故障

坑1:Worker装成了Server,6444端口冲突

现象:kafka-node2执行完安装脚本后,systemctl status k3s 一看进程是 /usr/local/bin/k3s server——不对啊,这台应该是agent才对。日志里一堆报错:

listen tcp 127.0.0.1:6444: bind: address already in use

原因:第一次安装时手抖漏了 K3S_URL 环境变量。安装脚本的逻辑是这样的——检测到没有 K3S_URL 就认为你要启动一个独立的Server节点。

修复

# 先卸载干净
/usr/local/bin/k3s-uninstall.sh

# 杀残留进程,释放端口
pkill -9 -f k3s
fuser -k 6444/tcp

# 确认端口释放了
ss -tlnp | grep 6444

然后重新跑带 K3S_URL 的安装命令。这里有个细节:k3s-uninstall.sh会把 /usr/local/bin/k3s 也删掉,所以还得重新下载一遍binary。我第二次就学聪明了,先下binary再跑安装脚本。

坑2:x509证书时间校验失败

现象:Agent启动后一直连不上Server,journalctl -u k3s-agent 里反复刷这个错:

CA cert validation failed: x509: certificate is not valid before [某个未来时间]
current time [Worker当前时间] is before [证书签发时间]

原因:这破虚机的时间不知道什么时候飘了,比Master节点慢了好几分钟。K3s的Server在安装时会自动生成CA证书,证书的 Not Before 字段是Master的当前时间。Worker的系统时间落后于这个值,TLS握手直接就拒绝了。

说白了就是时钟不同步

修复

关键点在于,当时间偏差比较大的时候,chronyd默认不会自动跳跃纠正(它只做缓慢调整)。得手动强制同步:

# 停掉chronyd避免冲突
systemctl stop chronyd

# 强行对齐Master节点的时间
chronyd -q 'server kafka-node1 iburst'

# 验证一下时间
date

# 重新启动时间同步
systemctl start chronyd

# 重启agent
systemctl restart k3s-agent

如果你的环境里几台机器都没配NTP源,建议在Master上先搞好时间,然后所有Worker都以Master为NTP server同步。或者更粗暴:

# 直接用ntpdate一步到位(如果装了的话)
ntpdate kafka-node1

预防措施:部署前先在所有节点跑一遍 timedatectl 确认时间一致,差个几秒没事,差几分钟就会出这个问题。

坑3:镜像拉取失败,Pod一直ContainerCreating

现象:节点Ready了,deploy也创建了,但Pod卡在 ContainerCreating 状态不动。kubectl describe pod 一看Events:

Failed to pull image "rancher/mirrored-pause:3.10.2": 
  connect: connection refused

连基础的pause镜像都拉不下来。

原因:K3s用的容器运行时是内置的containerd,不是Docker。containerd的镜像拉取走的是自己的配置,跟你系统里装的Docker daemon mirrors完全无关。默认去 docker.io 拉镜像,国内裸连基本没戏。

修复

所有节点创建 /etc/rancher/k3s/registries.yaml

mirrors:
  "docker.io":
    endpoint:
      - "https://docker.m.daocloud.io"
      - "https://docker.nju.edu.cn"

然后重启对应的服务:

# Master节点
systemctl restart k3s

# Worker节点
systemctl restart k3s-agent

如果已经有Pod卡住了,可以用crictl手动拉一下pause镜像再打tag:

crictl pull docker.m.daocloud.io/rancher/mirrored-pause:3.10.2
ctr -n k8s.io image tag \
  docker.m.daocloud.io/rancher/mirrored-pause:3.10.2 \
  docker.io/rancher/mirrored-pause:3.10.2

打完tag之后那些卡住的Pod会自动恢复。

补充:DaoCloud的镜像源现在有白名单限制了,不是所有镜像都能加速。如果碰到某些业务镜像还是拉不到,考虑自建Harbor或者用阿里云ACR做中转。

坑4:systemctl restart k3s-agent 直接卡死

现象:执行重启命令后终端完全没反应,Ctrl+C都中断不了,只能新开终端窗口。

原因:之前的Agent进程可能处于死锁状态或者卡在网络重试循环里,systemd在等待它优雅退出(默认90秒超时)。如果进程完全不响应SIGTERM,就会一直卡住。

修复

新开一个终端:

# 暴力杀进程
pkill -9 -f k3s

# 清理Agent的缓存数据(可选但推荐)
rm -rf /var/lib/rancher/k3s/agent/data

# 重新启动
systemctl start k3s-agent

清理 /var/lib/rancher/k3s/agent/data 目录会让Agent重新下载一些运行时数据,启动会慢几秒但能解决很多奇怪的状态残留问题。

改进建议:可以修改systemd的stop超时时间,免得每次都卡90秒。编辑 /etc/systemd/system/k3s-agent.service(或者创建override):

[Service]
TimeoutStopSec=30

然后 systemctl daemon-reload。30秒杀不死就直接SIGKILL了,不至于干等。

服务暴露:让外面能访问到集群里的应用

集群跑起来了,部署个nginx测试一下外部访问。先创建一个Pod:

kubectl run test-nginx --image=nginx:alpine --port=80

NodePort方式

最简单直接,开一个节点端口映射:

kubectl expose pod test-nginx --type=NodePort --port=80 --target-port=80 --name=nginx-nodeport
kubectl patch svc nginx-nodeport --type='json' \
  -p='[{"op": "replace", "path": "/spec/ports/0/nodePort", "value":30080}]'

访问 http://192.168.198.201:30080 就能看到nginx欢迎页。端口范围30000-32767,适合快速验证和非HTTP类服务。

LoadBalancer方式

K3s自带了Klipper LoadBalancer(ServiceLB),不需要额外装MetalLB:

kubectl expose pod test-nginx --type=LoadBalancer --port=80 --target-port=80 --name=nginx-lb

创建完之后 kubectl get svc 看EXTERNAL-IP会显示节点IP。直接访问 http://192.168.198.201 或任意节点IP的80端口就行。

注意:Klipper的原理是在每个节点上启动一个daemonset做端口转发,所以会占用宿主机的80端口。如果你的节点上跑着其他web服务占了80,会冲突。

Traefik Ingress(推荐)

生产环境还是Ingress最合适,K3s默认就装了Traefik,直接写Ingress规则:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
spec:
  rules:
  - host: nginx.k3s.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: test-nginx-svc
            port:
              number: 80

本地hosts文件加一条 192.168.198.201 nginx.k3s.local,然后浏览器访问 http://nginx.k3s.local 就通了。多个服务按域名或路径分发,干净利落。

排障速查表

故障现象排查方向修复命令
Worker启动为Server检查是否传了K3S_URLk3s-uninstall.sh → 重新安装
x509证书未生效检查节点间时间差chronyd -q 'server <master> iburst'
镜像拉取超时/refused检查registries.yaml配置国内mirror + 重启服务
restart命令卡死Agent进程死锁pkill -9 -f k3s → 清缓存 → 重启
Pod一直Pending检查节点资源和taintkubectl describe node 看Conditions
节点NotReady检查agent日志journalctl -u k3s-agent -f

写在后面

K3s确实是目前搭建小规模K8s集群最省事的方案,一个binary包含了containerd、Flannel、CoreDNS、Traefik、metrics-server全家桶,512MB内存的Agent节点都能跑。对于开发测试环境、边缘计算、IoT网关这些场景,比kubeadm那套东西舒服太多。

但"轻量"带来的代价是排障时能找到的社区资料相对少,很多问题的错误信息不太直观。特别是时钟同步和镜像源这两个国内环境的"特色"问题,官方文档基本不会提,全靠自己踩。

希望这篇能帮你少走几步弯路。集群搭好之后下一步该考虑的是持久化存储(local-path还是NFS)和监控告警(Prometheus全家桶),后面有机会再写。


我是「运维躬行录」,专注分享云计算和运维的生产实践经验。如果这篇文章对你有帮助,帮忙点个赞、转发一下。
关注公众号:耕云躬行录
个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码