
直接上结论OpenStack on Kubernetes 这件事如果你只是把 OpenStack 的服务做成容器丢进 K8s那基本属于“自娱自乐”。真正的生产部署难点从来不在于“能不能跑起来”而在于控制面怎么保持稳定、数据面怎么不拖后腿、故障出现之后能不能在十分钟内定位到根因。这个系列写到第十一篇前面已经把从零搭建、基础组件容器化、网络插件选型都过了一遍。这篇我会把视角切换到“生产环境真正运转起来之后”的事情围绕控制面高可用设计、数据面存储与网络的性能兜底、日常运维排障这三个大方向展开把我在实际项目里踩过的坑和沉淀下来的配置经验直接分享出来。适合正在做 OpenStack on K8s 生产化改造的团队参考也适合那些已经把服务跑起来但心里没底的运维同学对照自查。1. 为什么要把 OpenStack 控制面搬进 Kubernetes1.1 传统部署方式的生产瓶颈先聊一个本质问题OpenStack 的控制面服务比如 Keystone、Nova API、Neutron Server、Glance 这些本质上都是一堆无状态或者轻状态的 Python 进程。传统方式下直接装在虚拟机或者物理机上靠 systemd 托管用 Keepalived 做 VIP 漂移再用 HAProxy 做负载均衡。这种架构不是不能用但一旦规模上来痛点会非常明显。第一个痛点是部署效率。控制面组件十几个每个组件又要改配置文件、又要配数据库连接串、又要处理服务发现关系纯手工维护一套生产环境少说也要一两天。而且不同版本的 OpenStack 配置项差异很大升级的时候经常出现“这个参数在某个版本被废弃了”的情况排查起来非常费劲。第二个痛点是扩缩容。传统部署方式下Nova API 实例如果扛不住了你得重新准备一台机器、装好依赖、改好配置、加入负载均衡池整个过程等于重新部署一遍服务机动性基本为零。第三个痛点是故障恢复。虽然 Keepalived 可以做 VIP 漂移但 OpenStack 的很多服务之间是有调用链的比如 Nova 要调 Neutron、要调 Glance、要调 Cinder。如果只是 API 层做了 HA底层某个服务卡死了上游服务往往只能干等超时。传统方式下的健康检查通常比较简单只能探活端口没法感知真实的业务可用性。这些痛点叠加在一起让我越来越倾向于把控制面容器化并且交给 Kubernetes 来编排。因为 K8s 天然提供了探针机制、滚动更新、声明式部署和自我修复这些能力正好能覆盖上面提到的三个问题。1.2 迁移到 K8s 之后的架构收益把控制面搬进 K8s 之后最直观的收益是部署方式从“脚本 手工操作”变成了“声明式清单 自动调度”。你只需要把每个服务的 Deployment、Service、ConfigMap 定义好剩下的事情交给调度器去处理。这里有一个很关键的架构决策控制面拆分成 Sidecar 容器。比如 Keystone 容器只负责跑 WSGI 应用而数据库连接池、缓存客户端、日志采集这些能力通过 Sidecar 容器注入。这样做的最大好处是职责单一某个服务要升级依赖库只需要替换对应的 Sidecar 镜像而不影响主业务进程。再一个收益是故障域控制。通过 nodeAffinity 和 PodDisruptionBudget 的组合可以把控制面服务强制打散到多个物理节点上避免单点故障。比如 RabbitMQ 的三个副本分别调度到三个不同的宿主机并且设置pod.kubernetes.io/zone的拓扑约束这样就算整个机架断电也不至于把消息队列的全部副本一起带走。从实际数据来看我这边把控制面容器化之后一次完整的控制面滚动升级从原来的 2 小时缩短到 20 分钟以内。而且升级过程中如果某个 Pod 启动失败K8s 会自动回滚到上一个可用版本不需要人工介入。这种体验是传统部署方式完全给不了的。当然收益归收益落地过程中也有不少需要额外处理的事情比如存储卷的挂载方式、数据库连接池的数量上限、健康检查探针的阈值设置等等。这些细节我会在后面的章节里逐个展开。2. 部署前必须想清楚的事情网络、存储与资源规划2.1 物理网络规划的几个硬性要求OpenStack 控制面容器化之后网络拓扑会比传统部署多一层东西——Pod 网络。控制面服务之间的通信走的是 K8s 集群的 Overlay 网络而对外提供 API 的流量则需要通过 NodePort 或者 LoadBalancer 暴露出去。这里最需要关注的是南北向流量的质量。OpenStack 的 API 请求虽然不像数据面流量那样大包大包地传输但它的特点是“请求频率高、单个请求延迟敏感”。比如 Nova 创建虚拟机的时候要依次调用 Glance、Neutron、Cinder每调用一次就是一轮 HTTP 往返。如果 Pod 网络本身的延迟很高或者 kube-proxy 的转发规则把连接导到了跨机房的节点上整个创建流程的耗时会明显上升。我建议把控制面 Pod 的调度范围限定在与 OpenStack API 接入层同一机架或者同一可用域内的节点上并且开启 K8s 的拓扑感知路由Topology Aware Routing让 Service 的流量优先转发到最近的后端 Pod。这样做的效果非常明显实测下来 API 的平均响应时间能降低 30% 左右。另外Neutron 的 OVN 组件和计算节点上的 OVS 之间需要建立隧道这部分流量建议走独立的 VLAN 或者 VXLAN 网络不要跟管理网络混在一起。否则一旦有租户产生大流量比如大规模的数据迁移很容易把管理面的网络打爆。2.2 存储选型控制面与数据面要分开对待在 OpenStack on K8s 的环境里存储问题要分两层来看第一层是 K8s 自身的存储也就是控制面容器需要的持久化卷第二层是 OpenStack 对外提供的存储也就是 Cinder 后端、Glance 镜像存储、以及 Nova 临时磁盘的存储池。控制面这一层我强烈不建议使用那些不适合小文件高并发读写的存储类型。像 MariaDB 的 binlog、RabbitMQ 的消息持久化、ZooKeeper 的 snapshot这些数据的写入模式是“频繁的小块随机写”对 IOPS 的要求很高。如果后端接了对象存储性能会非常难看。我们实际压测的时候把 MariaDB 的数据卷放在分布式文件存储上结果事务提交延迟直接飙到了 200 毫秒以上完全扛不住。后来换成了本地的 NVMe 磁盘配合 Rook-Ceph 的 PVC 方案延迟才降下来。数据面这一层的选型逻辑就不太一样了。Cinder 后端如果全部使用本地存储虽然性能好但没法做跨节点的漂移。生产环境里我建议采用分布式存储比如把 Ceph 作为 Cinder 的统一后端。关键在于存储池的划分策略——高性能池用 NVMe 介质、容量池用 HDD 介质、冷存储池用纠删码模式。这样在创建卷类型Volume Type的时候就能把不同性能要求的磁盘对应到不同的池上。存储这一块一定不要贪图省事把所有数据丢到同一个池子里。我见过很多团队为了减少运维复杂度所有卷都用默认池结果高 IO 的数据库实例和低 IO 的备份卷互相干扰整体性能都被拉低了。2.3 资源预留与调度策略OpenStack 控制面容器化之后资源预留是个特别容易出错的地方尤其是内存资源。Control Plane 的组件大多基于 Python 的事件循环模型比如 Nova 的 Conductor、Neutron 的 Server 进程它们在内存使用上有一个特点启动时占用不高但运行一段时间之后随着连接的建立和缓存数据的累积内存占用会缓慢上升。如果 Pod 的内存 limit 卡得太紧运行几天之后就可能触发 OOM Kill。我们的经验是给这类组件设置内存请求为 512Mi限制为 2Gi同时开启--eviction-hardmemory.available500Mi这样的节点驱逐策略作为兜底。另外一个细节是 JVM 类的组件比如 Elasticsearch 或者 Cassandra要单独设置堆内存大小不能直接依赖 K8s 的内存限额否则容器被限制住的时候 JVM 可能还没有触发 GC。调度策略方面我用的是两个核心标签的组合openstack-controlplanetrue和openstack-dataplanetrue。控制面服务只允许调度到打了第一个标签的节点上计算节点只允许调度到打了第二个标签的节点上。这样做的目的是防止控制面服务跑到计算节点上跟虚拟机争抢资源同时也方便在控制面节点上做专门的网络策略和防火墙配置。资源预留的数字仅供参考真实环境一定要根据自己的业务规模进行调整。有一个小技巧是在测试环境里把所有控制面服务跑起来观察一个星期的资源占用曲线然后取峰值再乘以 1.5 作为生产环境的资源请求值这样既不会浪费也不会被打爆。3. 控制面核心组件的容器化部署细节3.1 数据库与消息队列的高可用部署整个 OpenStack 控制面里最不能出问题的是 MariaDB或者 Galera Cluster和 RabbitMQ。这两个组件如果挂了整个控制面基本就瘫痪了所以它们的高可用设计是重中之重。MariaDB 的容器化部署我推荐用 Galera 模式的集群三个副本组成一个 cluster。每个 Pod 的配置文件里需要设置wsrep_node_address为 Pod 的 IPwsrep_cluster_address为三个副本的地址列表。这里要注意一个关键点Galera Cluster 在初始化第一个节点的时候要设置--wsrep-new-cluster或者通过环境变量指定初始节点否则其他节点连不上来就会一直报错。K8s 里的 StatefulSet 天然适合跑这类有状态服务。我用了一个 ClusterIP 为 None 的 Headless Service配合service.alpha.kubernetes.io/tolerate-unready-endpoints: true注解让各节点之间可以直接通过 Pod 的 DNS 名称互相发现。启动顺序上第一个 Pod 会等待自己 ready 之后再由第二个 Pod 启动这个依赖关系通过initContainers里对第一个 Pod 的连通性探测来实现。RabbitMQ 的高可用则是另外一套逻辑。我使用 RabbitMQ Cluster Operator 来管理它会生成一个包含自动探活、节点发现、状态更新的控制器。在生产环境里需要额外开启镜像队列模式把队列的副本数设置为 2这样即使某个节点挂掉消息也不会丢。另外管理端口 15672 不要直接暴露到公网建议通过 K8s Ingress 加一层认证代理对外提供服务。数据库这块我踩过一个比较深的坑就是 Galera Cluster 的 SSTState Snapshot Transfer行为。如果某个节点落后太多其他节点会尝试对它做全量同步这个过程中同步节点的性能会受到很大影响。解决办法是限制wsrep_sst_method使用mariabackup并且把wsrep_sst_receive_address指向一个独立的带宽充足的接口避免同步流量跟业务流量互相竞争。3.2 Keystone 与 Nova 服务的容器化要点Keystone 在 OpenStack 里负责身份认证和令牌管理它在容器化部署时跟其他组件的最大区别在于它对 Fernet 密钥的依赖。Fernet 密钥需要持久化保存并且所有 Keystone 实例之间要共享同一套密钥否则就会出现“获取 token 的节点和校验 token 的节点不是同一套密钥”导致的间歇性认证失败。我把 Fernet 密钥放在一个独立的 PVC 里挂载到所有 Keystone 副本上同时通过 K8s 的configMap挂载一个固定名称的配置文件确保每个副本读取到的密钥文件路径完全一致。这里要特别提醒千万不要把密钥写死在镜像里因为镜像一旦重建密钥就变了老的 token 全部失效所有租户都要重新登录。Nova 服务的容器化部署相对复杂一些因为 Nova 包含了 API、Conductor、Scheduler、Placement、NovncProxy 等多个子服务。其中最容易出问题的是 Nova-Conductor它在创建虚拟机的流程中负责协调数据库操作和 RPC 调用如果并发上来了它的 CPU 消耗会非常高。生产环境里我建议给 Conductor 配置独立的 HPAHorizontal Pod Autoscaler根据 CPU 使用率自动伸缩副本数。Nova 的 Placement 服务需要独立的数据库并且在配置文件中要明确区分connection和placement_database.connection两个连接串。很多初次上手的人会不小心把这两个配置混在一起结果整个服务明明在数据库里建好了表但调用的时候就是报找不到资源供应商这种问题排查起来非常浪费时间。3.3 探针配置与优雅停机K8s 对容器的健康检查用的是 LivenessProbe 和 ReadinessProbe。OpenStack 服务的探针配置虽然看起来简单但实际操作中有很多细节需要打磨。以 Keystone 为例LivenessProbe 应该探测/v3/auth/tokens这个 API而不是简单探测 80 端口。因为端口能通不代表服务真正可用如果 Fernet 密钥加载失败了端口照样能响应但返回的状态码会是 500。ReadinessProbe 的探针应该比 LivenessProbe 更严格建议探测到一个实际的认证流程成功之后再标记为 Ready。优雅停机是另一个容易忽略的点。OpenStack 服务在处理完当前请求之后才能安全退出否则可能会产生未完成的事务或者丢失 RPC 消息。我在 Deployment 的lifecycle里配置了preStop钩子执行sleep 15让 K8s 在发送 SIGTERM 之前给服务留出时间处理存量请求。同时在服务的主进程里也做了 SIGTERM 的信号捕获确保能够主动关闭数据库连接池和消息队列的消费线程。探针的failureThreshold和periodSeconds设置也有讲究。设置得太激进比如每 2 秒探一次K8s 会把服务正常的启动过程误判为失败导致频繁重启设置得太宽松比如 30 秒探一次又会在服务真正出问题的时候反应迟钝。我一般把periodSeconds设置为 10failureThreshold设置为 3也就是服务连续 30 秒不健康才会被重启。4. 数据面组件落地的关键经验4.1 计算节点的最小化配置控制面容器化之后计算节点其实不需要跟着一起容器化它只需要跟控制面建立稳定的通信链路就行。我在生产环境里保持了计算节点的传统部署方式使用 Kolla 的二进制安装包或者直接手动配置 nova-compute、neutron-ovs-agent 和 ceilometer-compute 这几个核心服务。这里有一个非常重要的架构判断计算节点容器化在技术上可行但运维复杂度会高很多。因为计算节点要管理真正的虚拟机资源虚拟机又要依赖 KVM 和 OVS 这些主机级能力如果在容器里操作这些能力需要做大量设备挂载和特权模式配置风险远大于收益。所以我的建议是控制面容器化、数据面传统化两者通过网络互通。计算节点接入容器化控制面的时候要重点检查配置文件里的transport_url和database连接串。这两个配置指向的是 K8s 集群内的服务地址但计算节点并不在 K8s 集群的网络里所以要通过 NodePort 类型的 Service 把 RabbitMQ 和 MariaDB 暴露出来。这里要注意把 NodePort 的端口固定下来否则每次重新部署 Service 的时候端口变了所有计算节点的配置都要跟着改。4.2 OVN 网络插件与安全组策略数据面的网络我推荐使用 OVN也就是 Open Virtual Network。它跟 OpenStack 的整合度很高Neutron 可以通过 OVN 机制驱动直接管理逻辑交换机、逻辑路由器和安全组不用再维护一堆 API 层的插件代码。OVN 容器化部署时的核心组件是 ovn-central它负责维护 OVN 的南向数据库SB DB和北向数据库NB DB。这两个数据库我分别用独立的 StatefulSet 来部署并且开启 HA 模式这样任何一个节点故障都不会影响逻辑网络的管理能力。安全组这块OVN 默认会使用 ACL 来实现它比较吃控制面的 CPU。当租户数量比较多安全组规则到达几千条之后ovn-controller 的计算负载会明显上升。我建议在计算节点的 OVS 上开启ovn-encap-typegeneve并且根据实际的隧道流量调整 MTU 到 8950避免 Overlay 网络带来的分片问题。另外要提醒的是OVN 的逻辑网络变更不是实时的。当你新建了一个网络或者修改了安全组规则之后可能需要几秒钟才会在计算节点的 OVS 上生效。这段时间内如果有虚拟机的流量产生可能会短暂的不符合预期。这是正常现象但如果延迟超过 10 秒就要检查 ovn-controller 的健康状态和 NB DB 的同步状态了。4.3 Ceph 集成与虚拟机磁盘性能调优Ceph 作为 Cinder 后端是 OpenStack 生产环境的标配。容器化部署的 Ceph 跟裸机上的 Ceph 在架构上没有本质区别但在 K8s 环境下要注意的细节是不要把 Ceph Monitor 和 Ceph OSD 跑在同一个节点上因为两者的资源需求完全不同Monitor 需要低延迟的磁盘做日志OSD 需要大容量、高吞吐的磁盘做数据存储。Cinder 的容器化服务在读取 Ceph 配置的时候要用外部的 Ceph 集群地址。官方的 Cinder 镜像默认会尝试通过内部的 cephx 认证文件连接 Ceph所以提前把ceph.client.cinder.keyring文件和ceph.conf封装到 ConfigMap 里挂载进去。虚拟机磁盘性能调优方面我遇到最多的情况是缓存策略设置不当导致性能上不去。Cinder 后端配置里的backend_host如果设置成none虚拟机的 IO 会直接打到 Ceph 的 RBD 上数据库这种小 IO 场景会非常吃亏。建议在创建卷类型的时候同时设置rbd_volume_cache_enabledtrue和rbd_multi_cache_enabledtrue这样 RBD 客户端会启用本地缓存对热点数据的读取性能提升非常明显。还需要注意的是RBD 客户端缓存跟 Nova 虚拟机的磁盘缓存是两层。Nova 配置里的disk_cachemode建议设置为writeback这样的话虚拟机的写请求先落到宿主机内存再异步刷到 Ceph性能能提升一大截。代价是如果宿主机突然断电内存里未刷盘的写请求会丢失所以一定要在计算节点上配置 UPS 或者集群级别的电源联动来兜底。5. 生产环境日常运维与故障排查实录5.1 监控告警指标清单OpenStack on K8s 的监控体系相比传统私有云架构多了一层容器指标。除了要知道虚拟机层面的 CPU、内存、网络流量还得知道 K8s 集群里的 Pod 状态、节点资源余量、容器重启次数。我这里整理了一份实际正在使用的告警指标清单指标告警条件说明Keystone API 响应时间大于 2 秒持续 3 分钟排除网络抖动导致的一次性波动MariaDB Galera 同步延迟大于 3 秒持续 1 分钟同步延迟过高会导致事务不可用RabbitMQ 未消费消息数大于 10000 条持续 5 分钟说明消费者可能已经卡死Ceph OSD 延迟大于 80 毫秒持续 10 分钟正常应该在 20 毫秒以内控制面 Pod 重启次数多于 3 次持续 1 小时说明服务可能在反复崩溃OVN 隧道故障出现不可达隧道需要立即响应可能影响租户网络这套告警在 Prometheus 里通过groups组合规则来实现。有一个经验值得分享不要一次性把阈值定得太严格否则上线第一天就会被告警淹没。先用观察模式跑两周基于历史数据调整阈值后再开启真正的 AlertManager 通知。5.2 日志采集链路搭建在生产环境里排查问题时日志的作用甚至比监控还重要。我们把 OpenStack 控制面日志和 K8s 集群日志统一收集到同一个日志平台用的是 Loki Promtail 的组合。每个控制面 Pod 的日志路径不一定一样比如 Keystone 日志打到了 stdoutNova-Conductor 因为通过 oslo.log 写日志会写到文件再通过 sidecar 转发。我在每个 Pod 的配置里通过annotations注明日志文件的路径然后 Promtail 根据这些注解动态抓取。这样不但收集了 stdout 日志还能抓到文件日志排查问题时能拿到完整的信息链。对于计算节点上的 nova-compute 和 neutron-ovs-agent我用 Promtail 的静态配置抓取/var/log/neutron/*.log和/var/log/nova/*.log。日志的采集频率不要太高两秒轮询一次足够了否则会白白增加宿主机 IO 负载。5.3 三个踩过的坑与排查思路第一个坑是 Keystone 的 token 突然全部失效。现象是用户登录控制台没问题但执行 openstack 命令行的时候报 401。排查日志发现 keystone 报了 Fernet token 的 InvalidToken 错误。原因是某次容器重启之后新副本从镜像中覆盖了持久化的 Fernet 密钥目录导致密钥不一致。解决方法是运维过程中绝对不要把密钥放在镜像层要使用独立的 PVC并且定期做密钥备份。第二个坑是 Nova 创建虚拟机卡在调度阶段。现象是创建虚拟机的任务长时间处于 scheduling 状态没有报错。排查思路是先看 scheduler 是否收到了请求然后在 scheduler 的日志里过滤 node 的过滤条件。最终定位到是某个计算节点的 RAM 资源被系统进程吃满而 filter scheduler 依然保留了这个节点导致调度器把任务发到一个资源不足的节点上卡住了。解决方法是开启RamFilter的软阈值校正并给计算节点上的系统进程设置 cgroup 限制。第三个坑是 RabbitMQ 镜像队列同步风暴。现象是某个节点重启后RabbitMQ 集群的整体吞吐瞬间下降大量消息堆积。排查后确认是镜像队列在全量同步过程中把网络带宽打满了。解决方式是设置镜像队列的同步策略为ha-sync-mode: manual避免节点加入集群时自动触发全量同步等网络空闲时再手动触发。6. 升级与扩容的风险控制6.1 控制面的滚动升级流程OpenStack on K8s 的好处之一是升级变得相对可控但也不能掉以轻心。我的升级流程按照下面的顺序执行先备份数据库和 RabbitMQ 的数据目录依次升级消息队列、数据库、认证服务、核心组件每个组件升级完成后先验证老版本的业务接口确认没异常再进入下一个组件滚动升级的过程中需要重点关注 Pod 的启动顺序。我通过设置initContainers来保证依赖服务已经就绪比如先等 MariaDB 的 Pod 变成 Running再启动 Keystone 的 Pod。这样即使某个服务在升级过程中出现偶发的连接失败也不会把整个升级流程卡死。6.2 控制面扩缩容的注意点控制面扩展副本之前要确认底层的数据库连接池和消息队列的消费者数量能够匹配上。比如 Keystone 从 2 副本扩展到 4 副本数据库的连接数会翻倍如果数据库的max_connections没有同步调大新的副本启动时就会一直报“Too many connections”根本进不了 Ready 状态。针对这个问题我在部署清单里集成了一个检查脚本在扩展副本数之前自动查询当前数据库的最大连接数和当前连接数如果比例超过 70% 就给操作者一个警告。需要说明的是这个比例只是基于我这边环境的经验值。不同数据库的规格和负载模型差别很大用的时候要结合自己的实际情况来调整阈值。还有一个容易忽略的点是 RabbitMQ 的消费者连接数。Nova-Conductor 这类服务在启动的时候会建立 RPC 消费者连接如果扩展的副本数太多RabbitMQ 的 channel 数可能不够用。所以扩展之前要确认 RabbitMQ 的channel_max配置通常建议留出 2 倍的余量。6.3 版本兼容性评估方法OpenStack 的组件之间版本耦合度很高不是说你用了 Train 版本的 Nova就能直接配上 Yoga 版本的 Neutron。我每次做跨版本升级之前都会花大量时间做配置项的兼容性排查。具体方法是把新旧版本组件各自的 sample 配置文件做逐行 diff确认哪些参数在新版本里废弃了、哪些参数改为了默认值。比如新版本里use_neutron默认从 True 改成了 False如果不加注意升级之后计算节点跟网络的连接会直接断掉。另外升级 RabbitMQ 或者 MariaDB 这种基础组件时要特别小心 API 版本的变化。RabbitMQ 从 3.8 升到 3.10 的时候管理 API 的路径变了如果监控系统里的探活 URL 没跟着改升级完成后会看到一大堆虚假的告警容易被误导判断成业务故障。7. 一些基于实际部署的总结与建议走了这么长一段路我想分享几点从生产环境里总结出来的经验希望后来者少走弯路。OpenStack on Kubernetes 这个方向核心价值不在“容器化”本身而在通过容器化建立了一套“可声明、可重复、可回滚”的基础设施交付体系。我的体会是这套体系的真正难点不是技术选型而是团队内部要形成严格的流程纪律什么变更先测试、什么变更需要审批、哪些配置项不能随手改。我们有一段时间就是因为某个同事直接在集群里改了 Deployment 的副本数而没有走流程导致消息队列的消费者数量暴增把数据库的连接数打满了整个控制面瞬间不可用。最后再分享一个小技巧定期在测试环境里做一次“破坏性演练”。把控制面的某个 Pod 删掉或者直接 kill 掉某个计算节点看系统能否在预期时间内恢复。这种演练一开始你会觉得很折腾但做过几次之后你会发现很多平时发现不了的问题其实都在这种黑天鹅场景里藏得很深。演练的结果不要只看“恢复了没有”更要看恢复的时间、过程中的告警是否有效、日志是否足够支撑定位。等这些都能形成规范你才敢说这个 OpenStack on K8s 生产环境是真正稳的。