《OpenStack on Kubernetes 生产部署实战(十三)》
这已经是这个系列的第十三篇了。前十二篇从OpenStack各组件的容器化改造讲起,一路聊到Kubernetes之上的编排策略、网络选型、存储接入,好多朋友在评论区催更,问得最多的就是“你的生产环境到底踩了哪些坑”“参数到底怎么调”。说实话,OpenStack和Kubernetes两套体系叠在一起,复杂度不是简单的1+1,尤其到了生产阶段,遇到的每一类问题都值得单独拎出来拆一遍。这篇我打算把控制平面容器化的架构设计、生产部署时的关键参数、以及我在现网里遇到的几个典型故障整理出来,给你一条可以直接照着复盘的路径。
作为系列的中后段,这篇的定位不是给零基础朋友科普概念,而是面向已经准备把OpenStack控制面迁到Kubernetes、或者正在做方案评估的运维和架构师。你会看到为什么我敢把MariaDB、RabbitMQ这些有状态组件也放进K8s,也会看到网络节点、存储池在容器化形态下怎么划分故障域,最后还有几段排障实录,都是花过代价换来的经验。Kubernetes负责调度和自愈,OpenStack负责对外提供云主机、网络和存储能力,这两套系统真正耦合好的时候,控制平面的运维成本和故障恢复速度会有质的改变。
1. 架构选型:为什么敢把OpenStack控制平面交给Kubernetes
1.1 从物理机裸部署到K8s编排的演进逻辑
传统OpenStack部署方案里,控制节点通常是三台物理机或虚拟机,跑着Keystone、Nova API、Neutron Server、Glance、Cinder等十几个systemd服务。每台机器的Python环境、配置文件、依赖版本都像积木一样堆在一起,升级一个组件经常要连带重启好几个服务,而且一旦某台控制节点磁盘或内存出问题,故障恢复基本靠人工重装、重新同步配置。
这套模式跑了几年,最大的痛点不是功能不够用,而是“变更太难”。改一个配置项要逐台登录、逐服务重启,稍不留神就漏掉一台;扩容控制节点要手工装包、手工配HA;服务进程挂掉之后的恢复也做不到秒级。Kubernetes天然解决了其中一个核心问题:调度和自愈。把OpenStack各服务做成容器镜像,用Deployment、StatefulSet去描述期望状态,Pod挂了就重建,节点宕了就漂移,滚动升级也变成了标准动作。
我从实际维护经验出发,迁移到K8s之后最明显的改善是:控制平面的故障恢复时间从“小时级”缩短到“分钟级”。比如之前遇到Keystone进程OOM,要登录服务器、重启服务、确认日志;现在Pod内存超限被OOMKilled后,kubelet配合livenessProbe会自动重建,业务侧只有一两次请求超时,整体可用性提升非常明显。
1.2 控制平面容器化的边界:哪些服务适合上K8s,哪些留在宿主机
这里必须先说清楚一个容易误导人的点:并非所有OpenStack组件都适合丢进Kubernetes跑。我见过不少方案把Nova Compute、Neutron Open vSwitch Agent也容器化,导致性能和排障复杂度急剧上升。控制平面和数据平面要分开看待。
适合放进Kubernetes的是无状态API服务和可横向扩展的worker。Keystone、Nova API、Nova Conductor、Neutron Server、Glance API、Cinder API、Heat Engine这些,它们不直接承载数据面的流量,状态可以放在数据库和消息队列里,Pod重建不丢数据。还有Horizon这样的Web前端,容器化部署更是毫无压力。
不适合放进Kubernetes的是依赖宿主机内核特性、网卡特性、设备直通的服务。Nova Compute必须管理宿主机上的libvirt、KVM、物理网卡,容器化之后挂载/var/run/libvirt、/dev/vhost-net等路径非常别扭,而且性能损耗不可控。Neutron的OpenVSwitch Agent同样需要直接在宿主机上操作网桥、流表,如果塞进Pod,网络路径多一层namespace转换,生产环境排查VXLAN隧道问题时能把人折磨疯。还有Ceph的OSD进程,存储节点保持物理部署才是正道。
我在生产环境里划定的边界是:Kubernetes只管控制面,计算节点和存储节点继续沿用传统部署方式。控制面容器化带来的收益已经足够大,数据面保持稳定可控,两边的分工清晰明确,排障时也容易划分问题域。
2. 网络与控制面的关键设计
2.1 OVN与Kubernetes网络的共存关系
把OpenStack跑在Kubernetes上,网络规划是整个架构里最需要提前想清楚的部分。很多第一次接触这个组合的人会问:Kubernetes用Calico或Flannel给Pod分配网段,OpenStack Neutron也给虚拟机分配VXLAN网段,两套网络会不会冲突?
答案是不会,但前提是你要在物理网络拓扑上做清楚划分。我的方案是改成叠加组网:Kubernetes集群的Pod网段用于承载控制面容器之间的通信,这个网段和OpenStack给租户虚拟机分配的VXLAN大二层网络没有任何交集。控制面Pod挂的是K8s的Service网络,虚拟机数据面走的是OVN的Logical Switch和VXLAN隧道。
具体落地时,Kubernetes节点上的CNI我选用了Calico(也有环境用Canal,实测Calico在性能和排障资源上更成熟),负责整个K8s集群内Pod的互通。而在OpenStack侧,Neutron使用OVN作为ML2的mechanism driver。OVN的ovn-controller跑在计算节点上,负责VXLAN隧道的封装和解封装,以及OpenFlow流表的下发。两个网络平面在物理层共用网卡,但通过不同的VLAN或子网隔离,管理网流量和数据网流量互不干扰。
需要特别注意的是MTU问题。Kubernetes Pod网络和OVN数据面如果都走VXLAN,叠加封装之后报文会膨胀,物理网络MTU必须提前预留。默认物理网卡MTU是1500,VXLAN隧道会吃掉50字节,如果虚拟机内部再叠加VXLAN,可用MTU只有1400。生产环境通常建议把管理网和存储网MTU调到9000(走Jumbo Frame),租户数据网按underlay的实际能力下发MTU值,否则会出现“虚拟机内网卡显示UP,但大包ping不通、小包正常”的经典问题。
2.2 Neutron元数据代理和DHCP在容器化形态下的调度策略
Neutron组件里有一类特殊角色,比如neutron-metadata-agent和neutron-dhcp-agent,它们在传统部署里通常和网络节点绑在一起,容器化之后很容易被当成普通Deployment随便调度。我在上线初期就吃过亏:metadata-agent被调度到了没有网络节点的K8s worker上,租户虚拟机请求metadata地址时完全不通。
生产环境的处理方式是给这类Agent使用DaemonSet + nodeSelector固定到指定节点。具体来说,标记一组节点为network-node=true,然后在chart的values里把nodeSelector指向这一组节点。这样每个网络节点上都会跑一个metadata-agent的Pod,和宿主机上的OVN网桥配合,保证metadata服务的192.168.0.2路由可达。
DHCP Agent的情况类似,但更麻烦的是它需要绑定到固定的namespace和网桥。容器化后,DHCP Agent要正常工作,必须让Pod共享宿主机网络(hostNetwork: true),或者把host的网络namespace挂载进容器。我最终采用hostNetwork方式,这样DHCP Agent创建的dnsmasq进程能直接监听在宿主机网桥上,不引入额外的NAT链路,也方便在节点上直接抓包。
这里补充一个经验:不要把网络Agent设计成随处漂移的Pod。DHCP和metadata服务天然和特定节点上的网桥绑定,如果Pod被K8s调度到其他节点,不仅起不来,还可能因为重复绑定网桥导致网络节点之间出现路由竞争。用nodeSelector+DaemonSet固定拓扑,是最稳妥的方式。
2.3 数据面的VXLAN与Underlay互联互通
OpenStack租户网络的VXLAN流量实际是从计算节点上的br-int、br-tun网桥出去的,这些网桥由OVS或OVN管理。在K8s化架构里,计算节点的ovn-controller是独立的systemd服务,不跑在Pod里。这样设计的好处是OpenStack数据面的故障边界清晰,不会因为K8s集群重启导致所有租户虚拟机同时断网。
计算节点和网络节点之间的VXLAN隧道,需要保证underlay的IP路由可达。我建议用独立的VLAN和网段承载VXLAN流量,和Kubernetes Pod网段、管理网段分开。如果条件允许,最好走独立物理网卡,避免VXLAN流量和存储流量争抢带宽。生产流量模型下,VXLAN封装后的流量放大效应明显,尤其在东西向流量大的场景,网卡队列和中断绑定也要提前优化。
实际配置时,计算节点上的/etc/sysconfig/network-scripts/ifcfg-eth2这类underlay网卡不要配IP,或者只配一个管理地址,真正的VXLAN隧道通过OVN数据库里的隧道信息自动建立。不要手动去添加VXLAN接口,否则一旦OVN数据库和本地配置不一致,隧道状态会非常难排查。
3. 生产部署实操:核心步骤与参数解析
3.1 部署前检查清单:内核、DNS、NTP一个都不能少
正式执行Helm部署之前,我给团队定了一张检查清单,每一条都是真金白银换来的:
- 内核版本统一为4.18以上(生产环境建议5.4 LTS),开启
br_netfilter和nf_conntrack相关模块,否则K8s的Service转发和OVN流表匹配会不完整。 - DNS解析必须走集群内部CoreDNS,各控制节点
/etc/resolv.conf不要指向外部DNS。OpenStack组件之间通过服务名通信,如果Pod内DNS解析到外部,很多服务启动时找不到数据库和消息队列,报错方式千奇百怪。 - NTP时间同步必须全局统一。OpenStack组件对时间偏移极其敏感,尤其Keystone的token,Nova的虚拟机创建流程,时间差超过30秒就会出现认证失败或任务卡死的诡异现象。K8s节点和Pod内都要验证时间同步,很多基础镜像不带chrony,需要entrypoint里注入。
- 检查K8s集群的
PodCIDR和OpenStack的租户VXLAN网段不能重叠。我在测试环境遇到过Pod网段用了10.0.0.0/16,租户VXLAN也配了同网段,结果虚拟机访问metadata时直接路由到了Pod里,排查了两天。 - 确认所有节点可以拉取镜像仓库。生产环境一般有内网Harbor,提前把OpenStack各组件镜像push进去,避免部署时外网抖动导致镜像拉取超时。
3.2 关键Helm Chart参数选型逻辑
我用的是OpenStack-Helm这套chart体系(社区里有直接可用的chart,也可以基于它改)。核心values参数选型直接决定生产稳定性,按优先级排序如下:
数据库和消息队列是高可用核心。MariaDB用Galera三副本,配置wsrep_cluster_address时不要把Pod IP写死,要用K8s的StatefulSet网络标识,比如mariadb-cluster-galera-0.mariadb-cluster-galera.mariadb.svc.cluster.local。RabbitMQ同样用三节点镜像模式,开启pause_minority,避免脑裂时整个集群不可写。这里必须注意,MariaDB和RabbitMQ的PVC一定用独立存储池,不要和控制面其他无状态应用的存储混在一起,防止I/O争抢。
Keystone的bootstrap流程要单独跑一次Job。首次部署时,Keystone数据库初始化需要创建admin用户、service项目、endpoint。用Helm hook的Job来做这件事,并且在Job里带上--wait,确保数据库完全就绪后再初始化。我遇到过初始化Job跑太快,连不上数据库而失败,后续重复执行时又因为部分表已存在而报错,最后只能清库重来。
Nova的placement、conductor、scheduler三个服务要分开部署。虽然可以合成一个Deployment,但生产环境建议拆开,方便单独扩缩容和排查。Scheduler的filter_scheduler要开启available_filters,加上AggregateInstanceExtraSpecsFilter和RetriesFilter,否则调度时会忽略计算节点的资源规格限制。
Neutron Server需要两个副本并开启--workers 8。Neutron Server是API和RPC的汇聚点,生产负载下很容易成为瓶颈,多worker能明显提升并发处理能力。同时Neutron Server的service_plugins里加上router和segments,否则租户网络无法创建子网和路由器。
3.3 滚动升级与优雅终止的配置要点
Kubernetes管理OpenStack的最大优势之一是滚动升级。但OpenStack组件升级不是简单的镜像tag替换,有几个配置必须同步调整。
Deployment的strategy要设置为RollingUpdate,maxUnavailable设为0,maxSurge设为1。这意味着升级时先起一个新Pod,等它通过readinessProbe后再终止旧Pod,保证API服务零中断。我见过默认配置下maxUnavailable为25%,结果升级过程中一半Keystone Pod被杀掉,剩余Pod扛不住流量直接雪崩。
优雅终止时间terminationGracePeriodSeconds建议设到60秒以上。OpenStack组件的worker进程收到SIGTERM后,需要完成正在处理的请求和数据库事务。我在生产环境把Nova API的预停等待时间设为90秒,实测能覆盖99%的在途请求。
ReadinessProbe的配置我不能不强调,它直接决定Pod是否被Service负载均衡。对Keystone这类API服务,探针应该请求/v3/auth/tokens(用HEAD或直接POST空body),不要只检查TCP端口通不通。只检查端口的话,进程还活着但内部状态已经异常时,流量照样会打进来。对Neutron Server,探针要请求/根路径并验证返回200;对Nova API,请求/或者/v2.1检查版本信息。
3.4 存储:Ceph接入与PVC设计
OpenStack侧计算和存储的配合,我选择Ceph作为Glance、Cinder、Nova的后端存储。控制面容器自身的PVC不用Ceph,而是用本地块存储或者专门的高性能存储池,避免控制面数据库的I/O和虚拟机镜像读写互相干扰。
Cinder接入Ceph时,cinder.conf里的enabled_backends设为ceph,glance_api_version设为2。rbd_user和rbd_pool要单独创建,不要用Ceph的admin账号直接跑Cinder服务,权限隔离是生产安全的基础。
Glance镜像存储建议用rbd后端而非文件系统。镜像动辄几十G,存本地磁盘扩容麻烦,Ceph可以无缝扩展。show_image_direct_url=True这个参数要开,这样Nova创建虚拟机时可以直接通过RBD克隆镜像,不需要先下载镜像再上传,节省大量时间。
Nova的libvirt部分配置里,images_type用rbd,images_rbd_pool指向Nova专用池,并开启hw_scsi_model=virtio-scsi、hw_disk_bus=scsi。生产环境下虚拟机的磁盘I/O性能和稳定性远好于ide模拟。
4. 高可用与故障域设计
4.1 控制节点故障域的物理与逻辑划分
高可用不是靠Kubernetes的Pod副本数堆出来的。K8s可以保证Pod重新调度,但如果所有Pod都跑在同一个物理交换机下面,交换机宕机时一切白搭。生产环境必须做故障域设计。
我采用的是三控制节点+两计算节点集群的最小生产配置,控制节点和计算节点分布在不同的机柜、不同的TOR交换机下。在Kubernetes侧给节点打标签,比如failure-domain.beta.kubernetes.io/zone=az1/az2/az3,然后给OpenStack各API服务配置podAntiAffinity,让同一服务的多个副本尽量分散到不同可用区。
OpenStack本身也有故障域概念。Nova的aggregate可以把计算节点按机柜分组,metadata里填availability_zone,用户创建虚拟机时可以指定AZ。底层K8s的拓扑分布和上层OpenStack的AZ要对应起来,这样某一组节点整体宕机时,影响范围能控制在一个AZ内,不会拖垮整个Region。
4.2 数据库与消息队列的高可用陷阱
MariaDB Galera和RabbitMQ镜像模式都有各自的坑,我在生产中踩过的几个问题值得记录。
Galera集群最大的隐患是全量同步。某节点长时间失联后,重新加入集群时会触发SST全量同步,数据量大的时候会把网络打满,甚至导致其他节点也出现延迟。解决办法是:为MariaDB Service单独划分存储和网络QoS,SST走备份网络;同时定期做逻辑备份,万一全库损毁时能快速重建。
RabbitMQ镜像队列有一个隐藏问题:镜像队列的master节点切换后,消费者需要重连。OpenStack各服务用的pika或kombu连接池如果实现不严谨,连接断掉后不会自动重连,表现为消息堆积但不消费。生产上我的建议是不要把消息队列也塞进K8s,虽然可用StatefulSet管理,但RabbitMQ的节点发现、cookie同步、erlang版本升级在容器里都更复杂。如果一定要容器化,务必开启automatic_recovery并设置合理的heartbeat。
4.3 三副本控制面故障恢复演练实录
生产环境我做过一次控制节点宕机的演练,记录下来的恢复过程可以作为你的参考。
故障模拟:把三台控制节点中的一台直接强制断电。预期是K8s自动将Pod迁移到其他节点,OpenStack API保持可用,但迁移时间受Pod重新调度+镜像拉取+服务初始化影响。
实际恢复过程:断电瞬间,Keystone和Neutron Server的Pod漂移到了剩余两台节点,由于这两个服务都有3副本,流量由另外两个副本正常承载,没有故障感知。MariaDB主节点在故障节点上时,Galera自动选出新主,应用侧有少量连接中断,但OpenStack各服务的连接池自动重连后恢复正常。Nova API的Pod调度到新节点后,镜像从本地Harbor拉取约1分钟,readinessProbe通过后开始接收流量。整体影响窗口在3-5分钟内,对应opensatck用户侧表现为虚拟机列表查询变慢,创建虚拟机失败几次后会重试成功。
这次演练暴露的问题是:我们当初给Keystone只配了2副本,故障后只剩1个可用Pod,虽然没挂,但QPS高的时候会很吃力。之后我把所有API服务的副本数调整为3,并给关键服务配置了PodDisruptionBudget,保证任何时刻至少2个副本可用。
5. 监控与排障:生产环境真正依赖的几条命令
5.1 Prometheus与自定义指标采集
Kubernetes自身的监控用Prometheus全家桶已经是标配,但OpenStack侧还有自己的监控诉求。除了Pod CPU、内存这些基础设施指标,还要采集Nova的nova_api请求延迟、Neutron的rpc调用耗时、Cinder的卷操作队列长度。
我在每个控制面Pod里额外挂了一个exporter边车容器,通过socket.io和/var/log/路径把OpenStack组件的日志转化为指标,再被Prometheus抓取。重点关注的告警规则我有几条建议:
- Keystone认证失败率:5分钟内失败次数占总请求数超过5%就告警,这通常意味着token签发异常或数据库连接耗尽。
- Nova conductor的RPC消费延迟:如果延迟时间持续大于5秒,说明数据库或消息队列出现瓶颈。
- Cinder卷创建队列长度:超过阈值说明存储后端IO遇到问题,通常和Ceph的osd相关。
5.2 日志采集与快速定位问题的思路
容器化部署最大的排障痛点就是日志分散在多个Pod中。生产环境需要统一的日志采集方案,我用的是Loki+Promtail,Promtail以DaemonSet方式部署,直接采集每个节点上/var/log/containers/下的日志,并通过Pod标签自动加上namespace、pod、container这些元数据。
快速定位问题有个固定的思路:先查Keystone的token是否正常,再查消息队列的队列堆积,最后查数据库连接数。OpenStack组件之间依赖链很长,日志里经常出现“connection refused”或“timeout”,如果直接去查目标服务,可能会被表象误导。我的习惯是先在Grafana Loki里跑一条PromQL,比如{namespace="openstack"} |= "ERROR" |~ "keystone|neutron",把整个控制面的错误日志按时间聚合,看高频波浪出现在哪个组件,再层层往下排查。
5.3 常见故障速查表
| 故障现象 | 可能原因 | 排查命令/手法 | 解决方案 |
|---|---|---|---|
| 虚拟机创建后网络不通 | MTU设置过大或VXLAN underlay异常 | 虚拟机内ping -M do -s 1400测试 | 重新下发租户网络MTU,检查物理交换机端口MTU |
| Keystone认证偶尔超时 | 数据库连接池满了 | show processlist;看连接数 | 调大MariaDB max_connections,检查连接泄漏 |
| Neutron agent状态为down | OVN与数据库同步异常 | ovn-nbctl show对比数据库记录 | 重启ovn-controller,强制重新同步 |
| Cinder卷删除卡在deleting | Ceph RBD镜像仍被占用 | rbd lock ls查看锁 | 手动释放锁或重启对应实例 |
| Pod频繁重启 | livenessProbe配置过严 | kubectl describe pod查看重启原因 | 调整探测阈值,先排除服务自身问题 |
速查表只是入门,真正的生产环境问题往往是多个环节叠加出来的。遇到故障时,先不要急着猜原因,先把日志和指标时间戳对齐,找到第一个异常点,再顺着依赖关系往下跟,比盲目重启Pod有效得多。
聊一点我在实战中的体会
这个架构跑了近两年,我对“OpenStack on Kubernetes”这件事的认知在持续修正。最开始我期望的是用K8s解决所有运维问题,后来发现容器化只解决了调度和自愈,OpenStack的复杂性并没有消失,只是转移到了配置、网络、存储这些更底层的地方。真正让系统稳定下来的,不是某一个精巧的编排技巧,而是一整套围绕可观测性、变更管理和故障演练的工程习惯。如果你正在规划这套架构,我建议不要一上来就追求所有组件容器化,先跑通Keystone+Glance+Nova+Neutron这四个核心服务,把网络和存储边界理清楚,再逐步扩展。最后分享一个日常运维小技巧:每次线上变更前,手动在测试环境把对应组件的PVC快照做一份,变更失败时可以快速回滚,这个习惯已经救了我好几次,希望你也能用上。