说实话这两年被k8s折磨得不轻但翻看自己整理部署k8s的实践笔记时又觉得所有踩过的坑都值得。这年头分布式、微服务、容器化几乎每家公司都在谈上云、谈云原生而k8s作为云原生操作系统的底座地位就跟Linux在服务器领域一样。对开发来说它是能一键部署、弹性伸缩的“自动售货机”对运维来说它是能统一管理几百台服务器资源池的“调度大师”对于架构师来说它是一个可编程的、APIs驱动的基础设施抽象层。这篇笔记不是贴着一堆命令教你复制粘贴的教程而是把我从设计、部署、踩坑、调优全链路的心得记录下来尤其把那些章口就来的概念背后的“为什么”讲清楚希望给正在入门或者已经被生产环境折磨得焦头烂额的同行一些参考。这篇内容适合正好要搭建或维护一套自管k8s集群的开发者、运维和架构师。我会重点拆解部署方案选型、工作负载编排、调度与资源管理、持久化存储这些核心模块同时把生产环境中最常遇到的三类故障排查路径直接放到桌上说。1. 集群起步从安装方案选型到关键组件梳理1.1 为什么生产环境我最终选了kubeadm而不是二进制或托管版市面上部署k8s的方案五花八门从神秘的开源二进制部署到各式各样的自动化工具再到一键安装的迷你发行版真的挑花眼。我第一次实操时也纠结过是纯手工二进制安装还是直接用工具。这里我直接说结论如果是搭建自我控制、长期运转的生产集群我强烈建议使用kubeadm。为什么不是二进制二进制部署虽然能让你把每个组件的参数都亲手配置对理解原理很有帮助但它也意味着你需要自己处理证书签发、组件健康检查、高可用配置等所有的运维细节。有一说一现在这种复杂度对应的是巨大的维护成本失控概率太高。为什么不是托管的云服务托管版虽然省心但一来费用不低二来它会绑定特定生态不够灵活。在混合云、私有化交付场景中你依然需要自己掌握集群搭建的能力。所以对于大多数愿意自己掌控底层、又不想在安装过程里被劝退的团队来说kubeadm就是那个平衡点。它帮我们解决了证书、组件安全配置、静态Pod编排等大量脏活同时又保留了对集群完整控制权。1.2 部署前的资源规划与系统参数调优在跑kubeadm init之前硬件的坑一定要排掉。我建集群时的标准配置是3台节点1主2从每台机器4核8G。如果是纯粹的学习环境可以适当降低但生产环境建议起步就是16G内存否则etcd很容易成为瓶颈。操作系统我用的Ubuntu内核版本不能太老。重点来了很多人在部署前忽略系统参数调整结果Pod一跑就崩溃。你需要提前加载overlay和br_netfilter内核模块让iptables能正确看到桥接流量。然后是网络参数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这一步没做好的话最典型的症状就是集群内DNS解析偶尔超时或者Service的ClusterIP无法访问。很多人一脸懵地查网络插件其实根子在这里。另外如果机器开启了swap记得要关闭。因为k8s的kubelet默认不支持启用swap它认为内存应该由自己统一调度。虽然现在有些新版本支持swap开启模式但为了求稳建议swapoff -a并在/etc/fstab中注释掉相关条目。1.3 容器运行时与容器运行时接口的认知刷新容器运行时选型现在基本没什么悬念我用的是containerd而不是Docker。注意这不是二选一而是替代。k8s从1.24版本之后彻底移除了对Docker作为运行时的一部分支持因为kubelet通过容器运行时接口CRI与运行时通信而Docker本身并不直接实现CRI。中间还需要dockershim这个胶水层虽然我记不清具体版本历史但这层维护成本实在太高官方干脆踢掉了。使用containerd的好处是链路过短、资源占用更低也更稳定。不过平时的命令习惯要改一改以前用docker ps现在得用crictl ps。这里给个温馨提示containerd的镜像仓库地址默认可能指向一个不太友好的源拉取镜像很容易超时。生产环境务必配置好registry mirror否则创建Pod能卡到你怀疑人生。2. 工作负载编排从Deployment到Ingress的流量闭环2.1 为什么是Deployment而不是直接跑裸Pod很多人刚开始玩k8s喜欢直接用一个Pod的YAML跑起来然后发现Pod一删就真的没了或者节点一挂服务直接凉凉。这就是没有理解Deployment存在的意义。Deployment的本质是一个声明式的控制器它描述的是“期望状态”比如某个应用的副本数量是3个镜像版本是v1.0.0。控制器会在后台默默工作保证实际状态无限向期望状态靠拢。如果Pod挂了它会立即创建新的替换如果镜像要升级它能平滑地滚动更新并且可以随时回滚。这种“自动驾驶”的能力是所有无状态服务上k8s的首要理由。写Deployment的YAML时有几个点特别容易出问题。一个是spec.selector.matchLabels这个标签选择器是建立控制器与Pod之间关联的“钩子”一旦创建不可更改。另一个是strategy的配置比如滚动升级时maxSurge和maxUnavailable的百分比这决定了你在发布时的可用性。我习惯设置maxSurge: 25%和maxUnavailable: 0这样发布过程中先额外拉起新的Pod等新Pod健康了再下线旧的全程无感知。2.2 Service的几种类型与集群网络转发逻辑Deployment解决了副本管理问题但Pod的IP是不稳定的随时可能重建变化。这时候就需要Service来提供一个稳定的访问入口。Service本身有两种主流实现模式一种是userspace模式旧的基本废弃另一种是iptables或IPVS模式也是现在默认的。它的原理其实不复杂就是一个虚拟IP加一组后端Pod的负载均衡规则。Service类型我基本只用三种ClusterIP集群内部访问比如微服务之间的调用。NodePort在每台节点上开一个端口通过节点IP:端口从集群外部访问。LoadBalancer对接云厂商的负载均衡器或者自建环境里的MetalLB这类方案。这里有个经典坑位当你在集群内部通过Service访问自己时需要注意是否开启了externalTrafficPolicy。如果设为Local源IP会保留但设为Cluster的话请求可能会被转发到其他节点访问日志里看到的全是转发地址排查问题时容易混淆。2.3 Ingress的选型与流量闭环实例流量真正打进集群单靠NodePort是又low又难管理的因为端口数量有限管理混乱。所以生产环境必须上Ingress它的本质是一个七层负载均衡器通过域名和URL路径把请求路由到不同的Service上。Ingress控制器我实测推荐使用ingress-nginx。部署时需要注意它的Service类型通常我会设置为NodePort或LoadBalancer再在前面套一层外部LB。举一个我之前做过的访问闭环例子假设我部署了一个应用叫demoapiVersion: apps/v1 kind: Deployment metadata: name: demo-app spec: replicas: 3 selector: matchLabels: app: demo template: metadata: labels: app: demo spec: containers: - name: demo image: registry.example.com/demo:v1.0.0 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: demo-svc spec: selector: app: demo ports: - port: 80 targetPort: 8080 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-ingress spec: ingressClassName: nginx rules: - host: demo.example.com http: paths: - path: / pathType: Prefix backend: service: name: demo-svc port: number: 80整个链路就是浏览器访问域名 → DNS解析到LB → LB转发到Ingress控制器所在的NodePort → ingress-nginx根据规则转发到Service的ClusterIP → Service通过iptables/IPVS转发到后端的Pod。排查问题时我们就沿着这条链路一层层拔掉洋葱。3. 调度与资源管理让Pod被合适地安置3.1 理解requests与limits别再随意设置了资源管理这块是最容易因为“看着没出事”而翻大车的地方。Kubernetes的调度器在决定把Pod放到哪台机器时仅依据requests值因为这是一个承诺值代表Pod最低需要的资源下限limits则是上限是强制约束。很多人图省事把requests和limits设置得一模一样结果就是节点资源严重碎片化。比如一个Pod设置了2核4G的requests和limits但实际上它平时只用不到10%的CPU这个空间就白白浪费了别的Pod挤不进来。我的经验是根据应用的压测数据来设置。CPU的requests可以根据平均负载limits可以设为峰值的80%左右内存就保守一点因为内存不可压缩如果超过limits容器会直接被killer干掉。建议requests设为内存峰值的70%limits设为峰值的120%并用HPA自动扩缩容来兜底。3.2 节点亲和性与污点容忍度的实战场景调度不仅仅看资源够不够有时候你希望某些应用固定跑在某些机器上比如有SSD的节点或者GPU节点。这时候就轮到节点亲和性上场了。它通过节点上的标签来做软性或硬性约束。举例我有几台机器专门加了SSD盘并打了标签disktypessd那跑高IO的ES集群时就在Pod的spec里加上affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd与节点亲和性相对的是污点和容忍度。污点Taint是打在节点上的标记表示“我不希望普通的Pod跑到我这里”而容忍度Toleration是Pod上的标记表示“即使你有这个污点我也愿意来”。这就像包租婆写了个“只租给单身狗”你符合条件就能入住。生产中最常见的污点就是给专门跑监控或者核心组件的机器打上CriticalAddonsOnly这个污点防止被其他普通业务抢占资源。需要注意污点和容忍度只是“允许”调度并不指定“一定”调度到某个节点这是它和节点亲和性本质的区别。3.3 揭开Pod被驱逐的内幕我见过不少人半夜被电话打醒发现Pod全被驱逐了节点状态变成NotReady然后抄起屠龙刀就想重装集群。其实这大概率是节点内存压力导致的。kubelet每10秒扫描一次当节点memory压力大时就会根据QoS等级开始驱逐Pod。Pod的QoS等级是由requests和limits的配置推导出来的由高到低分为Guaranteed、Burstable、BestEffort。最惨的就是BestEffort即完全不设置requests和limits的Pod它们会被最先牺牲掉。所以如果你不想自己的服务在资源紧张时第一批被干掉至少得设置requests让它进入Burstable的等级这样存活率会高很多。排查驱逐原因可以用kubectl describe node node-name在Events里你会看到Eviction事件。还有一个常用的命令是journalctl -u kubelet通常能看到MemoryPressure的字样。4. 持久化存储从静态PV到动态StorageClass4.1 PV、PVC、StorageClass三者之间的抽象协作k8s里的存储应该是所有模块里抽象度最高的很多新手容易绕晕。这里我用一个选房类比来帮你理顺。Pod是住户PVC持久化卷声明是住户的购房需求比如“要三房两厅、朝南、带车位”PV持久化卷就是你真正买到的房子它由管理员事先准备好或者由StorageClass动态新盖StorageClass就是一个会自动为你建造房子的房地产开发商。如果没有动态供给你得自己创建一堆PV再让PVC去匹配。这种方式非常死板比如容量写大了小PVC匹配不上写小了又不够用。有了StorageClass后PVC创建时只需声明storageClassName云厂商或自建的provisioner就会自动调用底层接口创建一块新的PV并绑定给PVC。4.2 本地存储的局限与网络存储的接入实践自建集群最常见的存储方案是使用云硬盘或者分布式存储。对于单机实验环境我图省事用过hostPath但生产环境绝对不建议。纯血的临时存储不仅数据绑死在节点上还很容易因为目录混乱而互相污染。后来我把集群迁移到使用网络存储上。在私有云场景一个非常成熟的方案是用Rook-Ceph它可以把Ceph集群跑在k8s里面并通过CSI接口为应用动态提供块存储、文件存储和对象存储。这个方案虽然安装略重但胜在稳定、易扩展。如果是存储设备相对简单的场景也可以考虑NFS动态供给通过一个现成的NFS Client Provisioner配合已有的NFS服务器实现PVC的动态创建。这种方式的好处是实现成本极低缺点是性能和可靠性上限受限于NFS服务器本身所以更适合对性能要求不高的数据备份类应用。4.3 有状态应用StatefulSet的存储细节说到有状态应用比如MySQL、Zookeeper它们和Deployment最大的区别在于每个Pod需要独立的稳定标识和独立存储。StatefulSet里的Pod名称是固定有序的比如mysql-0、mysql-1并且每个Pod对应自己的PVC。这样做的好处是Pod重建后存储会被重新挂载到同名的新Pod上保证数据不丢。配置时有个注意事项就是volumeClaimTemplates它有点类似Pod模板里的volumes但作用域是每个副本独立创建的PVC。我有一次就是因为把PVC模板写错了位置结果所有Pod共享了一个卷一扩容数据就乱套。这里再提醒一点删除StatefulSet时默认不会删除PVC。如果你希望连存储一起清理需要手动删除PVC不然下次重新部署时数据还在容易造成“惊喜”。这在某些情况是有意的但如果是测试环境你可能会浪费一堆时间在“数据为什么还是旧的”上面。5. 监控告警与故障排查实战记录5.1 监控体系Metrics Server与Prometheus生态的分工监控是运维的眼睛没有监控k8s集群就是一片黑。最基础的是Metrics Server它负责采集节点和Pod的CPU、内存使用量并供kubectl top命令和HPA使用。如果没装Metrics Server你会发现自己连kubectl top nodes都跑不了。但Metrics Server的数据只保留很短时间无法回溯历史趋势也做不了告警。所以生产环境还得上Prometheus全家桶Prometheus负责采集和存储时序数据Grafana负责可视化Alertmanager负责报警通知。我常用的采集器是kube-state-metrics它能把集群各种对象的状态比如Pod重启次数、Deployment副本数量转换成指标这是从集群整体视角观察健康度的关键一环。实际部署时我用的是kube-prometheus-stack这个Helm Chart一条命令就能装好一整套Prometheus、Grafana和告警规则。虽然不是所有指标都尽如人意但作为起步已经相当够用。5.2 高频故障Pod一直Pending或CrashLoopBackOff的排查路径故障排查是我最想跟新人分享的。很多人一遇到Pod有问题第一反应就是把Pod删掉重建。拜托那只是缓解症状不是根因。当Pod一直是Pending状态时本质是调度失败。按照这条路径查基本不会走弯路使用kubectl describe pod pod-name查看Events里的调度提示。查看是否有0/3 nodes are available的警告再往后看原因。常见原因一般是资源不足Requests超了、节点有污点且Pod未容忍、节点亲和性不满足或者是PVC没有绑定成功。CrashLoopBackOff则是另一大高频问题它表示容器启动了但进程立即崩溃然后被kubelet反复重启。排查的顺序先看日志kubectl logs pod-name --previous注意加--previous看上一次崩溃的日志因为当前容器可能什么都没打出来就已经退出了。如果日志末尾显示无法连接数据库或者配置文件缺失八成是环境变量或配置映射的问题。如果容器直接OOMKilled用kubectl describe pod pod-name看Last State的reason是否为OOMKilled然后调整limits。5.3 疑似网络问题的真实案例我的一次ImagePullBackOff处理还有一个让我印象很深的关于镜像拉取失败的经历。当时部署了一个新服务Pod状态一直是ImagePullBackOff点击Events后发现报错是Failed to pull image registry.example.com/demo:v2.0: rpc error: code Unknown desc failed to resolve reference ...: not found。一开始我以为是镜像名称写错了但检查后发现镜像明明存在。后来才意识到我虽然配置了containerd的registry mirror但那个私服仓库里根本还没有v2.0这个tag。发布流程里漏了一步根本忘了push。这个案例告诉我们遇到ImagePullBackOff别急着骂网络先按这个顺序排查镜像名是否完全正确注意私有仓库路径别丢了前缀 → 镜像tag是否存在 → 私服仓库是否需要认证imagePullSecrets → 节点和仓库之间的网络连通性 → containerd的mirror配置。有些自建的Harbor仓库还会开启白名单策略导致未认证的拉取被拒。这种情况下kubectl create secret docker-registry是标准解法然后把这个secret挂到Deployment的imagePullSecrets字段里。5.4 高频问题速查表现象排查命令最常见原因快速解法Pod一直Pendingdescribe pod资源不足/污点/亲和性不满足扩容节点/去掉污点容忍Pod反复重启logs --previous启动命令错误/配置缺失修复env或configmapClusterIP访问不通kubectl get endpointsService选择器与Pod标签不匹配检查selector对不上没镜像拉取一直失败describe pod看event私有仓库认证/tag不存在配imagePullSecrets节点NotReadyjournalctl -u kubelet资源压力/运行时故障排查kubelet日志Ingress 404查看nginx日志后端Service的targetPort写错检查后端端口这个速查表看起来很简单但每一条背后都是实际生产环境踩过的坑。最怕的就是查问题时东一榔头西一棒子最后发现自己连日志都没看。在这套体系稳定运行之后我个人的最大体会是k8s的复杂度其实来自于它的分层抽象。遇到任何问题不要慌先定位自己处于哪一层是Pod层、Service层、Ingress层、存储层还是调度层。然后逐层排查。还有一个小技巧善用kubectl get events --sort-by.lastTimestamp按时间排序观察事件变化往往比瞎猜快得多。最后再分享一点如果你想让这套集群更加完善建议往GitOps的方向演进把所有的YAML都管理到Git仓库里。这样无论是变更还是回滚都有迹可循、有源可溯。这个方向我也实践过收益巨大具体怎么搭相信你已经心中有数了。