资讯详情

资讯详情

K8s全栈编排与高可用架构设计:从原理到生产实践

K8s 的课程看了不少但很多半路就断了要么停在 Pod 和 Service 的演示要么丢给你一堆 YAML 让你复制。真正到了生产环境你会发现集群不启动、网络不通、Pod 排不上一堆问题涌出来——这时候才明白“云原生架构落地”这几个字的重量。这门第六课就是把整套 K8s 全栈编排和高可用架构设计从原理讲到实操。我把它整理成文字版分享给正在转型的运维、后端以及所有被 YAML 折磨过的同学。这轮内容不适合只看不练。如果你负责的业务系统已经在容器化或者正在设计一套面向生产的私有化平台那么这一课的核心价值在于如何用 K8s 本身提供的能力把服务编排、基础设施容错、流量调度、配置管理和故障自愈串成一条完整链路。所谓“全栈编排”不是把 Pod 跑起来就算完而是让整条链路具备在真实故障下仍然可用、可恢复、可观测的能力。1. 我先掰开讲清楚“终极落地”到底落了什么地1.1 理论跑通和生产可用之间差着一整个故障演练先泼一盆冷水跟着文档把 K8s 集群装出来再跑一个 Nginx 实例这不叫落地。这叫“环境验证”。真实逻辑里你的业务系统在跑促销活动时流量突然翻五倍某个节点凌晨被宿主机内存打爆某条链路超时导致雪崩——这些场景下集群还能不能正常对外服务才叫落地。云原生架构里的“终极落地”在我来看应该具备三件事业务能够快速交付、系统能够弹性伸缩、故障发生后能够自愈。这三个目标不是靠某一个组件实现的而是靠一套完整设计。K8s 把容器编排、服务发现、配置注入、资源调度、持久化存储这些能力全部收敛成声明式 API然后我们基于它做架构设计。我经常跟别人打比方K8s 像一个24小时在班的酒店大堂经理办理入住调度 Pod、控制房间分配资源配额、处理客人需求Service 和 Ingress、排查房间设施故障就绪探针和重启策略。但要让它当好这个经理你必须告诉它你有多少房间、什么房型、客人什么时候来、最坏情况能接受什么级别的服务降级。1.2 全栈编排编排的不只是容器很多人把“全栈编排”理解成Secrets 用了、HostPath 挂载了、Deployment 给了我三副本就算是编排了。这远远不够。实际生产里编排的对象至少包括四类计算资源容器怎么调度、怎么扩容、怎么保证不挤爆宿主机网络资源Pod 之间的通信、跨节点的网络平面、外部流量的接入方式存储资源持久化数据放哪里、存储能不能动态供给、扩容后数据怎么重新分布配置与策略环境变量、密钥、路由规则、灰度发布策略、资源配额。真正的全栈编排是用一套描述文件把业务从“代码”推到“对外可访问且数据安全”的状态。这个过程中你写的每一个 Deployment、Service、Ingress、ConfigMap、StorageClass 都在向集群声明“我希望它最终长成什么样”。K8s 的控制循环controller loop会不断对比期望状态和实际状态差异从哪里出现它就尝试修正到哪里。这里“声明式”三个字特别重要。以前我们在脚本里写的是“如果服务挂了执行重启命令”这是命令式逻辑。K8s 是“我声明这个服务要有三个副本”然后由调度器和控制器自行判断当前只有两个副本自动创建第三个。这不仅是写法上的不同它是从“我盯着系统”到“系统自己盯自己”的本质转变。2. 高可用架构设计的几个关键判断2.1 必须理解可用性数字背后的故障域约束高可用不是一个抽象感念它首先是一个可以量化的目标。比如“三个九”意味着一年 365 天最多只有 8.76 小时不可用。你要想清楚这个 8.76 小时分配到多次故障事件里单次故障允许恢复多久如果是“四个九”全年的不可用时间就只剩 52 分钟那任何需要人工半夜爬起来敲 kubectl 的流程基本都不达标。我把这套量化思考带到设计里后第一件事就是规划故障域。故障域是什么简单说就是某一个物理或逻辑组件挂掉之后会影响到的范围。一台宿主机是一个故障域一个机架是一个更大的故障域一个可用区又是更大的故障域。设计高可用架构的时候你的副本不能放在同一个故障域里。比如三个副本全部跑在同一台宿主机节点一挂整个服务的可用性直接归零。很多初学者在云平台上买一台大规格虚拟机然后用一条命令装完单机版 K8s再把服务部署上去这种做法我只能说“用于学习没问题用于生产是给自己挖坑”。2.2 控制面和数据面每一层都要留退路高可用设计要分成控制面和数据面分别考虑。控制面是大脑包括 API Server、调度器、控制器管理器和 etcd。API Server 负责接收所有请求etcd 负责存储所有状态。它们挂了你连“查看集群状态”的动作都做不了。所以控制面至少需要三台独立节点组成高可用组etcd 必须跨节点冗余存储并且开启 TLS 通信这里说的是内部组件通信的证书保障机制与任何外部网络访问无关。生产环境中etcd 不能和业务 Pod 混布太紧避免存储写入 IO 波动影响到共识协议的心跳。数据面是业务实际跑的地方也就是各种工作负载所在的节点。数据面的高可用主要靠副本冗余、反亲和性和资源预留。反亲和性就是让同一服务的不同副本尽量分布到不同节点上。如果不做这个设置调度器可能为了资源利用率把多个副本打包到同一台机器结果一场硬件故障让整个服务直接团灭。还有一个容易忽略的控制面组件——Controller Manager 里的各种控制器比如节点控制器、副本控制器它们的任务就是不断检查实际状态和期望状态是否一致。它们本身要有合理的 leader 选举机制避免多个控制器同时操作产生脑裂。所以设计控制面时不要把这三台机器当一个“更稳的单点”看而要把它们当三个相互备份的大脑通过一致性协议协同工作。2.3 流量路径上的高可用网关和接入层也要多副本外部流量进来一般先经过 Ingress 或负载均衡器再到 Service再到 Pod。很多高可用方案把大量精力放在业务 Pod 上结果网关入口本身就是单点。Ingress Controller 如果只有一个副本那你后端再妖娆也没用——流量根本进不来。正确的做法是入口网关至少双副本并且配置 PodDisruptionBudget确保在进行节点维护或滚动升级时至少有一个副本保持可用。很多人没注意到K8s 节点上有时候会因为驱逐操作把 Pod 迁移走如果你没有设置 disruption budget一个误操作可能瞬间把入口层的所有副本全部腾空。我通常还会在 Ingress 到后端 Service 之间启用健康检查。K8s 的 Service 本身会通过 Endpoints 自动剔除不健康的 Pod但这里有个细节如果 Pod 的 readiness 探针配置得太简单比如只检查“进程活没活”没检查“下游数据库连接池还能不能拿连接”那么流量还是会打到已经处于半死不活状态的 Pod 上。所以探针的检查维度必须贴近业务真实依赖而不是简单的“TCP 握手成功”。3. 实操过程从零搭一套带高可用属性的编排架构3.1 集群规划三件套网络、存储、域名定下来再动手这一步我可以给你一套直接能复用的规划方式。网络K8s 集群至少需要三个网段宿主机节点网段、Service 网段、Pod 网段。你选择网络插件的时候核心关注点应该放在“跨节点通信怎么实现”、“是否支持网络策略”、“封装开销有多大”。在设计网络模型时我会把 Pod 网段选得足够大避免后期扩容时重新规划比如 /16 以下的段位。别小看这个等集群规模到了几百台节点Pod 地址不够分配到时你要重构网络插件那叫一个酸爽。存储业务如果是无状态那存储规划可以简单一旦涉及数据库、消息队列或者文件上传就要引入存储类。我这里推荐的做法是创建一个 StorageClass由 K8s 通过 CSI 接口动态调用底层存储创建持久卷。为什么不要手动创建 PV因为手动 PV 需要你在创建 Pod 之前先手工把磁盘挂好、格式化好这个流程没法规模化。动态存储类的好处是你写 PVC 声明“我要 20Gi 的读写卷”存储控制器自动去底层创建好并绑定过来扩容、回收策略都由集群统一管理。域名内部服务域名直接使用 K8s 的 DNS 解析能力格式通常是 . .svc.cluster.local。外部域名则通过 Ingress 的规则映射到具体服务。我建议从一开始就梳理一份服务域名清单把各服务的对外路径和内部依赖关系写清楚。不要等几十个服务上线之后再盘点那时候你根本理不清谁调用了谁。3.2 业务上线链路Deployment、Service、Ingress 逐个击破下面我用一个模拟业务场景来做例子一个订单查询服务两个环境变量一个只读数据库依赖要求至少三个副本滚动更新时不能中断请求。先看 Deployment 的关键参数apiVersion: apps/v1 kind: Deployment metadata: name: order-query spec: replicas: 3 selector: matchLabels: app: order-query strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 maxSurge: 1 template: metadata: labels: app: order-query spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - order-query topologyKey: kubernetes.io/hostname containers: - name: order-query image: registry.example.com/order-query:v2.3.1 ports: - containerPort: 8080 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 15 periodSeconds: 10 livenessProbe: httpGet: path: /livez port: 8080 initialDelaySeconds: 30 periodSeconds: 20 resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2 memory: 2Gi这个配置里有几个点我着重说一下滚动更新策略maxUnavailable1 表示更新时最多允许一个副本不可用maxSurge1 表示最多允许临时多出一个副本这样既保证更新过程不停服也不会让资源瞬间冲顶。反亲和性三个副本尽量分布在不同宿主机上避免一台挂掉全挂。readinessProbe 和 livenessProbe 分开readiness 管服务能不能接流量liveness 管进程要不要重启。两个探针路径故意分开因为健康检查失败触发重启和摘除流量是完全不同的处置。资源预留requests 是调度依据limits 是运行上限。requests 太小会调度到资源不足的节点limits 设置过小会触发 OOM Kill。我这里 requests 设 512Mi 内存limits 设 2Gi是为了给 GC 和高峰留缓冲。CPU requests 设了 500m但 limits 到了 2 核允许容器在业务高峰时突破基础占用用满两个核。存储这个场景是无状态订单查询但如果查询有缓存数据落盘需求可以挂一个 PVC。做法是增加 volumeClaimTemplates 或单独 volumeMounts我这里建议不要用 emptyDir 保存关键数据因为 Pod 重建即丢。再说 Service。对于内部服务间的调用ClusterIP 类型就够。但如果你想从集群外部访问就需要 NodePort 或 LoadBalancer。NodePort 直接把节点端口暴露出来适合测试环境生产环境更合适的做法是 LoadBalancer由云平台或自建负载均衡器把流量转发到各节点端口。LoadBalancer 类型的 Service 通常会自动创建外部负载均衡实例并且绑定了健康检查会自动把故障节点摘掉。最后是 Ingress 路由。对外统一的域名和路径由 Ingress 来负责。我这里写一段常用的配置apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: order-query-ingress annotations: nginx.ingress.kubernetes.io/proxy-connect-timeout: 10 nginx.ingress.kubernetes.io/proxy-read-timeout: 60 spec: rules: - host: api.example.com http: paths: - path: /order pathType: Prefix backend: service: name: order-query port: number: 8080这里注解里设置的两个超时参数很多人在上线后才发现默认值不合适。connect timeout 太短会在地域性网络波动的时候频繁报 502read timeout 太短会在大查询返回慢的时候把请求拦掉。所以我建议先在压测环境把这两个值调到你接口的真实耗时再放大一点别一上来就抄网上配置。3.3 可观测性设计指标、日志、追踪三件套必须同时上没有可观测性的集群就相当于让司机蒙着眼睛开高速。K8s 集群里服务数量多了之后你不可能靠一台机器一台机器地登录上去敲 docker logs 来排查问题。可观测性要从一开始就作为基础设施的一部分来设计而不是等出现问题以后再补。指标的选型核心是分成三个层次宿主机层、K8s 对象层、业务应用层。宿主机层看 CPU、内存、磁盘 IOK8s 对象层看 Pod 重启次数、调度失败原因、集群事件业务应用层看请求数、错误数、耗时分布。最实用的做法是建立一套统一的采集体系把这三层数据汇总到同一个查询入口然后配置几个关键告警。告警别搞太多宁可少而准不要三天两头被无关告警打断。日志方面容器日志默认是写到标准输出的你在宿主机 docker logs 能看到但 Pod 重建后日志就跟着容器一起没了。所以日志必须前送到集群外部的统一日志中心。集群内部只保留短暂缓存避免占用节点磁盘。我见过很多次节点磁盘被容器日志写满导致整个节点 NotReady 的案例所以一定记得配日志轮转和大小限制。链路追踪则是用来排查“两个服务之间到底哪个环节慢了”的利器。如果业务已经有 APM 类工具就把链路追踪作为云原生落地的一部分集成进去如果没有至少要做到从 Ingress 进入的所有请求都打上统一的 trace id让前端、后端、数据库调用能在日志中心串起来。3.4 发布与升级把“优雅发布”当成默认动作发布是云原生架构中最常见的变更动作。全栈编排做得好的团队发布应该接近无感。这里关键在于让“旧版本停止接收新流量新版本完全 ready 之后再接流量”这两步平滑衔接。具体配置上除了前面说的 RollingUpdate 参数还有两个细节会明显影响发布体验第一个是 terminationGracePeriodSeconds。Pod 收到删除信号后默认有 30 秒宽限期来处理存量请求。如果你的服务有长连接或定时任务需要根据业务调整这个时间。我建议是至少大于一次接口最长耗时的两倍避免正在处理中的请求被杀掉。第二个是 preStop hook。你可以在容器进程被终止前先向注册中心或网关反注册然后等待几秒让流量调度系统把该实例摘掉再真正停止进程。这个“摘流量再杀进程”的顺序非常关键。很多服务发布时出现 5xx问题往往不在新版本代码而在旧实例还在接流量的时候进程就没了。发布过程中我还建议配合这个流程走先发布一个实例观察日志和指标稳定后再继续剩余实例。K8s 本身有 rollout 状态查看但那是“看有没有按计划滚动”业务级健康还是要盯请求成功率、错误数、响应时间这几个核心指标。4. 我在反复实操中遇到的高频问题与处置记录4.1 Pod 一直处于 Pending 状态怎么办Pending 状态说明 Pod 还没有被调度到合适的节点上。最常见的原因是资源不满足或者调度约束无法满足。快速排查三步先 kubectl describe pod看 Events 里的调度失败提示再去查看节点剩余资源是否够用如果集群规模大还要确认污点和容忍度配置。还有一种隐蔽情况Pod 需要挂载 PVC但 PVC 没有被动态供给成功。这时 describe 输出里会显示等待卷绑定的提示。修复方式是查看 StorageClass 是否存在、底层存储是否放开了 API 访问权限。遇到这类问题先不要怀疑代码往往报错就在调度层。4.2 容器频繁 CrashLoopBackOffCrashLoopBackOff 说明容器启动后马上退出了。这个问题出现在应用层比较多但也可能是镜像启动命令在容器内依赖了不存在的目录或环境变量。最直接的定位方式是 kubectl logs 查看退出前打印的最后几行日志。我建议不要只盯着应用代码先确认启动命令里是否有依赖外部配置的部分。比如数据库地址是从 ConfigMap 注入的ConfigMap 的 key 写错启动时就会因为空地址连接失败退出。这种错误在 YAML 里很难一眼看出来所以我的习惯是准备一套“本地参数渲染”脚本把 K8s 里的 ConfigMap 渲染成类似本地环境变量文件再跑一遍。4.3 节点突然 NotReadyPod 被驱逐到其他机器后数据丢失节点 NotReady 的常见原因是心跳超时或者资源耗尽。K8s 默认在节点失联 40 秒后开始驱逐 Pod5 分钟标记为 NotReady如果集群配置了 TaintBasedEvictions会更快。节点恢复后之前被驱逐的 Pod 会以新 Pod 的形式重新调度。如果业务有状态数据存放在 emptyDir那数据会在节点故障期间一并丢失。我强烈建议所有有状态数据都使用持久卷存储并且给存储层做备份。这也是为什么我在前面反复强调 StorageClass 规划要提前做。遇到节点资源耗尽的情况K8s 会根据服务质量等级驱逐 Pod。优先级最低的是 BestEffort 类型的容器完全没有 requests/limits 的 Pod 最容易先被杀。所以在任何业务容器中都要设置 requests哪怕只是很小的水位也要给调度器一个清晰的资源画像。4.4 网络时通时不通Ingress 入口偶发 502这类问题往往不在 K8s 本身的编排逻辑而在网络链路各层的超时和健康检查配置不一致。比如 Ingress 里设置的 proxy-read-timeout 太短后端接口在数据库慢查询时耗时超过这个时间请求就会被网关直接终止。排查思路按顺序走先看后端服务有没有日志记录请求进入再看负载均衡器到 Ingress 的探针是否通过最后看 Ingress 到 Service 的联路是否健康。很多时候 502 是因为一个副本处于 Terminating 状态但还在 Endpoints 列表里。等个几秒Endpoint 同步完成后就恢复了。为减少这种情况我给 Gateway 配置了更敏感的健康检查间隔。4.5 一套“踩坑速查表”直接保存收好现象可能原因处置建议Pod 一直 Pending资源不足、PVC 未绑定、调度约束不满足describe 查看事件确认节点资源和 PV 状态CrashLoopBackOff启动命令失败、配置缺失、镜像自身问题logs 看退出日志先检查环境变量和挂载数据丢失使用了 emptyDir 或宿主目录保存状态数据改用持久卷并做好存储层备份发布出现 5xx探针配置不合理、terminationGracePeriod 过短、preStop 缺失调长宽限期增加摘流量 hookIngress 偶发 502超时配置不匹配、Endpoints 同步延迟、健康检查不敏感调参压测调整探针间隔节点磁盘告急容器日志未轮转、镜像堆积、持久卷绑定过多配置 log rotate定期清理无用镜像告警风暴告警阈值过低、没有关联聚合收敛告警项增加抑制规则按业务分级这张表是长期战斗中一点点攒下来的你先照抄就能少走一半弯路。5. 最后一个压箱底的建议永远预留断网演练时间踩过几次坑之后我的体会是任何高可用设计最终都要靠故障演练来验证。你不是在集群搭建完成的时候确认它高可用而是在“故意杀掉一个节点看业务是否无感”之后才有资格说自己高可用。我建议每个季度做一次混沌演练。先把一个节点标记为不可调度然后模拟内存压力迫使 K8s 驱逐 Pod观察整个业务链路的恢复时间。重点看三个指标多少请求发生错误、服务降级持续时间、从故障到完全恢复用了多久。如果能做到“用户基本无感”你的高可用设计才真正落地了。最后再分享一个小技巧所有 YAML 文件应该纳入版本管理并且每个环境测试、预发、生产使用独立的资源配置模板不要在测试环境验证过的 YAML 直接往生产扔。环境差异导致的问题比代码逻辑问题更隐蔽也更难查。每个环境上线前都要跑一遍 kubectl diff 看看实际生效的配置和你预期改的差多少再放量。云原生架构这条路没有终点只有持续设计、持续验证。把 K8s 当基础设施打磨把高可用当默认要求你的系统会越跑越稳。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →