RFC-0069:ELF Runner 中的标准 I/O

RFC-0069:ELF Runner 中的标准 I/O
状态已接受
区域
  • 组件框架
说明

ELF 组件将 stdout 和 stderr 流转发到 LogSink 服务的机制。

Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-02-02
审核日期(年-月-日)2021-02-17

摘要

引入了两个新标志 forward_stdout_toforward_stderr_to,用于控制组件在启动时接收 stdout 和/或 stderr 流的选择性加入行为。启用后,流将由 LogSink 服务提供支持。

设计初衷

组件管理器启动时,其标准输出和标准错误流会绑定到 debuglog,因为在启动过程的此阶段没有其他替代方案。例如,提供 LogSink 服务的组件 Archivist 本身由组件管理器启动。

最近之前,组件管理器会将所有组件的 stdout 和 stderr 流重定向到内核的调试日志中。这是通过复制组件管理器自身的 stdout 和 stderr 句柄来实现的,这些句柄本身绑定到调试日志,如上所述。不过,这带来了一些问题。首先,用户模式组件不应写入调试日志,因为该日志专供内核使用和高度特权的用户模式组件使用;相反,大多数用户模式组件应写入 LogSink 服务。其次,在没有明确选择启用两个出站流的情况下提供这两个出站流违反了最小权限原则

目前,组件管理器完全缺少此功能,我们希望在解决之前存在的两个缺点后重新引入此功能。我们建议为 ELF 运行程序添加一个新标志,以支持显式选择加入,而不是隐式向所有组件授予 stdout 和/或 stderr。

组件框架团队正在进行从 appmgr(组件 v1)到组件框架(组件 v2)的长期迁移。这项工作的一项主要任务是迁移 Netstack 团队拥有的所有组件。stdout/stderr 支持是迁移所有这些组件的前提条件。之所以有此要求,是因为 Netstack 组件是用 Go 编写的。与用 C++ 或 Rust 编写的程序不同,Go 程序在设置期间会在开发者编写的程序开始运行之前向 stderr 发出错误。由于错误是在程序入口点之前记录的,因此 Go 组件的作者无法将 stdout 和 stderr 句柄绑定到日志记录服务。为了解决这个问题,我们可以派生并修改 Go 的运行时,使其在开始执行用户编写的 Go 程序之前包含必要的日志记录初始化。当然,维护 Go 分支会给我们的团队带来巨大的技术负担。或者,组件管理器可以在组件(和 Go 运行时)启动之前将 stdout 和 stderr 句柄绑定到日志记录服务,从而允许 Fuchsia 的日志记录服务捕获错误消息。

更广泛地说,我们希望减轻上述 v1 到 v2 迁移工作的技术负担。目前,appmgr 会向所有 v1 组件提供 stdout 和 stderr 句柄,并将它们路由到 debuglog。因此,可以合理假设 Fuchsia 中的许多开发者都依赖此功能。随着我们进入 2021 年并开始迁移越来越多的组件,我们应该允许开发者维护他们依赖的 stdout 和 stderr 支持。

要求

支持的日志记录服务必须是 LogSink,而不是 debuglog。我们必须改用 LogSink 的原因有多种。首先,如上所述,debuglog 旨在供内核使用。其次,debuglog 为所有进程使用一个小型 (128kb) 共享环形缓冲区,并按 FIFO 方式轮换消息。归档程序会定期清空这些消息,并将它们转发到 LogSink。不过,如果它们在被 Archivist 读入 LogSink 之前从 debuglog 缓冲区中滚出,则可能会“丢失”。使用 LogSink 进行 stdout 和 stderr 日志记录服务不仅可以消除任何消息被丢弃的可能性,还可以降低使用 debuglog 的其他组件和进程的消息被丢弃的可能性。此外,如果缓冲区轮换速度快于清空速度,则没有机制来跟踪损失了多少数据。第三,debuglog 不支持严重程度级别(例如 DEBUG、INFO 等)这是一项关键要求,因为我们需要区分 stdout 消息和 stderr 消息。在日志记录方面,我们只能通过将每个输出流映射到特定严重程度来实现这一点。

设计

该提案旨在为 ELF 组件的 program stanza 引入两个新的枚举值:forward_stdout_toforward_stderr_to。对于 ELF 组件,该枚举将是可选的,并且默认值为 none。如果为 none,ELF Runner 将不会将 stdoutstderr 句柄绑定到 LogSink 服务(当前行为),并且这些流将继续被忽略。当这些值设置为 log 时,ELF 运行程序将创建一个套接字,用于捕获 stdoutstderr 流的输出。然后,它会将读取的字节转发到 LogSink 服务。对于 stdout,它会发送 INFO 消息;对于 stderr,它会发送 WARN 消息。消息将以换行符分隔,每行将作为一条原子消息分区到 LogSink 服务。LogSink 服务的最大消息大小为 32KB,因此我们将字节流缓冲区的大小上限设为 30KB(以便为消息元数据留出一些空间)。超出该边界的字节将被舍弃,并且只会将部分消息发送到 LogSink 服务。这会带来一个有趣的极端情况,即部分代码点可能会在 30 KB 边界处拆分。在这种情况下,系统不会进行特殊处理,而是会按原样解码无效的码点前半部分。所有输入字节都会使用 String::from_utf8_lossy 解码为 UTF-8。根据此函数的 API,所有无效的 UTF-8 序列都将替换为 U+FFFD REPLACEMENT CHARACTER

{
    program: {
        "runner": "elf",
        "forward_stdout_to": "log",
        "forward_stderr_to": "log",
    }
}

实现

由于该功能将仅限于 ELF Runner,因此实现所需的更改相当少。我们预测,实现此更改最多需要 2 个 CL。

性能

由于只需将行发送到 LogSink,因此性能开销极小。此功能将引入的最明显的开销是字节流的解析和换行的拆分。不过,由于日志记录是不规则的操作,并且字节流本身往往较短,因此这种开销相对较低。更重要的是,这只会影响明确选择启用此行为的组件。因此,如果出现性能问题,我们只需将这些组件中的 forward_stdout_toforward_stderr_to 设置为 none,暂时回到原点,直到我们解决潜在的性能问题,即可快速解决该问题。

安全注意事项

Archivist 已经具备了归因和流量控制机制,因此行为不当的组件无法通过向其标准输出发送垃圾内容来拒绝服务。因此,您无需执行任何其他操作。不过,我们应该注意到,此功能提供了一种将任意字节放入另一个进程的地址空间(从组件到 Archivist)的机制。不过,如果缓冲区过大,这可能会带来问题,但我们已在上面提及了相应的缓解措施。此外,由于此实现对输入流的处理最少,因此我们降低了提权风险。如果这是一个复杂的解析器,那么出现 bug 和漏洞的可能性就会增加。

隐私注意事项

LogSink 后端和所有 LogSink 客户端均已符合隐私保护要求,日志会归因于其来源,并且已具备足够的 PII 清理机制。因此,您无需执行任何其他操作。

测试

除了单元测试之外,我们还会添加集成测试,以确保日志写入 Archivist。这些集成测试将使用所有受支持的语言(C/C++、Rust 和 Go)编写。由于 ELF 运行程序不处理 Dart 组件执行,因此不会测试 Dart。

文档

此标志将在组件 v2 文档的 ELF Runner 部分中进行说明。

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

带编号的把手

我们已探索将编号句柄从进程框架提升到组件框架,以支持此功能。虽然我们最终决定不采用此方法,但清单文件的大致设计如下所示:

{
    program: {
        "runner": "elf",
        "handles": ["STDOUT", "STDERR"]
    }
}

ELF 运行程序会读取常量字符串,并在内部将其映射到相应的编号句柄。最终,我们决定不这样做,因为我们不知道其他编号标识名有何即时用例。此外,如果我们确实找到了应在 ELF 运行器的 program 节中配置的另一个编号句柄,那么我们可以轻松更新清单文件语法和 ELF 运行器实现。

Component Manager

我们还探索了为编号句柄引入新的框架级功能。清单文件的大致设计如下所示:

{
    use: [
        {
            "handle": "stdout-to-log",
            "from": "framework",
            "number": "STDOUT",
        },
        {
            "handle": "stderr-to-log",
            "from": "framework",
            "number": "STDERR",
        }
    ]
}

出于上述与编号句柄相关的原因,以及在组件管理器级别引入此方法会引发有关组件管理器 POSIX 兼容性的新问题,因此我们认为此方法不可取。例如,是否所有 runner 都必须实现此功能?对于不使用 stdout/stderr 的 runner(例如“web”runner),它会是什么样子?因此,我们决定将 POSIX 兼容性问题作为单独的工作流来处理,而不纳入此 RFC 的范围。

推出新组件

我们还探讨了在由组件框架团队拥有和管理的新组件中实现翻译层(用于解析 stdout/stderr 字节流并将其转发到 LogSink 服务的部分)。不过,经过多次讨论,我们认为这种方法不切实际,因为我们必须设计一种在记录消息时保留日志记录归因的方法。Archivist 使用事件功能来获取组件源信息(例如,Moniker),如果我们使用中间层组件,所有这些信息都会丢失。

FDIO

我们还探讨了使用 fdio(Fuchsia 的 POSIX 兼容性库)来实现此功能。也就是说,创建一个可识别 stdout/stderr 文件描述符并在内部(在 fdio 中)将输出重定向到 LogSink 的新类型。不过,经过多次讨论,我们决定放弃修改 fdio,因为这样做会使实现更加困难。我们发现,POSIX 兼容性存在一些边缘情况,无法使用 fdio 中的 LogSink 转发器来实现。此外,基于 fdio 的实现会带来更多不确定性,并导致重复工作。或者,如果我们使用上述建议的套接字,它将“开箱即用”地符合 POSIX 标准。