RFC-0127:结构化配置

RFC-0127:结构化配置
状态已接受
区域
  • 组件框架
说明

一种新的配置系统,用于解决一组常见的组件配置问题。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-07-08
审核日期(年-月-日)2021-09-22

摘要

此 RFC 提出了一种新的“结构化”配置系统,可让组件开发者使用组件框架轻松一致地解决一组常见的组件配置问题。它旨在补充而非取代 Fuchsia 上现有的配置机制。

组件开发者可以在组件清单中为其组件声明配置键,然后组件框架会在每个实例启动时将配置值传递给组件。初始配置值是在程序集时定义的。如果在组装时允许,配置值也可以在系统运行时由组件的父级或通过 FIDL 接口设置。

此 RFC 涵盖了动机、预期用例和总体设计。未来的 RFC 将定义实现方式以及开发者与此新系统互动的语法。

设计初衷

如果软件可以“配置”,即其行为的各个方面可以由外部控制,而不是由其源代码固定,那么软件会更灵活且更可重用。Fuchsia 旨在用于大规模生产环境中的各种产品;配置对于实现这种灵活性和随着时间的推移安全地发展平台至关重要。

其他大型平台提供的基础架构可帮助开发者向其软件(例如 Chromium)添加配置,但 Fuchsia 上的配置目前是一个手动流程,需要进行定制工作,才能在运行时使用配置值并为不同环境提供这些配置值。

目前,Fuchsia 上最常用的配置工具是从 config-data 软件包读取文件,以及从面向配置的 FIDL API 读取数据。迄今为止,这些现有工具已用于解决多个重要问题,但它们的使用并不广泛,也不一致。

此 RFC 提出了一种新的“结构化”配置系统,可让组件开发者使用组件框架轻松一致地解决一组常见的组件配置问题。它旨在补充而非取代现有的配置机制。

启用简单且一致的组件配置可为 Fuchsia 带来多项好处,例如:

  • 平台组件可以变得更加灵活,以支持更广泛的产品。
  • 通过提供替代方案来取代 CFv1 在启动子组件时提供实参的功能,可以简化向 Component Framework v2 的迁移。
  • 通过使用功能标志,可以更安全地将新的平台和产品功能部署到生产环境中。
  • 可以降低与开发、测试和维护可配置行为相关的成本。

利益相关方

此 RFC 的利益相关方包括 Fuchsia 工程委员会、范围因其而扩大的组件平台团队(即组件框架和软件交付)以及致力于流程改进的团队(PDK 和安全性)。

系统的潜在客户也很重要,但由于此 RFC 未提出通用配置系统,因此不应期望它能满足所有潜在客户的所有配置需求。

教员:abarth

审核者:geb(组件框架)、wittrock (SWD)、aaronwood(SWD 和 PDK)、ampearce(安全性)

咨询对象:ddorwin、hjfreyer、ejia、thatguy、shayba、jamesr、ypomortsev、crjohns、surajmalhotra、curtisgalloways、adamperry

社会化:此设计或上述文档的早期草稿已在组件框架、安全、软件交付、Cobalt 和 PDK 团队中进行审核。我们还与潜在客户进行了多次讨论。

用例

常见用例:功能标志

向已部署的系统添加新功能可能是一项风险较高的提议;新设计和新代码有时会包含错误或无效的假设,这些错误或假设在部署到生产环境后才会发现。许多其他平台通过使用“功能标志”来降低此风险:用于控制功能是否处于活跃状态的布尔值配置参数。功能标志具有以下几项优势:

  • 新软件版本的部署可以与新功能的启用分离;在同一软件版本中添加的功能不必同时启用。
  • 在启用新功能之前,可以对其进行全面测试,确保其正常运行。
  • 您可以逐步在设备上启用各项功能,方法是使用发布渠道、百分比推出或两者结合使用。
  • 如有必要,您可以安全快速地停用各项功能;停用某项功能无需回滚到更早的软件版本。

我们以最近推出的一项本可受益于功能标志的功能为例:向 Timekeeper 引入频次估算Timekeeper在提交的 CL 中,此功能已在所有产品的所有版本中立即永久启用。

estimator/mod.rs(不含结构化配置)

match self.frequency_estimator.update(&sample) {
  // use resulting frequency update
  ...
}

借助结构化配置,我们可以在 Timekeeper 的组件清单中声明一个功能标志,然后根据此标志来限制频次估算器的使用:

timekeeper.cml

{
  ...
  config: {
    enable_frequency: {
      type: "boolean",
      default: "false"
    }
  },
  ...
}

main.rs

import config_timekeeper as config;
...
let config: config::Struct = config::parse();
// Pass config through to each new Estimator

estimator/mod.rs

if self.config.enable_frequency {
  match self.frequency_estimator.update(&sample) {
    // Use resulting frequency update
    ...
  }
}

在此示例中,在清单中声明配置部分会导致构建系统生成一个库,其中包含相应配置的 FIDL 结构定义,以及在运行时从运行程序提供给组件的输入中填充此结构所需的代码。组件的实现可以导入此库,并使用结构体中的字段来控制其行为。

请注意,组件开发者可以使用大约 10 行代码通过标志来控制新功能的发布。他们能通过这 10 行代码获得什么?

  • 此功能最初将在所有地区停用。由于组件作者提供了“false”的默认值,因此在程序集流程的其他输入明确设置不同的值之前,该标志将处于关闭状态。

  • 最初,在生产版本中运行系统时,无法启用该功能。哪些配置键可以在系统运行时发生变化是一个政策问题,而此 RFC 未指定政策。不过,出于安全考虑,除非明确要求并经过审核,否则在生产(例如“用户”)版本中很可能不允许任何运行时突变。

  • 开发者可以在系统以工程版本运行时测试该功能。哪些配置键可以在系统运行时发生变化是一个政策问题,而此 RFC 未指定政策。不过,为了简化开发、测试和调试,工程 build 中可能会允许对大多数配置键进行运行时突变(例如,“eng”)版本。 开发者可以使用 ffx 为本地设备启用该功能。例如(使用概念性语法):ffx target config set timekeeper.cml enable_frequency=true。如果政策允许,开发者还可以启用该功能,使其在设备电源循环中保持启用状态。

  • 测试可以涵盖“已启用功能”和“已停用功能”这两种情况。 对于单元测试和组件级测试,这涉及手动构建和注入 FIDL 配置结构;对于集成测试,这涉及在启动被测组件时提供配置,例如使用 Realm Builder

  • 构建和组装工具可以控制该功能,作为平台边界的一部分。目前,我们正在通过 DPISPAC 努力构建和组装工具。这项工作的成果将使平台维护人员能够控制是否向产品公开每个平台功能。根据政策和功能的风险,平台可以控制功能的发布,也可以将功能的发布委托给产品。对于更复杂的情况,平台可以在系统运行期间(即使在生产环境中)启用功能标志的突变(例如,“用户”)。这样一来,产品就可以在单个设备上启用该功能,例如根据实验发布系统或企业管理控制台设置配置值。

  • 标志状态会反映在设备指标中。对于在系统运行时标志状态可以发生突变的发布版本,其值会包含在额外的哈希中,该哈希可用于在指标分析期间将启用功能的同类群组与停用功能的同类群组分开。

常见用例:产品/主板/build 类型定制

Fuchsia 是一种通用操作系统,可用于各种不同的产品和设备类别。这意味着,平台组件有时需要根据其运行的产品或主板的特性来调整其行为。

我们再以 Timekeeper 为例:UTC 维护算法需要了解设备振荡器的准确性,才能了解误差范围的增长情况并对连续样本进行加权。有些振荡器比其他振荡器精确得多(也更昂贵!),但目前还没有简单的方法来表达基于电路板的变化,因此振荡器误差目前硬编码为一个常数:

estimator/mod.rs(不含结构化配置)

const OSCILLATOR_ERROR_STD_DEV_PPM: u64 = 15;

kalman_filter.rs(不含结构化配置)

static ref OSCILLATOR_ERROR_VARIANCE: f64 =
    (OSCILLATOR_ERROR_STD_DEV_PPM as f64 / MILLION as f64).powi(2);
...
self.covariance_00 += monotonic_step.powf(2.0) * *OSCILLATOR_ERROR_VARIANCE;

借助结构化配置,我们可以在 Timekeeper 的清单中声明一个整数配置键,然后使用产品或主板提供的值:

timekeeper.cml

{
  ...
  config: {
    oscillator_error_std_dev_ppm: {
      type: "uint8"
    }
  },
  ...
}

main.rs

import config_timekeeper as config;
...
let config: config::Table = config::parse();
// Pass config through to each new KalmanFilter

estimator/mod.rs

// Delete hard-coded constant.

kalman_filter.rs

let oscillator_error_variance: f64 =
    (config.oscillator_error_std_dev_ppm as f64 / MILLION as f64).powi(2);
...
self.covariance_00 += monotonic_step.powf(2.0) * oscillator_error_variance;

与第一个使用情形一样,组件开发者只需编写大约 10 行代码即可引入此可自定义的参数,并获得相同的实用属性。在此使用情形中,清单未提供默认值(因为组件作者不知道振荡器可能被使用的好坏程度),因此,如果没有其他输入提供该值,组装过程将失败并显示信息性错误。

其他用例

前面的部分讨论了我们认为结构化配置的两个非常常见的用例,但同一系统也可用于解决一系列其他简单的配置用例。例如:

  • 禁止测试标志。某些组件自然会表现出使集成测试难以进行的行为(例如,时间系统在时间源发生故障后会进入 5 分钟的冷却期)。在集成测试或端到端测试期间,可以使用仅限测试的标志来禁止这些行为。

  • 组件实例创建时配置。有时,在即将创建组件实例时,才能确定组件的相应配置。可以定义配置键,以便创建新组件实例的父级在创建时提供配置值。这既可以取代 CFv1 中 launch args 的使用,也可以支持为不同的角色定制组件的多个实例(例如,为每个有效账号启动一个 AccountHandler 实例)。

  • 简单的 A/B 测试。有时,为了创建最佳设计,需要同时对两个或更多个选项进行实验,每个选项都使用不同的设备组。有权访问服务器端实验系统的产品可以使用此实验系统来驱动一个或多个组件的配置值,从而执行 A/B 测试。

哲学

结构化配置的设计基于四种一致的理念:

  • 简单。该系统必须易于使用,并且设计得易于理解和分析。简单性有助于提高采用率,并支持“可靠”和“安全”理念。
  • 可靠。可靠性可简化开发和调试,并允许系统用于对 Fuchsia 设备运行至关重要的组件。
  • 安全。安全性直接符合 Fuchsia 的平台目标,并允许系统用于对 Fuchsia 设备安全至关重要的组件。
  • 可测试性。配置会向组件添加新的输入,因此可能会出现新问题。开发者必须能够全面测试组件对这些新输入的响应。

范围

结构化配置并非旨在成为解决所有配置问题的通用解决方案。基于 Fuchsia 构建的产品的配置需求范围非常广泛,它们带来的要求往往相互冲突 - 试图满足所有需求的系统要么非常复杂(从而违背了简单哲学,并威胁到其他三种哲学),要么过于简单和通用(从而无法提供可靠且安全的哲学所必需的数据存在性、稳定性、含义和可审核性保证)。

相反,结构化配置旨在轻松而妥善地解决一组常见用例。组件可以使用基于文件和基于 API 的通用解决方案来解决其他配置问题。具体而言,结构化配置并不打算解决以下问题:

  • 任意大小和复杂程度的配置数据。使用通用工具难以审核和选择性地限制大型复杂数据,这与安全理念相冲突。此类数据通常需要额外的特定于网域的解读和验证,从而引入配置系统无法识别的故障模式。最后,来自多个来源的大型复杂数据难以组合,这限制了组装工具的实用性以及在系统运行时覆盖配置的能力。

    • 需要大型复杂配置数据的组件应改为从文件中读取此数据。
  • 频繁更改的配置数据。必须多次将频繁变化的数据传递给组件,而不是仅在组件启动时传递一次。这会引入新的故障模式,并增加组件的复杂性,与可靠且简单的理念相冲突。测试难度也大大增加。请注意,频繁变化的数据不符合下文对“配置”的定义。

    • 需要频繁更改的配置数据的组件应通过 FIDL 协议接收此数据。
  • 由其他组件(父组件和管理员组件除外)设置的配置数据。结构化配置支持父组件为其创建的组件设置配置,并支持管理组件为设备设置所有可变配置。结构化配置无法提供所需的访问权限控制,以允许任意组件跨特定组件设置配置子集。

    • 需要由系统中的其他任意组件设置配置数据的组件应通过 FIDL 协议接收此数据,并使用服务路由限制访问权限。
  • 由最终用户控制的配置数据。由最终用户(而非开发者或管理员)控制的配置需要用户界面。此界面未能通过“由其他组件设置的配置数据”测试,可能也未能通过“频繁更改的配置数据”测试。

    • 由最终用户控制的配置应包含在 fuchsia.settings 中,或者使用类似的方法,通过 FIDL API 分离前端和后端组件。

“配置”的含义

组件会使用各种不同的输入。这些输入中的大多数可能会改变组件的行为,但只有部分输入应被视为“组件配置”,而不是更一般的“系统状态”或“输入数据”。

就本 RFC 而言,我们将“配置”视为组件实例用于根据其启动的上下文(例如产品、主板、 build 类型、渠道、监管区域或集成测试领域)调整其操作的输入。配置值在组件实例的生命周期内保持不变,并且通常在某些设备上保持不变。配置值通常由开发者、维护者或管理员(而非最终用户)设置。

数据类型

结构化配置适用于每个组件具有适量有界限且定义明确的配置键。以这种方式限制配置的范围和大小有助于鼓励使用有完善文档、可测试、可变性最小且易于审核的配置。它还支持自动组合来自多个来源的配置。这些明确定义的键值对构成了“结构化配置”中的“结构”。

由于上述“任意大小和复杂程度的配置数据”限制,我们不打算支持字节或任意长度的字符串。最初支持的数据类型集将在后续工作中定义,但至少包括:布尔值、整数、有长度限制的字符串以及这些数据类型的列表(其中列表长度有上限,并且列表中的所有条目都是以原子方式提供的)。

未来非常需要支持枚举,但由于验证配置值的所有位置(在组装期间和运行时)都必须能够访问有效枚举器的集合,因此这会更加复杂。如果系统能够在枚举器名称和值之间进行转换,开发者工具会更加符合人体工程学,但这会增加进一步的复杂性。

未来可能需要支持可组合的列表(即条目可由多个配置源提供的列表),但这会增加复杂性。对于原子类型,操纵配置键意味着只需替换其值,但对于可组合的列表,则需要更复杂的操作,例如附加、插入或移除值,或者合并列表片段。这些操作需要在汇编期间和运行时都获得支持,并且会创建新的故障模式,例如因插入项会导致列表长度超出最大长度而无法插入项。系统需要区分以下两种列表:条目顺序对使用者没有影响的列表(因此应使用某种规范顺序执行配置哈希)和条目顺序有影响的列表(因此必须明确进行配置操作才能保持该顺序)。

组件框架版本

结构化配置仅支持组件框架 v2 组件。这是一个涉及许多领域的大型项目,在 2022 年初之前不会准备好进行广泛采用。支持两种不同的框架会大幅增加范围,并将结束日期推迟到组件框架 v1 预期弃用日期之后。

设计

概览

本子部分简要介绍了系统的总体设计。以下各小节将更详细地介绍此处介绍的各个步骤。

该设计总结如下图所示:

此图展示了系统中的顶级互动,如下文所述。

可配置数据的每个元素都是一个键值对。组件作者在组件清单中声明其组件的配置键(以及可选的默认值),然后构建系统和组件框架共同负责在启动时将配置值传递给组件。

构建和组装过程会生成一个配置定义文件,其中包含配置键数据类型和名称,还会生成一个配置值文件,其中包含每个配置键的值和可变性。在初始实现中,这两个文件都放置在与组件相同的软件包中(或者对于在 pkgfs 可用之前运行的组件,放置在单独的 bootfs 文件中)。如需了解相关理由以及有关未来方向的讨论,请参阅下文中的替代方案 3

每次启动组件实例时,组件框架都会检查是否存在配置定义文件。如果存在配置定义文件,组件框架会将配置值文件中的静态值与父组件实例提供的任何值以及配置替换服务提供的任何值相结合,同时遵守配置值文件中的可变性限制。组件框架会在启动时使用最适合运行时的技术将这些组合值传递给新的组件实例。

组件框架公开了一个新的 FIDL 接口,开发者工具或产品组件可以使用该接口查询配置值并定义新的替换项。使用此接口设置的新值将在组件实例下次启动时被提取。

配置值的统计信息包含在检查中,因此也包含在快照中,以帮助进行调试。通过 Cobalt 为每个组件报告与组装时不同的配置值的哈希,以便可以独立评估具有不同配置值的设备群组。

配置定义

组件作者可以在其组件清单中定义配置键(每个配置键都具有数据类型,并且可以选择性地具有默认值)。未来的工作将定义确切的语法,但从概念上讲,这可能如下所示:

{
  program: {
    ...
  },
  config: {
    enable_frequency: {
      type: "boolean",
      default: "false"
    },
    oscillator_error_std_dev_ppm: {
      type: "uint8"
    }
  },
  ...
}

在初始实现中,所有键都定义在每个组件的单个“扁平”命名空间中,但我们将配置键中的合法字符限制为 [-_a-z0-9]。如果将来认为嵌套或分组的配置键有价值,则可以使用以英文句点或斜杠分隔的语法来支持和引用它们。

某些运行时(例如 Cast 和 Web)将自动生成 CFv2 ComponentDecl,而不是从组件清单生成。最初,这些运行时将不支持结构化配置。

在清单中包含配置部分会导致组件 build 规则生成一个库,其中包含相应配置的 FIDL 表定义,以及在运行时从 runner 提供给组件的输入填充此表所需的代码。然后,组件的实现可以导入此库,并使用表中的字段来控制其行为。

组件清单描述了组件的需求以及组件必须满足的合同。在此 RFC 之前,*_binary 构建规则不依赖于清单的内容,而 fuchsia_component 构建规则对二进制文件具有可选依赖关系。必须对 build 规则进行一些更改,以避免出现循环依赖。

在某些情况下,多个组件会使用单个二进制文件;这些情况需要进行一些重构才能使用结构化配置。一种方法是确保所有组件通过包含一个通用 CML 分片来定义相同的配置。另一种方法是将组件合并为单个定义,并使用结构化配置来描述之前使用不同清单表达的行为差异。

构建、组装和发布

构建、组装和发布流程负责为每个可配置的组件生成两个文件。这两个文件都与组件放置在同一软件包中(未来将扩展为支持通过其他软件包提供值,请参阅此替代方案)。

配置定义文件

此文件包含每个配置键的以下信息:

  • FIDL 字段编号
  • 字段名称
  • 数据类型

所有这些信息都可在组件清单中找到,因此可以在组件构建过程中生成此文件。最好还计算并包含配置定义文件中所有信息的哈希,以用作配置版本 ID。

请注意,我们将配置定义描述为“文件”,以简化对其使用的讨论,但实现可能会将此信息包含在已编译的组件清单(即 *.cm 文件),而不是作为单独的文件。

配置值文件

此文件包含每个配置键的以下信息:

  • FIDL 字段编号
  • 配置值
  • 可由 ChildDecl 更改(布尔值)
  • 可由 override 修改(布尔值)

此 RFC 未定义文件的格式。

在成熟且可扩缩的程序集系统中,多个不同的参与者可能希望为一个或多个键指定或限制此信息。例如:组件作者、主板启动工程师、平台边界所有者、产品集成者或安全审核人员。启用此功能所需的部分工具目前正在通过 DPISPAC 平台路线图条目进行开发,并且此 RFC 未指定如何使用这些工具来生成配置值文件。

在过渡期间,由于只有少量组件针对少量键使用结构化配置,因此我们将在源代码库中手动维护这些文件。如果基于树构建的产品需要修改平台组件的配置,它将通过替换平台软件包中的配置值文件来实现。

程序集进程必须验证配置值文件的内容是否与相应的配置定义文件一致,即它们是否包含相同的字段编号集,并且数据类型是否一致。

VBMeta

最好在基于同一映像生成的正式版本之间实现配置差异化。例如,使用开发密钥签名时可以启用调试功能,而为正式版签名时则可以停用调试功能。使用相同的映像可降低生产版本和开发版本之间出现意外差异的可能性,并有可能减少我们维护的映像数量。

未来,我们打算通过允许在 vbmeta 中替换某些配置值来支持此功能。VBMeta 是 Fuchsia 的启动时验证实现所使用的中央数据结构,包含 Fuchsia 版本中包含的软件的元数据。由于 vbmeta 已签名,因此被 vbmeta 替换的配置值将受到经过验证的执行的保护。

当可以从同一映像创建多个版本,并且这些版本可以表现出有意义的不同行为时,通过 vbmeta 进行配置将增加价值。这反过来又要求多个基础架构组件已与结构化配置集成,因此我们从初始最小范围中排除了通过 vbmeta 进行的配置。

组件启动

每次启动组件实例时,组件管理器都会解析配置定义文件和配置值文件,并使用它们的内容来确定多个来源中的哪一个可以提供配置值。下文将更详细地介绍这些来源。组件管理器会合并来自允许来源的贡献,以生成最终的一组配置值。

确定配置值后,组件管理器会将这些值传递给 runner。运行程序以最符合运行时习惯的方式将配置值传递给新启动的组件。在许多情况下,我们预计这将传递一个新的 procarg,其中包含指向包含 FIDL 表的 VMO 的句柄。在某些情况下,可能需要将配置作为 key=value 命令行实参传递。

组件可以信任框架始终会在启动时为其清单中声明的每个配置键提供配置值,如果未提供,则应因出现严重错误而失败。组件绝不应定义内部默认值来适应缺失的配置 - 这样做可能会导致运行时错误,从而改变原本打算在组装时修复的行为。

包含发布版本中的值

最简单的情况是使用配置文件中提供的值。如果配置值文件表明某个组件的任何配置键都不能通过 ChildDecl 或通过替换进行更改,则情况始终如此,并且组件管理器不需要查询下文讨论的配置替换服务。

此流程如下图所示:

此图展示了使用发布版本中的配置在组件启动期间的互动,如上文所述。

使用 ChildDecl 中的值

我们扩展了 ChildDecl,使其包含配置键值对的向量,从而有效地取代了 CFv1 在启动新组件实例时提供命令行实参的功能。ChildDecl 可由以下对象提供:向组件集合添加新实例的父级、使用 realm 构建器构建测试环境的测试,或父级组件清单的作者。

如果 ChildDecl 中存在配置,组件管理器会执行以下操作:

  • 验证配置定义文件中是否存在 ChildDecl 键,以及这些键是否具有正确的数据类型。
  • 验证 ChildDecl 键是否可通过配置值文件中的 ChildDecl 进行更改。
  • 使用 ChildDecl 值填充提供的键。
  • 使用配置值文件填充剩余的键。

如果找不到任何键、键包含错误的数据类型,或者键无法通过 ChildDecl 进行更改,组件管理器会记录一条信息性错误并返回失败。

此流程由父组件调用 CreateChild 来启动,如下图所示:

此图展示了使用父组件中的配置在组件启动期间发生的互动,如上文所述。

使用来自替换服务的值

当配置值文件声明一个或多个配置键“可被替换”时,组件管理器会向配置替换服务发出 FIDL 请求,以获取替换值。此请求包含新组件实例的组件实例 ID 和配置定义文件的句柄。响应包含一组要应用的替换配置值(可能为空)。

配置替换服务的典型实现是一个新的“配置替换管理器”组件,但配置替换服务通过组件拓扑(类似于存储功能,并使用字符串进行参数化)作为功能进行路由,因此拓扑的不同部分可以使用由替换服务 API 的不同实现提供的配置替换。

配置替换管理器维护一个“已替换”的配置键值对数据库,可通过 FIDL(如下所述)进行编辑。此数据库中的每个条目都在组件实例级别定义,并按组件实例 ID 进行索引(如需更多理由,请参阅此替代方案)。每个替换条目要么存储在磁盘上以在电源周期之间保持不变,要么保留在内存中以在当前电源周期的剩余时间内保持不变。持久性是在创建条目时指定的。

在收到配置替换请求后,配置替换管理器会执行以下操作:

  • 检查替换数据库中是否有匹配的条目。
  • 验证被替换的键是否在配置定义文件中存在,以及是否具有正确的数据类型。
  • 返回匹配的键值对。

如果找不到键或数据类型不正确,配置替换管理器会记录一条信息性错误并删除数据库条目(如果在设置配置替换后下载了新的组件版本,可能会出现这些情况)。

在收到配置替换响应时,组件管理器会执行以下操作:

  • 验证配置值文件中的替换项是否可变。
  • 使用替换后的值填充替换后的键。
  • 使用配置值文件填充剩余的键。

如果组件管理器未从配置替换服务收到有效响应,则组件启动失败。

如果配置替换管理器实现了配置替换服务,则此流程如下图所示:

此图展示了使用来自替换项的配置在组件启动期间的互动,如上文所述。

请注意,配置键以字符串形式存储在替换数据库中。随着组件的不断发展,配置字段集可能会经常发生变化,但只要键名称和数据类型不发生变化,配置替换就会保持有效。作为一种优化,上次看到的配置版本 ID 和字段编号也可以缓存在数据库中。

值选择摘要

将这些流程组合在一起后,组件管理器会为每个配置键选择一个值,如下所示:

  1. 如果该键可通过替换进行更改,并且配置替换服务返回了匹配的替换,则使用此值。
  2. 否则,如果键可由 ChildDecl 更改,并且 ChildDecl 中提供了值,则使用此值。
  3. 否则,使用配置值文件中的值。

配置 FIDL 接口和替换数据库

配置替换管理器公开了两个可用于与其替换数据库交互的 FIDL 服务:

  1. 一种可以执行以下操作的服务:
    • 读取所有配置替换项。
    • 创建和删除保存在内存中但在配置替换管理器重新启动后不会保留的配置替换项。
  2. 一种可以执行以下操作的服务:
    • 读取所有配置替换项
    • 删除所有配置替换项
    • 创建保存在内存中但不会在配置替换项管理器重启后保留的配置替换项
    • 创建存储在磁盘上并在配置替换管理器重启后保持不变的配置替换

第一个服务不能对配置引入长期更改,可能对自动化端到端测试有用。这两项服务都比较敏感,其使用情况已列入许可名单。

我们将推出一个使用第二个服务的 ffx 插件,让开发者能够查询和修改测试设备上的配置。特定版本中可通过 FIDL 编辑的配置键集是一个政策问题,而非由本 RFC 定义,但我们认为在工程 build 中,几乎没有必要限制通过 FIDL 编辑配置。

即使在创建时,系统会根据配置定义文件验证替换数据库中的条目,但随着组件被移除或升级到包含不同配置键的新版本,条目可能会逐渐失效。我们通过以下几种垃圾收集措施来解决此问题:

  • 替换数据库中的每个条目都可以设置过期时间,过期后系统会将其删除。在生产系统中,我们会考虑强制设置过期时间。
  • FIDL 服务包含用于轻松删除冗余条目的方法,包括删除组件实例的所有条目和删除整个数据库。
  • 在未来的工作中,我们将研究在软件包被移除或升级时接收来自软件交付的通知,以便删除或重新验证相应的替换条目。

诊断

了解组件正在使用的配置对于调试问题至关重要。将运行不同配置的设备群组的指标分开,对于评估小规模发布和 A/B 研究非常重要。

大多数设备上的大多数配置键将使用在组装期间设置的值。对于这些配置,了解发布版本(或应用可更新时的软件包版本)即可推断出配置值。每次启动组件实例时,组件管理器都会计算两个哈希:一个是对 ChildDecl 设置的所有配置键和值计算的“ChildDecl 配置哈希”,另一个是对替换项设置的所有配置键和值计算的“替换配置哈希”。如果未设置任何字段,则相应的哈希值为零。

如果使用的不同配置数量适中,这些配置哈希足以识别每个群组。如果可能存在大量配置值(例如,开发者在测试期间设置任意网址),配置哈希可能不足以确定配置值,但仍可指示运行的配置与其程序集不同的设备,以及运行的配置与其同类设备不同的设备。

日志记录

组件管理器会记录组件配置的每次计算的统计信息,但不会记录原始配置值。配置替换管理器会记录所有用于修改替换数据库的 FIDL 请求。

检查和快照

组件管理器的检查数据包含有关每个组件实例配置的统计信息,有助于进行调试。这包括从每个来源设置的配置值的数量以及上文介绍的配置哈希。由于快照包含检查数据,这意味着所有正在运行的组件实例的配置哈希都包含在快照中。

如果配置键对组件的运行和调试至关重要,则组件可以选择将这些配置值包含在自己的检查数据中。

钴蓝

组件管理器会在启动时(通过 Runner)将这两个配置哈希发送到组件实例。我们扩展了 fuchsia.metrics.MetricEventLoggerFactory 以在创建新的 MetricEventLogger 时接受这些哈希。

Cobalt 会将这些配置哈希与现有的 SystemProfile 字段结合使用,以定义组件的运行环境,从而允许单独分析具有不同配置的设备的指标。标准阈值仍适用,因此只有当一定数量的设备共享同一配置时,指标才可用。

实现

在开发结构化配置的过程中,我们会选择少量“抢先体验”组件作为客户端,同时语法和工具也会不断发展。

实现将分为三个阶段,每个阶段都提供更强大的功能:

  • 第 1 阶段:静态价值
    • 在此阶段之后,您将可以创建配置定义(可能使用非最终的语法和工具)、将配置值放置在软件包中,并在启动时将这些值传递给组件实例。
    • 此阶段提供了一种基本方法,可让抢先体验组件的行为在不同产品或 build 类型之间有所不同。
  • 第 2 阶段:父级值
    • 在此阶段之后,还可以限制封装的配置值的可变性,在 ChildDecl 中指定配置值,并在启动时将这些值传递给组件实例。配置的定义应通过组件清单进行,并使用接近最终的语法。
    • 此阶段可解除对配置和使用情形的集成测试的限制,这些配置和使用情形需要父组件为其子组件提供配置。
  • 第 3 阶段:替换值
    • 在此阶段之后,还可以通过 FIDL 接口或使用该接口的 ffx 插件来设置和读取配置替换项,以便在启动时将这些值传递给组件实例,并在 Cobalt 中按配置隔离指标。
    • 此阶段将完成此 RFC 中定义的工作,并解除对本地开发者测试、端到端测试和进一步产品集成的限制。

性能

此 RFC 引入了一个新组件,会产生(适度的)CPU、内存和存储费用。所有这些都会随着使用结构化配置的组件数量和配置键的数量而扩展。

计算配置值会导致启动可通过替换来更改配置的组件时出现短暂延迟,这是因为需要额外的 FIDL 调用和额外的文件读取。我们会监控此费用,并在必要时优化配置替换管理器的实现。

安全

许多组件可以使用结构化配置来控制其行为的许多不同方面。如果攻击者能够修改配置,则可能会以多种不同的方式危害设备的安全性。例如:

  • 启用调试输出以泄露用户信息。
  • 将网络请求重定向到攻击者控制的服务器。
  • 启用实验性功能并利用其实现中的漏洞。

此设计包含多项旨在防范这些攻击的功能:

  • 可配置数据的范围受到限制,并且每个元素都有明确的定义,因此更易于审核。
  • 在对发布版本进行签名时,会为每个配置键定义一个值,这些值会纳入经过验证的执行范围。
  • 在对版本进行签名时,系统会针对每个键和每个突变机制,单独设置在系统运行时修改配置键的能力。这些可变性由经过验证的执行涵盖。
  • 可变性和默认值是在组件级别而非组件实例级别定义的。只有在为配置键设置了“可变(通过 ChildDecl)”或“可变(通过替换)”时,才能创建具有不同配置的组件的新实例。
  • 临时和永久更改配置的功能以单独的 FIDL 服务形式公开。
  • 用于更改配置的 FIDL 服务由许可名单控制。
  • 配置数据由经过充分审核的现有 FIDL 绑定(而非应用专用逻辑)进行解析。
  • 配置替换管理器将成为一个有利可图的目标,因此我们将要求对其实现进行安全审核。

隐私权

结构化配置并非旨在存储用户设置(这些设置具有不同的稳定性、持久性、访问和时间安排需求),因此配置值绝不应包含用户生成的数据。这将在开发者指南中明确说明。

对于父组件来说,将 PII 配置传递给动态创建的子组件可能很有用(例如,新组件实例应使用的硬件 UID 或网络地址)。如果不加注意,此信息可能会通过 ChildDecl 配置哈希泄露到日志或指标中。这将在详细设计阶段解决,但有多种选择(例如,可以在配置定义中标记敏感字段,然后在将其纳入哈希之前进行加盐处理)。

测试

测试多个配置值对于验证正确性非常重要。此设计可在所有阶段测试不同的配置值:

  • 单元测试组件测试可以手动构建 FIDL 配置结构(即组件运行器在正常运行下提供的结构),并将其传递给使用配置的方法。
  • 集成测试可以在构建测试 Realm 时,通过 Realm 构建器(使用 ChildDecl)为每个组件实例提供配置。
  • 端到端测试可能会使用配置 FIDL 接口从主机设备设置其他配置。可能需要执行额外的工作来暂停正常启动序列,以避免组件启动时出现竞态条件。
  • 手动测试可以使用 ffx 命令通过配置 FIDL 接口方便地设置其他配置。
  • 未来的工作将考虑对组件的配置进行自动化模糊测试。

有多种不同的选项可用于避免从封闭式集成测试中隐式依赖于配置替换服务。例如,集成测试软件包可以构建为使用不允许替换的配置值文件,并且测试 realm 的构建可以避免路由配置替换服务。

结构化配置本身的实现将使用标准最佳实践进行测试,包括单元测试和组件管理器 - 配置替换管理器 - 运行程序交互的集成测试。

文档

此 RFC 获得批准后,我们将在 /docs/concepts 中发布一份文档,说明 Fuchsia 上可用的配置机制及其关系。

一旦语法稳定下来,并且结构化配置实现准备好更广泛地采用,我们将发布开发者指南和参考文档。

随着开发者开始使用结构化配置并发现效果良好的模式,我们将开发有关最佳实践和推荐样式的文档。

考虑的替代方案

在配置替换管理器中实现配置合并

此 RFC 的早期修订版将合并不同组配置数据的逻辑放在组件替换管理器(当时称为配置管理器)内,而不是组件管理器内。

该设计会缩小组件管理器的范围,但组件管理器和配置管理器的范围都会不太明确:

  • 组件管理器需要充分了解配置,才能知道何时调用配置管理器,但又不够了解配置,无法合并值。
  • 配置管理器除了负责维护数据库之外,还负责一些业务逻辑,以向组件呈现配置。

该设计还会增加需要 FIDL 调用的情况数量。

在组件管理器中实现配置替换数据库

此 RFC 将维护配置替换数据库的责任分配给了一个新组件:配置替换管理器。另一种方法是在组件管理器内部执行此功能。

此替代方案本可以消除 FIDL 调用和一些故障模式,但会增加组件管理器的复杂性,并且是组件管理器首次需要持久保存自己的数据,而不是仅仅为其他组件分配存储空间。这种存储使用方式会引发额外的安全问题,并需要在 CF 中使用新的基础设施。

通过中央软件包交付配置

此 RFC 将组件的配置值放在组件的软件包中。另一种方法是将所有组件的所有配置都放在一个配置软件包中,类似于 CFv1 中 config-data 软件包的设计。

我们之所以更倾向于采用去中心化方法,而不是集中式方法,主要有以下两个原因:

  1. 未来,Fuchsia 需要运行基础映像中未知的组件(例如,由于应用更新)。这些未知组件的配置无法在中央软件包中分发,因此需要不同的解决方案。
  2. 以原子方式交付二进制文件及其配置,可让我们更确信它们的一致性,尤其是在软件包可能独立于基础映像进行更新时。导致组件可用但其配置不可用,或者导致组件及其配置不兼容的故障模式较少。

如 RFC 正文中所述,我们目前的去中心化方法是将配置值放置在与它们所应用的组件相同的软件包中。这意味着,在组装时更改组件的配置会更改其软件包的根哈希。从长远来看,这是不可取的,因为它不支持一个组织(例如 Fuchsia 平台维护者)发布和签名组件,而另一个组织(例如产品集成者)提供配置。

未来,我们设想每个软件包都将包含发布组织设置的默认配置值,并声明哪些子集的值可以通过可能由其他组织发布的其他软件包进行替换(例如,平台组件可以选择哪些配置“旋钮”可供产品集成商访问)。未来的工作将定义这种“不同的软件包”的性质 - 选项包括元软件包、伴随软件包或封装软件包。

按组件网址和别名进行的索引配置替换

此 RFC 会按组件的实例 ID 对配置替换项进行索引。实例 ID 用于在组件框架中为其他持久性资源(例如隔离的持久性存储空间)编制索引,因此是为配置替换编制索引的理想选择。

不过,目前组件实例 ID 是在 build 时通过索引文件手动分配的。这意味着,对于组件集合中的组件实例或通过应用更新而非在基本映像中引入的组件实例,实例 ID 和结构化配置替换不可用。

作为替代方案,我们考虑了按组件网址和 moniker 编制索引的配置替换。这样可以避免实例 ID 的限制,但组件网址和 moniker 可能会随着系统重构而发生变化,因此除了在处理组件框架持久性资源时造成不一致之外,这种替代方案还会带来新的稳定性问题。

我们更倾向于在未来的 RFC 中通过更改实例 ID 的设计来解决实例 ID 的限制。

支持在组件级层替换配置

此 RFC 按组件的实例 ID 对配置替换项进行索引,这意味着需要单独替换组件的每个实例。

未来,除了在组件实例级别支持替换之外,还可能需要在组件级别支持替换。在组件实例化多次或组件实例事先未知的情况下,这会特别有用。

目前,我们还没有明确定义的规范且稳定的组件标识符,可用于组件级替换。由于我们尚未发现对组件级替换的具体需求,因此我们推迟了该功能的纳入。

对上一个替代方案中引用的实例 ID 设计所做的更改,除了组件实例标识符之外,还可能会提供稳定的组件标识符(例如,实例 ID 可以是组件的自认证标识符,通过某些怪异数据库与实例的别名的某个函数相结合,以处理拓扑中的更改)。这样可以简化未来添加组件级替换项的操作。

使用 FIDL 定义配置键

此设计使用 JSON 在组件清单中定义配置键。其中一些信息用于构建 FIDL 表。另一种方法是在 .fidl 文件中定义配置键。

首先,我们注意到 FIDL 工具链有意由前端和后端组成,两者之间通过 IR 分隔开来 - 即使输入未以 FIDL 语言定义,也可以使用 FIDL 技术。

决定使用 .cml 而不是 .fidl 的主要原因是开发者体验:

  1. 组件清单是开发者向框架描述其组件需求的地方,而定义配置键符合此定义。维护单个文件比添加新文件的工作量要少。
  2. 用于定义配置的语法和数据类型是完整 FIDL 语言的一小部分(请参阅范围)。要求使用 FIDL 语法,但仅支持部分功能,这可能会令人困惑和沮丧。
  3. 配置需要的信息无法用 FIDL 语言表示(目前是表格字段的默认值,未来可能还会增加其他限制)。此信息可以存储在自定义属性中,但这会与 FIDL 语言的其余部分不一致,并让开发者感到困惑。

如果 JSON 配置定义中使用的概念在 FIDL 语言中存在,我们将使用一致的语法。例如,数据类型名称将保持一致。

cmc 已经可以从 JSON 组件清单构建 FIDL 表,因此从清单解析配置所需的工作量不大。

支持组件之间的配置路由

组件框架中的许多资源都支持从一个组件实例到另一个组件实例的路由,例如协议、目录和存储功能。自然而然地,我们需要支持组件之间的配置路由,以便子组件可以使用来自父组件的一些配置值。

此初始设计不支持配置路由,以避免引入新的版本控制难题。如果我们支持在一个组件中定义配置,并在另一个组件中使用该配置(这两个组件在不同的时间点打包,可能是因为它们是在不同的代码库中定义的,也可能是因为它们不是以单体形式交付给设备的),那么我们无法再保证编译到组件中的配置定义与用于配置值的定义相匹配。我们会在这两个组件之间引入新的 ABI,但由于此接口将在 PDK 而不是 IDK 中表达,因此我们无法使用 RFC-0002 中定义的流程来管理版本兼容性。

随着 PDK 和树外组件组装工作的继续,我们可能会更多地讨论组件界面和版本控制,并且在这些方面更加成熟之前,我们会推迟组件之间配置的路由。与此同时,如果需要,可以使用程序集工具在多个软件包中提供一致的配置值。

我们通过支持父组件在创建时动态配置子组件,引入了这种跨组件兼容性问题的更有限版本。我们认为,这些情况在短期内不太可能造成问题,因为这种可变性必须选择启用,并且配置字段通常会由子级明确设计,供父级使用。

支持可重用库的透明配置。

许多组件都是使用支持某种形式的配置的可重用库构建的。如本 RFC 中所定义的结构化配置可用于提供此配置,但只能以手动方式进行;使用库的每个组件都需要在其自己的清单中声明匹配的配置键,然后在初始化时将配置值路由到库中。运行程序也存在类似情况,即运行程序可以直接控制其运行的组件所使用的库中的可配置行为。

另一种方法是为库支持更“透明”的配置,其中库可以声明其配置键,并且可以在运行时直接使用配置值。组件只需声明其对库的使用情况,即可让配置提供程序为该组件中的库实例设置配置值。

与组件之间的路由(如上文所述)一样,这需要就不同软件制品(例如树外组件使用的平台库)的配置键达成更广泛的共识。这可能需要对配置进行更正式的定义,以便更可靠地保证配置版本之间的向前和向后兼容性。库的透明配置还会带来一个问题,即如何向组件提供多组配置值。

虽然从长远来看,库的透明配置是一项理想的功能,但我们会将这项工作推迟到此 RFC 中定义的更简单、更“私密”的配置系统正常运行之后再进行。

支持在组件启动后更新配置

此设计仅在组件实例启动时提供组件配置。如果要在组件启动后修改配置,则配置在下次启动之前不会到达组件。另一种方法是通过 FIDL 定期向组件传送配置。

决定专注于组件启动与“配置”绑定到组件生命周期的定义相符,同时也简化了组件作者的实现:

  1. 仅接收一次配置的组件实例可以在组件启动期间初始化其所有配置驱动的资源。必须定期接收新配置的组件还需要定期分配或收集资源,因为配置的更改需要这样做。
  2. 必须定期接收新配置的组件需要一些定期或异步处理,否则可能不需要这些处理。
  3. 必须定期接收新配置的组件还必须通过其诊断连接记录配置中的任何更改,例如通过创建新的 MetricEventLogger
  4. 必须定期接收新配置的组件具有必须处理的其他故障模式,例如 FIDL 连接终止和超时。

如今,启动后需要更新配置的情况非常有限:在发布版本中或通过 ChildDecl 提供的配置值在组件启动后无法更改。FIDL 接口可以在启动后引入更改,但最初只会用于开发者工具,这些工具可以轻松提供在更改组件配置后自动重启组件的方法。

未来,某些特定于产品的组件可能更倾向于实现上述复杂性,而不是重新启动。如果是这样,我们将考虑为组件提供选择性接收 FIDL 更新的功能。

在先技术和参考资料