这个系列写到第十三篇,前面十二篇我们把环境准备、Kubernetes 集群搭建、OpenStack 各个核心组件的容器化镜像制作、以及初始化的编排逻辑都过了一遍。这一篇我想集中聊一个很多人问过我的问题:当 OpenStack 控制平面真正跑在 Kubernetes 之上以后,生产环境里到底还有哪些坑等着你,以及这些坑应该怎么填。
先说清楚这篇的定位。它不是一个从零开始的安装教程,也不会把 Helm Chart 的每一行 values.yaml 再贴一遍。我假设你已经有一个能跑起来的 Kubernetes 集群,也已经用容器化的方式把 OpenStack 的核心服务(Keystone、Nova、Neutron、Cinder、Glance、Horizon 这些)调度上去了。这篇文章要解决的是更现实的问题:控制平面容器化之后,数据平面怎么和它对接,网络怎么打通,存储怎么不拖后腿,以及出问题的时候怎么快速定位。换句话说,这是把 OpenStack 搬上 Kubernetes 之后,真正进入生产运维阶段的那一套东西。
如果你正好处于“容器化 OpenStack 已经能跑通 demo,但一上生产就心里没底”的阶段,这篇文章应该能帮到你。
1. 整体设计思路:为什么选择 OpenStack on Kubernetes,以及架构上怎么拆
1.1 这种架构到底解决了什么问题
传统 OpenStack 部署方式里,控制节点就是几台物理机或虚拟机,每个节点上跑着十几个 Python 进程,由 systemd 托管。这种方式最大的问题不是不能跑,而是“变更”很痛苦。升级一个组件要小心翼翼,回滚更是灾难,节点硬件故障意味着业务中断,而且不同组件的依赖冲突经常会让你在 pip 依赖地狱里爬不出来。
把控制平面搬上 Kubernetes 以后,这些痛点被换了一种方式解决。镜像不可变,版本升级就是换镜像滚动更新;进程崩溃了有 kubelet 自动拉起;节点挂了 Pod 会调度到其他节点;声明式配置让整个环境的期望状态都写在 Git 里。听起来很美好,但实际落地的时候你会发现,OpenStack 并不是天生为 Kubernetes 设计的,尤其是那些有状态的服务和依赖主机网络的组件,需要你花很多心思去做适配。
我之前在社区里看到过一个比喻,说 OpenStack on Kubernetes 就像“把一辆老式手动挡汽车的动力系统装进一台自动挡新能源车的底盘里”。比喻不一定精确,但意思是对的:你能开,但离合器得自己想办法。
1.2 控制平面与数据平面的职责划分
生产级 OpenStack on Kubernetes 架构,一定要在脑子里把两个平面分清楚。
控制平面跑在 Kubernetes 集群里,包含 Keystone、Glance、Nova API、Neutron Server、Cinder API、Horizon 这些无状态或半无状态的服务。它们的共同特点是通过数据库和消息队列通信,自身不直接处理底层硬件资源,所以非常适合容器化调度。
数据平面则是真正干活的那些组件,典型代表是 Nova Compute(也就是 nova-compute)、Neutron Agent(包括 L2 Agent、DHCP Agent、Metadata Agent),以及可选的 Cinder Volume 的 LVM 后端、Manila Share 的存储节点。这些组件必须贴近物理主机运行,因为它们要操作宿主机上的网桥、网卡、存储卷组,如果让它们跑在随机的 Pod 里,网络和存储直接就断掉了。
这里有一个很多人第一次做架构设计时会犯的错误:试图把 nova-compute 也做成一个普通的 Deployment 丢进集群里。这样做的结果通常是计算节点上的虚拟网桥和 Pod 网络互相打架,流量绕来绕去,性能差不说,还特别难排查。正确的做法是让数据平面组件以特权 DaemonSet 的方式调度到指定计算节点,或者直接用 systemd 在宿主机上托管,Kubernetes 只负责控制平面。
生产环境里我见过两种主流方案,一种是把 nova-compute 用 DaemonSet 部署,Pod 直接共享宿主机的网络命名空间,并且挂载宿主机的 /var/lib/libvirt、/run 等目录;另一种是干脆保持传统方式,计算节点上的组件用容器运行,但不纳入 Kubernetes 调度,而是由单独的容器运行时管理。这两种没有绝对的好坏,取决于你的团队更熟悉哪套运维体系。
1.3 为什么我最终选了 Helm 作为交付方式
在控制平面容器化的落地过程中,你一定会碰到一个选择:用 Kolla-Kubernetes、OpenStack-Helm,还是自己写一套 Deployment 和 Service。
Kolla-Kubernetes 项目很早就不太活跃了,而且它和 OpenStack 版本的绑定关系比较紧,升级路径不灵活。OpenStack-Helm 是 OpenStack 社区里相对活跃的方案,用 Helm Chart 管理整个控制平面,提供了比较完整的 values.yaml 参数体系。如果团队里没人愿意从零维护几百个 YAML,OpenStack-Helm 是一个可以接受的起点。
但我要多说一句,OpenStack-Helm 的上手成本并不低。它把很多运维细节封装起来了,一旦出问题,你面对的是层层嵌套的 Helm 模板,想定位一个参数从 values.yaml 到最终的 ConfigMap 是怎么传的,需要花不少时间。
我个人建议的做法是:把 OpenStack-Helm 当作参考实现,在它的基础上裁剪出自己团队需要的 Chart。不要盲目全量部署,因为默认配置里有很多服务在生产环境根本用不到,它们白白占用资源不说,还会增加故障面。控制平面组件其实就那么核心的七八个,其他边缘组件按需开启就行。
2. 生产部署前的关键决策:资源规划、版本选型和存储网络底座
2.1 版本搭配不是越新越好
OpenStack 和 Kubernetes 的版本兼容性,是生产部署里第一个需要认真对待的问题。OpenStack 社区每半年出一个版本,Kubernetes 每三个月出一个小版本,两者的发布节奏完全对不上,所以你必须选一个经过验证的组合,而不是两手一摊直接 latest。
以我目前环境为例,OpenStack 用的是 Yoga 版本,Kubernetes 用的是 1.24 到 1.26 之间的某个稳定版本。为什么选这个组合?因为 Yoga 版本的容器镜像对 Python 版本、gRPC 库的依赖相对稳定,和 Kubernetes 1.24 之后的稳定 API 配合没什么兼容性问题。
另外要注意的是 Kubernetes 的 API 版本弃用策略。新版本集群会把一些旧 API 直接移除,比如 networking.k8s.io/v1beta1 这种,如果你的 OpenStack-Helm Chart 还在用旧 API,部署的时候就会直接报错。所以生产环境选型时,最好先看一下 Chart 里用到的 API 版本,再反推 K8s 集群的版本范围。
2.2 资源规划的计算逻辑
控制平面的资源规划经常被低估。很多人觉得 OpenStack 控制服务都是 API 进程,吃不了多少内存,实际上 Keystone、Nova API、Neutron Server 这些服务加起来,再加上数据库和消息队列,资源消耗是相当可观的。
分享一个我在生产环境用来估算资源的经验公式。假设你的云平台规模在 50 台计算节点以内,控制平面至少需要 16 核 CPU、64GB 内存,这是底线。其中数据库独占 8GB 到 16GB,消息队列独占 4GB 到 8GB,剩下的分给各个 API 服务。如果你的计算节点规模超过 100,控制平面的资源要按比例往上加,尤其是数据库的连接池和消息队列的吞吐能力,会成为瓶颈。
存储方面,etcd 是 Kubernetes 的大脑,必须使用高性能磁盘,建议 SSD 起步。OpenStack 自己的数据库(通常是 MariaDB)同样要放在高性能存储上,因为每次虚拟机创建、删除、热迁移,背后都是大量的数据库读写。我见过一个生产事故,数据库跑在机械盘上,虚拟机创建并发一高,整个控制平面全部卡死,最后排查下来 IO 延迟已经高到不可接受。
网络方面,Kubernetes 集群的管理网络、OpenStack 内部 API 网络、OpenStack 的数据网络要尽量物理隔离。即使不能物理隔离,也要用 VLAN 或 VXLAN 做逻辑隔离。否则,OpenStack 的 Neutron 数据流量会把 Kubernetes 的 etcd 心跳和 Pod 间通信全部打爆。
2.3 存储后端选型:Ceph 依然是最稳的选择
OpenStack 的存储后端,我个人强烈建议直接上 Ceph,不要在这个环节省事。Ceph RBD 可以作为 Cinder 的后端给虚拟机提供块存储,也可以作为 Glance 的后端存镜像,还能作为 Nova 的临时磁盘后端。一套 Ceph 解决所有存储问题,运维上也只用维护一套分布式存储系统。
Ceph 和 OpenStack on Kubernetes 的配合有一个需要注意的点:Ceph 集群建议独立部署,不要和 Kubernetes 节点混部。除非你非常清楚 Ceph 和 K8s 的资源隔离边界,否则混部会导致 IO 竞争和故障域重叠。生产环境的稳定性需要的是故障隔离,而不是硬件的“充分利用”。
Cinder 的 Ceph 后端配置里,有一个参数很容易被忽略,就是rbd_connect_timeout和rbd_operations_timeout。默认值可能只有几十秒,但生产环境里 Ceph 集群偶尔会有高负载,如果超时时间太短,Cinder 会频繁报错,甚至把卷状态置为 error。我一般会把这两个参数调大到 60 秒以上,给自己留出容错空间。
Glance 后端用 Ceph 的话,镜像上传会直接通过 RBD 写入 Ceph,不走 Glance 自身的本地文件系统。这个配置很关键,因为它让 Glance 变成无状态服务,Pod 随便怎么调度都不会丢数据。如果你使用 Glance 本地文件存储,那 Glance 的 PVC 就成了单点,Pod 一迁移就找不到镜像了。
2.4 网络方案:Calico 与 OpenStack 数据平面的边界
Kubernetes 集群内部的 Pod 网络,我用的是 Calico,原因很简单:稳定、成熟、支持 BGP 路由通告,而且和 OpenStack 的 Neutron 网络没有直接的地址冲突问题。
但有一点要特别注意:OpenStack 的 Neutron 网络和 Kubernetes 的 Pod 网络是两套完全独立的网络体系。Neutron 管理的是租户的虚拟网络(VXLAN、VLAN、Flat 等),由 Neutron Agent 在计算节点上通过 Linux Bridge 或 Open vSwitch 实现;Kubernetes 的 Pod 网络则是 Calico 等 CNI 插件管理的 Overlay 网络。两者之间不能互相打通,也不应该有路由交叉。
如果你需要在 OpenStack 虚拟机里访问 Kubernetes 里的服务(或者反过来),正确的做法是通过负载均衡器或 API Gateway 暴露服务,而不是试图在 Neutron 和 Calico 之间做路由打通。强行打通会带来不可控的 ARP 风暴和路由环路,我见过不止一次因为这个设计失误导致的整个网络瘫痪。
还有一个经验是,Neutron 的 Open vSwitch 代理要使用宿主机网络,不要走 Kubernetes 的 Pod 网络。Neutron Agent 需要操作宿主机上的 ovsdb、网桥和 tunnel 接口,这些操作必须发生在宿主机网络命名空间里。如果 Pod 网络隔离了这些操作,agent 会一直处于异常状态。
3. 控制平面容器化的核心实操环节
3.1 数据库与消息队列的高可用部署
OpenStack 控制平面里,最需要认真对待的就是 MariaDB 和 RabbitMQ。这两个服务一旦出问题,整个 OpenStack 控制平面就会瘫痪。在 Kubernetes 里部署高可用的 MariaDB,和部署一个普通 Web 服务完全不是一个复杂度。
MariaDB 的高可用我用的是 MariaDB Galera Cluster,它支持多主同步复制,任何一个节点挂掉都不会影响数据读写。在 Kubernetes 里跑 Galera,最关键的是每个 Pod 必须有一个稳定的标识符,通常用 StatefulSet 来实现。StatefulSet 的 Pod 名称是固定的,比如 mariadb-0、mariadb-1,Galera 集群的节点地址就靠这些固定的 Pod 名称来互相发现。
这里有个实操细节:Galera 集群的 SST(State Snapshot Transfer)对网络延迟非常敏感。当新节点加入集群时,它需要从现有节点同步全量数据,如果网络抖动,SST 可能会超时失败。所以 MariaDB 的 Pod 应该通过节点亲和性绑定到同一个机架或者同一批高性能网络互通的节点上,避免跨地域部署。
RabbitMQ 的高可用方案是镜像队列。在 Kubernetes 里部署 RabbitMQ,一般用 StatefulSet 加 Headless Service 来保证每个实例的稳定网络标识,然后在 RabbitMQ 内部配置镜像队列策略,让队列在主节点挂掉后能自动切换到镜像节点。
有一个常见的配置坑是 RabbitMQ 的autocluster插件和 Kubernetes 的 DNS 解析配合问题。RabbitMQ 节点启动时需要通过 DNS 解析其他节点的地址,如果 Kubernetes 的 CoreDNS 出问题,RabbitMQ 集群会一直起不来。我建议在 RabbitMQ 的 StatefulSet 里配置一个独立的 Headless Service,并且把 DNS 解析超时时间调大一些。
3.2 Keystone 与各个服务的交互配置
Keystone 作为 OpenStack 的身份认证中心,它的稳定性决定了整个控制平面的可用性。容器化部署 Keystone 时,最重要的配置文件是keystone.conf里的数据库连接串和 Fernet Key。
Fernet Key 是用来签发和验证用户 token 的,它必须在一个环境中保持一致。如果你用 Deployment 部署 Keystone 并且有多个副本,每个副本必须挂载同一个包含 Fernet Key 的 Secret。如果 Fernet Key 不一致,用户在一个 Keystone 节点上获取的 token,在另一个节点上验证会直接失败,表现为用户时不时收到 401 Unauthorized。
实际操作中,我会把 Fernet Key 放到 Kubernetes Secret 里,然后挂载到 Keystone 的 Pod 中,确保所有副本读取到的是同一份 Key。同时,Fernet Key 的轮换需要专门安排维护窗口,不能随意操作,因为新旧 Key 需要共存一段时间才能平滑过渡。
其他服务(Nova、Neutron、Cinder 等)与 Keystone 的交互,主要通过 service account 和 endpoint 两种方式。容器化部署时,各个服务的*.conf文件里需要配置 Keystone 的地址,这个地址应该是 Kubernetes 内部的 Service 地址,而不是外部 LoadBalancer 的地址,这样可以减少网络延迟并且避免额外的故障点。
3.3 Endpoint 注册与内部访问地址管理
OpenStack 的各个服务之间通信,靠的是 Keystone 里注册的 Endpoint。容器化部署时,Endpoint 的地址设置需要格外小心。
我在生产环境中的做法是,把 Endpoint 分为两种:内部 Endpoint 和公共 Endpoint。内部 Endpoint 使用 Kubernetes 的 Service 名称,比如http://keystone.openstack.svc.cluster.local:5000,供 OpenStack 各服务之间内部调用;公共 Endpoint 则使用外部负载均衡器的地址,比如https://keystone.example.com:5000,供用户和运维工具访问。
这个设计的好处是,内部流量不走外部负载均衡器,减少了延迟和故障暴露面,同时也让各服务之间的调用不依赖外部 DNS 解析。如果你只用一套 Endpoint,内部所有服务都走外部地址,一旦负载均衡器抖动,整个 OpenStack 控制平面都会出现间歇性不可用。
这里有一个常见的问题是,Nova 的nova.conf里配置的oslo_messaging_rabbit_hosts需要指向 RabbitMQ 的服务地址,如果你在多个 Region 或多套环境中用同一个 RabbitMQ 集群,要确认地址是否正确,否则服务之间无法通信,虚拟机创建会一直卡在调度阶段。
3.4 镜像构建与 ConfigMap 管理
控制平面容器化之后,镜像构建的规范就变得非常重要。我在生产环境中维护了一套基于 Kolla 镜像的定制化构建流程。Kolla 提供了非常完整的 OpenStack 组件镜像,但我不会直接用它官方的最新镜像,而是会在它的基础上加上我们团队自己的配置和补丁,构建出私有镜像仓库里的版本。
镜像tag 的管理也很关键。我建议使用带有版本号和构建日期的 tag,比如nova-api-yoga-2024.01.15。这样回滚时只需要改一下镜像 tag 重新部署,不用去猜某个镜像到底是什么时候构建的。
ConfigMap 是用来管理 OpenStack 服务配置的主要方式。所有服务的*.conf文件都可以通过 ConfigMap 挂载到 Pod 中。但这里有一个容易踩的坑:OpenStack 服务启动时,如果 ConfigMap 挂载的配置文件里有语法错误,服务不会启动,但 Kubernetes 不会给你明显的报错,只是 Pod 一直处于 CrashLoopBackOff。
我通常会准备一套配置校验脚本,在构建镜像或者发布前先跑一遍,用oslo-config-validator检查配置文件的合法性。这个工具会帮你检查配置项的类型、范围、是否在配置文件中被正确识别,能省掉大量调试时间。
4. 数据平面服务调度与底层资源打通
4.1 Nova Compute 的调度策略
Nova Compute 是 OpenStack 数据平面最核心的组件,它直接负责虚拟机的生命周期管理。容器化部署 Nova Compute 时,调度策略是首先要考虑的问题。
我的做法是给计算节点打上专门的标签,比如openstack-compute-node=true,然后让 Nova Compute 的 DaemonSet 只调度到这些节点上。同时,Nova Compute 的 Pod 需要配置hostNetwork: true,使用宿主机的网络命名空间。这是因为 libvirt 创建的虚拟机网卡要直接挂到宿主机的 Linux Bridge 或 OVS 网桥上,如果 Pod 使用独立的网络命名空间,虚拟机网络根本无法工作。
另外,Nova Compute 的 Pod 需要挂载宿主机的/var/lib/libvirt、/etc/libvirt、/run等目录。一开始你可能会觉得挂载这么多目录有安全风险,但在生产环境里这是没办法的事,libvirt 管理虚拟机必须要访问这些宿主机的资源。Privileged 模式也需要打开,因为没有它,nova-compute 无法创建和管理虚拟机。
这里有一个严重的性能问题需要注意:Nova Compute 的 Pod 里运行的 libvirt 进程,会直接使用宿主机的 CPU 和内存来跑虚拟机。如果 Kubernetes 的 kubelet 开启了 CPU Manager 或者给 Pod 设置了 CPU limit,可能会影响虚拟机的性能。我在生产环境里建议不要让 Nova Compute 的 Pod 设置 CPU limit,让它直接使用宿主机的全部资源。
4.2 Neutron Agent 与宿主机网络的协同
Neutron 的各个 Agent 是数据平面和网络平面的桥梁,它们和宿主机的网络栈紧密耦合。和 Nova Compute 类似,Neutron Agent 的 Pod 也需要使用hostNetwork: true和特权模式。
DHCP Agent 和 Metadata Agent 相对简单,它们只需要监听宿主机的网络命名空间里对应网桥的 IP,提供 DHCP 和 metadata 服务。L2 Agent(Open vSwitch Agent 或 Linux Bridge Agent)则要复杂得多,它需要管理宿主机上的各种网桥和隧道接口。
有一个经验是,Neutron 的 L2 Agent 不要和 Nova Compute 放在同一个 Pod 里跑,即使它们调度到同一个节点。分开跑的坏处是通信要经过额外的网络栈,好处是故障隔离更清晰,一个组件崩溃不会影响另一个。我的建议是分开跑,虽然看起来多占了一点资源,但排查问题时你会感谢这个决定。
Neutron Agent 的配置里,tunnel_type和local_ip参数非常重要。local_ip必须是宿主机上用于 VXLAN 隧道通信的真实 IP 地址,而且这个 IP 对应的网卡必须稳定可靠。如果这个 IP 配置错了,虚机之间的东西向流量会完全不通,而且故障排查起来非常困难,因为控制平面一切正常,就是虚拟机之间 ping 不通。
4.3 Cinder Volume 如何在容器里管理 LVM 和 Ceph
Cinder Volume 负责管理和提供块存储。如果你的后端是 Ceph,情况相对简单,因为 Cinder Volume 只需要通过 librbd 库连接 Ceph 集群,不需要直接操作宿主机的存储设备。
但如果后端是 LVM,情况就复杂了。Cinder Volume 的 Pod 必须挂载宿主机的/dev目录,才能看到宿主机上的物理卷和卷组。你需要在宿主机上预先创建好 Volume Group,然后在 Cinder 的配置里指定这个 Volume Group 的名称。Cinder Volume 会在运行时通过 LVM 命令创建逻辑卷,然后把它们作为 iSCSI 或 FC 设备导出给 Nova。
有个坑是,Cinder Volume 容器里跑的 LVM 命令可能和宿主机的 LVM 版本不一致,导致无法识别卷组。这个问题比较隐蔽,我遇到过几次,后来直接在构建镜像时锁定了 LVM 版本,避免这种兼容性问题。
Cinder 的后端配置中,volume_backend_name参数要特别注意。创建 Volume Type 时,Extra Specs 里的volume_backend_name必须和 Cinder 配置中的volume_backend_name完全一致,否则创建卷时会报 “No valid backend was found” 的错误。这个错误信息很常见,但让人头疼的是它不会告诉你是哪个参数不匹配。
4.4 如何优雅地处理计算节点下线
生产环境里,计算节点总会有维护、硬件更换的时候。在 OpenStack on Kubernetes 架构里,计算节点退出集群的操作要格外谨慎,因为你不仅要处理 Kubernetes 节点,还要处理 OpenStack 数据平面的状态。
第一步是先把计算节点上的虚拟机迁移走,使用nova host-evacuate或nova live-migration把虚拟机热迁移到其他节点。迁移完成之后,把该节点上的 OpenStack 服务禁用,比如nova service-disable、neutron agent-disable。
第二步才是处理 Kubernetes 节点。把节点标记为不可调度,清理 Pod。这里要注意,Nova Compute 的 DaemonSet 在节点下线后会自动在新节点上启动新的 Pod,但如果你没有预留足够的计算资源,新的 Pod 会处于 Pending 状态,影响整个环境的可用性。所以节点下线前的容量评估非常关键。
我在实际运维时,会先做一个kubectl drain的演练,确认所有数据平面的 Pod 都迁移干净了,再真正执行节点下线。演练能发现很多意外情况,比如某个 PVC 没有设置合适的回收策略,导致节点下线时数据被删掉。
5. 常见问题与故障排查实录
5.1 数据库连接池耗尽导致的控制平面卡死
一个典型场景:虚拟机创建并发突然升高,控制平面所有 API 请求变慢,甚至超时。排查下来发现是 Nova API 连接 MariaDB 的连接池被打满了,新请求全部在等待数据库连接。
这个问题在传统部署里也存在,但容器化部署更容易被忽略,因为每个 API 服务的副本数可能不止一个,连接池的总数是各个副本的叠加。如果你的 Nova API 有 5 个副本,每个副本连接池上限是 50,那么数据库端的最大连接数就是 250,很容易超过 MariaDB 的max_connections限制。
解决方案分两步。第一步,调整 Nova API 的oslo_db连接池大小,把max_pool_size和max_overflow控制在一个合理范围;第二步,调整 MariaDB 的max_connections,并且监控连接数增长趋势,提前扩容数据库节点。
5.2 RabbitMQ 节点重启后集群脑裂
Kubernetes 滚动更新或节点重启时,RabbitMQ 集群偶尔会出现脑裂或分区状态。如果你用的是镜像队列,表现是部分队列的消息无法被消费,OpenStack 里的虚拟机创建任务会一直卡在等待状态。
排查方法很简单,登录 RabbitMQ 管理界面,看每个节点的分区状态。如果出现{partitions, [node@host1]}之类的提示,说明集群已经分区了。
生产的解决思路是,先手动恢复网络连通性,然后选择保留哪个分区的数据。如果两个分区的数据差异不大,可以尝试把分区节点逐个重启,让它们重新加入集群。如果数据差异较大,就只能选择保留数据较新的分区,然后把旧分区的数据丢弃。这个过程一定要谨慎操作,最好先用快照备份一遍数据。
为了防止出现这种情况,我建议在配置 RabbitMQ 时开启pause_minority模式。这个模式会让集群在分区时自动暂停少数派节点的服务,避免脑裂导致的数据不一致,虽然会在分区时牺牲一小段可用性,但总比数据损坏好。
5.3 Neutron Agent 状态假死的问题
Neutron Agent 的状态在控制平面显示为 UP,但实际上已经无法处理网络请求。这种情况通常发生在 OVS Agent 上,表现为虚拟机的网络不通,但neutron agent-list里 Agent 的状态依然是活的。
原因通常是 OVS Agent 和 Neutron Server 之间的 RPC 通信断开,但 Agent 进程本身没有退出。它还在向 Neutron Server 发送心跳包,但实际的工作线程已经卡住了,或者 OVS 网桥上的流表已经和 Neutron Server 上的期望状态不一致。
遇到这种问题,第一件事是重启 OVS Agent 的 Pod,然后观察流表是否重新同步。如果频繁出现假死,就要检查宿主机上的 OVS 进程是否有异常日志,以及 Neutron Server 是否频繁重启。还有一种可能是宿主机的 ovsdb 和 Open vSwitch 内核模块版本不匹配,导致流表下发失败。
5.4 Ceph 超时导致卷创建失败的排查
Cinder 创建卷超时失败,是 OpenStack on Kubernetes 里比较常见的存储问题。表现为创建卷的请求一直处于creating状态,最终超时变成error。
排查思路是看 Cinder Volume 的日志,确认是连接 Ceph 超时还是 RBD 操作超时。如果是连接超时,检查 Cinder Volume Pod 到 Ceph Monitor 的网络连通性,尤其要看防火墙是否放行了 Ceph 的 6789 端口。如果是 RBD 操作超时,多半是 Ceph 集群本身负载过高,需要查看 Ceph 的健康状态和 IO 延迟。
另外,容器里跑的 Cinder Volume 需要能够解析 Ceph Monitor 的地址。如果你的 Ceph 集群启用了域名解析,要确认容器里的 DNS 配置正确,否则会报 “Name or service not known” 的错误。
5.5 常见问题速查表
| 问题 | 可能原因 | 快速处理方法 |
|---|---|---|
| Keystone 401 间歇性出现 | Fernet Key 多个副本不一致 | 检查 Secret 挂载,统一 Fernet Key |
| 虚拟机创建卡在调度 | RabbitMQ 队列拥塞或分区 | 查看 RabbitMQ 队列状态和节点分区状态 |
| 虚拟机网络不通 | Neutron Agent 状态假死或流表异常 | 重启 Agent Pod,检查 OVS 流表 |
| Cinder 卷创建超时 | Ceph 连接超时或集群负载过高 | 检查 Ceph 健康状态,调整超时参数 |
| API 请求普遍变慢 | 数据库连接池耗尽 | 调整连接池参数,监控数据库连接数 |
| Pod 一直 CrashLoopBackOff | 配置文件语法错误 | 用 oslo-config-validator 检查配置 |
6. 生产上线前的检查清单与运维建议
6.1 检查清单
生产环境不像测试环境,出问题可以推倒重来。上线前把下面的清单过一遍,能避免大部分低级事故。
第一,确认所有 Secret 和 ConfigMap 都已经正确创建,并且内容没有遗漏。尤其是 OVS 和 Nova 需要使用的各种密钥和凭据,少一个都没法正常工作。
第二,确认各个服务的 Endpoint 已经注册到 Keystone,并且通过openstack endpoint list验证一下。注意检查公开 Endpoint 是否可以通过外部地址访问。
第三,确认 Cinder 的 Volume Type 和 Extra Specs 配置正确。先用 Ceph 创建几个测试卷,然后挂载到虚拟机上实际跑一下读写,确认存储链路没有隐藏问题。
第四,验证 Neutron 的网络功能。创建一个测试网络和子网,启动两台虚拟机关联到这个网络,测试它们之间的网络通信是否正常。同时测试安全组规则是否生效,浮动 IP 是否能正常绑定。
第五,确认监控和告警已经接入。至少要监控宿主机 CPU、内存、磁盘、网络,以及 MariaDB 和 RabbitMQ 的关键指标。同时要对 OpenStack 的服务进程做健康检查,建议接入 Prometheus 加 Grafana。
6.2 日常运维里的两个小技巧
先说备份。数据库和消息队列是 OpenStack 控制平面的神经中枢,备份不能省。我建议每天全量备份 MariaDB 的数据目录,并且至少保留最近 14 天的备份。RabbitMQ 的配置和用户定义也要备份,虽然它本身存储的是临时数据,但配置丢了重建起来很麻烦。这些备份应该放在独立于 Kubernetes 集群之外的存储里,防止集群故障时备份也一起丢了。
再说日志。容器化部署的日志采集和传统方式不太一样。OpenStack 各服务的日志打到 stdout,由容器运行时收集,再通过 fluentd 或 Filebeat 发送到 Elasticsearch。如果你用 Loki,也可以直接把 Pod 日志拉取到 Loki 里。关键是日志检索能力,生产环境排查问题时,能在一个地方同时搜索 Nova、Neutron、Cinder 的日志,会让你心里的踏实程度完全不一样。
最后要提的一点是,在 K8s 里跑 OpenStack 控制平面,你仍然要把 OpenStack 本身的知识和运维经验学扎实。容器化只是把进程的运行环境规范化了,OpenStack 组件之间的依赖关系、配置项的作用、消息队列里的交互逻辑,这些底层的东西跟传统部署没有区别。工具换了,但理解问题的框架没有换。把这一点想透,你在生产环境里遇见任何奇怪的问题,都能有一套可靠的思路去拆解它,而不是被容器那一层障眼法带着走。