
我在实际工作中见过太多人把 Kubernetes 集群搭起来之后就丢给测试环境跑流水线开发人员只能在本地开 Minikube或者干脆连不上集群日志排一次障要翻好几个窗口。这次我想聊一聊在 CentOS Stream 10 上从零搭建 Kubernetes 集群并且把开发调试和日志查看这两块也一起安排明白。这篇文章的核心是解决“集群建好了人怎么进去看东西、查问题”这个实际诉求。内容覆盖环境准备、集群初始化、开发调试工具链、日志收集方案和常见坑位适合正在做本地开发环境、内部测试集群或者想在自己工作站上搭一套 K8s 环境用来跑微服务的同学参考。你可以把它当成一个可复用的操作手册中间每一步我都标注了为什么这么做参数怎么来的。1. 环境准备与版本选型1.1 为什么选择 CentOS Stream 10CentOS Stream 10 是 RHEL 10 的上游开发版本相比传统 CentOS 7/8它的内核和系统库要新很多对 Kubernetes 这类对内核特性有要求的组件更加友好。比如 K8s 节点需要 cgroup v2、iptables 或 nftables 支持到新版本Containerd 也要求内核不低于 5.x用老系统有时候光环境就折腾半天。Stream 10 的另一个优势是 DNF 包管理器实际是 DNF5对模块化流、AppStream 仓库的处理更成熟安装 containerd、python、gcc 这类基础依赖的速度和稳定性都要好。我在多台机器上实测下来用 Stream 10 初始化 kubeadm 出现版本冲突的概率比 CentOS 7 低很多特别是网络插件和 kubelet 的兼容性上。不过要注意CentOS Stream 10 是滚动发行模型它的软件包会持续小步更新。如果你要在生产环境长期跑建议锁定好内核版本和 kubeadm、kubelet 版本并且关闭自动更新避免某天重启后 Kubernetes 组件的兼容性出问题。1.2 节点规划与系统基础配置我的测试环境是三台机器一台作为控制平面Control Plane两台作为工作节点Worker Node都是 CentOS Stream 10内核版本我锁在 6.6 左右内存建议 4GB 以上CPU 至少 2 核。这只是开发调试环境的最低配正式环境按业务量翻倍。系统基础配置我分成几步来做:设置主机名控制节点叫 k8s-master工作节点叫 k8s-node1、k8s-node2每台机器都要配置 /etc/hosts把三台机器的主机名和 IP 对应写进去避免主机名解析出问题关闭 swapKubernetes 的 kubelet 从 1.24 开始强制要求关掉 swap不然无法启动关闭 SELinux或者设置为 permissive 模式这是最省心的方式加载 br_netfilter 和 overlay 内核模块并配置 sysctl 让 iptables 能看透桥接流量# 临时关闭 swap swapoff -a sed -i / swap / s/^/#/ /etc/fstab # 关闭 SELinux setenforce 0 sed -i s/^SELINUXenforcing/SELINUXpermissive/ /etc/selinux/config # 加载内核模块 modprobe overlay modprobe br_netfilter cat EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF # sysctl 配置 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这里的核心点在于bridge-nf-call-iptables如果不开启集群内 Pod 之间的 Service 流量转发会出现丢包或不通的情况。很多新手搭建完集群发现 NodePort 访问不通一半问题是这里没配好。1.3 容器运行时containerd 安装要点Kubernetes 从 1.24 版本开始已经移除对 Docker 的直接支持底层容器运行时直接和 CRI 接口对接。我选 containerd因为它最轻量、稳定也是社区默认推荐的运行时。CentOS Stream 10 的 AppStream 仓库直接提供 containerd但版本可能会比较旧为了稳定建议使用 Kubernetes 官方仓库提供的版本。先把 containerd 装好再生成一份默认配置dnf install -y containerd.io mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml拿到默认配置后需要修改一处关键参数把SystemdCgroup改成true。这是因为 Kubernetes 节点上的 kubelet 默认使用 systemd 作为 cgroup 驱动如果 containerd 使用 cgroupfs两者会对同一份 cgroup 配置打架轻则 Pod 无法分配资源重则节点直接 NotReady。定位到plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options这一段把 SystemdCgroup 改成 true:[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup true改完之后重启 containerd 并设置开机自启顺便检查镜像拉取是否正常:systemctl restart containerd systemctl enable containerd ctr version提示如果拉取镜像非常慢建议检查一下网络和镜像源Kubernetes 初始化时会拉取 control-plane 组件镜像这一步耗时最久也是翻车率最高的地方。2. 集群初始化与网络方案2.1 kubeadm 初始化参数说明kubeadm 是目前主流的集群搭建工具它把证书生成、组件部署、引导 token 管理都自动化了。先把 kubeadm、kubelet、kubectl 装好注意这三个组件的版本要一致我安装的时候 Kubernetes 最新稳定版是 1.31就用这个版本举例。添加仓库然后安装cat EOF | tee /etc/yum.repos.d/kubernetes.repo [kubernetes] nameKubernetes baseurlhttps://pkgs.k8s.io/core:/stable:/v1.31/rpm/ enabled1 gpgcheck1 gpgkeyhttps://pkgs.k8s.io/core:/stable:/v1.31/rpm/repodata/repomd.xml.key EOF dnf install -y kubelet kubeadm kubectl systemctl enable --now kubelet初始化控制平面节点这步是集群搭建的核心。提前规划好 Pod 网段和 Service 网段不要和物理机网段冲突。我习惯用三组网段宿主机网段 192.168.31.0/24Pod 网段 10.244.0.0/16Service 网段 10.96.0.0/12。kubeadm init \ --apiserver-advertise-address192.168.31.100 \ --control-plane-endpoint192.168.31.100 \ --pod-network-cidr10.244.0.0/16 \ --service-cidr10.96.0.0/12 \ --kubernetes-versionv1.31.0为什么要分三段网段因为 kube-controller-manager 会给 Service 自动分配 ClusterIPkube-proxy 会给 Pod 网段维护路由规则一旦网段重叠路由表就会乱套表现出来就是某些 NodePort 时而通时而不通。初始化成功后按照 kubeadm 输出的提示操作mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config然后输出 join 命令等 worker 节点使用kubeadm token create --print-join-command2.2 CNI 网络插件选择集群初始化完成后节点状态会是 NotReady因为还没有部署容器网络接口CNI插件。CNI 负责给每个 Pod 分配 IP、配置路由、打通跨节点通信。可选方案很多Calico、Flannel、Cilium 各有优势我在开发环境选 Flannel因为配置最简单、资源占用低适合内网环境。Flannel 用 VXLAN 模式工作在 Pod 网段内建立一个虚拟二层网络三层封装转发。它在每个节点上起一个 flanneld 进程通过 etcd 或者 Kubernetes API 维护路由信息。对于开发调试来说完全够用但如果生产环境要求网络策略NetworkPolicy、BGP 路由、或者高级负载均衡那应该选 Calico 或 Cilium。部署 Flannelkubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml部署完成后观察节点状态kubectl get nodes kubectl get pods -n kube-flannel -o wide正常情况下几分钟内节点变为 Ready。如果一直卡在 NotReady先看 kubelet 日志通常是 CNI 配置问题或镜像拉取失败。网络插件数据面网络策略适合场景FlannelVXLAN/Host-gw不支持开发、内网测试CalicoBGP/VXLAN支持生产环境CiliumeBPF支持对性能和可观测性要求极高的场景2.3 worker 节点接入与验证工作节点的步骤简单很多配置好 hosts、应用内核模块、关闭 swap、安装 kubeadm/kubelet/containerd然后执行控制节点输出的 join 命令。kubeadm join 192.168.31.100:6443 --token xxxx --discovery-token-ca-cert-hash sha256:xxx加入后回控制节点查看状态kubectl get nodes如果一切正常能看到三个节点都是 Ready。然后再跑一个简单的测试 Pod 验证跨节点通信和 Service 转发kubectl run test --imagenginx kubectl expose pod test --port80 --typeNodePort kubectl get svc test访问任意节点的 NodePort如果都能正常打开 Nginx 页面说明网络和 Service 转发链路都是通的。这里有个小经验在验证 NodePort 时最好确认 NodePort 端口范围默认 30000-32767并且检查安全组和防火墙是否放通我遇到过好几次节点没问题、防火墙把端口挡了的乌龙。3. 开发调试支持配置3.1 开发环境的 kubectl 配置集群搭好了接下来要解决开发人员怎么接入的问题。我最推荐的方式是把控制节点的/etc/kubernetes/admin.conf文件拷贝到开发机器上用 kubeconfig 方式认证访问集群。拷贝之后在开发机上设置好环境变量mkdir -p $HOME/.kube cp admin.conf $HOME/.kube/config kubectl get nodes如果集群有多套建议用--kubeconfig参数或者KUBECONFIG环境变量分别管理。管理多个集群时给每个配置文件命名清楚例如k8s-dev-config、k8s-prod-config别都叫config不然切来切去容易搞混。提示admin.conf是管理员权限如果只是给开发人员日常看日志、调试用可以先创建一个只读角色的 ServiceAccount避免权限过大误删资源。3.2 本地端口转发与快速调试开发调试最常用的功能就是端口转发。在没有对外暴露负载均衡、也不想动 Service 类型的情况下kubectl port-forward能直接把远程 Pod 的端口映射到本地kubectl port-forward -n dev pod/orders-service-7b8b6f9d6c-abcde 8080:8080这样本地访问http://localhost:8080就能直连集群内的 Pod简直不要再方便。开发调试的典型场景是本地只跑 IDE所有依赖服务数据库、Redis、内部 API都在集群里通过端口转发一个个映射到本地。代码要调试的逻辑跑在本地 IDE依赖的服务全部走集群这样既能获得完整环境又保留了本地热部署能力。port-forward支持直接转发 Service 名kubectl port-forward -n dev service/orders-service 8080:8080调试多个服务时可以用--address参数监听指定网卡。如果要看 Pod 内部文件直接通过kubectl exec进入容器里面查看比单独配置调试端口更直接kubectl exec -it -n dev deploy/orders-service -- /bin/sh如果容器里没有 shell还可以用kubectl cp把文件拷贝出来在本地用 IDE 查看日志、配置。3.3 插件工具链kubectx、kubens、krew日常开发调试图省事命令行工具链是加分项。kubectl自带的命令在切换 namespace 时需要记参数管理多个上下文也不够直观。我推荐装上 kubectx、kubens 和 krew。kubectx快速切换集群上下文kubectx dev-clusterkubens快速切换 namespacekubens devkrewkubectl 插件管理器安装 kubectl-debug、access-matrix 等插件kubectx 和 kubens 装完后就是两个 shell 函数但生产力提升立竿见影。特别是同时维护开发、测试、生产三种集群时眼睛看 prompt 就知道当前在哪个环境避免一通kubectl delete删错集群的灾难。krew 装好后我首推kubectl-debug插件。它能在运行中的 Pod 里临时起一个包含调试工具比如 curl、netstat、tcpdump的容器共享目标容器的网络命名空间排障时非常管用kubectl debug -it -n dev pod/orders-service-7b8b6f9d6c-abcde --imagenicolaka/netshoot:latest --targetorders-service这样等于瞬间给业务容器装上了一套“瑞士军刀”网络工具。注意--target参数指定的是被共享命名空间的容器名前提是 Pod 开了shareProcessNamespace或运行时支持临时容器能力Kubernetes 1.23 以后默认支持 ephemeral container 调试。3.4 应用热更新开发模式开发过程中改动代码后想知道效果传统流程是 build 镜像然后 push 到仓库再kubectl rollout restart。这套流程在开发环境太沉重了改一行字要等好几分钟。推荐两种轻量方案第一种是配合 Dockerfile 挂载源码目录到容器。开发模式下用hostPath卷把宿主机代码目录挂载进去容器内跑个 nodemon 或者 spring-boot-devtools 监听文件变化自动重启。这种方式要求代码在集群节点上某处保持同步适合单机测试环境。第二种是使用 Skaffold 或 DevSpace 这类开发工具。它们能监听本地文件变化自动构建镜像、推送到集群内 registry、更新 Deployment。对于 Java 这种构建慢的语言配合 Jib 或 Buildpacks 还能省去写 Dockerfile 的功夫。我在 Go 项目上用 Skaffold 体验很好改动源码后 2-3 秒就能看到新结果。开发调试的核心诉求归结起来就是快速迭代、可控观测、故障隔离。port-forward kubectl-exec Skaffold这三板斧基本覆盖了绝大多数场景。4. 日志查看体系搭建4.1 Kubernetes 原生日志机制K8s 里的容器日志默认输出到 stdout/stderr由容器运行时containerd负责采集写到宿主机目录下的 json 文件中。kubectl logs本质上是读取容器运行时暴露的日志文件而不是通过什么复杂的日志系统。理解这个机制对排查问题很有帮助即使 Pod 处于 CrashLoopBackOff 状态已经退出的容器日志依然可以通过kubectl logs --previous查看。# 查看上一次重启前的容器日志 kubectl logs -n dev pod/orders-service-7b8b6f9d6c-abcde --previous # 持续跟踪日志 kubectl logs -f -n dev deploy/orders-service如果 Pod 里有多个容器必须用-c参数指定容器名。很多新人在多容器 Pod 上直接用kubectl logs会报错提示你要指定容器。4.2 kubectl logs 高效用法开发调试阶段kubectl logs的正确打开方式是组合参数# 只看最近 100 行并且持续跟踪 kubectl logs -n dev -f --tail100 deploy/orders-service # 按时间过滤日志查看最近 10 分钟 kubectl logs -n dev --since10m deploy/orders-service # 同时查看多个 Pod 的日志通过 label 选择器 kubectl logs -n dev -l apporders-service --tail50 # 输出带时间戳 kubectl logs -n dev --timestamps deploy/orders-service --tail200--tail这个参数很实用。不指定它的时候kubectl logs会把当前所有日志全部拉下来如果容器打印比较啰嗦输出量巨大人眼看不过来网络传输也浪费时间。先--tail100看个大概再按--since缩小范围排查速度会快很多。提示kubectl logs只能看到容器 stdout/stderr 的输出。如果你的应用把日志写到文件里而不是 stdout需要进容器查看或者做日志文件采集。开发调试阶段建议让应用把所有日志打到 stdout统一走标准输出方便 PIPE 处理。4.3 轻量级日志收集方案Loki当集群规模上来了或者有多个 namespace 多个应用用kubectl logs一个个看就不太现实了。这时候需要一个日志收集系统。生产级选择是 EFKElasticsearch Filebeat Kibana或 PLGPromtail Loki Grafana但开发环境搭建 EFK 太笨重我推荐 Grafana 全家桶中的 Loki 方案。Loki 的设计理念是“只索引元数据不索引日志内容”。它只做标签索引日志内容本身以压缩块存储资源占用比 Elasticsearch 小一个量级。对于开发调试环境一台 4GB 内存的机器就能同时跑起 Promtail Loki Grafana。部署方式用 Helm 最简单:helm repo add grafana https://grafana.github.io/helm-charts helm repo update helm install loki grafana/loki-stack \ --namespace loki --create-namespace \ --set grafana.enabledtrue安装完成后把 Loki 数据源配置到 Grafana然后在 Dashboards 里按 namespace、Pod、label 过滤日志查询{namespacedev} | ERROR {apporders-service} | timeout | 12345这种标签查询比去 Elasticsearch 写 DSL 舒服多了。开发调试时我只需要在 Grafana 里盯住关键应用的错误日志再配合告警规则不用再开着终端不停刷kubectl logs -f。4.4 日志持久化与轮转容器日志如果不做处理会一直增长时间久了把磁盘占满节点就会进入 DiskPressure 状态。containerd 默认会做简单轮转但在长时间高输出场景下依然不够。建议在部署应用时通过resources声明日志存储量或者在节点上配置 logrotate 规则。其中一个简单有效的方式是把容器日志目录接到独立分区为日志单独留磁盘空间核心是别让日志把根分区打爆。另外如果在开发环境遇到 Pod 反复崩溃导致磁盘大量残留日志可以先在 Deployment 的template.spec.containers里配上resources: limits: ephemeral-storage: 1Gi这样单个 Pod 的临时存储被限制在 1GB超过后 kubelet 会主动杀掉容器避免日志无限增长拖垮整个节点。5. 常见问题与排查手册5.1 集群初始化失败排查初始化失败是高频问题报错位置千奇百怪但核心原因通常就这么几个端口被占用kubeadm 的 API Server 默认监听 6443etcd 监听 2379/2380swap 没关干净swapoff -a只是临时关闭要检查 /etc/fstab 是否注释掉镜像拉取失败国内网络经常拉不到registry.k8s.io下的镜像可以用 kubeadm 的--image-repository参数切换容器运行时异常检查 containerd 是否启动成功crictl 是否能正常连接排查第一步是拿到现场信息kubeadm reset -f kubeadm init --dry-run journalctl -u kubelet -f crictl ps -a建议动手前先记录好报错日志然后逐条排查别一次性把环境全部 reset 掉不然问题没定位就又要重新初始化。5.2 Pod 长时间 Pending 或 CrashLoopBackOffPod Pending 最直接的原因是调度失败常见有节点资源不足、节点亲和性找不到匹配节点、PVC 无法挂载。先用 describe 看事件kubectl describe pod -n dev pod-nameEvents 会给出明确原因比如FailedScheduling会写清楚是资源不足还是节点选择器无匹配。再加kubectl get events --sort-by.lastTimestamp看集群级事件很多问题一眼就出来。CrashLoopBackOff 则是容器启动即退出。排查思路是先看当前容器日志再看上一次容器日志必要时进入容器的上一状态拿到现场。常见原因是启动命令参数错误、依赖服务没就绪、环境变量缺失。开发环境经常遇到依赖 MySQL 的 Pod 起不来因为 MySQL 还没 Ready应用连不上数据库。这种情况建议在 Deployment 里加 initContainer 先检查依赖服务状态。5.3 日志查看不显示的常见原因日志不显示不一定是没日志很可能是查询方式不对。第一检查 namespace所有 kubectl 查询都需要指定 namespace默认是 default。如果应用部署在 dev namespace直接用kubectl logs deploy/app会提示找不到 Pod。第二检查 Pod 是否是多个容器多容器 Pod 必须指定-c。第三检查打印方式如果应用直接使用 log4j 的 RollingFileAppender 写文件而不写 stdoutkubectl logs会看到空输出。开发调试阶段强烈建议把应用的所有日志镜像到 stdout既方便本地调试也方便集中收集。Spring Boot 项目在application.yml里加logging.file.name/dev/stdoutNode.js 直接console.logGo 用log.Printf就够了。第四检查容器是否处于退出状态退出容器需要用--previous参数查看上次日志。5.4 高频率操作 Remedy直接重启而非重建开发调试时最常犯的错误是把“重启”和“重建”混为一谈。kubectl delete pod是删掉重建会拉取镜像、重新调度耗时较长而kubectl rollout restart deploy/app是滚动重启保留原有 ReplicaSet 的调度策略只是滚动重新创建 Pod通常能少踩一些避免同 IP 和 hostname 问题的坑。遇到配置变更导致的异常优先用 rollout restart 而不是 delete pod。遇到容器内文件系统损坏、状态残留、hostname 绑定等情况时才考虑 delete pod 强制重建。写在最后我在实际操作中最深的感受是搭建集群本身不复杂真正耗时的是后续的开发调试体验打磨和日志链路串联。一个能自己掌控的 K8s 集群配合顺手的 port-forward、kubectl-debug、Loki 日志查询会让本地开发和联调效率提升一个档次。如果你在 CentOS Stream 10 上照着这个思路操作时遇到问题不妨先把报错信息、节点状态、事件输出三样东西收集好再逐项排查。我个人的建议是优先保证网络、存储、DNS这几个基础组件稳定再考虑上更丰富的调度能力和运维工具。集群跑顺了后面的开发调试就是顺手的事。