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

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_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 中的协议公开给其父级。这意味着 root 会将该协议公开给位于 root 上方的节点,即 component_manager。如需详细了解此反转,请参阅设计

原型

您可以在此处查看原型。

性能

此设计没有任何性能注意事项。只有当 on_terminate: reboot 组件实际终止时,component_manager 才会打开与 fuchsia.hardware.power.statecontrol.Admin 的连接。

工效学设计

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

  • 在父级的 ChildDecl(CML 中的 children 声明)中设置 on_terminate: reboot
  • 如果父级的 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_rustcm_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 中管理。此替代方案由两部分组成。首先,引入“组件级范围”事件,以便使用方监控范围限定为单个组件实例的事件(尤其是 StartedStopped 事件)。其次,引入一个名为 supervisor 的组件,该组件会使用这些事件来监控异常终止或启动失败,并相应地重启系统。

组件级事件

组件框架团队讨论过的一个想法是,提供一种方法,允许将事件功能的范围限定为单个组件实例,而不是整个 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",
    },
],

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

组件级范围的事件不会在其载荷中携带与组件身份相关的信息,例如标识名或网址。通常,事件的载荷中可能会包含敏感信息,例如组件标识名或网址,而我们希望仅在需要时才公开这些信息。由于监督程序不需要此类信息,因此组件级范围的事件不会提供有关生成事件的组件的身份的信息。载荷中的其余信息是时间戳和终止状态,这些信息不敏感。

主管

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

  • 将静态 event_streamStartedStopped 事件列表搭配使用。
  • 如果通过此 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 中的类似问题。如果我们推迟到那时再设计更通用的解决方案,到时候或许就能更好地了解问题空间。

后续工作

basemgrsessionmgr 会实现自己的崩溃恢复策略,这些策略可以采用与supervisor 替代方案类似的方法。

fshostarchivist 目前使用 main_process_critical。他们或许可以改用“重启时终止”方法。这样一来,我们就可以将 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 的修订版有私密设计文档。