RFC-0110:重新启动以终止关键组件 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | v2 组件功能,与 v1 critical_components 功能等效 |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2021-05-26 |
审核日期(年-月-日) | 2021-07-21 |
摘要
提案:向组件清单的子声明中引入“终止时重新启动”选项,以便与 sysmgr 的 critical_components
功能保持一致。
设计初衷
在 Components v1 中,sysmgr 支持一项名为 critical_components
的功能,可让系统服务组件将自身标记为“关键”。这意味着,如果组件因任何原因(包括正常退出)而终止,sysmgr 将触发系统重启。此重启是由 power_manager
驱动的安全重启,这会导致组件拓扑按顺序关闭。正常重启会以一致的方式拆解系统,并让组件有机会正常关闭,从而保留诊断信息并正常关闭文件系统。
如果客户端不确定其组件发生故障时是否可以继续正常的系统行为,通常会在其组件上设置此选项。不出所料,此选项通常设置在服务在系统运行中发挥核心作用的组件上,例如:
netstack
wlanstack
omaha-client-service
system-update-checker
除了 critical_components
实现的相对简单的策略之外,崩溃恢复还有许多可能的策略。此设计旨在解决该用例。超出 critical_components
提供的崩溃恢复功能的范围(但请参阅未来的工作)。
要求
主要要求是提供与 critical_components
相当的功能。这意味着,core
下的组件或 core
的子领域应该能够选择在其组件终止时触发正常重新启动。
为何此时推荐?
在等待等效功能推出之前,动机中提及的使用 critical_components
的组件将无法迁移到 Components v2。
设计
我们将向 ChildDecl
添加 on_terminate
枚举(相当于组件清单的 children
部分),提供与 critical_component
等效的语义。有两个选项:none
(默认)或 reboot
。当具有 on_terminate: reboot
的子组件因任何原因(包括正常退出)而终止时,component_manager
将调用 power_manager
公开的 fuchsia.hardware.power.statecontrol.Admin
协议中的 Admin/Reboot
方法,以触发系统正常重启。
这需要在 component_manager
和 power_manager
之间建立依赖项循环。不过,这两者都位于 ZBI 中,因此没有严重的层次问题。无论如何,都无法避免出现一定程度的依赖项反转,因为重新启动会导致设备的电源状态发生变化,而这是驱动程序的责任。
如果对 Admin/Reboot
的调用失败,component_manager
将回退到 panic,从而触发非正常重新启动。
这是一个敏感功能;我们不希望任意组件在终止时单方面决定触发重新启动。因此,其使用将受到 component_manager
安全政策中许可名单的限制,该许可名单将在组件启动时在运行时进行检查。此外,如果在未获授权使用该功能的领域中为子项设置此选项,我们可以使用 restricted_features
GN 许可名单来产生构建时失败。
实现
第 on_terminate
个选项
我们需要将 on_terminate
选项添加到清单的 child
部分。这需要更改 cmc
、cmc_fidl_validator
和 cm_rust
,以便将选项传递到下游。由于这是一项特殊功能,我们允许将其在 ComponentDecl
中设置为 None
(当然,默认设置为 on_terminate: none
)。
我们将为 on_terminate
向 cmc
添加一个新的 restricted_feature
。只有此许可名单中的 CML 文件才能在其子项上设置 on_terminate: reboot
。首先,此许可名单将包含 core
和 network
领域。
我们还将向 component_manager
的配置中添加一个 reboot_on_terminate_enabled
布尔值,以便为组件管理器的非根实例(例如测试中的嵌套实例)停用该功能。
检测“终止时重新启动”组件的终止
必须向 component_manager
添加逻辑,以检测“终止时重启”组件何时终止。在 Stop
操作期间,component_manager
可以检查 on_terminate
选项。如果已设置此标志且组件未关闭,component_manager
会调用 Admin/Reboot
。关闭表示组件正在停止,并且永远不会再次启动,这在以下情况下会发生:
- 在系统关闭期间,该过程本身由
Admin/Reboot
协议触发。在这种情况下,系统已在关闭,因此没有必要再次触发关闭。 - 组件被销毁时。这可能是因为:(a) 对
DestroyChild
进行了显式调用;(b)transient
集合的父级停止了;或 (c)single-run
集合中的组件退出了。在 (a) 和 (b) 的情况下,不触发重新启动似乎是正确的决定,因为导致组件停止运行的是组件外部的操作,而不是组件内部的终止。对于 (c) 的情况,如果我们谨慎实现该功能,只在组件终止后触发销毁过程,仍然可以确保组件退出会触发重启。
调用 fuchsia.hardware.power.statecontrol.Admin
协议
如需触发正常重启,请连接到协议 fuchsia.hardware.power.statecontrol.Admin
并调用 Admin/Reboot
。此协议由 power_manager
组件实现。(由于历史原因,它实际上由 shutdown_shim
代理。)由于此协议由组件实现,component_manager
如何获得对它的访问权限?为此,我们可以让 root
将 #bootstrap
中的协议公开给其父级。这意味着 root 会将该协议公开给位于 root 上方的节点,即 component_manager
。如需详细了解此反转,请参阅设计。
原型
您可以在此处查看原型。
性能
此设计没有任何性能注意事项。只有当 on_terminate: reboot
组件实际终止时,component_manager
才会打开与 fuchsia.hardware.power.statecontrol.Admin
的连接。
工效学设计
此设计具有简单的人体工学设计:如需在组件上设置“终止时重新启动”功能,只需执行以下操作即可:
- 在父级的
ChildDecl
(CML 中的children
声明)中设置on_terminate: reboot
。 - 如果父级的 CML 不存在,请将其添加到
on_terminate: reboot
的cmc
restricted_features
许可名单中。 - 将组件的标识符添加到政策许可名单,以实现终止时重启。
由于 on_terminate
选项由父级(而非组件本身)设置,因此可以在测试中利用应在生产环境中触发重新启动的组件,而无需修改 CML。此外,这样一来,您就可以在希望以不同方式设置选项的不同产品配置中添加该组件,而无需更改该组件。
向后兼容性
此更改不会破坏兼容性。客户端必须明确选择启用“终止时重新启动”。
安全注意事项
假设用户将不应标记为“终止时重新启动”的组件标记为“终止时重新启动”,从而滥用此功能,不恰当地触发重新启动。不过,由于使用受到安全政策许可名单的限制,因此新的使用必须获得明确批准。请注意,不可信的组件无法通过嵌入已列入许可名单的组件来诱骗 component_manager
向其授予重新启动权限,因为该组件是通过其别名(拓扑路径),而不是网址列入许可名单的。
隐私注意事项
此提案没有引入新的隐私注意事项。
测试
我们可以通过模拟 fuchsia.hardware.power.statecontrol.Admin
协议来轻松集成测试此功能。我们应记得测试不正常的路径,例如协议缺失或失败的情况。
理想情况下,应为“终止时重新启动”组件添加端到端测试覆盖率,以验证其终止确实会触发正常重新启动。
文档
必须进行以下文档更改:
- 为
on_terminate
选项添加了文档,以与critical components
并行。 - 更新了迁移指南,以说明如何迁移
critical_component
缺点、替代方案和未知情况
优点和缺点
益处
- 配置非常简单。
- 与 v1 完全兼容,便于迁移。
- 由于该功能完全位于
component_manager
中,因此实现起来非常简单,并且不存在丢失事件等失败模式的风险。 - 可以将
main_process_critical
的某些用法替换为on_terminate: reboot
,后者性能明显更优越。 - 允许客户端使用在生产环境中设置了
on_terminate: reboot
的组件,而无需进行修改。
缺点
- 不是基于能力,这与传统框架模型不同。
- 直接在
component_manager
中编码某些崩溃恢复政策。虽然我们通常不鼓励这样做,但在本例中,政策很简单,因此虽然成本不为零,但似乎很小。 - 通过
component_manager
引入了对power_manager
的反向依赖项。不过,它们都位于 ZBI 中,因此这不是一个严重的叠加违规问题。 - 由于涉及 CML 架构更改,因此需要通过多个位置(
cmc
、cm_fidl_validator
、cm_rust
和cm_rust
的客户端)来布管此选项,即使它是一项小众功能也是如此。
替代方案:program
上的 system_critical
位
我们可以将该选项添加到组件清单的 program
部分,而不是添加到 ChildDecl
。与此方法的主要区别在于,此选项是在组件本身上设置的,而不是在父级中的子声明上设置的。
将该位放入 program
的好处在于,可将专用功能从 ComponentDecl
中移除。从 ComponentDecl
的角度来看,由于 program
采用自由格式语法,因此我们无需更改 cmc
、验证器或 Rust 绑定即可考虑新选项。我们只需在 component_manager
本身中添加逻辑,以便在组件停止时从 program
检索选项(以确定是否需要重新启动)。
不过,这种方法有一个明显的缺点:如果在测试中使用 system_critical
组件,则必须修改其 CML 以移除 system_critical
位(因为不允许在测试环境中设置该位,并且我们不希望测试触发系统重新启动)。这会增加编写利用该组件的集成测试的客户的维护负担。
替代方案:使用 main_process_critical
ELF 运行程序支持一项名为 main_process_critical
的功能,该功能会导致当组件以非零状态退出或被终止时,component_manager 的根作业终止。这会导致系统无法正常重启。由于重新启动不正常,这会导致系统不正常关闭,并且不给系统留机会保留诊断信息或指标。
main_process_critical
仅应在无法触发正常重启的情况下使用。例如,power_manager
本身被标记为 main_process_critical
。由于任何关键组件都不是这种情况,因此此选项不被视为可行的替代方案,但为了完整起见,我们还是将其列在这里。
替代方案:主管
我们可以在 core
领域中管理崩溃恢复,而不是在 component_manager
中管理。此替代方案由两部分组成。首先,引入“组件级范围”事件,以便使用方监控范围限定为单个组件实例的事件(尤其是 Started
和 Stopped
事件)。其次,引入一个名为 supervisor 的组件,该组件会使用这些事件来监控异常终止或启动失败,并相应地重启系统。
组件级事件
组件框架团队讨论过的一个想法是,提供一种方法,允许将事件功能的范围限定为单个组件实例,而不是整个 Realm。此设计为此想法提供了具体的应用。监督程序只需监控特定组件,因此它接收的事件应与这些组件(而非整个领域)相关。
对于速度,我们建议仅对 CML 进行必要的更改,以便启用此功能。未来,我们可能会进行更实质性的语法修订,以便以不同的方式指定事件的范围(请参阅组件事件 RFC)。我们将向 offer event
声明添加一个 scope
字段,该字段可以指定 #child
或 realm
(默认)。
// core.cml
offer: [
{
event: "started",
from: "framework",
scope: "#wlanstack",
to: "#supervisor",
as: "started-wlanstack",
},
{
event: "stopped",
from: "framework",
scope: "#wlanstack",
to: "#supervisor",
as: "stopped-wlanstack",
},
],
鉴于语法未来可能会进行修订,我们可以让 cmc
将 scope
功能列入 core.cml
和集成测试的许可名单。
组件级范围的事件不会在其载荷中携带与组件身份相关的信息,例如标识名或网址。通常,事件的载荷中可能会包含敏感信息,例如组件标识名或网址,而我们希望仅在需要时才公开这些信息。由于监督程序不需要此类信息,因此组件级范围的事件不会提供有关生成事件的组件的身份的信息。载荷中的其余信息是时间戳和终止状态,这些信息不敏感。
主管
监督器本身很简单。它是 core
下的组件,用于执行以下操作:
- 将静态
event_stream
与Started
和Stopped
事件列表搭配使用。 - 如果通过此 event_stream 接收包含错误的
Started
事件,或包含非“ok”状态的载荷的Stopped
事件,则通过调用 fuchsia.hardware.power.statecontrol/Admin.Reboot 触发正常重新启动。
这是 critical_components
功能的简单实现目标。未来,监督器可能会演变为支持更多用例,或者可能有多个监督器 - 请参阅未来工作。
将事件路由到监督程序
组件级范围的事件必须从每个关键组件路由到监督程序。对于 core
的子级关键组件,这需要进行两项更改:
- 对 core.cml 进行了修改,以将 Started 和 Stopped 事件从组件路由到 supervisor(请参阅组件级事件)
- 对监督器的 CML 进行了修改,以使用静态事件流中的事件。
如果关键组件嵌套在 core
的子领域下,则需要执行另一个步骤:
- 修改每个中间组件,以将事件从子级公开到其父级。
例如,netstack 可能就是这种情况,因为计划将 netstack
放置在 core
下的 network
子领域中。
以下是主管的 CML 示例:
// supervisor.cml
use: [
{
events: [
"netstack-started",
"netstack-stopped",
"wlan-started",
"wlan-stopped",
],
},
// The supervisor will trigger reboot under the following conditions:
// - It receives a `started` event with an error.
// - It receives a `stopped` event with a non-ok status.
{
event_stream: "EventStream",
subscriptions: [
{
event: [
"netstack-started",
"netstack-stopped",
"wlan-started",
"wlan-stopped",
],
on_receive: "start",
},
],
},
],
...
请注意,无需修改要监控的组件。这是有意为之:监督被视为一个函数,该函数取决于 realm 管理其组件的方式,而不是组件本身。换句话说,组件不负责决定是否要进行监督以及如何进行监督。
启动监督程序
我们需要确保 supervisor 始终能够及时启动以接收事件。为此,我们建议向 event_stream
订阅添加一个名为 on_receive: "start"
的选项。on_receive: "start"
会导致 component_manager
在收到该事件时自动启动组件。这样,component_manager
可保证事件绝不会丢失。默认选项 "dispatch_if_started"
仅在组件已在运行时才会将事件分派给该组件(默认行为)。
这需要对事件调度系统进行更改。具体而言,分派事件时,component_manager
必须遵循所有路由的事件功能,以防它们被静态事件流使用。否则,即使组件已标记为 on_receive: "start"
,但如果尚未解决,也可能会错过事件。
有人可能会提出将 on_receive: "start"
作为静态事件流的默认行为的论点,但这超出了本文提案的范围。
优点和缺点
益处
- 避免在
component_manager
中编码崩溃恢复政策。这有助于更好地分离关注点,因为通常情况下,我们无法充分了解哪些类型的崩溃恢复政策足够通用,值得在component_manager
中提供直接支持。 - 这种方法比
recovery
选项更具适应性。在 basemgr 和 sessionmgr 中,需要为代理和会话本身实现与重新启动恢复不同的崩溃恢复政策。
缺点
- 需要由需要构建的事件系统提供支持。这会增加事件系统的复杂性,并且可能需要比直接在
component_manager
中实现解决方案更多的时间和精力。 - 需要比
recovery
更多的样板代码。每个关键组件的事件都必须从每个关键组件路由到监督程序。 - 我们最终需要解决
basemgr
/sessionmgr
中的类似问题。如果我们推迟到那时再设计更通用的解决方案,到时候或许就能更好地了解问题空间。
后续工作
basemgr
和 sessionmgr
会实现自己的崩溃恢复策略,这些策略可以采用与supervisor 替代方案类似的方法。
fshost
和 archivist
目前使用 main_process_critical
。他们或许可以改用“重启时终止”方法。这样一来,我们就可以将 main_process_critical
限制为仅适用于重新启动过程中涉及的组件(driver_manager
和 power_manager
)。
某些路径仍会触发非正常重启:
- 这种设计会创建
component_manager
对power_manager
的反向依赖项,以及对driver_manager
的间接依赖项。因此,这些组件无法使用“重启时终止”功能,而是改为标记为main_process_critical
,这意味着这两个组件中的任一组件发生崩溃都会触发不正常的重启。 - 如果
Reboot
调用本身失败,component_manager
会 panic,这也会触发不正常的重启。
在这些情况下,我们或许可以执行更妥善的终止操作;例如,component_manager 可以执行正常的系统终止操作,然后退出。另一方面,由于 power_manager
和 driver_manager
对系统运行至关重要,因此如果它们发生崩溃,我们可能不希望系统继续运行任何时间。
我们可能会重新考虑如何分配电源管理职责;例如,component_manager
或许能够自行驱动重新启动(但仍需要依赖 driver_manager
来设置电源状态)。
系统完全迁移到 Components v2 后,组件管理器有望利用其对依赖项图的了解,支持更智能的恢复策略。
在先技术和参考文档
critical_components
功能和事件 API 的修订版有私密设计文档。