
上周五下午我把一套内部系统的Pod从40个扩到160个整个过程还算顺利但扩完后我一晚上没睡踏实。第二天早上起来第一件事就是看监控、查驱逐事件、翻Pod调度日志。K8s实践就是这样它会让你在“终于跑通了”和“怎么又出问题了”之间反复横跳。这篇文章算不上入门教程更像是我把一套从零搭建、上线、排障、加固的Kubernetes集群所踩过的坑、做过的选择、沉淀下来的配置和习惯按主题整理出来的实践笔记。适合刚把集群搭起来但总觉得“哪里不对”的同学也适合已经跑了一段时间、想系统排查隐患的工程师。内容里没有花哨的概念复述全是落地上遇到过的问题和验证过的做法。1. 集群规划那些事先算账再动手很多人搭K8s集群都是装完就完事机器一凑就开始部署业务。前期少算一笔账后面运维就多补无数的锅。K8s这套东西控制面、工作节点、存储、网络、监控各自有各自的资源脾气不提前算清楚后面会很被动。1.1 机器规格与数量怎么定先说我踩过的教训最开始图省事用一批2核4G的机器做了三个节点的集群控制面和工作负载全挤在一起。刚开始跑少量无状态服务还好一旦接入日志采集、监控采集、DNS缓存这些“隐形居民”节点内存直接告急。后来迫不得已把控制面拆出去才稳定下来。我的建议是控制面节点至少3台规格不用太高4核8G起步磁盘走SSD主要原因在etcd。etcd是一个对磁盘延迟极其敏感的系统机械盘在高并发写入时会产生明显的写延迟进而拖垮整个集群的API响应。工作节点按业务类型分开规划节点类型推荐规格用途通用型8核16G普通无状态服务、网关、定时任务计算型16核32G批量任务、数据处理类服务存储型4核8G 大容量数据盘数据库、消息队列等有状态服务另外要给系统组件预留资源。我一般按节点总量预留10%到15%的资源给kubelet、容器运行时、日志采集器、监控采集器这些“影子进程”。很多节点内存告警不是业务跑满了而是这些底座组件没有预算进去。1.2 版本和网络插件别追新这个我非常有发言权。有一年我图新鲜集群刚出来就升了大版本结果一个节点组件版本不兼容折腾了两天回滚。之后我给自己定了一条铁律K8s版本只选当前稳定大版本的上一个稳定版等新版本出来至少半年后再考虑升级。生产环境最重要的是“社区踩过的坑足够多”而不是“新功能足够酷”。网络插件是另一个牵一发动全身的选择。我对比过Flannel、Calico、Cilium三个方案插件优点短板适合场景Flannel配置简单、文档多不支持网络策略功能基础测试环境、小型集群Calico网络策略成熟、性能稳定配置项多排障有门槛生产环境默认选项CiliumeBPF路线、可观测性强内核版本要求高上手成本高需要深度可观测性的团队我最终选了Calico。原因很简单BGP模式下的性能表现稳定网络策略可以直接用YAML管理社区资料也多排障的时候能找到大量参考。更重要的是它支持IPIP和BGP两种封装模式不同网络环境可以灵活切换。这里有个实用建议如果集群节点都在同一个二层网络里优先用BGP模式如果是跨网段环境才用IPIP。封装模式选错最直观的体感就是跨节点Pod通信延迟偏高。2. 应用上线的核心链路从一份YAML到一条稳定链路集群搭好了真正的挑战才开始。业务方不会关心你的集群有几个节点他们只关心“我提了工单服务能不能尽快上线”。把Deployment、Service、Ingress这三者的关系理顺是应用上线的第一道门槛。2.1 Deployment、Service、Ingress各自管什么很多一开始接触K8s的同学会把这三个概念混在一起其实分工非常清晰Deployment管的是“副本到底有多少个、怎么滚动更新”它负责Pod的创建、销毁、版本切换。Service管的是“Pod的访问入口在哪里”它给一组Pod提供一个稳定的虚拟IP和DNS名字。Ingress管的是“外部流量怎么进到集群里”它负责HTTP/HTTPS路由把域名和路径映射到对应的Service上。我通常用生活化的类比来解释Deployment是幕后的演员管理Service是舞台的固定入口Ingress是剧场门口的引路员。2.2 一份可以直接抄作业的Deployment配置下面是我生产环境里常用的一份配置兼顾了滚动更新和探针检查。以某个模拟项目X的订单服务为例apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: business labels: app: order-service spec: replicas: 3 selector: matchLabels: app: order-service strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.example.com/business/order-service:v1.2.3 ports: - containerPort: 8080 name: http resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 terminationGracePeriodSeconds: 30这里几个细节值得多说一句maxSurge: 1表示滚动更新过程中最多比期望副本数多出1个maxUnavailable: 0保证旧版本Pod不出现全部不可用的情况。生产环境我偏向这种保守配置宁可慢一点不能出现服务断层。探针是必须写的不是可选项。readinessProbe告诉K8s“这个Pod可以接流量了”livenessProbe告诉K8s“这个Pod没救了重启它吧”。很多同学不写探针上线之后发现流量已经打进来了但应用还在初始化接口大量报错这是最常见的低级事故。2.3 探针踩过的坑别把健康检查当成简单HTTP请求探针本身很简单但设计不好会引发连锁反应。我见过一个服务readinessProbe的路径是/而这个接口内部会去查数据库数据库抖动的时候整个Pod都被摘除流量然后又恢复又摘除形成“抖动放大效应”。健康检查的路径应该尽量轻量只反映进程是否存活、依赖的核心组件是否就绪不要在健康检查里做重逻辑。另外initialDelaySeconds别拍脑袋写。最稳的做法是上线第一版时先看应用日志记录从进程启动到端口真正可用的时间再在这个基础上加10秒左右的缓冲。写短了会导致启动未完成就开始探测一直失败写长了会拖慢滚动更新节奏。3. 存储与网络的深水区我踩过的那些坑如果Deployment、Service是K8s的“明面功夫”那存储和网络就是“暗面功夫”。这两块出问题现象往往很诡异排查链路也最长。我单独把它们拎出来是因为这里踩过的坑几乎都成了团队后来排查故障的经典案例。3.1 网络排查从Service到Pod的那条链路有段时间我们某个内部系统反馈说“服务间歇性超时”不是完全连不上而是偶尔某一次请求会卡住十几秒。进入集群排查后发现问题出在Service的externalTrafficPolicy配置上。当时有个Service设置了externalTrafficPolicy: Cluster这意味着跨节点转发时可能会发生二次跳转源IP也会被改写。某些对IP有校验的服务就因此出现异常。我把策略改成Local后流量只转发到本节点上的Pod问题立刻消失。但要注意Local模式需要在每个节点上都保证有Pod运行否则流量到没有Pod的节点会被丢弃这个需要配合节点亲和性一起设计。还有一个高频问题Pod能ping通但域名解析不通。我总结的排查顺序是先看CoreDNS是否正常kubectl get pods -n kube-system -l k8s-appkube-dns再看Pod的/etc/resolv.conf最后看Service的Endpoints是否有内容。大部分“域名解析失败”的根因都是后端Pod没起来或者标签选择器写错了。3.2 存储分配PV/PVC看起来简单权限问题连环炸有状态服务的存储接入是另一座山。我第一次给人做数据库容器化的时候只配置了PV/PVC就以为完事了。结果MySQL启动失败日志里写着Permission denied。原因是K8s挂载卷默认使用root权限而容器内的MySQL进程以普通用户身份运行。这个问题不是PV/PVC本身的锅而是容器存储挂载时的fsGroup和runAsUser设置。后来我在StatefulSet里加上spec: securityContext: fsGroup: 999 runAsUser: 999用镜像里声明的用户ID挂载卷问题才解决。这里要提醒一件事修改StatefulSet的安全上下文不能直接滚动更新生效需要删除Pod后重新创建操作前一定确认数据卷是持久化的否则数据可能一起被销毁。再就是PV的回收策略。我建议所有用于数据库的PV都把persistentVolumeReclaimPolicy设为Retain而不是默认的Delete。一旦有人手滑删了PVCDelete策略会把底层存储数据直接清掉那种事故是恢复不了的。4. 稳定性不是碰运气是配出来的K8s平台搭起来、业务跑起来之后大家最怕的是什么是凌晨三点的告警电话。稳定性不是靠运气而是靠你在平静的时候做了多少防御性配置。这一章的东西全部属于“平时不起眼出事救命”的类型。4.1 requests/limits与QoS等级不设置等于裸奔我先说一个自己以前犯过的错误图省事不写resources结果某天一个服务的内存持续增长把节点内存打爆了kubelet开始疯狂驱逐其他Pod整个集群像多米诺骨牌一样倒下。后来我才意识到limits其实是一道安全护栏它不限制你正常使用资源而是防止一个进程的异常拖垮整个节点。K8s根据requests和limits把Pod分为Guaranteed、Burstable、BestEffort三个QoS等级驱逐优先级从低到高。生产环境里核心服务我尽量让requests等于limits这样Pod进入Guaranteed等级节点资源紧张时它不会被优先驱逐。非核心服务可以留出弹性但一定要有limits。资源数值怎么算我先说一个笨办法上线前压测观察稳态下的CPU和内存使用率然后取稳态值的1.5倍到2倍作为requestslimits再在requests基础上留一定余量。经验公式是requests不是“你想要多少”而是“你最少需要多少才不崩”limits才是“你最多能用多少”。4.2 优雅停机别让K8s把你的服务“掐死”滚动更新和节点维护时K8s会给Pod发送SIGTERM信号然后等待容器退出。很多Java应用在收到SIGTERM后并不会立即处理默认的优雅退出时间根本不够结果进程被SIGKILL强杀用户请求丢得一塌糊涂。我遇到过一次最典型的情况每次发版后监控里都会出现一小波5xx错误持续几秒钟。排查后确认是旧Pod还没处理完正在进行的请求就被杀掉了。解决方式分三步在容器启动命令里捕获SIGTERM先摘除注册中心里的实例再等待存量请求处理完。调整terminationGracePeriodSeconds从默认的30秒改到60秒给业务足够的收尾时间。给容器加preStop钩子执行一段休眠或清理脚本确保在SIGTERM发出后还有缓冲时间。lifecycle: preStop: exec: command: - /bin/sh - -c - sleep 5这里的逻辑是preStop先睡5秒给负载均衡器和注册中心一点时间摘除这个实例的流量然后再真正开始销毁流程。你问我为什么是5秒这个要根据负载均衡器的摘除周期来调。我见过有的团队设20秒有的是0秒都是看了实际流量分布后调出来的不存在万能值。4.3 HPA和PDB弹性伸缩与不可用预算的平衡HPA不是玄学它只是根据指标动态调整副本数。但很多人忽略了它的一个行为特点扩容是立刻的缩容却有默认的冷却时间。我习惯把scale-down的stabilizationWindowSeconds调大一点避免流量抖动导致副本反复增减。PDBPodDisruptionBudget则是另一个容易被忽略的配置。节点维护时kubelet驱逐Pod如果不看PDB可能一次性把所有副本都赶走造成服务不可用。我通常会为每个核心服务配置apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: order-service-pdb namespace: business spec: minAvailable: 2 selector: matchLabels: app: order-service意思很直白任何主动驱逐操作最多只能让这个服务少于2个可用副本。这样节点维护时K8s会等新Pod在其他节点起来后才继续驱逐下一个保证核心链路不断。5. 监控日志别等出事了才想起来很多团队的监控是“事故驱动”的——先出一次大事故再回头补监控。说实话我也经历过这种阶段。集群裸奔的时候你根本不知道哪天哪个节点磁盘会被日志写满。监控日志这件事应该和集群建设同步进行。5.1 监控指标收藏夹里必须有这几条我用的是Prometheus Alertmanager Grafana这套组合。组件本身不多说我重点说说哪些指标是必须盯的指标告警条件说明节点CPU / 内存使用率持续5分钟 85%过高会影响调度与驱逐Pod内存使用率持续5分钟 90%防止内存不断上涨打爆节点Node磁盘使用率持续5分钟 85%防止镜像、日志把盘写满API Server延迟p99 1秒控制面出现性能瓶颈etcd fsync延迟p99 100ms磁盘性能告急的第一信号被驱逐Pod数量1分钟内 0节点资源严重过载的信号CoreDNS错误率5分钟错误率 1%域名解析是全局基础依赖这里我尤其想强调etcd的fsync延迟。它不像CPU那样直观但一旦飙高整个集群的读写都会变慢表现是kubectl操作卡顿、Pod调度迟钝。我遇到过节点磁盘IO被打满导致etcd fsync延迟飙到几百毫秒API Server直接开始拒绝服务的情况。监控里加这条能帮你提前发现磁盘老化或IO争抢的问题。日志采集我用的是Filebeat Elasticsearch的轻量方案。业务容器只需要把日志输出到标准输出Filebeat以DaemonSet方式部署在每个节点上采集容器日志。这里有个经验所有服务日志必须使用JSON格式并包含traceId否则后期把日志和调用链关联起来会非常痛苦。5.2 告警分级别让告警变成“狼来了”我曾经把一群告警全部设成电话告警结果一天能响十几次最后大家都麻了真正重要的事故反而没人第一时间处理。后来我把告警分成三级P0服务不可用、节点不可调度、数据丢失风险必须电话通知负责人。P1资源水位过高、错误率上升、Pod频繁重启工作时间15分钟内响应。P2磁盘使用率接近阈值、证书即将过期邮件加群消息提醒。分级之后告警的响应率和真实度明显提升。这里给一个建议告警规则不要一次配太全先配最核心的十条运营一段时间后看告警的有效率再决定要不要增加。告警是“宁缺毋滥”不是“多多益善”。6. 日常运维里最值得沉淀的几类动作最后聊点日常。K8s的运维不是说集群稳了你就没事干了。新版要评估、证书要更新、镜像要清理、容量要规划。我沉淀了一套固定动作每个周期照着执行能省不少心。6.1 高频命令和参数贴到自己的速查本里下面这些命令是我最常敲的几乎每周都会用到# 查看所有命名空间的资源占用情况 kubectl top pods -A # 排查某个工作负载的事件 kubectl describe deployment/order-service -n business # 查看Pod调度到哪个节点以及IP信息 kubectl get pods -o wide -A # 查看节点资源分配情况 kubectl describe node | grep -A5 Allocated resources # 滚动发布状态确认 kubectl rollout status deployment/order-service -n business # 进入容器排查 kubectl exec -it pod/order-service-xxx -n business -- sh # 看镜像在哪些节点上以及占了多少磁盘 ctr -n k8s.io images list | awk {print $2}最后一条尤其要强调。镜像越积越多是节点磁盘占用的头号原因。我一般会写一个定时清理任务定期删除没有被容器使用的旧镜像保留最近几个版本即可。不清理的话节点磁盘会在不知不觉中被镜像填满。6.2 一次让我印象极深的故障复盘有一回某个内部系统突然大面积超时最开始所有人都在查应用代码。后来我进集群看节点状态发现某个节点的磁盘使用率已经100%这个节点上的容器日志盘被写满了kubelet无法正常工作导致节点上的Pod进入NotReady状态。更麻烦的是这个节点上的业务副本数本来就不够节点异常后直接超卖。这次事故的原因很简单日志采集器的索引策略不合理导致日志数据无限增长最终把节点磁盘塞满。修复过程也不复杂清掉无用日志、调整日志保留策略、给日志盘加告警。但它暴露了一个运维漏洞日志盘和系统盘混在一起且没有监控。所以我后来把所有节点做了一次磁盘分区规范系统盘跑操作系统和容器运行时数据盘放容器镜像和临时卷日志单独目录挂盘每块盘都配上独立的告警。这个改造之后节点磁盘问题再没成为过事故根因。6.3 把重复劳动变成脚本日常运维里有很多重复动作我会用脚本把它们固化下来。比如批量查看所有节点的kubelet状态for node in $(kubectl get nodes --no-headers | awk {print $1}); do echo node: $node kubectl get node $node -o jsonpath{.status.conditions[?(.typeReady)].status}{\n} done再比如清理终止状态的Pod和无效的命名空间kubectl get ns --no-headers | awk $2Terminating {print $1} | xargs kubectl delete ns --force --grace-period0这类脚本不需要多复杂目的是把容易出错、需要重复确认的动作变成一条命令降低人工遗漏的概率。维护一个名为“k8s-tools”的脚本仓库有新的重复劳动就加进去几个月下来你会发现自己轻松很多。最后再分享一点个人体会K8s实践拼到最后拼的不是会用多少功能而是对细节的控制力。探针写没写、limits配没配、PDB建没建、磁盘监控加了没这些琐碎的东西才决定你的集群在半夜两点钟是安静地运行还是把你从被窝里叫醒。我现在每次变更前都会问自己一句如果这个变更出问题了我能不能在15分钟内找到原因如果答案是否定的那就先把监控和排查工具补上再操作。愿大家的集群都稳定少接几通半夜的告警电话。