RFC-0110:重新启动以终止关键组件

RFC-0110:重新启动以终止关键组件
状态已接受
领域
  • 组件框架
说明

v2 组件功能,功能与 v1 specific_components 相当

Gerrit 更改
  • 535167
作者
审核人
提交日期(年-月-日)2021-05-26
审核日期(年-月-日)2021-07-21

总结

提议在组件清单的子声明中引入“reboot-on-teruation”选项,该选项可提供与 sysmgr 的 critical_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 的组件不能迁移到组件 v2,直到提供等效功能为止。

设计

我们将在 ChildDecl 中添加 on_terminate 枚举(相当于组件清单children 部分),提供等效于 critical_component 的语义。有两个选项:none(默认)或 reboot。当具有 on_terminate: reboot 的子组件因任何原因(包括正常退出)终止时,component_manager 将调用由 power_manager 公开的 fuchsia.hardware.power.statecontrol.Admin 协议中的 Admin/Reboot 方法,以触发系统正常重新启动。

这就需要在 component_managerpower_manager 之间形成依赖项循环。不过,这两者都在 ZBI 中,因此不存在明显的分层问题。在任何情况下,都无法避免一定程度的依赖项反转,因为重新启动会导致设备的电源状态发生变化,而这是由驱动程序的责任。

如果对 Admin/Reboot 的调用失败,component_manager 将回退到 panic,从而触发异常重新启动。

这是一项敏感功能;我们不希望任意组件单方面决定在终止时触发重新启动。因此,其使用将受 component_manager 安全政策中的许可名单的限制,系统会在该组件启动时在运行时检查此政策。此外,如果针对未获授权使用该功能的领域中的子级,我们可以使用 restricted_features GN 许可名单来生成构建时失败。

实现

on_terminate 个选项

我们需要将 on_terminate 选项添加到清单child 部分。这需要更改 cmccmc_fidl_validatorcm_rust 来深入探究选项。由于这是一项特殊功能,因此我们允许在 ComponentDecl 中将其设置为 None(当然,设置为 on_terminate: none)。

我们将为 on_terminatecmc 添加新的 restricted_feature。只有此许可名单中的 CML 文件才能为其子级设置 on_terminate: reboot。首先,此许可名单将包括 corenetwork 领域。

此外,我们还会在 component_manager 的配置中添加 reboot_on_terminate_enabled 布尔值,以便针对组件管理器的非根实例(例如,测试中的嵌套实例)停用该属性。

检测重新启动时终止组件的终止

必须将逻辑添加到 component_manager 才能检测组件在终止时重新启动的时间。在执行 Stop 操作期间,component_manager 可以检查 on_terminate 选项。如果已设置此属性,并且组件未关闭,component_manager 会调用 Admin/Reboot关闭意味着组件会停止并且再也不会重新启动,这种情况会在以下情况下发生:

  1. 系统关闭期间,这本身由 Admin/Reboot 协议触发。在这种情况下,系统已经关闭,因此再次触发关闭没有意义。
  2. 当组件被销毁时。可能发生这种情况的原因包括:(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 中的协议公开给其父项。这意味着根目录向根目录之上的节点(即 component_manager)公开了协议。如需了解有关此反转的详细说明,请参阅 Design

原型

如需查看原型,请点击此处

性能

此设计没有性能方面的考虑。仅当 on_terminate: reboot 组件实际终止时,component_manager 才会打开与 fuchsia.hardware.power.statecontrol.Admin 的连接。

工效学设计

这种设计采用简单的工效学设计:要在组件上设置重新启动时重新启动,只需执行以下操作即可:

  • 在父视图的 ChildDecl 中设置 on_terminate: reboot(CML 中的 children 声明)。
  • 如果父项的 CML 尚不存在,请将其添加到 on_terminate: rebootcmc 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 架构更改,因此需要通过以下几个位置了解此选项:cmccm_fidl_validatorcm_rust 以及 cm_rust 的客户端,尽管这是一项小众功能。

替代方案:program 上的 system_critical

我们可以将该选项添加到组件清单program 部分,而不是将该选项添加到 ChildDecl。这种方法的主要区别在于,在组件本身上设置选项,而不是在父声明中设置选项。

将位放在 program 中的好处是,可以在 ComponentDecl 之外正确处理某个特殊功能。由于 program(从 ComponentDecl 的角度来看)采用自由格式的语法,因此我们无需更改 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 中管理崩溃恢复。该备选网址由两部分组成。首先,引入“组件作用域”事件,让使用者能够监控作用域限定为单个组件实例的事件(特别是 StartedStopped 事件)。其次,引入一个名为“supervisor”的组件,该组件使用这些事件来监控异常终止或失败启动,并重新启动系统以做出响应。

组件级事件

组件框架团队讨论的思路是,提供一种方法,允许将事件功能的作用域限定为单个组件实例,而不是整个领域。该设计为此想法提供了具体应用。监督程序只需要监控特定组件,因此它更适合接收与这些组件(而非整个领域)相关的事件。

为了提高速度,我们提议在 CML 中引入启用此功能所需的最小更改。将来,我们可能会进行更实质性的语法修订版本,以不同方式指定事件的范围(请参阅组件事件 RFC)。我们将在 offer event 声明中添加 scope 字段,该字段可以指定 #childrealm(默认值)。

// 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",
    },
],

鉴于未来可能会对语法进行修订,我们可以将 cmcscope 功能列入许可名单,使其适用于 core.cml 和集成测试。

组件级范围的事件不会在其载荷中携带组件标识信息,例如名称或网址。一般来说,事件可能会在其载荷中携带敏感信息,例如组件名称或网址,而我们希望只在有必要知道的情况下公开这些信息。由于监督程序不需要此类信息,因此组件级事件不会提供有关生成事件的组件身份的信息。载荷中剩余的信息是时间戳和终止状态(不敏感)。

主管

监督程序本身很简单。它是 core 下的一个组件,用于执行以下操作:

  • 使用包含 StartedStopped 事件列表的静态 event_stream
  • 如果通过此 event_stream,它收到带错误的 Started 事件或载荷包含不正常状态的 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",
            },
        ],
    },
],
...

请注意,正在监控的组件无需进行修改。这是有意为之:监督被视为领域管理其组件的方式的功能,而非组件本身。换言之,是否监管或如何监管组件不负责决定。

启动 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 中的类似问题。如果在此之前我们暂缓设计更通用的解决方案,届时我们可以更好地了解问题空间。

后续工作

basemgrsessionmgr 会实现自己的崩溃恢复策略,这些策略可以采用与监督程序替代方法相同的方法。

fshostarchivist目前使用main_process_critical。也可能改用“重新启动时终止”。这样一来,我们便可以将 main_process_critical 限制为重新启动过程中涉及的组件(driver_managerpower_manager)。

某些路径仍会触发异常重新启动:

  • 这种设计会在 power_manager 上创建反转的 component_manager 依赖项,并且间接依赖于 driver_manager。为此,这些组件无法使用“重新启动时终止”,而是会被标记为 main_process_critical,这意味着其中任一组件的崩溃都会触发异常重新启动。
  • 如果 Reboot 调用本身失败,component_manager 会出现紧急警报,这也会触发异常重新启动。

在这些情况下,我们可以执行更安全的关闭操作;例如,component_manager 可以执行正常的系统关闭,然后退出。另一方面,由于 power_managerdriver_manager 对系统运行至关重要,因此我们可能不希望系统在发生崩溃时继续运行一段时间。

我们或许可以重新审视电源管理责任的分配方式;例如,component_manager 或许能够自行推动重新启动(它仍需要依赖于 driver_manager 来设置电源状态)。

系统完全迁移到 Components v2 后,组件管理器可以利用其依赖关系图知识支持更多智能恢复策略。

早期技术和参考资料

对于 critical_components 功能和 Events API 的修订版本,请查看非公开设计文档。