第一次被 cinder-scheduler 坑到,是在一个多存储后端的生产环境里。创建一块 2TB 的块存储卷,状态卡在creating半天不动,cinder-volume 那边却完全没动静。旁边同事瞟了一眼丢过来一句:“去翻 scheduler 日志。”翻到No valid host found那一行的瞬间,我才意识到自己一直没真正搞懂 OpenStack 的存储调度是怎么运转的。
这篇文章就当是那段时间的复盘笔记。先说清楚 cinder-scheduler 到底管哪一段事,再拆它的 Filter + Weigher 两阶段逻辑,然后聊怎么用 Volume Type 和 Extra Specs 控制卷的落盘位置,接着给出一套可以直接抄的生产配置,最后手写一个自定义过滤器和权重器,把排障经验也一并整理出来。这个系列走到第 48 篇,前面的文章已经把 Cinder 的架构、后端驱动、卷生命周期都过了,这篇正好卡在“卷建出来之后如何选择后端”这个关键点上。
1. 先弄明白 cinder-scheduler 管什么,再谈怎么调试它
1.1 没有调度器会怎样:多存储后端的现实场景
想象你面对的不是一台测试机,而是一套承载了多种业务的 OpenStack 云平台。后端挂着好几类存储:SAS 盘组成的本地 LVM 池、SSD 组成的高性能池、还有通过 FC 接进来的集中式存储阵列。业务方在申请卷的时候,可能明确要求“这块卷要放在高性能存储上”,也可能完全没概念,只求“能建出来”。
如果 Cinder 没有调度器,卷会创建到哪里?答案是:取决于enabled_backends里后端的顺序,或者干脆随机落在一个可用后端上。这在单后端环境里没问题,多后端的生产环境就乱了套——高性能卷可能落到慢速盘上,容量大户可能把某个池塞爆,而另一个池还在闲置。cinder-scheduler 存在的意义就是解决这件事:在收到创建卷的请求后,从所有启用后端中选出最合适的那一个,再让 cinder-volume 在那个后端上真正把卷建出来。
这里要说清楚一个容易被忽略的点:OpenStack 里有三个核心服务同时参与卷的创建。cinder-api负责接请求、校验参数;cinder-scheduler负责做选择,也就是“调度”;cinder-volume负责在选定的后端上执行实际创建动作。以前很多教程把目光都放在 cinder-volume 上,导致排障时只盯卷的驱动和存储池状态。实际上有相当大比例的“卷建不起来”问题,根源都在调度这一层。
1.2 调度器不是只在“创建卷”时工作
另一个常见误解是:cinder-scheduler 只在创建卷时才被调用。实际不止。卷迁移、卷跨后端复制、创建卷备份、从镜像创建卷、甚至在某些版本里创建一致性组时,都会触发调度流程。理解这一点对运维有帮助——你遇到“迁移走了半天没完成”的故障时,也要去查 scheduler 日志,而不只是看 volume 服务日志。
调度器本身是一个独立进程,通过消息队列消费请求。你在控制台上看到卷状态从creating变成available,中间经历了“API 接收请求 -> 调度器做过滤和权重计算 -> 选定 host -> 通知对应 cinder-volume -> 后端驱动执行创建”这条链路。链路里任何一环出问题,卷状态都会卡住。
2. Filter + Weigher 两阶段:调度逻辑的核心骨架
2.1 过滤器是“海选”:先把不合格的后端踢出局
cinder-scheduler 的调度逻辑核心是两阶段:先过滤(Filter),再加权(Weigher)。为什么这样设计?因为过滤和加权的目标不同。
过滤阶段是“一票否决制”。每个过滤器都会对每个候选后端做出通过或淘汰的判断,所有过滤器都通过了,这个后端才进入下一轮;任何一个过滤器不通过,直接出局。这个设计保证了一些硬性规则不会被权重计算覆盖,比如:容量不足的后端即使性能再好、得分再高,也不能选它,否则卷肯定建失败。
常用的内置过滤器有这几个,我按日常使用频率排个序:
| 过滤器 | 作用 | 判断依据 |
|---|---|---|
| CapacityFilter | 检查后端剩余容量是否足够 | 后端上报的free_capacity_gb、total_capacity_gb |
| CapabilitiesFilter | 匹配 Volume Type 的 Extra Specs 与后端能力 | 后端的 capability 字典 |
| AvailabilityZoneFilter | 检查后端所属可用域是否与请求匹配 | cinder-volume 服务的 availability_zone |
| DriverFilter | 围绕后端驱动自身的运行状态做基础检查 | 驱动上报能力、是否处于只读/异常状态 |
| JSONFilter | 支持自定义 JSON 形式的过滤规则 | 写在 Extra Specs 里的过滤表达式 |
每个过滤器都是一个 Python 类,核心方法接收host_state(后端的状态信息)和filter_properties(请求参数),返回 True 或 False。这个设计非常方便扩展,我后面会写一个自定义过滤器给大家参考。
2.2 权重器是“评分”:在海选结果里挑最优后端
经过过滤之后剩下的后端,理论上都能满足创建卷的条件,但它们之间仍然有优劣之分。权重阶段就是要给这些“合格选手”打分排序,得分最高的后端被选中。
和过滤器最大的区别是,权重器是“非一票否决”的,它产出的是数值,通过对比打分,让调度器选出一个综合最优解。最常见的默认权重器是 AllocatedCapacityWeigher(老版本叫 CapacityWeigher),它的逻辑很直观:同样是能放下这块卷的两个后端,一个剩余 100GB,另一个剩余 500GB,默认情况下调度器会把新卷放到剩余空间更多的那个上面。目的是什么?是让卷尽量均匀地分布在各存储池之间,避免出现“一个池满了另一个池闲着”的尴尬。
不同版本里权重器的名字会有差异,但核心逻辑都一样。如果在多可用域的复杂环境里,还可以通过调整权重器的 multiplier 和 offset 来改变排序策略。比如给某个后端设置更高的权重偏移,就能让调度器“偏心”它一点。需要知道的是,权重计算不是简单的“剩余容量大就赢”,它背后有归一化公式,但运维层面通常不需要深究公式,重点关注配置项里scheduler_default_weighers写谁就够。
2.3 为什么 OpenStack 坚持两阶段而不是一口气搞定
我最早看这块代码的时候也有个疑问:过滤和权重明明可以在一个阶段里用条件判断实现,为什么非拆成两层?
拆开的优势在于解耦。过滤器的职责是维护“硬性边界”,比如容量够不够、可用域对不对;权重器的职责是优化“软性偏好”,比如容量均衡、指定后端优先。硬性边界不能因为分数高就被突破,软性偏好不该承担“否决”的功能。这种设计让运维可以根据业务需求自由组合过滤器,比如我可以在默认过滤器后面追加一个自定义的“只允许 SSD 后端参加评选”的过滤器,而不影响原本的容量均衡逻辑。
另外,两阶段也方便做审计。日志里会分别记录哪个过滤器淘汰了哪个后端,以及权重计算的结果,排障的时候信息量比“一步到位”的方案大得多。
3. 让卷落到指定后端:Volume Type 和 Extra Specs 的绑定玩法
3.1 为什么官方一直强调用 Volume Type 管理后端
在只有一两个后端的测试环境里,你可能觉得调度器无所谓,所有后端都能建卷就好。但在生产环境里,用 Volume Type 区分后端不是建议,而是基本要求。
Volume Type 的作用有点像“套餐规格”。它可以定义性能等级(比如“高性能 SSD”)、可用性等级(比如“双副本”)、或者直接指定后端名称(比如“这个类型只允许建在后端 A 上”)。业务方在申请卷的时候带上 Volume Type 参数,cinder-scheduler 的 CapabilitiesFilter 就会把“这个类型允许的 Extra Specs”和“后端上报的能力”做比对,把不符合的后端全部剔除。
这样做的最大好处是,业务方完全不需要知道平台上到底挂了哪些存储,也不需要关心哪个后端叫什么名字,只要告诉 OpenStack“我要一块高性能卷”,平台就自动帮你匹配到 SSD 后端上。对运维来说也省心,新挂载一个存储后端时,只要它上报的 capability 能匹配已有 Volume Type,新后端就自动进入候选池。
3.2 Extra Specs 是如何变成调度条件的
理解 Extra Specs 之前,先要明白后端是如何“上报”自己的。每个后端驱动在启动时或定期会上报一组 capabilities,包括容量、池名、驱动支持的特性等等。这些数据保存在 scheduler 侧,通过cinder get-pools --detail或openstack volume service list可以看到。
Extra Specs 就是 Volume Type 上挂的一组键值对。常见的调度相关键有两个方向:一个是volume_backend_name,这直接对应后端配置里的volume_backend_name,属于“点名式”匹配;另一个是capabilities:xxx前缀,比如capabilities:disk_type=ssd,这会被转换为对后端自定义 capability 的匹配条件。
还有个细节:如果你在 Volume Type 上挂了volume_backend_name=ssd,调度器就会只选择volume_backend_name恰好等于ssd的后端。注意这里是“恰好”,大小写也要一致。我见过一个案例,类型里写的是SSD,而后端配置里写的是ssd,结果所有卷全部调度失败。这种问题在 get-pools 的输出里一眼就能看出来。
3.3 实操:定义 Volume Type 并验证调度效果
下面给出一套可落地的操作流程。先在控制节点或者装有 cinder 客户端的环境里执行。假设我的平台上有两个后端:一个叫sata-pool,一个叫ssd-pool。
第一步,创建两个 Volume Type:
openstack volume type create sata openstack volume type create ssd第二步,给 Volume Type 绑定后端名称,这是让调度器“认路”的关键:
openstack volume type set --property volume_backend_name=sata-pool sata openstack volume type set --property volume_backend_name=ssd-pool ssd第三步,验证绑定结果:
openstack volume type show ssd这时能看到 ssd 这个类型下面有volume_backend_name属性。接着用两个类型分别创建卷,创建完成后用openstack volume show查看卷的os-vol-host-attr:host字段,它标注了卷实际落在哪个后端上。
如果你发现指定了类型后卷仍然调度失败,第一件事就是看后端的可用性:
openstack volume service list cinder get-pools --detail正常情况下,输出里能看到每个后端上报的volume_backend_name、total_capacity_gb、free_capacity_gb、还有各种 capability。拿这个输出和 Volume Type 的 Extra Specs 一比对,问题基本就浮出水面了。
4. 改配置就能调优:scheduler 参数和生产配置示例
4.1 三个最重要的配置项
调度器相关的配置集中在/etc/cinder/cinder.conf的[DEFAULT]段下。核心项有四个:
scheduler_driver:指定使用哪个调度驱动。默认值是cinder.scheduler.filter_scheduler.FilterScheduler,绝大多数场景不需要换。scheduler_default_filters:设置启用哪些过滤器,按顺序执行,逗号分隔。scheduler_default_weighers:设置启用哪些权重器。scheduler_available_filters:设置可用的过滤器类路径,配合自定义过滤器使用。
这里面最容易踩坑的是过滤器列表的顺序。比如你既做了容量过滤又做了自定义磁盘类型过滤,顺序会影响最终结果吗?多数情况下不影响,因为过滤是“AND”关系,不管先执行哪个,最终都必须全部通过。但也有例外,像 CityCache 这类依赖缓存数据的过滤器,如果放在了缓存尚未初始化的过滤器之前可能导致误判。我个人的习惯是,把成本最低、最容易判断的过滤器放前面,比如可用域过滤和容量过滤,这样可以在早期就快速淘汰一批不相关的后端,减轻后续过滤器和权重器的计算压力。
4.2 一个可复制的多后端配置模板
下面这个是实际多后端环境里我会用的配置模板,做了脱敏处理。重点不是参数有多炫,而是结构合理、易于排障。
[DEFAULT] enabled_backends = sata-pool:ssd-pool,xfs-pool scheduler_driver = cinder.scheduler.filter_scheduler.FilterScheduler scheduler_default_filters = AvailabilityZoneFilter, CapacityFilter, CapabilitiesFilter scheduler_default_weighers = AllocatedCapacityWeigher [sata-pool] volume_backend_name = sata-pool volume_driver = cinder.volume.drivers.lvm.LVMVolumeDriver volume_group = cinder-volumes-sata volume_type = sata [ssd-pool] volume_backend_name = ssd-pool volume_driver = cinder.volume.drivers.lvm.LVMVolumeDriver volume_group = cinder-volumes-ssd volume_type = ssd [xfs-pool] volume_backend_name = xfs-pool volume_driver = cinder.volume.drivers.lvm.LVMVolumeDriver volume_group = cinder-volumes-xfs volume_type = xfs在这里我用了三个后端,每个后端都指定了对应的volume_type。这样一来,当用户创建卷时若指定了类型,调度器会严格按类型选择池;如果用户没指定类型,调度器就会在所有启用后端中进行过滤和权重排序,由 AllocatedCapacityWeigher 决定落盘位置。
顺带提醒,enabled_backends的写法不同版本略有差异,冒号和逗号混用可能导致后端无法正常加载。如果不确定,先只配一个后端测试,再逐步增加。改完配置后记得重启 cinder-scheduler 和 cinder-volume 服务:
sudo systemctl restart cinder-scheduler cinder-volume重启后第一时间用openstack volume service list确认后端全部处于 enabled 状态。
4.3 权重参数调整的实用思路
权重器的行为可以通过weight_multiplier和weight_offset微调。举个例子,默认的 AllocatedCapacityWeigher 会尽量把卷散到容量较多的池里。如果业务上希望优先使用某个高性能池,可以把该池对应的后端权重提高,而不是在过滤器里硬性指定。硬性指定会让低优先级池彻底失去使用机会,一旦高优先级池容量不够,卷就建不出来;软性偏好则保留了一定的容错空间。
在实际操作中,先跑一阵子默认配置,观察各池的容量使用曲线,再根据“哪个池消耗过快”来微调权重,比一开始就拍脑袋定参数靠谱得多。权重调优属于渐进式优化,别指望一步到位。
5. 扩展调度器:手写过滤器和权重器的实战代码
5.1 写一个只匹配指定磁盘类型的自定义过滤器
内置过滤器不能覆盖所有需求,比如我想根据后端上报的自定义能力disk_type来过滤,内置的 CapabilitiesFilter 只能匹配 Extra Specs 里的条件,不能直接表达“后端磁盘必须是 SSD”这种语义。这时候就需要自定义过滤器。
下面这段代码非常简单,但结构完整。在控制节点上新建一个 Python 文件,比如/etc/cinder/my_filters.py:
from cinder.scheduler import filters class DiskTypeFilter(filters.BaseFilter): """只调度到磁盘类型为 SSD 的后端""" def host_passes(self, host_state, filter_properties): capabilities = getattr(host_state, 'capabilities', None) if not capabilities: return False disk_type = capabilities.get('disk_type') return disk_type == 'SSD'这段代码的意图很直白:读取后端上报的 capabilities 字典,检查disk_type是否为SSD。不是就返回 False,直接淘汰。注意,后端上报的自定义能力需要在驱动里自行定义,比如 LVM 驱动会将配置里自定义的volume_capacity_disk_type之类键值上报上来。如果你的后端驱动没有上报disk_type,这个过滤器会永远返回 False,这也是排障时要注意的点。
配置里启用这个过滤器,有两个办法。第一种,直接在配置里指定可用的过滤器类路径:
[DEFAULT] scheduler_available_filters = my_filters.DiskTypeFilter scheduler_default_filters = AvailabilityZoneFilter, CapacityFilter, CapabilitiesFilter, DiskTypeFilter第二种,通过 entry_points 注册,这种方式适合把过滤器打包成独立 Python 包时使用。在包名对应的 setup.cfg 里加上:
[entry_points] cinder.scheduler.filters = disk_type = my_filters:DiskTypeFilter5.2 让权重器优先选择指定后端
过滤负责“能不能选”,权重负责“更倾向于谁”。如果想要的效果是“SSD 优先但不是唯一”,可以写一个权重器,而不是过滤器。
先看代码:
from cinder.scheduler import weights class PreferSSDWeigher(weights.BaseWeigher): def _weigh_object(self, host_state, weight_properties): capabilities = getattr(host_state, 'capabilities', None) if capabilities and capabilities.get('disk_type') == 'SSD': return 1 return 0这个权重器对所有后端打分:SSD 后端得 1 分,其他后端得 0 分。配合原有的容量权重器共同使用,排序时会优先选择 SSD 后端,但假如 SSD 后端被 CapacityFilter 淘汰了,其他后端依然能顶上,不会出现“一个后端都选不出来”的情况。
配置方式也类似:
[DEFAULT] scheduler_default_weighers = AllocatedCapacityWeigher, PreferSSDWeigher这里有个实践细节:自定义权重器和默认权重器同时使用时,各个权重器产生的分数会按一定规则合并。不同权重器的分数量级不同,可能导致其中一个权重器彻底主导排序。实际配置时,最好先只保留一个自定义权重器跑一段时间,观察卷的分布是否符合预期,再加第二个进去。别嫌慢,调度策略的调整就是靠实测说话。
5.3 开发自定制功能时绕不开的坑
自定义过滤器看起来简单,但有几个坑躲不掉。第一个是类路径加载问题。配置里写了scheduler_available_filters之后,重启 cinder-scheduler 时会立即加载你的类,任何语法错误或导入错误都会让调度器卡死。建议先在 Python 交互式环境里手动导入测试一遍,确认无误后再改配置重启。
第二个是后端 capability 的可见性。过滤器读的host_state.capabilities来自后端起上报的数据,但如果某个后端从未成功上报过数据,这个属性可能是空的。很多自定义过滤器在这个场景下直接 False,导致全部后端被淘汰。调试时可以临时加一行日志输出 capabilities 的内容,确认数据长什么样。
第三个是版本兼容。不同 OpenStack 版本里BaseFilter.host_passes的签名基本稳定,但权重器内部类名和方法位置可能有变化。跨版本升级时务必对照官方 Release Notes 检查自定义代码,别拿旧代码直接凑合。
6. 卷一直 creating?cinder-scheduler 排查实录
6.1 日志追查路径和关键日志行
卷卡在creating不动,这是 OpenStack 运维里出现频率极高的故障。很多人第一反应是去查 cinder-volume 日志,但更高效的做法是两边日志同时翻:/var/log/cinder/cinder-scheduler.log和/var/log/cinder/cinder-volume.log。
调度器日志里,我最关注的是这几个关键行:
Filtering out ...:某个过滤器淘汰了某个后端。No valid host found:所有后端都被淘汰,没有可用后端,这是卷无法创建的直接原因。Weighed Host ...:权重计算后选出的后端列表,排第一的就是最终选中的 host。
举个例子,一个比较典型的日志片段是:
Filtering out host xxxx due to Filters: ['CapabilitiesFilter'] No valid host found看到CapabilitiesFilter淘汰了全部后端,就知道问题出在 Volume Type 的 Extra Specs 和后端上报的 capability 不匹配,后面的排查方向就明确了。
6.2 常见调度失败原因速查表
我把这些年遇到过的调度失败原因整理成一个速查表,排障时可以直接对照:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| No valid host found,日志里所有后端被 CapabilitiesFilter 淘汰 | Extra Specs 与后端 capability 不匹配 | 对比 get-pools 输出与 Volume Type 属性 |
| No valid host found,CapacityFilter 淘汰所有后端 | 后端容量上报为 unknown,或超额配置比设置不合理 | 检查 get-pools 里的容量字段,调整 max_over_subscription_ratio |
| 卷一直 creating,scheduler 日志无异常 | cinder-volume 后端驱动创建失败 | 翻 cinder-volume 日志,检查存储连接 |
| 指定可用域创建失败 | 可用域内没有可用的后端服务 | 对比 service list 里的 availability_zone |
| 类型创建卷失败,换默认类型却能成功 | 类型绑定的后端名称写错或后端未启用 | 检查 Volume Type 属性拼写,检查 enabled_backends |
这份表不是教科书,是我在实操里一条条填出来的经验汇总。遇到新问题就往里补一行,时间久了就是自己的故障手册。
6.3 一次“容量充足却调度失败”的完整复盘
说一个我印象深刻的案例。那是给业务方创建一块 500GB 的卷,指定了ssd类型,后端 SSD 池的剩余空间有 3TB,怎么看都够用,但卷就是建不出来。
翻开 scheduler 日志,发现淘汰行指向 CapabilitiesFilter,但后端明明正常工作。我用cinder get-pools --detail一看,SSD 池上报的volume_backend_name是ssd-pool,而 Volume Type 里配的volume_backend_name=ssd,两边差了一个-pool。
原因是之前另一个同事配置类型时图省事,写成了ssd,跟后端配置里的实际名称对不上。造成的后果是:所有指定这个类型的卷全部调度失败。这种问题不好查,因为容量充足、后端在线、日志也不会报致命错误,只有逐行对比时才能发现。从那以后我的习惯是,Volume Type 里的volume_backend_name永远从后端配置里复制粘贴,绝不手打。
另外还想提醒一件事:改完 Volume Type 属性后,不需要重启任何服务,但后端上报的 capabilities 可能不是实时的,存在一个上报周期。刚改完配置立刻查 get-pools,看到的可能还是旧数据。别急着一遍遍重启 cinder-volume,稍微等几十秒再查询,你会看到数据刷新。我在这个细节上浪费过不少时间。
6.4 一个最实用的排障习惯
最后分享一个我踩过几次坑之后的习惯:任何卷建不起来,不要只盯 cinder-volume,先看 scheduler 日志。日志会直接告诉你哪个后端被哪个过滤器淘汰了,以及最终有没有选到宿主。很多人上来就去看存储池状态、看网络连通性,折腾半天,其实问题就出在类型属性写错这种小地方。
拿到No valid host found的时候,先别慌着调配置,先回答三个问题:后端服务是不是都正常?后端的容量和 capability 是什么?要求的 Volume Type 属性是什么?答出这三件事,九成以上的调度问题都能定位。
我个人在实际操作中体会最深的一点是:cinder-scheduler 的调度逻辑本身不难理解,它就是一个“先淘汰,再打分”的选人流程。真正难的是让你环境里所有的后端、类型、上报数据保持一致。OpenStack 的设计给了你很强的灵活性,你能写过滤器、改权重、调参数,但这意味着你维护的信息更多、更容易出错。所以生产环境的准则应该是:能少配就少配,能用类型约束就用类型约束,日志里每一条过滤记录都值得认真读一遍。