特性标志
概述
在混合版本集群(例如,某些版本为 3.11.x,而某些版本为 3.12.x)中进行升级期间,某些节点将支持不同的功能集,在某些情况下行为不同,并且其他方面不会完全相同:毕竟它们是不同的版本。
特性标志是一种机制,用于控制在所有集群节点上启用或可用的功能。如果启用了特性标志,则其关联的功能(或行为)也将启用。如果不是,则集群中的所有节点都将禁用该功能(行为)。
特性标志子系统允许具有不同版本的 RabbitMQ 节点确定它们是否兼容,然后一起通信,尽管具有不同的版本,因此可能具有不同的功能集或实现细节。
引入此子系统是为了允许 **集群成员的滚动升级,而无需关闭整个集群**。
特性标志不应被用作集群配置的形式。成功完成滚动升级后,用户应启用所有特性标志。
所有特性标志在某些时候都会变为强制性(毕业)。例如,RabbitMQ 3.12 需要在升级之前启用 3.11 系列中引入的特性标志,RabbitMQ 3.11 使所有 3.8 标志毕业,依此类推。
快速总结(TL;DR)
特性标志基本规则
- 只有当集群中的所有节点都支持某个特性标志时,才能启用它。
- 节点只有在以下情况下才能加入或重新加入集群:
- 它支持集群中启用的所有特性标志,并且
- 如果每个其他集群成员都支持该节点上启用的所有特性标志。
- 启用后,无法禁用特性标志。
例如,只要没有启用 3.13.x 特定的特性标志,RabbitMQ 3.13.x 和 3.12.x 节点就是兼容的。
关键 CLI 工具命令
- 列出特性标志
rabbitmqctl list_feature_flags
- 启用特性标志(或所有当前禁用的标志)
rabbitmqctl enable_feature_flag <all | name>
也可以从 管理插件 UI 中的“管理 > 特性标志”列出和启用特性标志。
示例
示例 1:兼容节点
- 如果节点 A 和 B 未集群,则可以将它们集群。
- 如果节点 A 和 B 已集群
- 可以启用“咖啡机”。
- 无法启用“榨汁机”,因为它不受节点 B 支持。
示例 2:不兼容节点
- 如果节点 A 和 B 未集群,则无法将它们集群,因为节点 B 不支持“榨汁机”。
- 如果节点 A 和 B 已集群并且在节点 B 停止时启用了“榨汁机”,则节点 B 在重新启动时无法重新加入集群。
特性标志和 RabbitMQ 版本
如前所述,特性标志子系统的主要目标是在最大程度上允许升级,而不管集群成员的版本如何。
特性标志使得可以安全地执行对下一个补丁或次要版本的滚动升级,除非发行说明中另有说明。实际上,某些更改无法作为特性标志实现。
但是,请注意,仅支持从一个次要版本升级到下一个次要版本或主要版本。要从例如 3.9.16 升级到 3.12.3,必须先升级到 3.9.29,然后升级到最新的 3.10 补丁版本,然后升级到最新的 3.11 版本,然后升级到 3.12.3。在升级过程中的某些步骤之后,还需要启用该版本中可用的所有稳定特性标志。例如,3.12.0 是一个版本,它要求在节点可以升级到它之前启用所有特性标志。
同样,如果在使用的次要版本和下一个主要版本之间存在一个或多个次要版本分支。这可能有效(即,主要版本之间可能没有不兼容的更改),但出于以下原因,此场景在设计上不受支持
- 跳过次要版本在 CI 中未经测试。
- 非顺序版本可能支持或不支持相同的功能标志集。几个次要分支中存在的特性标志可以标记为必需,并且其关联的功能/行为现在默认情况下隐式启用。在此过程中删除了兼容性代码,从而阻止了与旧节点的集群。请记住,它们的目的在于允许升级,它们不是配置机制。
没有定义一般特性标志生命周期的策略。例如,不能保证特性标志在 N 个次要版本后会从“稳定”变为“必需”。由于新代码建立在现有代码之上,因此只要需要,特性标志就会被标记为必需,并且兼容性代码会被删除。
如何列出支持的特性标志
节点首次启动时,默认情况下会启用所有稳定特性标志。将节点升级到更新版本的 RabbitMQ 后,新特性标志将保持禁用状态。
**要列出特性标志**,请使用 rabbitmqctl list_feature_flags
rabbitmqctl list_feature_flags
# => Listing feature flags ...
# => name state
# => empty_basic_get_metric enabled
# => implicit_default_bindings enabled
# => quorum_queue enabled
为了提高表格的可读性,请切换到 pretty_table
格式化程序
rabbitmqctl -q --formatter pretty_table list_feature_flags \
name state provided_by desc doc_url
这将生成如下所示的表格
┌───────────────────────────┬─────────┬───────────────────────────┬───────┬────────────┐
│ name │ state │ provided_by │ desc │ doc_url │
├───────────────────────────┼─────────┼───────────────────────────┼───────┼────────────┤
│ empty_basic_get_metric │ enabled │ rabbitmq_management_agent │ (...) │ │
├───────────────────────────┼─────────┼───────────────────────────┼───────┼────────────┤
│ implicit_default_bindings │ enabled │ rabbit │ (...) │ │
├───────────────────────────┼─────────┼───────────────────────────┼───────┼────────────┤
│ quorum_queue │ enabled │ rabbit │ (...) │ https://... │
└───────────────────────────┴─────────┴───────────────────────────┴───────┴────────────┘
如上例所示,list_feature_flags
命令接受要显示的列列表。可用的列为
name
:特性标志的名称。state
:如果特性标志已启用或禁用,则为 enabled 或 disabled,如果集群中的一个或多个节点不知道此特性标志(因此无法启用),则为 unsupported。provided_by
:提供特性标志的 RabbitMQ 组件或插件。desc
:特性标志的描述。doc_url
:了解有关特性标志的更多信息的网页的 URL。stability
:指示特性标志是 required、stable 还是 experimental。
如何启用特性标志
升级一个节点或整个集群后,就可以启用新的特性标志。请注意,一旦启用了新的特性标志,就无法回滚版本或使用旧版本添加集群成员。
**要启用特性标志**,请使用
rabbitmqctl enable_feature_flag <name>
**要启用所有稳定特性标志**,请使用
rabbitmqctl enable_feature_flag all
rabbitmqctl enable_feature_flag all
命令仅启用稳定特性标志,而不启用实验性特性标志。
可以再次使用 list_feature_flags
命令来验证特性标志的状态。假设最初所有特性标志都已禁用,以下是启用 quorum_queue
特性标志后的状态
rabbitmqctl -q --formatter pretty_table list_feature_flags
┌───────────────────────────┬──────────┐
│ name │ state │
├───────────────────────────┼──────────┤
│ empty_basic_get_metric │ disabled │
├───────────────────────────┼──────────┤
│ implicit_default_bindings │ disabled │
├───────────────────────────┼──────────┤
│ quorum_queue │ enabled │
└───────────────────────────┴──────────┘
也可以从 管理插件 UI 中的“管理 > 特性标志”列出和启用特性标志。
如何禁用特性标志
一旦启用,就 **无法禁用特性标志**。
如何在初始启动时覆盖要启用的特性标志列表
默认情况下,新的和未集群的节点将在启动时启用所有稳定特性标志,但此设置可以被覆盖。**由于已启用的特性标志无法禁用,因此仅对第一个节点引导覆盖已启用特性标志列表是安全的操作。**
此机制仅用于允许用户使用比集群其余部分更新版本的 RabbitMQ 扩展现有集群。仍然会验证与新节点的兼容性,如果它不兼容,则添加它到集群中仍可能失败。
有两种方法可以做到这一点
-
使用
RABBITMQ_FEATURE_FLAGS
环境变量# this is just an example, in practice this list will be much longer
RABBITMQ_FEATURE_FLAGS=quorum_queue,implicit_default_bindings -
在
advanced.config
中使用forced_feature_flags_on_init
设置%% ...
{rabbit, [
%% this is just an example, in practice this list will be much longer
{forced_feature_flags_on_init, [quorum_queue, implicit_default_bindings]}
]},
%% ...
环境变量优先于配置参数。
显然,无论如何,必需的特性标志都将始终启用。
特性标志成熟和毕业流程
特性标志在首次引入 RabbitMQ 后是可选的,也就是说,它们仅用于安全地进行滚动集群升级。
然而,随着时间的推移,功能变得越来越成熟,RabbitMQ 的未来开发假设某些功能集可用,并且用户和开发人员都可以依赖这些功能。当这种情况发生时,特性标志将在下一个次要功能版本中升级为核心(必需)功能。
在执行滚动集群升级后启用所有特性标志非常重要:将来这些标志将变为强制性,并且提前启用它们将有助于将来获得更流畅的升级体验。
特性标志列表
下面列出的特性标志由 RabbitMQ 核心或与 RabbitMQ 捆绑在一起的某个第一层插件提供。
列Required
显示 RabbitMQ 版本之前特性标志必须启用的版本。例如,如果某个特性标志在 3.12.0 中是必需的,则必须在升级到 3.12.x 之前在 3.11.x(或更早版本)中启用此特性标志。否则,如果在禁用此特性标志的情况下将 RabbitMQ 节点升级到 3.12.x,则 RabbitMQ 节点将拒绝在 3.12.x 中启动。
列Stable
显示引入特性标志的 RabbitMQ 版本。例如,如果某个特性标志在 3.11.0 中是稳定的,则在将 RabbitMQ 集群中的所有节点升级到 3.11.x 版本后,应立即启用该特性标志。
核心特性标志
以下特性标志由 RabbitMQ 核心提供。
下面列出的大多数特性标志都只有非常简短的描述。这是因为大多数特性标志仅仅是为了避免在混合版本集群中进行可能不安全的操作,纠正必须在所有集群节点之间保持一致的行为等等。
khepri_db
是一个例外,因为它与其范围有关。
必需 | 稳定 | 特性标志名称 | 描述 |
---|---|---|---|
4.0 | rabbitmq_4.0.0 | 启用 RabbitMQ 4.0 中引入的多个功能和更改。 RabbitMQ 4.0 使用单个标志来控制多个功能和更改。如果升级到 RabbitMQ 4.0,它将以向后兼容模式运行,直到启用此特性标志。例如,新的仲裁队列功能和新的 AMQP-1.0 流量控制机制将不可用。 | |
4.0 | khepri_db | 启用Khepri,一个基于 Raft 的模式数据存储,与 Mnesia 相比,具有大大优越(即更可预测)的节点和网络故障恢复特性。 信息 从 RabbitMQ 4.0 开始,Khepri 得到完全支持(就像 Mnesia 一样)。由于其范围,必须显式启用(选择加入)此特性标志。 重要 由于 RabbitMQ 4.0 中 Khepri 模式发生了广泛的变化,因此启用了 Khepri 的 3.13.x 集群将无法就地升级到 4.0。此类集群应使用蓝绿部署升级策略。 在生产环境中采用 Khepri 之前,请确保首先在非生产环境中使用适当的工作负载对其进行测试。 | |
4.0 | rabbit_exchange_type_local_random | 由本地随机交换使用。 | |
3.13.1 | quorum_queue_non_voters | 支持非投票仲裁队列副本状态 | |
3.13.0 | message_containers | 启用内部使用的新基于 AMQP 1.0 的消息格式 | |
3.13.0 | detailed_queues_endpoint | 引入了 | |
4.0.1 | 3.13.0 | stream_filtering | 流过滤支持 |
4.0.1 | 3.13.0 | stream_update_config_command | 从可以针对流动态更新的策略键列表中删除 |
4.0.1 | 3.12.0 | restart_streams | 支持重新启动流,并带有可选的首选下一个领导者参数。用于实现流领导者再平衡 |
4.0.1 | 3.12.0 | stream_sac_coordinator_unblock_group | 错误修复,以解除超级流分区中一组消费者的阻塞 |
3.12.0 | 3.11.0 | direct_exchange_routing_v2 | v2 直接交换路由实现 |
3.12.0 | 3.11.0 | feature_flags_v2 | 特性标志子系统 v2 |
3.12.0 | 3.11.0 | listener_records_in_ets | 将侦听器记录存储在 ETS 中而不是 Mnesia 中 |
3.12.0 | 3.11.0 | stream_single_active_consumer | 流的单个活动消费者 |
3.12.0 | 3.11.0 | tracking_records_in_ets | 将跟踪记录存储在 ETS 中而不是 Mnesia 中 |
3.12.0 | 3.10.9 | classic_queue_type_delivery_support | 错误修复,用于使用混合版本的经典队列传递 |
3.12.0 | 3.9.0 | stream_queue | 支持类型为流的队列 |
3.11.0 | 3.8.10 | user_limits | 配置用户的连接和通道限制 |
3.11.0 | 3.8.8 | maintenance_mode_status | 维护模式状态 |
3.11.0 | 3.8.0 | implicit_default_bindings | 默认绑定现在是隐式的,而不是存储在数据库中 |
3.11.0 | 3.8.0 | quorum_queue | 支持类型为仲裁的队列 |
3.11.0 | 3.8.0 | virtual_host_metadata | 虚拟主机元数据(描述、标签等) |
rabbitmq_management_agent 特性标志
以下特性标志由插件rabbimq_management_agent提供。
必需 | 稳定 | 特性标志名称 | 描述 |
---|---|---|---|
3.12.0 | 3.8.10 | drop_unroutable_metric | 统计不可路由发布被丢弃的次数 |
3.12.0 | 3.8.10 | empty_basic_get_metric | 统计空队列上 AMQP basic.get 的次数 |
rabbitmq_rabbitmq_mqtt 特性标志
以下特性标志由插件rabbimq_mqtt提供。
必需 | 稳定 | 特性标志名称 | 描述 |
---|---|---|---|
3.13.0 | mqtt_v5 | 支持MQTT 5.0 | |
3.12.0 | delete_ra_cluster_mqtt_node | 删除 Ra 集群 mqtt_node,因为 MQTT 客户端 ID 是在本地跟踪的 | |
3.12.0 | rabbit_mqtt_qos0_queue | 支持MQTT QoS 0 订阅者的伪队列类型,省略队列进程 |
特性标志的工作原理?
从操作员的角度来看
节点和版本兼容性
操作员需要考虑特性标志的情况有两种。
节点将其自己的特性标志列表与远程节点的特性标志列表进行比较,以确定它是否可以加入集群。规则定义为
- 本地启用的所有特性标志都必须在远程得到支持。
- 远程启用的所有特性标志都必须在本地得到支持。
了解启用和支持之间的区别非常重要
- 支持的特性标志是指节点已知的特性标志。它可以启用或禁用,但此时其状态无关紧要。
- 启用的特性标志是指节点激活并使用的特性标志。根据以上定义,它隐式地是一个支持的特性标志。
如果这两个条件之一未得到验证,则节点无法加入或重新加入集群。
但是,如果它可以加入集群,则启用的特性标志的状态将在节点之间同步:如果某个特性标志在一个节点上启用,则在所有其他节点上启用。
特性标志的范围
特性标志子系统仅涵盖节点间通信。这意味着以下场景未涵盖,并且可能无法按预期工作。
在远程节点上使用rabbitmqctl
仅当远程节点运行与rabbitmqctl
相同的 RabbitMQ 版本时,才支持使用rabbitmqctl
控制远程节点。
如果在远程节点上使用来自不同 RabbitMQ 次要/主要版本的CLI 工具,则它们可能无法按预期工作,甚至对节点产生意外的副作用。
将请求负载均衡到 HTTP API
如果发送到管理插件公开的 HTTP API 的请求通过负载均衡器(包括管理插件 UI 中的负载均衡器),则 API 的行为及其响应可能会有所不同,具体取决于处理该请求的节点的版本。如果 HTTP API 的域名解析为多个 IP 地址,则情况完全相同。
如果管理 UI 在浏览器中打开并定期自动刷新,则在滚动升级期间可能会发生这种情况。
例如,如果管理 UI 从 RabbitMQ 3.11.x 节点加载,但随后查询 RabbitMQ 3.12.x 节点,则浏览器中运行的 JavaScript 代码可能会因 HTTP API 更改而导致异常失败。
启用特性标志时会发生什么?
当使用rabbitmqctl
启用特性标志时,内部会发生以下情况
- RabbitMQ 会验证特性标志是否已启用。如果是,则停止。
- 它会验证特性标志是否受支持。如果不是,则停止。
- 它将特性标志状态标记为state_changing。这是一个内部过渡状态,用于通知此特性标志的使用者。大多数情况下,这意味着依赖于此特定特性标志的组件将被阻止,直到状态更改为enabled或disabled。
- 它会启用此特性标志依赖的所有特性标志。因此,对于每个特性标志,我们都会经历相同的过程。
- 如果存在迁移函数,则执行它。此函数负责准备或转换各种资源,例如更改数据库的模式。
- 如果以上所有步骤都成功,则功能标志状态变为已启用。否则,它将恢复为已禁用。
作为操作员,此过程中最重要的是要记住,**如果迁移需要时间**,则某些组件以及**RabbitMQ 中的一些操作可能会在迁移期间被阻塞**。
从开发人员的角度
在处理插件或 RabbitMQ 核心贡献时,应使用功能标志使新版本的代码与旧版本的 RabbitMQ 兼容。
何时使用功能标志
开发人员有责任查看现有和未来(即添加到 main
分支中的)功能标志列表,并查看新代码是否可以利用这些标志。
这是一个示例。在开发一个以前使用 rabbit_common/include/rabbit.hrl
中定义的 #amqqueue{}
记录的插件时,必须调整插件以使用隐藏先前记录(现在为私有)的新 amqqueue
API。但是,无需为此查询功能标志:该插件将与 RabbitMQ 3.8.0 及更高版本兼容(即无需重新编译)。一旦 amqqueue
出现在该分支中,它也应该与 RabbitMQ 3.7.x 兼容。
但是,如果插件针对 RabbitMQ 3.8.0 中引入的仲裁队列,则可能需要查询功能标志以确定它可以执行的操作。例如,它可以声明仲裁队列吗?它甚至可以期望作为仲裁队列实现的一部分添加到 amqqueue
中的新字段吗?
如果插件仔细检查功能标志以避免任何不正确的预期,它将与许多版本的 RabbitMQ 兼容:用户无需重新编译任何内容或下载另一个特定版本的插件副本。
何时声明功能标志
如果插件或核心代理更改修改了以下方面之一
- 记录定义
- 复制的数据库模式
- 节点之间传递的 Erlang 消息的格式
- 从远程节点调用的模块和函数
那么与旧版 RabbitMQ 的兼容性就成为一个问题。这时,新的功能标志可以帮助确保更平滑的升级体验。
功能标志的两个最重要部分是
- 作为模块属性的声明
- 迁移函数
声明是一个模块属性,如下所示
-rabbit_feature_flag(
{quorum_queue,
#{desc => "Support queues of type quorum",
doc_url => "https://rabbitmq.cn/docs/quorum-queues",
stability => stable,
migration_fun => {?MODULE, quorum_queue_migration}
}}).
迁移函数是一个无状态函数,如下所示
quorum_queue_migration(FeatureName, _FeatureProps, enable) ->
Tables = ?quorum_queue_tables,
rabbit_table:wait(Tables),
Fields = amqqueue:fields(amqqueue_v2),
migrate_to_amqqueue_with_type(FeatureName, Tables, Fields);
quorum_queue_migration(_FeatureName, _FeatureProps, is_enabled) ->
Tables = ?quorum_queue_tables,
rabbit_table:wait(Tables),
Fields = amqqueue:fields(amqqueue_v2),
mnesia:table_info(rabbit_queue, attributes) =:= Fields andalso
mnesia:table_info(rabbit_durable_queue, attributes) =:= Fields.
更多实现文档可以在 rabbit_feature_flags
模块源代码 中找到。
Erlang 的 edoc
参考可以从 RabbitMQ 存储库克隆或源代码存档中本地生成
gmake edoc
# => ... Ignore warnings and errors...
# Now open `doc/rabbit_feature_flags.html` in the browser.
如何在混合版本集群中调整和运行测试套件
当某个功能或行为依赖于功能标志(在核心代理或插件中)时,必须调整关联的测试套件以考虑此功能标志。这意味着在运行实际测试用例之前,设置代码必须验证是否支持功能标志,如果支持则启用它,或者跳过测试用例。对于在组或套件级别运行的设置代码,情况也是如此。
rabbitmq-ct-heleprs
中有一些辅助函数可以简化此检查。这是一个示例,取自 rabbitmq-server 中的 dynamic_qq_SUITE.erl
测试套件
init_per_testcase(Testcase, Config) ->
% (...)
% 1.
% The broker or cluster is started: we rely on this to query feature
% flags.
Config1 = rabbit_ct_helpers:run_steps(
Config,
rabbit_ct_broker_helpers:setup_steps() ++
rabbit_ct_client_helpers:setup_steps()),
% 2.
% We try to enable the `quorum_queue` feature flag. The helper is
% responsible for checking if the feature flag is supported and
% enabling it.
case rabbit_ct_broker_helpers:enable_feature_flag(Config1, quorum_queue) of
ok ->
% The feature flag is enabled at this point. The setup can
% continue to play with `Config1` and the cluster.
Config1;
Skip ->
% The feature flag is unavailable/unsupported. The setup
% calls `end_per_testcase()` to stop the node/cluster and
% skips the testcase.
end_per_testcase(Testcase, Config1),
Skip
end.
可以在混合版本集群的上下文中本地运行测试套件。如果配置为这样做,rabbitmq-ct-helpers
将使用 RabbitMQ 的第二个版本在启动集群时启动一半的节点
- 节点 1 将位于主副本上(用于启动测试套件的副本)
- 节点 2 将位于辅助副本上(显式提供给
rabbitmq-ct-helpers
的副本) - 节点 3 将位于主副本上
- 节点 4 将位于辅助副本上
- ...
要在混合版本集群的上下文中运行测试套件
-
克隆
rabbitmq-public-umbrella
存储库并检出相应的分支或标签。这将是辅助 Umbrella。在此示例中,使用v3.12.x
分支git clone https://github.com/rabbitmq/rabbitmq-server.git secondary-umbrella
cd secondary-umbrella
git checkout v3.12.x
make co -
在辅助 Umbrella 中编译 RabbitMQ 或正在测试的插件。
rabbitmq-federation
插件用作示例cd secondary-umbrella/deps/rabbitmq_federation
make dist -
转到主副本中的 RabbitMQ 或同一插件
cd /path/to/primary/rabbitmq_federation
-
运行测试套件。在这里,指定了两个环境变量来配置“混合版本集群”模式
SECONDARY_UMBRELLA=/path/to/secondary-umbrella \
RABBITMQ_FEATURE_FLAGS= \
make tests第一个环境变量
SECONDARY_UMBRELLA
会告诉rabbitmq-ct-helpers
在哪里找到辅助 Umbrella,顾名思义。这就是启用混合版本集群模式的方式。第二个环境变量
RABBITMQ_FEATURE_FLAGS
设置为空字符串,并告诉 RabbitMQ 以禁用所有功能标志的方式启动:这是为了使较新的节点与较旧的节点兼容所必需的。