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

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

提供与 v1 critical_components 对等功能的 v2 组件功能

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

摘要

一项提案,旨在为组件清单的子声明引入“终止时重新启动”选项,从而与 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 的子 realm 应该能够选择在组件终止时触发平滑重启。

为何此时推荐?

在提供等效功能之前,动机中提及的使用 critical_components 的组件无法迁移到组件 v2。

设计

我们将向 ChildDecl(相当于组件清单children 部分)添加 on_terminate 枚举,提供与 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 许可名单,以便在某个 realm 中子项设置了未经授权使用相应功能的选项时,生成 build 时失败。

实现

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)公开协议。如需详细了解此反转,请参阅设计

原型

点击此处可查看原型。

性能

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

工效学设计

此设计具有简单的人体工程学:只需执行以下操作,即可在组件上设置“终止时重新启动”:

  • 在父级的 ChildDecl(CML 中的 children 声明)中设置 on_terminate: reboot
  • 如果父 CML 不存在,请将其添加到 on_terminate: rebootcmc restricted_features 许可名单中。
  • 将组件的 moniker 添加到“在终止时重启”的政策许可名单中。

由于 on_terminate 选项是由父级设置的,而不是由组件本身设置的,因此在测试中可以利用应在生产环境中触发重新启动的组件,而无需修改 CML。此外,这样一来,您就可以在希望以不同方式设置该选项的不同产品配置中包含该组件,而无需更改该组件。

向后兼容性

此更改不会破坏兼容性。客户端必须明确选择启用“在终止时重新启动”功能。

安全注意事项

从理论上讲,用户可能会滥用此功能,将不应标记为“终止时重新启动”的组件标记为“终止时重新启动”,从而不恰当地触发重新启动。不过,由于使用情况受到安全政策许可名单的限制,因此新使用情况必须获得明确批准。请注意,不受信任的组件无法通过嵌入已列入许可名单的组件来欺骗 component_manager,使其授予该组件重启权限,因为组件是按其 moniker(拓扑路径)而非网址列入许可名单的。

隐私注意事项

此提案未引入任何新的隐私注意事项。

测试

我们可以通过模拟 fuchsia.hardware.power.statecontrol.Admin 协议轻松对此功能进行集成测试。我们应该记得测试不成功的路径,例如协议缺失或失败的情况。

理想情况下,应为终止时重启组件添加 E2E 测试覆盖率,以验证其终止是否确实会触发正常重启。

文档

必须进行以下文档更改:

  • on_terminate 选项添加了文档,以并行处理 critical components
  • 更新了迁移指南,以说明如何迁移 critical_component

缺点、替代方案和未知因素

优点和缺点

益处

  • 配置起来非常简单。
  • 与 v1 完全对等,便于迁移。
  • 由于该功能完全位于 component_manager 中,因此实现起来非常简单,并且不会像丢失事件那样带来太多的故障模式风险。
  • 这使我们能够将 main_process_critical 的某些用法替换为严格更优的 on_terminate: reboot
  • 允许客户端在生产环境中利用已设置 on_terminate: reboot 的组件,而无需进行修改。

缺点

  • 不基于能力,这与正统的框架模型不同。
  • 直接在 component_manager 中编码了一些崩溃恢复政策。虽然我们一般不鼓励这样做,但在本例中,政策很简单,因此成本虽然不为零,但似乎很小。
  • 引入了 component_managerpower_manager 的反向依赖关系。不过,它们都在 ZBI 中,因此不算严重的层次违规。
  • 由于涉及 CML 架构变更,因此需要通过多个位置(cmccm_fidl_validatorcm_rustcm_rust 的客户端)来实现此选项,即使它是一项小众功能。

替代方案:program 上的 system_critical

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

将位放在 program 中具有以下优势:可将专用功能排除在 ComponentDecl 之外。由于从 ComponentDecl 的角度来看,program 具有自由格式的语法,因此我们无需更改 cmc、验证器或 Rust 绑定来考虑新选项。我们只需要在 component_manager 本身中添加逻辑,以便在组件停止时从 program 中检索选项(以确定是否需要重新启动)。

不过,这种方法有一个明显的缺点:如果测试中使用了 system_critical 组件,则必须更改其 CML 以移除 system_critical 位(因为不允许在测试 realm 中设置该位,并且我们不希望测试触发系统重启)。这会增加编写利用该组件的集成测试的客户的维护负担。

替代方案:使用 main_process_critical

ELF Runner 支持一项名为 main_process_critical 的功能,该功能可使 component_manager 的根作业在组件以非零状态退出或被终止时终止。这会造成非正常重启。由于重新启动不正常,这会导致系统不干净地关闭,并且不会给系统保留诊断信息或指标的机会。

main_process_critical 只能在无法触发正常重启的地方使用。例如,power_manager 本身标记为 main_process_critical。由于任何关键组件都不是这种情况,因此此选项不被视为可行的替代方案,但此处列出是为了完整性。

备选:主管

我们可以通过 core realm 管理崩溃恢复,而不是在 component_manager 中管理。此替代方案包含两部分。首先,引入“组件级”事件,让消费者能够监控限定于单个组件实例的事件(尤其是 StartedStopped 事件)。其次,引入一个名为“supervisor”的组件,该组件会使用这些事件来监控异常终止或启动失败,并相应地重新启动系统。

组件级作用域事件

组件框架团队讨论过一个想法,即提供一种方法,使事件功能能够限定到单个组件实例,而不是整个 realm。此设计为这一想法提供了具体的应用。由于主管只需要监控特定组件,因此它接收有关这些特定组件的事件(而不是整个 Realm)是有意义的。

对于速度,我们建议对 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",
    },
],

鉴于未来可能会对语法进行修订,我们可以将 cmc 许可列入 scope 功能的 core.cml 和集成测试中。

组件范围的事件不会在其载荷中携带有关组件身份的信息,例如 moniker 或网址。一般来说,事件可以在其载荷中携带敏感信息,例如组件 Moniker 或网址,我们希望仅在需要了解的基础上公开这些信息。由于主管不需要此信息,因此组件范围的事件不会提供有关生成事件的组件身份的信息。载荷中的其余信息为时间戳和终止状态,这些信息并不敏感。

主管

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

  • 使用包含 StartedStopped 事件列表的静态 event_stream
  • 如果在此 event_stream 上收到包含错误的 Started 事件,或收到包含非“ok”状态的载荷的 Stopped 事件,则通过调用 fuchsia.hardware.power.statecontrol/Admin.Reboot 来触发正常重启。

这是 critical_components 功能的简单实现目标。未来,主管可能会发展为支持更多使用情形,或者可能会有多个主管 - 请参阅未来工作

将事件路由到主管

组件级范围的事件必须从每个关键组件路由到主管。对于 core 的子级关键组件,这需要进行两项更改:

  • 对 core.cml 的修改,用于将组件的 Started 和 Stopped 事件路由到主管(请参阅组件范围的事件
  • 对主管的 CML 进行修改,以使用静态事件流中的事件。

如果关键组件嵌套在 core 的子 realm 下,则需要执行另一个步骤:

  • 修改每个中间组件,以将子组件中的事件公开给其父组件。

例如,对于 netstack 来说,情况很可能就是这样,因为计划让 netstack 位于 core 下的 network 子 realm 中。

以下是主管的 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 管理其组件的方式,而不是组件本身的功能。换句话说,组件不负责决定是否或如何进行监督。

启动主管

我们需要确保主管始终及时启动,以便接收事件。为此,我们建议向 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 实现了自己的崩溃恢复策略,这些策略可能会采用类似于 supervisor 替代方案的方法。

fshostarchivist 目前使用 main_process_critical。不过,他们或许可以使用 terminate-on-reboot。这样一来,我们就可以将 main_process_critical 限制为与重新启动过程相关的组件(driver_managerpower_manager)。

某些路径仍会触发不正常的重新启动:

  • 此设计会创建 component_managerpower_manager(以及间接对 driver_manager)的反向依赖关系。因此,这些组件无法使用“在重新启动时终止”,而是标记为 main_process_critical,这意味着任一组件发生崩溃都会触发不正常的重新启动。
  • 如果 Reboot 调用本身失败,component_manager 会出现 panic,这也会触发不正常的重启。

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

我们可能会重新考虑如何分配电源管理职责;例如,或许 component_manager 可以自行驱动重启(但仍需要依赖 driver_manager 来设置电源状态)。

在系统完全迁移到 Components v2 后,组件管理器可能会利用其对依赖关系图的了解来支持更智能的恢复策略。

在先技术和参考资料

critical_components 功能和事件 API 的修订版本有私密设计文档。