K8s集群从部署到监控:Pod、RBAC与CPU限流实战指南
1. 先搞懂k8s和Docker的关系别在开头就栽跟头很多刚接触容器编排的朋友第一个卡住的地方就是k8s和Docker到底什么关系。面试题里问得最多的也是这个很多人会背结论——“k8s是容器编排平台Docker是容器运行时”——但一旦被追问“那k8s还用不用Docker”“Docker没了k8s还能不能跑”立刻就开始绕。先说结论k8s和Docker不是二选一的关系它们处在不同层。Docker解决的是“怎么把应用打包成镜像、跑成容器”k8s解决的是“机器上有一堆容器怎么调度、怎么保证不挂、怎么滚动升级、怎么暴露服务”。你可以只用Docker不用k8s但很难只用k8s不用任何容器运行时。1.1 Docker不是k8s的唯一选择早期k8s确实默认用Docker作为运行时这也是“k8s依赖Docker”这个印象的来源。但从k8s 1.24开始官方就移除了对Docker的直接支持默认改用containerd。你把containerd理解成“更轻量的Docker”——它负责拉镜像、启停容器但是没有Docker那些CLI、Build镜像、docker compose之类的附加功能。实际部署时我见过不少人在这个概念上配错以为“装了Docker就能跑k8s”结果kubeadm init一直报CRI容器运行时接口相关错误或者反过来说“k8s不用Docker了那就把Docker彻底卸载”结果发现有些中间件、镜像构建流程仍然需要Docker环境。正确的做法是集群节点上使用containerd作为运行时保留Docker单独用于开发和构建镜像。两者可以共存只要注意端口不冲突就行containerd默认监听unix socketDocker用的是/var/run/docker.sock互不干扰。1.2 形象类比Docker是“集装箱”k8s是“港口调度系统”你生产的镜像就是一个集装箱里面装好货物应用代码依赖环境。Docker是生成集装箱、把集装箱放到岸边的工作站k8s是整个港口的调度中枢——要知道哪个泊位有空、哪艘船该靠岸、哪个集装箱坏了要换掉、哪批货要在几秒内全部上船。这个类比能帮你理解一个高频面试题为什么不用Docker Compose而要上k8sDocker Compose只能在单台机器上编排属于“一个泊位里几个集装箱怎么摆”k8s把整个港口几千个泊位的集装箱统一编目调度还自带故障自愈、自动扩缩容、滚动发布——这些能力Compose是不具备的。提示如果在面试或文档里看到“k8s vs Docker”这种对比至少先判断提问者想问的是“运行时选型”还是“编排能力对比”。前者答案是containerd vs Docker后者答案是Compose vs k8s方向错了必跪。2. 集群搭建的核心原理从单机minikube到生产级多节点k8s安装部署相关的搜索量一直很高因为这块坑最多。从部署方式上可以分为几档本地学习用minikube、单机体验用k3s、生产级用kubeadm或二进制、云上直接用托管服务。它们解决的问题完全不同不要混为一谈。我见过最典型的错误是想学k8s一上来就在生产服务器上跑kubeadm然后被一堆网络插件、证书、镜像拉取问题劝退。正确的路径一定是先用minikube把核心概念玩熟再考虑自己搭集群。2.1 kubeadm部署的底层逻辑一切围绕“控制平面”kubeadm的核心动作就两个kubeadm init在控制平面节点上初始化集群kubeadm join把工作节点接入。但这两个命令背后做的事才是你真正要理解的生成证书和密钥k8s组件之间的通信全部走TLSinit时会在/etc/kubernetes/pki下生成CA证书、API Server证书、etcd证书等一系列文件。生成组件静态Pod清单API Server、etcd、Controller Manager、Scheduler这四类控制平面组件kubeadm把它们以静态Pod的方式跑起来清单文件写在/etc/kubernetes/manifests/目录。这算k8s“自举”的一种方式。安装kubelet和kube-proxy前者负责接收Pod清单并启停容器后者负责Service的负载均衡转发规则。下发集群管理配置/etc/kubernetes/admin.conf就是kubectl要用的kubeconfig文件相当于你的“集群超级管理员凭证”。执行kubeadm init之后输出会提示你复制某段kubectl配置到~/.kube/config这一步很多人会跳过然后发现kubectl命令全部报错——不是集群没起来只是你没有管理员权限文件。2.2 网络插件是集群能否就绪的分水岭kubeadm初始化完成不代表集群可用。这可能是新手最容易懵的地方明明显示初始化成功但kubectl get nodes看到控制平面节点一直是NotReady。绝大多数情况下是缺了CNI网络插件。k8s只管工作负载调度跨节点容器通信需要额外的网络方案来解决。常见的CNI有Calico、Flannel、Cilium选型上插件特点适用场景Flannel简单、性能较好基于VXLAN/overlay实现学习环境、内网小集群Calico支持网络策略BGP模式下性能高生产环境、需要细粒度网络控制Cilium基于eBPF功能最强可做透明加密大规模集群、对网络性能敏感安装CNI之后重启组件才会变成Ready这个顺序不能反。如果你在minikube里玩网络部分已经被minikube自动处理掉了所以很多人是在第一次手动搭集群时才遇到这个坎。注意控制平面节点默认taint为不可调度普通业务Pod。你搭单机集群想在上面跑应用需要执行kubectl taint nodes --all node-role.kubernetes.io/control-plane-去掉这个污点。生产集群千万别这么干。2.3 Ubuntu部署k8s的常见坑在Ubuntu上部署最容易踩的几个点swap必须关kubelet默认要求swap关闭。swapoff -a只是临时生效要持久化得编辑/etc/fstab注释掉swap行。内核参数要配桥接iptables透传必须打开涉及net.bridge.bridge-nf-call-iptables这个内核参数不配的话节点间网络会出现诡异不通。containerd的systemd cgroup驱动k8s推荐cgroup驱动使用systemd而containerd默认配置可能是cgroupfs必须在/etc/containerd/config.toml里改掉否则节点CPU内存状态会异常甚至kubelet直接报错。这些步骤顺序看起来麻烦但本质上都在为一个目标服务让节点上的容器运行时和kubelet的预期保持一致。3. Pod如何真正跑起来细聊entrypoint、cmd和容器启动流程“k8s里Pod的启动流程和Docker run一个容器有什么区别”——这也是高频面试题同时也是很多人写工作负载YAML时翻车的重灾区。先说最基础的概念Pod是k8s的最小调度单位里面可以有一个或多个容器这些容器共享同一个网络命名空间和存储卷。为什么k8s不直接调度容器因为在很多场景下几个进程需要紧密协作、共享网络和存储比如一个sidecar容器负责收集主容器日志一个代理容器负责转发主容器的流量。它们必须同生共死、共享资源这时候把它们打包进同一个Pod就非常合理。3.1 Dockerfile的CMD和k8s的command、args这个问题在面试里经常被拆成Docker镜像里的ENTRYPOINT和CMD有什么用k8s的command和args到底覆盖谁先看Docker层面的规则ENTRYPOINT定义容器启动时固定执行的命令很难被覆盖CMD要么作为ENTRYPOINT的参数要么是容器启动的默认命令。在k8s中更直接command对应Docker的ENTRYPOINTargs对应Docker的CMD。所以如果你在k8s的容器定义里只写command镜像里的CMD会被当成参数传给command如果你只写args镜像的ENTRYPOINT会被保留args作为参数传进去。举例来说镜像里默认是ENTRYPOINT [nginx]、CMD [-g, daemon off;]你在k8s里写containers: - name: nginx image: nginx:1.25 args: [-g, global off;]实际执行的就是nginx -g global off;。如果写成command: [/bin/sh, -c, echo hello sleep 3600]那镜像里的ENTRYPOINT完全被绕过了。3.2 探针决定Pod的“生死判断”容器启动了不代表应用就绪了。k8s里Pod的状态维护依赖三种探针livenessProbe决定容器要不要重启。如果应用死锁但没有退出进程liveness失败后kubelet会杀掉容器重启。readinessProbe决定服务是否接入负载均衡。失败时Pod的IP会被摘掉但容器不会重启。startupProbe为启动慢的容器设计的保护期期间CPU占满也不会被liveness打扰。很多新手只配置了liveness或者干脆都不配结果流量打到还没就绪的Pod上、或者启动慢的容器被反复重启。实际部署服务时我建议至少配置readinessProbeHTTP服务就用HTTP探针检查某个健康接口比如readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10这个配置的含义是容器启动5秒后开始探测每10秒探测一次/healthz路径。返回2xx/3xx就视为就绪。4. 创建只读权限用户RBAC授权链路的完整实操“k8s里怎么创建一个只有只读权限的用户”——这个问题很实际因为交给别人或程序去查看集群状态时绝对不能把admin权限散出去。我之前见过有人图省事直接把kubeconfig复制给别人相当于把集群的“root密码”交出去了出事只是时间问题。k8s的权限体系核心是RBAC。要创建一个只读用户需要走完整的链路签发用户证书 → 生成kubeconfig → 创建Role/ClusterRole → 绑定Role/RoleBinding。每一步都不能省。4.1 第一步用集群CA给用户签发证书k8s本身不管理用户它信任API Server配置的CA签发的证书。所以你要扮演“CA机构”给新用户发一张由集群CA签名的客户端证书。实际操作时登录到控制平面节点进到/etc/kubernetes/pki目录执行# 生成用户私钥 openssl genrsa -out readonly.key 2048 # 用私钥生成证书签名请求 openssl req -new -key readonly.key -out readonly.csr -subj /CNreadonly/Oreadonly-group # 用集群CA签发证书 openssl x509 -req -in readonly.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out readonly.crt -days 365这里CNreadonly就是用户名Oreadonly-group是用户所属组。组名在RBAC里可以做到批量授权不过只读场景下直接绑定用户就行。4.2 第二步生成kubeconfig有了证书需要把它们组装成kubeconfig文件。这里最容易出错的是server地址必须填集群API Server的访问地址通常是https://控制平面IP:6443。如果你填成localhost拿到其他机器上用必挂。kubectl config set-cluster mycluster \ --serverhttps://192.168.1.10:6443 \ --certificate-authorityca.crt \ --kubeconfigreadonly.kubeconfig kubectl config set-credentials readonly \ --client-certificatereadonly.crt \ --client-keyreadonly.key \ --kubeconfigreadonly.kubeconfig kubectl config set-context readonly-context \ --clustermycluster \ --userreadonly \ --kubeconfigreadonly.kubeconfig kubectl config use-context readonly-context --kubeconfigreadonly.kubeconfig注意我用了--certificate-authorityca.crt而不是--embed-certsfalse这样会把CA证书内容内嵌进kubeconfig别人拿到单个文件就能用。4.3 第三步RBAC角色定义与绑定证书只解决“你是谁”的问题还不解决“你能做什么”。接下来用RBAC说清楚只读用户能读什么资源apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: readonly rules: - apiGroups: [] resources: [pods, services, endpoints, namespaces, nodes, events] verbs: [get, list, watch] - apiGroups: [apps] resources: [deployments, statefulsets, daemonsets, replicasets] verbs: [get, list, watch]ClusterRole是整个集群范围的权限。如果你只想让用户看某个命名空间可以用Role然后绑定到对应的命名空间。这里更灵活但需要想清楚边界。绑定ClusterRolekubectl create clusterrolebinding readonly-binding \ --clusterrolereadonly \ --userreadonly绑定完成之后用kubectl --kubeconfigreadonly.kubeconfig get pods验证。如果能列出Pod但执行kubectl delete pod xxx时报forbidden说明权限设置正确。注意verbs里的get只允许获取单个资源详情list允许列出集合watch允许持续监听变化。开监控告警场景下通常三者都要给缺一个就会导致某些客户端请求报错。5. CPU Throttling容器被“限速”后延迟从哪里来CPU Throttling这个问题生产环境踩过的坑绝对不少而且非常隐蔽。现象是服务没有报错、CPU使用率也不到100%但接口延迟时高时低qps上不去。5.1 cgroup CPU限流机制CFS周期k8s里设置Pod的resources.limits.cpu底层的实现是cgroup的CPU控制组。Linux内核用CFS完全公平调度器来分配CPU时间其中两个关键参数cpu.cfs_period_us调度周期通常100000100mscpu.cfs_quota_us在这个周期内允许该容器使用的CPU时间量。如果你设置limits.cpu为2意味着每100ms周期内容器最多使用200ms的CPU时间对应2个核心。当容器在这个周期内用满了配额后续的线程调度就会被throttle——强制挂起等到下个周期再继续执行。问题恰恰出在这里如果你把线程数开得很大、或者GC线程瞬时暴增即使平均CPU使用率不高也会在一个周期的某个瞬间触及配额上限导致大量线程被短暂卡住。表现出来就是P99延迟飙升但平均CPU看起来完全正常。5.2 如何确认容器的CPU是否被Throttling排查方式看这几个指标# 进入容器所在节点读取cgroup统计 cat /sys/fs/cgroup/cpu/kubepods/pod-id/cpu.stat输出里的nr_throttled表示被限流的次数throttled_time是被限流的累计时间。如果nr_throttled持续增长、throttled_time占比高基本可以实锤CPU Throttling。更直观的方式是把Prometheus的container_cpu_cfs_throttled_seconds_total和container_cpu_usage_seconds_total画在一起对比如果前者的增长曲线跟后者几乎同步说明CPU大部分时间被强制定住而不是在正常执行。5.3 应对策略不是盲目调大limit遇到CPU Throttling很多人的第一反应是调大limit。但如果你观察到的现象是“平均使用率远低于limit”调大limit可能依然逃不过throttling——因为问题出在瞬时峰值而不是稳态负载。更务实的几招检查Go的GOMAXPROCS、Java的线程池大小很多运行时会按节点CPU核数创建线程如果Pod被限制为2核但物理机是32核线程数多就会加剧争抢。Java配合JDK 10可用-XX:ActiveProcessorCount或容器感知参数Go可以设置GOMAXPROCS与limits一致。把requests和limits分开生产环境我通常建议只设requests不设limits或者limits给一个明显高于峰值的水位。没有limits时CFS完全没有配额限制自然不存在throttling问题。调整单周期配额不能全局解决cpu.cfs_period_us从100ms调大到1s相当于给更宽的评估窗口能缓解但不能根治瞬时峰值问题而且会影响公平调度。这个问题的核心在于理解CPU limits对应用来说不是“上限提醒”而是“强制断水”。你在配额内的任何高频使用都可能触发throttle。6. 监控告警体系搭建node-exporter、Prometheus与磁盘告警配置生产集群跑起来之后监控告警才是让你晚上能睡着的关键。很多教程会把Prometheus、Grafana、node-exporter的部署步骤罗列一遍但你照着做完了遇到“磁盘要爆了却没人通知”的情况才知道告警规则才是最核心的。6.1 三件套各自干什么node-exporter采集节点的物理指标如CPU、内存、磁盘、网络、文件系统状态。它暴露在/metrics端点上Prometheus定时去抓取。Prometheus指标存储查询引擎。它按时间维度存指标提供PromQL查询语言也负责评估告警规则。Grafana可视化看板更直观地展示Prometheus里存的指标。Prometheus在k8s里部署方式比较标准——用helm chart或者YAML清单以Deployment方式运行通过Service的/metrics端口暴露自身指标node-exporter以DaemonSet方式跑在每个节点上。6.2 磁盘告警规则的写法与配置逻辑磁盘告警要分两个维度看节点磁盘使用率和文件系统剩余可用空间。先看最常用的节点磁盘使用率告警apiVersion: v1 kind: ConfigMap metadata: name: prometheus-alert-rules namespace: monitoring data: disk-alert.yml: | groups: - name: disk-alerts rules: - alert: NodeDiskUsageHigh expr: (1 - (node_filesystem_avail_bytes{fstype~ext4|xfs} / node_filesystem_size_bytes{fstype~ext4|xfs})) * 100 85 for: 5m labels: severity: warning annotations: summary: 节点磁盘使用率超过85% description: 节点 {{ $labels.instance }} 文件系统 {{ $labels.mountpoint }} 使用率已超过85%当前值 {{ $value | humanizePercentage }}这里的PromQL逻辑要拆开看node_filesystem_size_bytes是文件系统总大小node_filesystem_avail_bytes是可用空间两者相减得已用量再除以总大小得到使用率过滤fstype~ext4|xfs是排除伪文件系统比如tmpfs、overlay这些刷存在感的虚拟文件系统不参与计算。for: 5m表示持续5分钟超过阈值才触发告警这能有效过滤磁盘写入抖动造成的误报。还有一类场景是根分区可用空间不够但使用率不高——比如大文件系统虽然有90%是空闲但空闲值只剩10GB对于日志密集的服务仍然危险。所以更完备的规则会同时监控绝对剩余空间- alert: NodeDiskSpaceWillFill expr: node_filesystem_avail_bytes{fstype~ext4|xfs} 20 * 1024 * 1024 * 1024 for: 10m labels: severity: critical annotations: summary: 节点磁盘剩余空间不足20GB两种情况结合使用既能防止大盘子慢速写入被忽略也能防止小盘子的使用率告警在1分钟内连环轰炸。6.3 告警从发现到通知的流水线Prometheus把规则算出来了还需要一个动作把告警推出去。标准做法是加一个Alertmanager组件它会接收Prometheus推送的Alert然后做分组、去重、路由再通过webhook或邮件等方式通知你。Prometheus配置里加上alertmanager的地址alerting: alertmanagers: - static_configs: - targets: - alertmanager.monitoring.svc:9093我想提醒一个很实际的问题告警规则先想好联系人和静默策略再推到线上。身边真实案例是一条磁盘使用率告警因为没配置路由直接把所有人都拉进了一个群结果某次凌晨文件系统报警值班群炸了但真正负责的人在休假没有静默当天所有消息都变成了重复轰炸。Alertmanager的路由配置里常用的能力route: group_by: [alertname] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: default-receiverrepeat_interval一定要配避免每5分钟收到一遍同一条告警。group_wait是在同一时间窗口内的告警做聚合后统一发出避免多个节点同时触发磁盘告警时瞬间发送N条通知。7. entrypoint、CMD之外的高频追问Pod生命周期与常见排障命令前面几节其实已经覆盖了k8s最核心的命脉架构、部署、PID、权限、性能、监控。但“问答题初始化版”不能少了排障环节。面试或实操中很多问题最终都落到“怎么查Pod为什么起不来”上这里给一套可以背下来的排查链路。7.1 Pod状态机从Pending到RunningPendingPod已被调度器接收但容器还没起来。常见原因是镜像拉不下来、节点资源不足、PVC无法挂载。ContainerCreating正在创建容器。长时间停留在这里多半是镜像拉取失败或存储卷挂载卡住。Running容器正常运行。CrashLoopBackOff容器启动后马上崩溃kubelet拉起重试反复循环。这是最常见的故障状态。排障第一步永远是看事件kubectl describe pod pod-name -n namespaceEvents部分会直接告诉你卡在哪里比如FailedScheduling后面通常会跟“Insufficient memory”这样明确的资源不足原因或者“Failed to pull image”说明镜像拉取失败。走到这一步80%的问题都能定位。7.2 从Pod反查日志与进入容器确认容器起来了但业务异常就要看日志# 查看容器标准输出 kubectl logs pod-name -n namespace # 指定容器多容器Pod必用 kubectl logs pod-name -c container-name -n namespace # 进入容器 kubectl exec -it pod-name -n namespace -- /bin/sh进入容器后先看进程和端口ps aux ss -tlnp curl localhost:8080/healthz这套命令能快速判断应用到底有没有在监听、健康检查接口通不通。很多资源配了探针但路径写错导致的反复重启也是这个阶段才能看到真相。7.3 实战案例镜像拉取失败与CrashLoopBackOff我在一次线上排查中遇到Pod一直ImagePullBackOffdescribe事件显示Failed to pull image xxx:latest。检查了镜像仓库配置发现没设置imagePullPolicy: IfNotPresent。虽然这只影响每次重启时要不要拉取镜像但结合一个频繁重启的Pod就变成了无限拉取失败。修复办法是把镜像tag固定成具体版本并在deployment里显式设置containers: - name: demo image: registry.example.com/demo:v1.2.3 imagePullPolicy: IfNotPresent另一个高频问题应用启动时需要连接数据库但Pod起来了数据库还没就绪导致应用狂报连接错误kubelet不停重启。典型的CrashLoopBackOff。这种情况用startupProbe根本不行因为应用启动就崩。正确解法是给数据库和应用的启动顺序做依赖或者在代码里做启动重试。k8s只能保证你的调度逻辑不能帮你解决应用自身的启动依赖。8. 从“初始化版”到“实战派”最后补几个必须会的kubectl技巧前面几节集中在概念和部署上但日常操作效率很大程度上取决于你kubectl用得顺不顺。这里分享我在实际使用中最常碰到的几个场景和对应的命令。8.1 多集群kubeconfig切换你手里可能同时有开发、测试、生产几套集群的kubeconfig直接覆盖~/.kube/config是最粗暴的。更规范的做法是把不同集群的配置合并到同一个文件里用context切换kubectl config get-contexts kubectl config use-context production生产环境一条命令切过去出错就危险了。我每次切context之前都会先看一眼当前指向哪个集群kubectl config current-context更保险的做法是靠KUBECONFIG环境变量区分不同环境配置文件比如KUBECONFIG~/.kube/dev.conf kubectl get pods。8.2 快速查所有命名空间的东西刚开始用k8s时我习惯一个个命名空间查资源直到学会这一行kubectl get all -A-A是--all-namespaces的缩写一条命令看全集群的Pod、Service、Deployment、StatefulSet等。排障时先跑这个看哪个命名空间的Pod异常再进去细查。8.3 用标签筛选代替输Pod全名Pod名字每次部署都会变化输全名不现实。学会用标签选择器kubectl get pods -l appnginx -n production kubectl logs -l appnginx -n production注意kubectl logs -l支持多个Pod的日志聚合输出这个在查看多个副本日志的时候非常方便不用一个个kubectl logs去敲。8.4 临时调试Pod与删除资源调试阶段经常需要起一个临时Pod来做端口转发或者验证网络连通性kubectl run debug -it --rm --imagebusybox --restartNever -- /bin/sh--rm表示退出后自动删除Pod非常干净。删除资源时也要注意直接kubectl delete deployment xxx会连带删除Pod但如果你只是删PodDeployment会立即重新创建新的Pod很多人误以为“删不掉”其实是工作负载控制器在自愈。# 删除一组相关资源 kubectl delete deployment,service -l appnginx8.5 最后分享一个排障习惯无论你是新手还是有一定经验遇到Pod状态异常时固定按这个顺序排查kubectl get pods -A看全局确定哪个命名空间有异常kubectl describe pod name -n namespace看事件定位阶段性问题kubectl logs name -n namespace --tail50看最近日志确认应用层异常如果还解不了kubectl get events -n namespace --sort-by.lastTimestamp把最近的事件按时间排序拉出来。这套组合拳在我排障时几乎没有失手过。记住k8s的报错信息绝大多数情况下会直接告诉你原因问题是很多人没去看Events就凭感觉猜。把事件、日志、资源状态三件套看全你就超过了80%的运维。