尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Kubernetes进阶:控制器、服务路由、存储与调度实战解析

Kubernetes进阶:控制器、服务路由、存储与调度实战解析 Kubernetes 入门学到下半程很多人会明显感觉到难度上来了上篇还能靠 Pod、Deployment 这些形象的概念撑住到了 Service、存储、调度这块光靠“想当然”就不行了。这篇文章就接着上一篇往下走把控制器怎么实现“自愈”、流量怎么在集群里正确路由、配置和存储怎么从容器里剥离出来以及资源调度背后那套权衡逻辑逐一讲透。适合已经知道 Pod 是什么、但还没有把控制面各组件之间逻辑串起来的读者我会按生产环境里真正会用的方式来讲不绕弯子。1. 工作负载与控制器Kubernetes 的“自愈”核心1.1 控制器模式Kubernetes 凭什么“自愈”很多人第一次听到“Kubernetes 能自愈”这个概念第一反应是“它怎么知道 Pod 挂了”答案不在某个神秘组件里而在控制器模式这套机制中。你提交的每个资源对象比如 Deployment都会在 YAML 里写清楚“期望状态”。Kubernetes 的控制平面里有一堆控制器它们的工作就是盯着当前状态然后想方设法把当前状态推向期望状态。这个“盯”的动作不是轮询而是通过List-Watch机制从 API Server 那里实时监听资源变化。一旦发现 Pod 数量少了、镜像版本不对、健康检查失败控制器就开始干活。你可以把它理解成家里空调的恒温器你设了 26 度它就一直测量室温高了制冷、低了制热不把室温拉回 26 度不罢休。明白这一点之后很多困惑会迎刃而解。比如新手经常问“为什么不直接用 Pod非要包一层 Deployment”如果用 Pod你等于告诉 Kubernetes“目标状态就是这 3 个 Pod别动”。但 Pod 本身是有生命周期的节点宕机也好、被驱逐也好Pod 一旦没了谁都不记得它存在过。而 Deployment 表达了“我要 3 个副本版本是什么”这个目标状态控制器会保证有 3 个健康的副本存在Pod 本身反而不重要了。这就是声明式 API 和指令式操作的差别你只描述“要什么”不用关心“怎么变过去”。1.2 工作负载选型不要只会 DeploymentDeployment 确实最常见但它不是万能钥匙。生产环境里选错工作负载类型是相当隐蔽的问题。你对工作负载的选型本质上是对“这个应用到底有没有状态”的判断。工作负载核心适用场景关键特征Deployment无状态应用如 Web API、后端服务任意副本可替换共享存储或不需要持久化DaemonSet每个节点只需要一个实例如日志采集、节点监控新节点加入自动部署节点删除实例回收StatefulSet有状态应用如数据库、消息队列稳定网络标识、有序伸缩、每个副本独立存储Job / CronJob一次性任务、定时任务Job 跑完自动退出CronJob 按时间表调度选错会有什么后果举一个我印象很深的例子有人把日志采集 Agent 做成了 Deployment副本数设为 2。结果集群有 10 个节点只有 2 个节点上跑了采集器剩下 8 个节点的日志全部丢在本地无人问津。这种问题用 DaemonSet 会好很多DaemonSet 的原理就是控制器遍历每个节点保证符合条件的节点上刚好运行一个 Pod。节点宕机、新节点加入它都会自动补位。StatefulSet 是另一类重点。相比 Deployment它给每个 Pod 提供了稳定的网络标识和独立的持久化存储。比如一个三节点的 ZooKeeper 集群Pod 名分别是 zk-0、zk-1、zk-2不管怎么重新调度zk-1 永远叫 zk-1也永远挂载它自己的那份数据卷。这个“稳定身份”是有状态应用能跑在 Kubernetes 里的基础。没有这个机制数据库重启后 IP 变了、数据丢了整个状态就乱套了。1.3 滚动更新的两个关键参数Deployment 的滚动更新是很多人用了很久却不清楚细节的功能。你更新镜像版本后Deployment 不是一次性把旧 Pod 全删掉再起新 Pod而是分批替换。这里面有两个参数决定了替换的节奏maxSurge和maxUnavailable。maxSurge表示滚动更新过程中最多可以比期望副本数多跑多少个 PodmaxUnavailable表示最多允许有多少个 Pod 处于不可用状态。它们的默认值都是 25%但这里的计算方式有一个容易踩坑的地方当副本数不足 4 时25% 的向上取整会让节奏变得很奇怪。比如你只有一个副本maxUnavailable: 25%取整后是 1意味着滚动更新时老 Pod 可以直接删掉新的还没起来服务就暂时不可用了。生产环境里如果你的服务对可用性要求高要么把这两个参数显式写清楚比如maxSurge: 1、maxUnavailable: 0保证先起一个新 Pod 再停一个旧 Pod要么至少知道默认行为是什么。很多线上事故不是镜像有问题而是滚动更新策略把服务“滚”挂了。2. 服务发现与流量路由让 Pod 地址“漂移”不再可怕2.1 Service 与 kube-proxyClusterIP 是怎么把流量转给 Pod 的Pod 是有 IP 的但没人敢在应用代码里写死某个 Pod 的 IP因为 Pod 随时可能被重建IP 会变。这个问题靠Service解决。Service 是一层稳定的虚拟入口它通过Label Selector选择一组 Pod然后提供一个固定的 ClusterIP。这个 ClusterIP 是虚拟 IP本身不绑定任何实体真正干活的是集群里每个节点上的kube-proxy组件。kube-proxy 会持续监听 API Server拿到 Service 和后端 Pod 的变化然后写 iptables 或者 IPVS 规则把访问 ClusterIP 的流量转发到某一个实际的 Pod IP 上。早期版本默认用 iptables规则一多性能就会有瓶颈现在很多集群用 IPVS性能和灵活性好很多。但在使用层面你不一定需要关心底层实现只要记住一个逻辑Service 就是 Pod 前面的负载均衡器。请求到了 ServiceService 根据负载均衡策略把请求转发给后面的某个 Pod。Service 还会自动维护一个叫Endpoints或EndpointSlice的对象里面记录了这个 Service 当前关联的所有 Pod IP。这个对象非常有用排查问题时kubectl get endpoints能直接告诉你 Service 后面到底有几个可用后端。如果 Endpoints 是空的就算 Service 配置得再花哨也是白搭。2.2 三种 Service 类型怎么选Service 的type字段决定了这个服务的可访问范围新手最容易在这里犯迷糊。类型访问方式适用场景ClusterIP集群内部通过虚拟 IP 访问服务间内部调用、不需要外部访问NodePort通过每个节点的 IP 固定端口访问临时对外暴露、调试、小规模服务LoadBalancer云厂商负载均衡器对接外部通过 LB 地址访问生产环境对外提供 HTTP/HTTPS 服务一个常见误区是为了图方便所有服务都配 NodePort。但 NodePort 的端口范围默认只有 30000-32767数量有限而且如果业务量大了每个服务占用宿主机端口既难管理也容易冲突。更合理的做法是内部调用用 ClusterIP需要外网访问时统一走 Ingress 或 LoadBalancer。还有一个细节容易被忽略Service 不一定要有自己的 IP。把clusterIP设为None可以得到一个 Headless Service它不做负载均衡而是直接把后端 Pod 的真实 IP 暴露给调用方配合 StatefulSet 使用非常合适。比如数据库集群里每个 Pod 需要知道其他节点的真实地址来组成集群Headless Service 配合 DNS 就能做到。2.3 Ingress七层路由才是生产环境的主角NodePort 和 LoadBalancer 都工作在四层不关心 HTTP 的路径、域名这些信息。生产环境里你通常会希望一个入口既能按域名路由又能按路径分流。这就是 Ingress 存在的意义。但这里有一个经常让新人迷惑的点Ingress 本身只是个声明式资源不干活。你写了一个 Ingress 对象它不会自动产生任何路由能力集群里必须有一个Ingress Controller比如 nginx-ingress、traefik才能真正读取 Ingress 规则并配置负载均衡器。可以这么类比Ingress 是规则Ingress Controller 是执行规则的人。实际使用中流量路径是这样的外部请求到达 Ingress Controller通常是一个暴露出来的 LoadBalancer 或 NodePortController 根据 Ingress 规则里的域名和路径把请求转发到对应的 Service再由 Service 转发给 Pod。所以 Ingress 不是替代 Service而是在 Service 前面加了一层智能路由。我在项目里更喜欢 nginx-ingress原因很简单它基于 OpenResty生态成熟注解丰富能处理复杂路由、重写、跨域、限流这些需求。新手入门也不用害怕先把 Ingress 资源里的host、path、backend三个字段吃透后面再花时间研究 Controller 的高可用和性能调优。3. 配置、机密与存储把状态从容器里“拿出来”3.1 ConfigMap 与 Secret配置和敏感信息容器镜像应该保持不可变这句话的落地方式之一就是把配置从镜像里抽出来。ConfigMap和Secret就是干这个的。它们的本质很相似都是存在 etcd 里的键值数据注入 Pod 的方式也一样只有两个环境变量和挂载成文件。用环境变量注入适合简单的键值对比如“数据库地址”“日志级别”。但环境变量有个缺点应用启动时读一次之后改 ConfigMap 环境变量也不会变。挂载成文件的好处是ConfigMap 更新后文件也会跟着更新有短暂延迟应用如果支持监听文件变化就能实现配置热加载。Secret 和 ConfigMap 的最大区别在于用途存敏感信息比如密码、Token、证书。但请记住一个残酷的事实Secret 默认只是做了 Base64 编码不是加密。任何人如果有权限读取 etcd或能通过 Kubernetes API 拿到 Secret 对象都能轻松解码。所以生产环境必须另外开启 etcd 加密存储配合 RBAC 严格限制访问权限。这里还有个实操建议对于基本不会变的配置比如生产环境的数据库连接串记得为 ConfigMap 和 Secret 设置immutable: true。不可变对象的好处显而易见不用担心有人偷偷改了配置应用莫名其妙重启同时对于 kubelet 来说不可变对象的监听开销也小很多。3.2 PV、PVC、StorageClass存储抽象三件套存储是 Kubernetes 里最容易被忽略、又最容易出问题的领域。很多人第一眼看到 PV、PVC、StorageClass 三个概念直接懵了我提供一个简单的类比来帮助理解PVPersistentVolume管理员准备的一块存储资源相当于仓库里的一块硬盘。PVCPersistentVolumeClaim应用提出的存储需求相当于“我要一块 100GB 的硬盘”。StorageClass动态提供存储的“模板”PVC 提出需求后StorageClass 自动帮你创建对应的 PV。理解这三者关系的关键在于PVC 和 PV 是一对一绑定的。Pod 通过 PVC 声明“我要用多少存储”Kubernetes 找到匹配的 PV 并绑定。绑定之后这块 PV 就归这个 PVC 使用了。如果你用的是 StorageClass 动态供给那么 PV 是自动创建出来的PVC 删掉后 PV 的回收策略决定了数据是保留还是删除。回收策略是生产环境的一个重点。Retain表示 PVC 删除后 PV 还在需要管理员手动处理数据Delete表示云盘直接删除。对于数据库这类不能丢数据的应用我会建议至少用Retain否则误删 PVC 导致云盘被清空的教训一次就够刻骨铭心了。3.3 StatefulSet 接存储的完整链路前面说过 StatefulSet 给有状态应用提供稳定身份但真正让数据不丢的是它和 PVC 的组合拳。StatefulSet 有一个特殊字段叫volumeClaimTemplates可以理解成“为每个副本单独创建一个 PVC”。工作中我用这个模式部署过一套 Elasticsearch 集群流程是StatefulSet 声明了 3 个副本每个副本通过 volumeClaimTemplates 申请一块 500GB 的云盘。扩容时新 Pod 会自动创建新的 PVC缩容时PVC 默认保留。这套机制保证了一个核心理念Pod 可以随便死数据永远跟着 PVC 走。哪怕整个 Pod 被删了重建新 Pod 通过相同的 PVC 挂载数据依然完整。但要注意一个反直觉的点StatefulSet 的扩缩容是有顺序的。扩容时Pod 编号从 0 开始依次创建缩容时是从最后一个开始删除。这个有序性既是特性也是限制。如果你需要同时把所有副本全部启停StatefulSet 会显得非常笨拙。但正因为有这个顺序很多主从类应用比如 ZooKeeper、Kafka才能在上面跑得稳定。4. 资源管理、调度与多租户隔离把集群资源“算清楚”4.1 Requests/Limits 与 QoS不让一个 Pod 拖垮全家没有资源限制的 Kubernetes 集群就像没有交规的高速公路。某个应用内存泄漏能把整个节点搞到 OOM所有 Pod 一起遭殃。要想避免这种情况必须给每个容器设置资源配额requests和limits。这两个字段的区别很多人说不清。简单讲requests 是调度依据调度器看的是每个节点是否满足所有 Pod 的 requests 总和limits 是运行限制容器最多能用多少 CPU、内存。CPU 的 limits 可以靠内核限流硬控但内存一旦超过 limits容器会被内核直接杀掉。根据 requests 和 limits 的设置方式Pod 会被划分成三个 QoS 等级Guaranteed、Burstable、BestEffort。等级越高系统内存不足时被优先保留的可能性就越大。这是很多生产事故的根源核心服务没设 limits反而被系统误判为 BestEffort节点内存紧张时它比那些设了 requestslimits 的下游服务先被杀掉。我的建议是核心业务尽量做到requests limits牺牲一点弹性换稳定性非核心任务requests 可以低一些让集群有超卖能力提高资源利用率。但请你一定要设置 limits尤其是内存——这是集群稳定运行的最低底线。4.2 调度器是怎么决定 Pod 位置的每个新 Pod 被创建后都要经过调度器决定它落在哪个节点上。调度器的工作流程分两步过滤和打分。过滤阶段把不满足条件的节点剔除比如资源不够的、有污点不容忍的打分阶段对剩余节点按各种维度评分最终选出分数最高的节点绑定。很多人在做调度时会想到给某些 Pod 指定节点。最粗暴的方法是nodeSelector它只能做简单的标签匹配。如果需求更复杂比如“把我调度到有 SSD 的节点上而且最好和另一个服务在同一个可用区”就需要用节点亲和性、Pod 亲和性这些高级特性。另外taint污点和toleration容忍是一对互逆机制。给节点打上污点比如“这个节点是给数据库专用的”普通 Pod 就不应该被调度上来如果某个 Pod 明确表示能容忍这个污点它才允许被调度到这个节点。这个机制在隔离混部场景里非常有用。不过新手入门阶段建议先把 nodeSelector 用熟再去研究亲和性和污点否则很容易被调度规则之间互相“打架”的问题困扰。4.3 命名空间与配额多团队共用集群的边界Kubernetes 单集群能支撑很大的规模当多个团队共用一套集群时命名空间Namespace是资源隔离的基础单元。它把对象按逻辑分组也充当了一个安全边界和配额边界。有了命名空间还需要配套ResourceQuota和LimitRange。ResourceQuota 给整个命名空间设置资源上限比如总内存不能超过 200G、最多只能创建 50 个 PodLimitRange 给单个 Pod 或容器设置默认值和上下限防止有人创建没有资源声明的巨型容器。听我一句劝多团队共用集群时配额一定不要偷懒。没有配额约束的命名空间最终都会走向“公地悲剧”——每个团队都觉得自己用得不多结果整个集群的 kubelet 压力越来越大节点频繁上报 NotReady。我自己就处理过一次典型的案例某团队误把巡检脚本写成了无限循环Pod 疯狂产生日志因为没设日志轮转和资源限额半天时间打满了节点磁盘。后来重建集群排查才发现从根上就少配了LimitRange所有 Pod 都没有资源限制。5. 生产环境避坑经验几个让我印象深刻的“翻车现场”5.1 探针配置不对发布就是事故上线前你以为服务是好的Kubernetes 也以为服务是好的但用户就是报错。这种问题十有八九出在探针配置上。Kubernetes 有三种探针livenessProbe检查容器是否还活着失败会重启容器readinessProbe检查容器是否准备好了失败会把 Pod 从 Service 后端中摘除startupProbe用于保护启动慢的应用在启动阶段不执行前两种探针。三者各有用途但很多人只配了 liveness没配 readiness导致滚动更新时新 Pod 还没完成初始化就已经被拉进负载均衡池开始接收流量结果自然是接口大面积超时。正确做法是针对启动慢的应用必须配 startupProbe关注业务的就绪状态必须配 readinessProbelivenessProbe 的阈值要留足余量不能因为一次偶发的慢请求就频繁重启。探针里的initialDelaySeconds、periodSeconds、timeoutSeconds都是要按应用实际启动时间来调的不能照抄网上的模板。5.2 不设 Limits 的“隐形炸弹”有一次我排查一个节点频繁 NotReady 的问题看监控发现这个节点的内存使用率一直顶到 100%kubelet 都开始不稳定了。用 kubectl describe node 一看上面跑着十几个 Pod大部分是测试团队的临时应用全都没有设置 limits。内存一紧张内核开始到处杀进程连 kubelet 自己的 Pod 都差点被 OOM 干掉。这是一个非常要命的恶性循环越不设 limits节点越不稳定节点越不稳定上面的 Pod 被驱逐重建越频繁重建的 Pod 又继续不设 limits继续霸占资源。所以我现在接手任何集群第一件事就是扫描全部工作负载找出没有设置 limits 的 Pod一个都不放过。你至少要把内存的 requests 和 limits 写明白这是底线。5.3 滚动更新参数与优雅停机滚动更新还有一个容易忽略的问题旧 Pod 退出时的优雅停机。Pod 被删除时kubelet 会向容器主进程发送 SIGTERM 信号然后等待一个terminationGracePeriodSeconds默认 30 秒后才强制 SIGKILL。很多应用没处理 SIGTERM收到信号直接退出正在处理的请求就被粗暴中断了。配合 preStop Hook 可以解决这个问题。比如在 preStop 里执行sleep 5给 Service 摘除该 Pod 留出时间窗口同时把 readinessProbe 的失败检测周期调短让新 Pod 在被删除前先被标记为不可用避免新流量进来。这一套组合拳下来滚动更新基本可以做到用户无感知。我见过很多业务方抱怨“发布期间有零星报错”排查到最后都是优雅停机没做好和业务代码本身关系不大。如果你正在学习 Kubernetes 的中间阶段我特别建议你亲手搭一个小集群把上面这些概念逐个验证一遍。尤其是控制器、Service、存储、调度这几个主题只看文档不实操理解会一直停留在表面。我当年就是踩了滚动更新和资源配额这两个坑之后才真正理解声明式 API 的边界在哪里它能保证“结果正确”但“过程优雅”还得靠你自己配置。
返回列表