做OpenStack on Kubernetes生产部署这一系列,前面两篇分别梳理了整体架构选型和底层基础设施的搭建,按道理说第三篇可以直接进入服务部署环节了。但我发现一个很现实的问题:很多同行听到“OpenStack跑在Kubernetes上”的第一反应不是兴奋,而是拒绝——“OpenStack已经够复杂了,再套一层K8s,以后出了事谁来背锅?”这恰恰就是我写这一篇的价值所在。今天不聊概念,只聊生产实战,重点解决三件事:核心控制面服务怎么编排、存储和网络这两个决定生死的环节怎么接入、以及联调阶段最容易让人熬夜的那几个坑究竟怎么排查。
1. 先想清楚一件事:OpenStack上K8s不是把YAML叠起来
很多人以为所谓“OpenStack on Kubernetes生产部署”,就是把原来装在虚拟机里的Keystone、Nova、Neutron这些服务写成Deployment,扔进集群里跑起来就算完事。如果你的目标只是“能在K8s上部署nginx”,那确实简单,但OpenStack是一整套IaaS的控制面系统,组件之间依赖关系极深,任何一个服务的高可用、配置同步、网络模式都直接影响整朵云。所以动手之前,必须先把部署拓扑和服务边界这两个问题想透。
1.1 PackStack能装出OpenStack,但装不出生产系统
早期接触OpenStack的朋友多半用过PackStack,一条命令下去,all-in-one环境几分钟就能装出来,控制台也能登录,镜像能上传,虚机也能建。但那种方式本质上是把一堆服务进程装在同一台机器上,靠systemd拉起,带不动任何规模的真实业务。生产环境至少要面对三个PackStack不会帮你解决的问题:
- 控制面任何一个服务宕机,必须自动恢复,并且不能影响正在运行的云主机。
- 数据库、消息队列这类有状态组件,需要单独考虑数据一致性和故障转移。
- 调度、网络、存储三类服务必须能横向扩展,不能出现单点瓶颈。
Kubernetes恰好擅长前两点,但第三点需要你自己做架构决策。换句话说,K8s在这里不是替代OpenStack,而是给OpenStack的控制面做了一层“编排和自愈”的底座。你仍然需要按照生产标准去规划计算节点、网络后端和存储后端,只不过服务进程的托管方式变了。
1.2 控制面与数据面的边界划分
这是整个部署里最重要的一张图,先画清楚再动手。OpenStack的服务可以分成两类:
- 无状态API服务:Keystone、Nova-API、Neutron-Server、Glance-API、Cinder-API、Nova-Conductor、Nova-Scheduler等。它们本身不存数据,状态都在数据库里,很适合做成普通Deployment,通过K8s的副本和探针保证可用性。
- 有状态/宿主机强相关服务:Nova-Compute、Neutron-OVS-Agent或OVN-Controller、Cinder-Volume、Swift存储节点等。这些服务必须运行在特定的计算节点或存储节点上,直接操作宿主机的网络设备、块设备、虚拟化设备,不能当作普通无状态Pod随便调度。
我在实际项目中采用的做法是:控制面API服务全部容器化跑在K8s集群的master节点池,节点池用污点和标签固定;计算节点单独打标签,Nova-Compute和相关的Neutron Agent用DaemonSet部署上去,而且直接共享宿主网络。这样既有K8s的调度和自愈能力,又不牺牲底层性能。
1.3 为什么用Helm而不是裸写YAML
OpenStack-Helm这个开源项目是社区给出的参考实现,但我并不建议直接全量照抄,原因是它的默认值面向的是一套标准环境,跟真实生产环境总有偏差。不过它的价值在于给出了完整的Chart结构,每个服务该有哪些组件、需要挂载哪些配置、依赖哪些Secret,都是一份非常健全的“地图”。
实际部署时,我会用OpenStack-Helm作为基线,然后在上面做三层修改:
- 镜像仓库地址覆盖,改成自己环境可以稳定访问的镜像源。
- 资源配额调整,API服务的requests和limits一定要给足,OpenStack控制面最怕的就是Pod因为内存不足被OOMKilled。
- 存储类和Ingress域名改成自己的物理环境参数。
裸写几百个YAML不是不行,但后期升级和统一改配置会非常痛苦。Helm的values覆盖机制,在OpenStack这种组件多到上百个的场景下,几乎是唯一能维持心智负担的选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心控制面组件入K8s的编排方式
架构想清楚之后,第二步就是把控制面服务一个个编排进集群。这一节只挑几个容易出问题、而且最能体现“生产部署”和“实验环境”差别的组件来讲。
2.1 Keystone:所有服务调用的起点,先把它跑稳
Keystone是整个OpenStack的“门禁”,所有API请求都要先经过它做认证和鉴权,所以它绝对不能成为瓶颈。生产部署里我有两个建议:
- Keystone至少要开3个副本,并且设置合理的资源配额。这个服务很轻,但并发高的时候Python解释器的GIL会成为瓶颈,所以CPU配额建议给到2以上。
- 用K8s的Service做内部负载均衡即可,不需要像网上一些教程那样给每个服务单独搞Ingress。OpenStack服务之间的内部调用走ClusterIP,外部用户通过Horizon或命令行工具访问时,只需要暴露一个统一的API网关入口。
OpenStack-Helm的默认Chart里,Keystone的配置是通过ConfigMap挂载的/etc/keystone/keystone.conf。生产环境里我们会在values里覆盖掉Fernet Key的生成方式,确保所有副本用的是同一套Key,否则重启后token校验会全部失败。这个坑很隐蔽,排错的时候如果不看keystone日志,很容易误判为网络问题。
2.2 Nova:控制面与计算面的协作关系
Nova是OpenStack最复杂的组件,它拆成了API、Conductor、Scheduler、Compute等多个服务。生产环境里我建议的部署形态是:
| Nova组件 | 部署形态 | 说明 |
|---|---|---|
| nova-api | Deployment(3副本) | 无状态,处理所有API请求,要放在负载均衡后面 |
| nova-conductor | Deployment(2副本) | 无状态,管理数据库访问和RPC调度,可以多副本跑 |
| nova-scheduler | Deployment(2副本) | 无状态,负责选择计算节点,生产上建议用FilterScheduler并设置合理的过滤链 |
| nova-novncproxy | Deployment(2副本) | 无状态,VNC控制台服务,用Ingress暴露wss端口 |
| nova-compute | DaemonSet /裸机进程 | 有状态,必须在计算节点上运行,直接操作libvirt |
Nova-Compute容器化部署有个关键点:OpenStack-Helm默认的Chart不会给计算节点自动部署nova-compute,它依赖你把计算节点预先配置好。我这边是把nova-compute做成一个独立DaemonSet,调度器用nodeSelector匹配带有openstack-node=compute标签的节点。
容器里跑nova-compute需要注意挂载的设备:
/dev/kvm和/dev/vhost-net,否则虚机无法启动或网络性能极差。/var/lib/libvirt,保持libvirt的状态持久化,否则Pod重建后虚机处于“未知”状态。/lib/modules和/sys/fs/cgroup,让nova-compute容器内的libvirt能正确识别宿主机内核模块和cgroup信息。
这三个挂载缺一个,你在创建云主机的时候都会遇到“看起来在启动,实际上一直卡在building”这种问题。
2.3 Neutron:网络服务最容易和K8s的CNI打架
Neutron容器化入K8s,是整个部署里最反直觉的部分。K8s自带一套CNI网络,管理的是Pod与Pod之间的通信;而Neutron管理的是云主机与云主机之间的虚拟网络,两者天然是两套体系,在同一个节点上共存时,必须明确各自负责的网卡和路由。
我的生产实践是:
- Neutron-Server跑在K8s里,作为无状态API服务。
- Neutron的DHCP Agent和Metadata Agent跑在控制面节点池的DaemonSet里,它们需要宿主机网络,因为要监听宿主机的网卡和命名空间。
- 网络后端采用OVN,不用传统的OVS+L3 Agent组合。OVN的架构更现代,把控制逻辑集中到了OVN-Northd,在K8s上部署更干净,排查问题也更直观。
这里特别提醒一个最容易踩的坑:K8s的Pod网络和OpenStack的云主机网络不能共用网桥。我在测试环境里图方便,把Neutron的br-int网桥和K8s的cni0网桥建在同一台宿主机上,结果两个网络的ARP广播互相干扰,云主机和Pod的DNS解析都断断续续。生产环境的做法是物理隔离,要么用不同网卡,要么在Neutron的网桥配置里明确设置独立的物理接口。
2.4 数据库和消息队列:有状态服务的选择
MariaDB和RabbitMQ是OpenStack的“心脏”,这两个服务一挂,整朵云的控制面全部瘫痪。它们是有状态服务,在K8s里的部署要格外慎重。
我的建议是:数据库用K8s的StatefulSet部署MariaDB,至少3副本,启用Galera同步复制。原因很简单:OpenStack的并发写入比较多,特别是Nova的实例状态变更,异步复制容易丢数据。Galera虽然写入性能有一定损耗,但对控制面这种写入量级完全扛得住。
RabbitMQ同样用StatefulSet,3副本组成一个集群。这里面有一个生产级别的细节:RabbitMQ的节点名称必须固定下来,不能随着Pod重建而改变,否则集群会分裂。OpenStack-Helm的处理方式是给每个节点绑定PVC并使用稳定的headless service域名,我在实际部署时确认了这一点没有问题,但要提醒不要自己随意改Pod的名称策略。
数据库和消息队列的存储后端,我会选择本地高可用存储或者Ceph RBD,千万不要用普通云硬盘当单副本存储,生产系统一旦存储节点故障,整个控制面恢复会非常痛苦。
3. 存储与网络:真正决定生产可用性的两个环节
控制面服务编排完毕,只是让OpenStack“能启动”。生产环境里,用户真正关心的是:云主机能不能创建成功,硬盘能不能挂载,网络能不能通。所以这一章专门讲存储和网络的接入方案,这也是和“实验环境”拉开差距的地方。
3.1 Ceph RBD对接:Glance、Cinder、Nova的三方配合
生产环境里,我强烈建议存储后端统一走Ceph RBD。OpenStack对接Ceph之后,三者的配合关系是:
- Glance:上传的镜像文件直接存到Ceph的
images池,不再依赖本地文件系统。 - Cinder:卷存放在Ceph的
volumes池,Cinder-Volume通过librbd创建和删减卷。 - Nova:云主机的临时磁盘(也就是系统盘)可以存放在
vms池,从镜像创建云主机时,Nova会做一次RBD的clone操作,秒级完成。
容器化部署Cinder-Volume时,注意把Ceph的ceph.conf和ceph.client.openstack.keyring做成K8s的ConfigMap和Secret,挂载进Pod的/etc/ceph目录。如果挂载路径不对,Cinder-Volume启动时不报错,但创建卷时一定会提示rados连接失败。
Nova对接Ceph还有一个隐藏配置:libvirt.rbd_user和libvirt.rbd_secret_uuid要和Ceph侧保持一致。这个参数写在nova-compute.conf里,即使数据库和镜像都对,漏了这两个配置,云主机创建到一半会卡在“磁盘信息获取失败”。
3.2 Neutron与OVN:容器化网络插件与宿主网络的桥接
前面提到网络后端选OVN,这里补充一下具体接入方式。OVN架构下,Neutron-Server通过OVN-Plugin和OVN-Northd的数据库通信,计算节点上的OVN-Controller作为Agent,负责把逻辑流表同步到本地的OVS网桥上。
在K8s里部署这套体系时,需要明确三个组件的部署位置:
- OVN-Northd:可以跑在控制面节点池的Pod里,它只和数据库打交道,不需要宿主机网络。
- OVN-Controller:必须跑在计算节点的DaemonSet里,并且要设置
hostNetwork: true,因为它要管理节点上的ovsdb和流表。 - Neutron-Server:跑在K8s集群控制面,但它要能访问到OVN数据库的地址,这个地址可以通过Service暴露,也可以用外部数据库。
实际运维中,排查网络问题时最常用的命令是ovn-trace,它能模拟一个数据包在OVN逻辑网络里的完整流向。我在生产环境里遇到过云主机无法访问外网的问题,用ovn-trace一步步走,最终定位到是网关路由的L3 Agent规则在数据库里出现了重复条目,清理后恢复。这个排错思路在传统OVS环境下比较困难,OVN的数据面控制和逻辑流分离,可视化程度高很多。
3.3 数据面与控制面网络的高可用设计
说到生产环境,就不能不提高可用。K8s本身提供控制面的自愈能力,但底层的物理网络和存储链路必须有冗余。我这里的做法是:
- 控制面API服务的Ingress前端挂SLB或者用Keepalived暴露VIP,确保K8s集群即使整体重启,外部访问OpenStack API的入口不中断。
- 存储网络单独走万兆物理链路,和控制面网络分开,避免云主机大量读写镜像时把管理网络打满。
- 计算节点至少预留一个空闲的物理网口,用于Neutron的外部网络流量接入,不能让外部流量和存储流量挤在一根线上。
这些设计看起来都在K8s之外,但直接决定了K8s里面那一层控制面的稳定性。很多人把OpenStack部署在K8s上之后发现云主机网络时好时坏,排查到最后往往不是K8s的问题,而是底层物理网络的广播域没有规划清楚。
4. 从“能登录控制台”到“能建出机器”:联调踩坑全记录
这一章是本文的重头戏。OpenStack部署成功不等于可用,从“控制台能登录”到“能正常创建一台云主机”,中间有大量联调问题。我把最近一次项目中遇到的两个经典问题完整复盘出来,排查链路比最终修复方案更值得看。
4.1 问题一:云主机创建成功,但ping不通网关
这个现象很典型:OpenStack控制台显示云主机状态是ACTIVE,但在云主机里执行ping网关完全不通。我当时的排查链路是这样的:
第一步,先看计算节点的OVS流表。执行ovs-vsctl show,确认实例所在的网桥和端口都正常。如果端口都没起来,问题大概率出在nova-compute配置或宿主机网卡绑定上。
第二步,确认之后,ovn-trace模拟云主机发出的第一个ARP广播,结果发现逻辑流表在ls-routing这里被丢弃了。这意味着Neutron的逻辑路由配置有问题,但控制台里看到的子网和路由器状态都是正常的。
第三步,查Neutron日志,发现有一个非常隐蔽的报错:L3 agent is dead。因为我的环境里用了OVN,但Neutron侧的L3 agent插件配置没有完全切干净,导致逻辑路由器状态无法同步到OVN数据库。
最终修复方案:卸载多余的Neutron L3 agent配置,只保留OVN插件,重启Neutron-Server和OVN-Northd后,网络恢复正常。这个问题排查了将近两个小时,如果一开始就用ovn-trace而不是反复看控制台状态,会快很多。
4.2 问题二:卷挂载超时,云主机一直等待Cinder响应
第二个问题是块存储联调时遇到的。云主机创建成功,但是挂载Cinder卷时,云主机里看到的磁盘迟迟不出现,日志显示mount timeout。
这次我采取了不同策略,直接看Cinder-Volume的日志。生产环境里,Cinder-Volume跑在K8s的Pod里,日志输出到stdout,用kubectl logs -f cinder-volume-xxx查看。
日志里循环报一句话:Byte of volume is [0B]。这个提示很直接,Cinder-Volume拿到的卷大小是0。问题定位到Ceph侧:Cinder后端驱动配置里的volume_size参数没有正确传递。
修复方法:检查Cinder配置文件中rbd_ceph_conf和rbd_pool两处路径,确认Ceph的pool名称和实际一致。当时我把vms池误写成了volumes池,Ceph侧找不到对应pool就返回0字节。修改配置并重启Cinder-Volume后,卷挂载恢复正常。
这个案例其实暴露了一个共性:容器化部署让服务配置以ConfigMap形式统一管理,改起来方便,但也容易在复制values文件时引入低级错误。建议每次修改配置后,在Pod里执行openstack volume service list确认所有服务都处于enabled up状态,再继续后面的联调。
4.3 从K8s源码机制反哺排错效率
联调过程中还有一类问题,直接看OpenStack日志找不到答案,需要理解Kubernetes本身的运行机制。比如有一次RabbitMQ集群节点滚动重启时,控制面短暂出现了消息堆积,很多API请求超时。
表面原因是RabbitMQ节点重启,但深层原因是StatefulSet默认的OrderedReady更新策略会逐个重启节点,而我们的PodReadinessProbe只检测了端口连通性,没有检测RabbitMQ是否完成了集群状态同步。一个节点还在同步中就被标记为Ready,外部请求继续打进来,导致连接堆积。
这种情况,如果你只是会看OpenStack的日志,会很长时间摸不着头脑。反过来,如果你理解Kubelet对Pod生命周期的管理逻辑、Controller-Manager对StatefulSet滚动更新的实现方式,就能很快想到:问题可能出在探针判活标准上,而不是RabbitMQ本身。所以我一直建议做OpenStack on Kubernetes项目的朋友,花时间读一读《深入理解kubernetes源码》这类资料,不需要逐行背源码,但一定要理解调度、探针、控制器这几个核心机制的设计意图,排错效率会翻倍。
5. 生产验收清单与可观测性配置
最后的环节是验收。部署完成不代表能交付,我有一套固定的验收清单和监控接入方式,每个项目都按这个标准来,能提前滤掉绝大部分潜在故障。
5.1 面向生产的验收清单
| 检查项 | 检查方式 | 通过标准 |
|---|---|---|
| 控制面服务高可用 | 随机删除某个API服务的Pod | 服务自动恢复,业务无感知,虚拟IP不中断 |
| 数据库灾备 | 停止一个MariaDB节点 | 集群自动完成选主,控制面无写入失败 |
| 消息队列恢复 | 重启一个RabbitMQ节点并观察同步 | 节点重新加入集群,消息无丢失 |
| 云主机生命周期 | 创建、挂卷、迁移、删除全流程 | 所有操作在预期时间内完成,无异常报错 |
| 网络连通性 | 同子网、跨子网、访问外网三组测试 | 均能互通,延迟无异常抖动 |
| 存储持久性 | 删除计算节点Pod,重建后检查虚机 | 虚机数据完整保留,重启后状态恢复 |
| 迁移与疏散 | 标记一个计算节点为维护模式,迁移其云主机 | 云主机迁到其他节点,服务不中断 |
| 监控告警 | 人为触发一个Audit级别告警 | 告警能正常投递,监控面板数据实时更新 |
这份清单不是我随手写的,每一项都在某个项目里真实暴露过问题。删除Pod恢复这一项,特别建议在生产业务低峰期做,一方面验证自愈,一方面验证你的Pod安全策略和后端存储连接是否足够健壮。
5.2 监控与日志接入
生产环境没有一个像样的监控体系,部署得再漂亮都没底气。OpenStack on Kubernetes的监控分两层:
第一层是K8s集群本身的监控。Prometheus + Grafana是标配,重点盯Pod重启次数、节点资源水位、API Server延迟。另外要给Kubelet设置合理的--max-pods参数,避免单个节点运行太多Pod导致docker或containerd进程耗尽文件句柄。
第二层是OpenStack服务内部的监控。每个服务都会有健康检查接口,以Nova为例,nova-api暴露了/health端点,Cinder和Neutron也都有对应的服务状态上报。把这些接口接入Prometheus Blackbox Exporter,配合Alertmanager设置告警规则,能比单纯看K8s事件更早发现问题。
日志方面,我们的做法是每个Pod把stdout输出到统一的日志采集Agent,用Loki做集中存储,Grafana统一查询。OpenStack的日志量大且重复信息多,建议在日志采集侧就做一次level过滤,只保留WARNING及以上级别的日志,否则存储成本会非常夸张。
5.3 配置管理与升级备份策略
最后一个生产级话题是升级和备份。OpenStack on Kubernetes的优势在于,服务升级可以走K8s的滚动发布,不用像传统方式那样在物理机上做复杂的依赖升级。但升级前必须把数据库和配置备份做扎实。
我这边固定每周做一次完整的Ceph RBD快照备份,每天做一次控制面数据库的逻辑导出。对于OpenStack的配置,所有自定义配置都集中在Helm的values文件里,用Git管理,并在CI流水线里做helm template渲染校验。这样每次升级前,我只需要对比Git里的配置差异,就能判断哪些配置变更可能引发问题。
备份恢复这件事,建议至少每个季度完整演练一次。我见过一些团队备份做得很好,但从来没有真正恢复过,等到故障发生才发现备份数据是坏的。演练的过程很痛苦,但演练过一次之后,后续每次升级心里都有底。
6. K8s上的OpenStack运维,关键是把“期望状态”刻进脑子里
写到这里,第三个实战部分的经验基本分享完了。最后再单独聊一个我认为最重要的运维思维转变。
传统方式部署OpenStack,运维人员脑子里装的是“服务进程”,比如哪个节点上的nova-compute挂了、哪个机器的rabbitmq内存涨了。到了Kubernetes这里,思维必须切换成“期望状态”:服务挂了不要紧,关键是控制器的调谐逻辑有没有按预期把它拉回来;Pod重建了不要紧,关键是它能不能恢复到和之前一样的状态。
我在实际运维中养成了一个习惯:每次故障处理完之后,不急着关工单,先花二十分钟复盘“为什么这个故障没有在第一时间被探针发现”“为什么控制器没有自动恢复”,把答案补进监控规则和Pod配置里。这样同类故障的处理成本会一轮比一轮低。
OpenStack on Kubernetes这条路,网上能查到的参考资料并不算多,很多细节都是踩坑踩出来的。这篇文章里提到的所有配置思路和排错路径,都是我在真实生产项目里验证过的,希望能给正在做同样事情的朋友省下一些熬夜的时间。如果你在部署过程中遇到类似的问题,顺着我给的排查链路走一遍,大概率能快速定位到根因。
