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 提供替代方案(用于在启动子组件时提供参数),可以简化向组件框架 v2 的迁移。
- 通过使用功能标志,您可以更安全地将新的平台和产品功能部署到生产环境。
- 可以降低与开发、测试和维护可配置行为相关的成本。
利益相关方
此 RFC 的利益相关方包括 Fuchsia 工程委员会、其范围扩大的组件平台团队(即组件框架和软件交付)以及负责改进流程的团队(PDK 和安全)。
系统的潜在客户也很重要,但由于此 RFC 并未提出通用配置系统,因此不应期望它满足所有潜在客户的所有配置需求。
教员:abath
审核者:geb(组件框架)、wittrock(SWD)、aaronwood(SWD 和 PDK)、ampearce(安全)
咨询人员:ddorwin、hjfreyer、ejia、thatguy、shayba、jamesr、ypomortsev、crjohns、surajmalhotra、curtisgalloways、adamperry
共享:此设计的早期草稿或上述文档已在组件框架、安全、软件交付、Cobalt 和 PDK 团队中进行了审核。我们还与潜在客户进行了几次额外的讨论。
用例
常见用例:功能标志
向已部署的系统添加新功能是一项冒险的举措;新设计和新代码有时会包含错误或无效的假设,这些错误或假设直到部署到生产环境后才会被发现。许多其他平台通过使用“功能标志”来降低此风险:功能标志是用于控制功能是否处于启用状态的布尔值配置参数。功能标志具有以下几项优势:
- 新软件版本的部署可以与新功能的激活解耦;同一软件版本中添加的功能不必同时启用。
- 您可以在启用新功能之前对其正确运行情况进行全面测试。
- 您可以使用发布渠道、百分比发布或两者结合的方式,逐步在设备上启用各项功能。
- 如有必要,可以安全快速地停用每项功能;停用功能无需回滚到较低的软件版本。
我们来举一个最近的功能示例,该功能可以通过功能标志获得益:在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 不指定政策。不过,出于安全考虑,除非明确请求并经过审核,否则正式版(例如“user”)发布版本很可能不允许任何运行时变异。
开发者可以在系统以工程版本运行时测试该功能。在系统运行时可以更改哪些配置键是一个政策问题,此 RFC 不指定政策。不过,为了简化开发、测试和调试,工程中的大多数配置键(例如“eng”)版本。 开发者可以使用
ffx
为本地设备启用此功能。例如(使用概念语法):ffx target config set timekeeper.cml enable_frequency=true
。如果政策允许,开发者还可以启用该功能,使其在设备电源周期中保持不变。测试可以同时涵盖“功能启用”和“功能停用”情况。 对于单元测试和组件级测试,这涉及手动构建和注入 FIDL 配置结构体;对于集成测试,则涉及在测试组件启动时提供配置,例如使用 Realm Builder。
构建和汇编工具可以作为平台边界的一部分来控制该功能。目前,我们正在通过 DPI 和 SPAC 计划开发构建和汇编工具。通过这项工作,平台维护者可以控制是否向产品公开每个平台功能。根据政策和功能的风险,平台可以控制功能的发布,也可以将功能的发布委托给产品。对于更复杂的情况,平台可以在系统运行时(即使是在生产环境中)启用功能标志的更改(例如,“user”)版本。这样,产品就可以在个别设备上启用该功能,例如根据实验发布系统或企业管理控制台设置配置值。
标志状态会反映在设备指标中。对于在系统运行期间可以更改标志状态的版本,其值包含在额外的哈希中,该哈希可用于在指标分析期间将启用相应功能的同类群组与停用相应功能的同类群组区分开来。
常见用例:产品/开发板/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 中使用启动参数,又支持针对不同的角色调整组件的多个实例(例如,为每个活跃账号启动一个 AccountHandler 实例)。
简单的 A/B 测试。有时,为了打造最佳设计,您需要并行实验两个或更多选项,每个选项在不同的设备上进行测试。有权访问服务器端实验系统的产品可以使用此实验系统来驱动一个或多个组件的配置值,从而执行 A/B 测试。
哲学
结构化配置的设计遵循以下四种一致的理念:
- 简单。该系统必须简单易用,并且设计上也要简单易懂、易于分析。简单易用有助于提高采用率,并支持“可靠”和“安全”理念。
- 可靠。可靠性可简化开发和调试,并让系统可用于对 Fuchsia 设备运行至关重要的组件。
- 安全。安全性与 Fuchsia 的平台目标直接相关,可让系统用于对 Fuchsia 设备的安全性至关重要的组件。
- 可测试性。配置会向组件添加新输入,因此可能会导致新问题。开发者必须能够全面测试组件对这些新输入的响应。
范围
结构化配置并非适用于所有配置问题的通用解决方案。基于 Fuchsia 构建的各种产品的配置需求非常广泛,它们带来的要求往往相互冲突 - 试图满足所有需求的系统要么非常复杂(从而违背了简单哲学并威胁到其他三个哲学),要么简单且过于笼统(从而无法提供可靠性和安全性哲学所必需的数据存在性、稳定性、意义和可审核性保证)。
相反,结构化配置旨在轻松且妥善地解决一组常见用例。组件可以使用基于文件和基于 API 的常规解决方案来解决其他配置问题。具体而言,结构化配置不打算解决以下问题:
任意大小且复杂的配置数据。使用通用工具难以审核和选择性地限制大量复杂数据,这与安全理念相悖。此类数据通常需要额外的领域专用解读和验证,从而引入配置系统无法感知的故障模式。最后,很难从多个来源组合大型复杂数据,这限制了汇编工具的实用性,以及在系统运行时替换配置的能力。
- 需要大量复杂配置数据的组件应改为从文件中读取此类数据。
频繁更改的配置数据。频繁更改的数据必须多次传递给组件,而不是仅在组件启动时传递一次。这会引入新的故障模式,并给组件带来额外的复杂性,这与可靠且简单的设计理念相悖。测试起来也更难。请注意,频繁更改的数据不符合我们下文中对“配置”的定义。
- 需要频繁更改的配置数据的组件应改为通过 FIDL 协议接收此类数据。
由其他组件(父级组件和管理组件除外)设置的配置数据。结构化配置支持父级组件为其创建的组件设置配置,并支持管理组件为设备设置所有可变配置。结构化配置不提供任意组件在特定组件中设置配置子集所需的访问控制。
- 需要由系统中的其他任意组件设置配置数据的组件应改为通过 FIDL 协议接收此类数据,并使用服务路由限制访问权限。
由最终用户控制的配置数据。由最终用户(而非开发者或管理员)控制的配置需要界面。此界面未通过“由其他组件设置的配置数据”测试,也可能未通过“频繁更改的配置数据”测试。
- 由最终用户控制的配置应包含在
fuchsia.settings
中,或者使用类似的方法,通过 FIDL API 分隔前端和后端组件。
- 由最终用户控制的配置应包含在
“配置”的含义
组件会使用各种不同的输入。其中大多数输入可能会改变组件的行为,但只有部分输入应被视为“组件配置”,而不是更笼统的“系统状态”或“输入数据”。
在本 RFC 中,我们将“配置”视为组件实例用来根据其启动环境(例如产品、开发板、build 类型、渠道、监管区域或集成测试环境)量身定制其操作的输入。配置值在组件实例的生命周期内保持不变,并且通常在某些设备上保持不变。配置值通常由开发者、维护者或管理员设置,而不是最终用户。
数据类型
结构化配置适用于每个组件具有适当数量的边界清晰且定义良好的配置键。以这种方式限制配置的范围和大小有助于确保配置有详尽的文档、可测试、可变性极低且易于审核。它还支持自动组合来自多个来源的配置。这些定义良好的键值对会在“结构化配置”中创建“结构”。
由于上述“任意大小且复杂的配置数据”限制,我们不打算支持字节或任意长度字符串。最初支持的数据类型集将在后续工作中定义,但至少包括:布尔值、整数、长度受限的字符串以及这些数据类型的列表(其中列表长度受限,并且列表中的所有条目都是原子提供的)。
将来支持枚举非常有用,但更为复杂,因为验证配置值的所有位置(在汇编期间和运行时)都必须有权访问一组有效的枚举器。如果系统能够在枚举器名称和值之间进行转换,开发者工具将更加符合人体工学,但这会增加复杂性。
未来可能需要支持可组合列表(即可由多个配置源提供条目的列表),但这会增加很大的复杂性。对于原子类型,操作配置键意味着只需替换其值,但可组合列表需要更复杂的操作,例如附加、插入或移除值或合并列表 fragment。这些操作需要在编译期间和运行时都提供支持,并且会产生新的失败模式,例如因插入项会超出列表长度上限而导致插入项失败。系统需要区分以下两类列表:条目顺序对使用方没有影响的列表(因此应使用某种规范顺序执行配置哈希),以及条目顺序确实重要的列表(因此必须明确地在维护该顺序时进行配置操作)。
组件框架版本
结构化配置仅支持组件框架 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,而不是从组件清单生成。最初,这些运行时不支持结构化配置。
在清单中添加配置部分会导致组件构建规则生成一个库,其中包含该配置的 FIDL 表定义,以及从运行时通过其运行程序向组件提供的输入填充此表所需的代码。然后,组件的实现可以导入此库,并使用表格中的字段来控制其行为。
组件清单同时描述了组件的需求和组件必须遵守的合约。在此 RFC 之前,*_binary
build 规则不依赖于清单的内容,而 fuchsia_component
build 规则对二进制文件具有可选依赖项。为了避免循环依赖项,必须对 build 规则进行一些更改。
在某些情况下,多个组件会使用单个二进制文件;在这些情况下,您需要进行一些重构才能使用结构化配置。一种方法是,通过添加通用 CML 分片来确保所有组件都定义了相同的配置。另一种方法是将这些组件合并到单个定义中,并使用结构化配置来描述之前使用不同清单表达的行为差异。
构建、汇编和发布
build、汇编和发布流程负责为每个可配置组件生成两个文件。这两个文件都放置在与组件相同的软件包中(未来,此功能将扩展为支持通过其他软件包提供值,请参阅此替代方案)。
配置定义文件
此文件包含每个配置键的以下信息:
- FIDL 字段编号
- 字段名称
- 数据类型
这些信息全部可在组件清单中找到,因此此文件可能会在组件构建过程中生成。此外,最好还计算并添加配置定义文件中所有信息的哈希值,以用作配置版本 ID。
请注意,我们将配置定义描述为“文件”,以简化对其用法的讨论,但实现可能会在已编译的组件清单(即*.cm
文件)中,而不是作为单独的文件。
配置值文件
此文件包含每个配置键的以下信息:
此 RFC 未定义文件的格式。
在成熟且可伸缩的汇编系统中,多个不同的参与者可能希望为一个或多个键指定或约束此信息。例如:组件作者、开发板启动工程师、平台边界所有者、产品集成商或安全审核人员。目前,我们正在通过 DPI 和 SPAC 平台路线图条目开发实现此功能所需的一些工具,而本 RFC 并未指定将如何使用这些工具生成配置值文件。
在此期间,虽然只有少数组件使用结构化配置来处理少数键,但我们会在源代码代码库中手动维护这些文件。如果在树外构建的产品需要修改平台组件的配置,则可以通过替换平台软件包中的配置值文件来实现。
汇编过程必须验证配置值文件的内容是否与相应的配置定义文件一致,即它们是否包含一组相同的字段编号且数据类型一致。
VBMeta
最好针对从同一映像生成的正式版本采用不同的配置。例如,在使用开发密钥签名时可以启用调试功能,而在发布签名时可以将其停用。使用相同的映像可降低生产版和开发版之间出现意外差异的可能性,并有可能减少我们维护的映像数量。
未来,我们打算通过允许在 vbmeta 中替换某些配置值来支持此功能。VBMeta 是 Fuchsia 的启动时验证实现所使用的中央数据结构,其中包含 Fuchsia 版本中包含的软件的元数据。由于 vbmeta 已签名,因此由 vbmeta 替换的配置值将由经过验证的执行覆盖。
如果可以从同一映像创建多个版本,并且这些版本可以表现出有意义的不同且实用的行为,则通过 vbmeta 进行配置会很有价值。这反过来又要求多个基础架构组件已与结构化配置集成,因此我们将 vbmeta 配置从初始最小范围中排除。
组件启动
每次启动组件实例时,组件管理器都会解析配置定义文件和配置值文件,并使用其内容确定多个来源中哪个来源可以提供配置值。下文将详细介绍这些来源。组件管理器会组合来自允许来源的贡献,以生成最终的一组配置值。
确定配置值后,组件管理器会将这些值传递给运行程序。运行程序会以适合运行时的最惯用方式将配置值传递给新启动的组件。在许多情况下,我们预计这将会将包含句柄的新 procarg 传递给包含 FIDL 表的 VMO。在某些情况下,可能需要将配置作为 key=value
命令行参数传递。
组件可以信任框架始终会在启动时为其清单中声明的每个配置键提供配置值,如果未发生这种情况,则应会失败并出现严重错误。组件绝不应定义内部默认值来适应缺少的配置;否则,可能会导致运行时错误更改打算在组装时修正的行为。
使用版本中的值
最简单的情况是使用配置值文件中提供的值。如果配置值文件声明组件的任何配置键都不能通过 ChildDecl 或替换项进行更改,则始终如此,并且组件管理器无需查询下文中所述的配置替换项服务。
此流程如下图所示:
使用 ChildDecl 中的值
我们扩展了 ChildDecl
,使其包含配置键值对的矢量,从而有效替换了 CFv1 在启动新组件实例时提供命令行参数的功能。ChildDecl 可以由向组件集合添加新实例的父级提供,也可以由使用 Realm 构建器构建测试环境的测试提供,还可以由父级组件清单的作者提供。
当 ChildDecl 中存在配置时,组件管理器会执行以下操作:
- 验证 ChildDecl 键是否存在于配置定义文件中且具有正确的数据类型。
- 验证 ChildDecl 键是否可由配置值文件中的 ChildDecl 修改。
- 使用 ChildDecl 值填充所提供的键。
- 使用配置值文件填充其余键。
如果找不到任何键、键包含错误的数据类型或无法由 ChildDecl 修改,组件管理器会记录一条信息性错误并返回失败。
此流程由调用 CreateChild 的父组件发起,如下图所示:
使用来自替换服务的值
当配置值文件指明一个或多个配置键可“通过替换进行更改”时,组件管理器会向配置替换服务发出 FIDL 请求,以获取替换值。此请求包含新组件实例的组件实例 ID 和配置定义文件的句柄。响应包含一组(可能为空)要应用的替换配置值。
配置替换服务的典型实现是新的“配置替换管理器”组件,但配置替换服务会作为功能通过组件拓扑进行路由(类似于存储功能,并在这种情况下使用字符串进行参数化),因此拓扑的不同部分可以使用替换服务 API 的不同实现提供的配置替换项。
配置替换项管理器会维护一个包含“被替换”配置键值对的数据库,该数据库可通过 FIDL 进行修改,如下所述。此数据库中的每个条目均在组件实例级别定义,并按组件实例 ID 编入索引(如需了解更多理由,请参阅此替代方案)。每个替换项条目都会存储在磁盘上,以便在电源周期中保留,或者在当前电源周期的其余时间内保留在内存中。持久性是在创建条目时指定的。
收到配置替换项请求后,配置替换项管理器会执行以下操作:
- 检查替换项数据库中是否有匹配的条目。
- 验证替换的键是否存在于配置定义文件中,以及是否具有正确的数据类型。
- 返回匹配的键值对。
如果找不到键或数据类型不正确,配置替换项管理器会记录一条信息性错误并删除数据库条目(如果在设置配置替换项后下载了新组件版本,可能会出现这些情况)。
收到配置替换项响应后,组件管理器会执行以下操作:
- 验证替换的键是否可通过配置值文件中的替换项进行更改。
- 使用被替换的值填充被替换的键。
- 使用配置值文件填充其余键。
如果组件管理器未收到配置替换服务的有效响应,组件启动将失败。
在配置替换项管理器实现配置替换项服务的情况下,此流程如下所示:
请注意,配置键会以字符串形式存储在替换数据库中。随着组件不断演变,这组配置字段可能会经常更改,但只要键名称和数据类型保持不变,配置替换项就仍然有效。作为优化措施,上次看到的配置版本 ID 和字段编号也可以缓存在数据库中。
值选择摘要
将这些流程组合起来,组件管理器会为每个配置键选择一个值,如下所示:
- 如果键可通过替换项进行更改,并且配置替换项服务返回了匹配的替换项,请使用此值。
- 否则,如果键由 ChildDecl 控制且 ChildDecl 中提供了值,请使用此值。
- 否则,请使用配置值文件中的值。
配置 FIDL 接口和替换数据库
配置替换项管理器公开了两个 FIDL 服务,可用于与其替换项数据库进行交互:
- 一项服务,可执行以下操作:
- 读取所有配置替换项。
- 创建和删除存储在内存中但不会在配置替换项管理器重启后保留的配置替换项。
- 一项服务,可执行以下操作:
- 读取所有配置替换项
- 删除所有配置替换项
- 创建存储在内存中但不会在配置替换项管理器重启后保留的配置替换项
- 创建存储在磁盘上且在配置替换项管理器重启后保留下来的配置替换项
第一个服务无法对配置进行长期更改,但可能对自动化端到端测试很有用。这两项服务都属于敏感服务,其使用情况已列入许可名单。
我们将引入一个 ffx 插件,该插件使用第二种服务,让开发者能够在测试设备上查询和修改配置。在特定版本中可以通过 FIDL 修改的一组配置键是一个政策问题,并未由此 RFC 定义,但我们认为在 eng build 中,几乎没有必要限制通过 FIDL 修改配置。
即使替换项数据库中的条目在创建时已通过配置定义文件进行验证,但随着组件被移除或升级到包含不同配置键的新版本,这些条目可能会随着时间的推移而失效。我们通过多种垃圾回收措施来解决此问题:
- 替换项数据库中的每个条目都可能具有到期时间,到期后该条目将被删除。在生产系统中,我们会考虑将失效时间设为必填。
- FIDL 服务包含用于轻松删除多余条目的方法,包括删除组件实例的所有条目以及删除整个数据库。
- 在未来的工作中,我们将研究在移除或升级软件包时如何从软件提交中接收通知,以便删除或重新验证相应的替换条目。
诊断
了解组件使用的配置对于调试问题至关重要。若要评估分阶段发布和 A/B 研究,请务必将指标与运行不同配置的设备同类群组分开。
大多数设备上的大多数配置键将使用在组装期间设置的值。对于这些应用,只需知道发布版本(或应用可更新时软件包版本)即可推断出配置值。每次启动组件实例时,组件管理器都会计算两个哈希:一个是针对由 ChildDecl 设置的所有配置键和值的“ChildDecl 配置哈希”,另一个是针对由替换项设置的所有配置键和值的“替换配置哈希”。如果未设置任何字段,相应的哈希值将为零。
如果使用的不同配置数量适中,这些配置哈希就足以识别每个同类群组。如果可能存在大量配置值(例如,开发者在测试期间设置了任意网址),配置哈希可能不足以确定配置值,但仍可指明运行配置与其汇编不同的设备,以及运行配置与其同类设备不同的设备。
日志记录
组件管理器会记录每次计算组件配置的统计信息,但不会记录原始配置值。配置替换项管理器会记录所有用于修改替换项数据库的 FIDL 请求。
检查和快照
组件管理器的检查数据包含有关每个组件实例配置的统计信息,以便于调试。这包括从每个来源设置的配置值的数量,以及上面介绍的配置哈希。由于快照包含检查数据,这意味着快照中包含所有正在运行的组件实例的配置哈希。
如果配置键对组件的操作和调试至关重要,则组件可以选择在自己的检查数据中包含这些配置值。
钴蓝
组件管理器会在启动时(通过运行程序)将这两个配置哈希发送到组件实例。在创建新的 MetricEventLogger
时,我们会展开 fuchsia.metrics.MetricEventLoggerFactory
以接受这些哈希。
Cobalt 会将这些配置哈希与现有的 SystemProfile
字段结合使用,以定义组件所运行的上下文,从而允许单独分析具有不同配置的设备的指标。系统仍会应用标准阈值,因此只有当有一定数量的设备共享相同配置时,才会提供指标。
实现
在结构化配置开发期间,我们将选择少量“抢先体验”组件作为客户端,同时不断改进语法和工具。
该实现将分为三个阶段,每个阶段都会提供更多功能:
- 第 1 阶段:静态价值
- 在此阶段之后,您将能够创建配置定义(可能会使用尚未最终确定的语法和工具),将配置值放入软件包中,并在启动时将这些值传递给组件实例。
- 此阶段提供了一种基本方法,可让抢先体验组件的行为因产品或 build 类型而异。
- 第 2 阶段:父级值
- 在此阶段之后,您还可以限制打包的配置值的可变性,在 ChildDecl 中指定配置值,并在启动时将这些值传递给组件实例。配置的定义应通过组件清单进行,并使用接近最终的语法。
- 此阶段会解除对配置和使用情形的集成测试的阻塞,这些配置和使用情形需要父级组件为其子级提供配置。
- 第 3 阶段:被替换的值
- 在此阶段之后,还可以通过 FIDL 接口或使用该接口的 ffx 插件设置和读取配置替换项,以便在启动时将这些值传递给组件实例,并在 Cobalt 中按配置隔离指标。
- 此阶段将完成此 RFC 中定义的工作,并解除本地开发者测试、端到端测试和进一步的产品集成的限制。
性能
此 RFC 引入了一个新组件,会产生(适当的)CPU、内存和存储开销。所有这些都会随着使用结构化配置的组件数量和配置键数量而扩缩。
由于需要额外的 FIDL 调用和额外的文件读取,计算配置值会导致配置可通过替换进行更改的组件的启动出现短暂延迟。我们将监控此费用,并根据需要优化配置替换项管理器的实现。
安全
许多组件都可以使用结构化配置来控制其行为的许多不同方面。能够修改配置的攻击者可能会以多种不同的方式破坏设备的安全性。例如:
- 启用调试输出以泄露用户信息。
- 将网络请求重定向到攻击者控制的服务器。
- 启用实验性功能并利用其实现中的漏洞。
此设计包含多项旨在防范此类攻击的功能:
- 可配置数据的范围受到限制,并且每个元素都定义明确,因此更易于审核。
- 签名版本时,系统会为每个配置键定义一个值,这些值均由经过验证的执行过程涵盖。
- 在系统运行时修改配置键的功能是在签名版本时设置的,每个键和每个变更机制都有各自的设置。这些可变性由经过验证的执行涵盖。
- 可变性和默认值是在组件级别(而非组件实例级别)定义的。只有在为配置键设置了“mutable by ChildDecl”或“mutable by override”后,才能创建具有不同配置的新组件实例。
- 以暂时性和永久性更改配置的功能会作为单独的 FIDL 服务公开。
- 用于更改配置的 FIDL 服务由许可名单控制。
- 配置数据由经过充分审核的现有 FIDL 绑定进行解析,而不是由特定于应用的逻辑进行解析。
- 配置替换项管理器将是一个诱人的目标,因此我们会要求对其实现进行安全审核。
隐私权
结构化配置不适用于存储用户设置(这些设置具有不同的稳定性、持久性、访问权限和时间需求),因此配置值绝不应包含用户生成的数据。我们会在面向开发者的指南中明确说明这一点。
父级组件将 PII 配置传递给动态创建的子级(例如,新组件实例应使用的硬件 UID 或网络地址)可能很有用。如果不加以注意,这些信息可能会通过 ChildDecl 配置哈希泄露到日志或指标中。我们将在详细设计期间解决此问题,但有几种可用方案(例如,敏感字段可以在配置定义中标记,然后在添加到哈希之前进行盐处理)。
测试
能够测试多个配置值对于验证正确性至关重要。这种设计支持在所有阶段测试不同的配置值:
- 单元测试和组件测试可以手动构建 FIDL 配置结构体(即组件运行程序在正常运行期间提供的结构体),并将其传递给使用配置的方法。
- 集成测试在构建测试 Realm 时,可以通过 Realm 构建器(使用
ChildDecl
)为每个组件实例提供配置。 - 端到端测试可以使用配置 FIDL 接口从主机设备设置其他配置。您可能需要执行额外的工作来暂停正常的启动顺序,以避免组件启动时出现竞态条件。
- 手动测试可以使用
ffx
命令,通过配置 FIDL 接口轻松设置其他配置。 - 未来的工作将考虑对组件的配置进行自动模糊测试。
您可以通过多种不同的方法来避免对密封集成测试中的配置替换服务的隐式依赖项。例如,集成测试软件包可以使用不允许替换的配置值文件构建,并且测试领域的构建可以避免路由配置替换服务。
我们将使用标准最佳实践来测试结构化配置本身的实现,包括组件管理器 - 配置替换项管理器 - 运行程序交互的单元测试和集成测试。
文档
此 RFC 获得批准后,我们将在 /docs/concepts
中发布一份文档,介绍 Fuchsia 上提供的配置机制及其关系。
语法稳定下来且结构化配置实现准备好更广泛采用后,我们将发布开发者指南和参考文档。
随着开发者开始使用结构化配置并发现行之有效的模式,我们将编写文档,介绍最佳实践和推荐的样式。
考虑的替代方案
在配置替换项管理器中实现配置合并
此 RFC 的早期修订版将用于合并不同组合数据集的逻辑放置在组件替换项管理器(当时称为配置管理器)中,而不是组件管理器中。
这种设计会使组件管理器的范围缩小,但组件管理器和配置管理器的范围定义会不那么清晰:
- 组件管理器需要对配置有足够的了解,才能知道何时调用配置管理器,但又不能了解足够的信息来合并值。
- 除了维护数据库之外,配置管理器还负责一些业务逻辑,以向组件呈现配置。
这种设计还会增加需要进行 FIDL 调用的情形数量。
在组件管理器中实现配置替换数据库
此 RFC 将维护配置替换数据库的责任交给了一个新组件:配置替换管理器。另一种方法是在组件管理器内执行此功能。
这种替代方案可以消除 FIDL 调用和某些失败模式,但会增加组件管理器的复杂性,并且这是组件管理器首次需要保留自己的数据,而不是仅分配存储空间供其他组件使用。这种存储用法会带来额外的安全问题,并需要在 CF 中构建新的基础架构。
通过中央软件包提交配置
此 RFC 会将组件的配置值放置在组件的软件包中。另一种方法是将所有组件的所有配置放入单个配置文件包中,类似于 CFv1 中的 config-data 文件包的设计。
我们之所以更倾向于采用分散式方法而非集中式方法,主要有以下两个原因:
- 未来,Fuchsia 需要运行基准映像不认识的组件(例如,由于应用更新而导致)。这些未知组件的配置无法在中央软件包中分发,因此需要其他解决方案。
- 以原子方式提交二进制文件及其配置,让我们可以更明确地断言二进制文件和配置的一致性,尤其是在软件包可能独立于基础映像进行更新时。导致组件可用但其配置不可用,或导致组件与其配置不兼容的失败模式更少。
如 RFC 正文中所述,我们的去中心化方法目前会将配置值放置在与其应用到的组件位于同一软件包中。这意味着,在组装时更改组件的配置会更改其软件包的根哈希。从长远来看,这种做法并不理想,因为它不支持一个组织(例如 Fuchsia 平台维护者)发布和签署组件,而另一个组织(例如产品集成商)提供配置。
我们预计,未来每个软件包都将包含发布组织设置的默认配置值,并且还会声明这些值中的哪些子集可能会通过可能由其他组织发布的其他软件包被替换(例如,平台组件可以选择产品集成商可以访问其哪些配置“旋钮”)。未来的工作将定义此“其他软件包”的性质 - 选项包括元软件包、边车软件包或封装容器软件包。
按组件网址和标识符的索引配置替换项
此 RFC 会按组件的实例 ID 编入配置替换项索引。实例 ID 用于为组件框架中的其他永久性资源编制索引,例如隔离的永久性存储空间,因此是用于为配置替换项编制索引的理想选择。
不过,目前组件实例 ID 是在构建时通过索引文件手动分配的。这意味着,实例 ID 和结构化配置替换项不适用于组件集合中的组件实例,也不适用于由应用更新(而非基本映像)引入的组件实例。
作为替代方案,我们考虑按组件网址和标识名编制索引配置替换项。这可以避免实例 ID 的限制,但随着系统重构,组件网址和标识名都可能会发生变化,因此,除了在处理组件框架持久性资源时造成不一致之外,这种替代方案还会引入新的稳定性问题。
我们更倾向于在未来的 RFC 中通过更改实例 ID 的设计来解决实例 ID 的限制。
支持在组件级别替换配置
此 RFC 会按组件的实例 ID 为配置替换项编制索引,这意味着需要单独替换组件的每个实例。
将来,除了在组件实例级别支持替换项之外,还可能需要在组件级别支持替换项。如果组件被多次实例化,或者组件实例无法提前知晓,这种方法会特别有用。
目前,我们还没有为组件定义明确规范且稳定的标识符,无法用于组件级替换项。由于我们尚未确定对组件级替换项的具体需求,因此我们会推迟添加该功能。
对上一个方案中提及的实例 ID 设计所做的更改可能会提供稳定的组件标识符,以及组件实例标识符(例如,实例 ID 可以是组件的自认证标识符,与实例标识名的某个函数结合使用,通过某个奇怪数据库来处理拓扑结构的变化)。这有助于日后简化添加组件级替换项的操作。
使用 FIDL 定义配置键
此设计使用 JSON 在组件清单中定义配置键。其中一些信息用于构建 FIDL 表。另一种方法是在 .fidl
文件中定义配置键。
首先,我们注意到 FIDL 工具链是刻意由前端和后端组成的,前端和后端由 IR 分隔 - 这样,您就可以使用 FIDL 技术,而无需将输入定义为 FIDL 语言。
决定使用 .cml
而非 .fidl
主要取决于开发者体验:
- 在组件清单中,开发者可以描述其组件对框架的需求,并定义符合此定义的配置键。与添加新文件相比,维护单个文件的工作量更少。
- 可用于定义配置的语法和数据类型是完整 FIDL 语言的一小部分(请参阅范围)。要求使用 FIDL 语法,但仅支持部分语法,这可能会令人困惑和沮丧。
- 配置需要使用 FIDL 语言无法表示的信息(目前是表格字段的默认值,未来可能还有其他约束条件)。此类信息可以存储在自定义属性中,但这与 FIDL 语言的其余部分不一致,并且会让开发者感到困惑。
如果 JSON 配置定义使用 FIDL 语言中存在的概念,我们将使用一致的语法。例如,数据类型名称将保持一致。
cmc
已从 JSON 组件清单构建 FIDL 表,从清单解析配置所需的额外工作量很少。
支持在组件之间进行配置路由
组件框架中的许多资源都支持从一个组件实例路由到另一个组件实例,例如协议、目录和存储功能。自然而然地,我们需要支持在组件之间路由配置,这样一来,子组件就可以使用其父级的某些配置值。
此初始设计不支持配置路由,以免引入新的版本控制问题。如果我们支持在一个组件中定义配置,并在其他组件中使用该配置(因为这些组件是在不同时间点打包的,或者因为这些组件并非作为单体提交到设备),我们将无法再保证编译到组件中的配置定义与用于配置值的定义一致。我们将在这两个组件之间引入新的 ABI,但由于此接口将在 PDK 中而非 IDK 中表达,因此我们无法使用 RFC-0002 中定义的流程来管理版本兼容性。
随着 PDK 和外部树组装工作不断推进,我们可能会更多地讨论组件接口和版本控制,并推迟在组件之间路由配置,直到该功能更加成熟。在此期间,如果需要,可以使用汇编工具在多个软件包中提供一致的配置值。
我们通过支持父级在创建时动态配置子组件,引入了此跨组件兼容性问题的更有限版本。我们认为,这些情况不太可能在短期内造成问题,因为必须选择启用这种可变性,并且配置字段通常由子项明确设计为供父项使用。
支持为可重复使用的库配置透明配置。
许多组件都是使用支持某种形式配置的可重复使用库构建的。此 RFC 中定义的结构化配置可用于提供此配置,但只能以手动方式提供;使用库的每个组件都需要在自己的清单中声明匹配的配置键,然后在初始化时将配置值路由到库。运行程序也存在类似的情况,即运行程序可以直接控制其运行的组件使用的库中的可配置行为。
另一种方法是为库支持更“透明”的配置,其中库可以声明其配置键,并可以在运行时直接使用配置值。组件只需声明其对库的使用,即可让配置提供程序为该组件中的库实例设置配置值。
与组件之间的路由(在上述替代方案中进行了讨论)一样,这需要在不同软件工件之间就配置键达成更广泛的共识,例如当平台库被树外组件使用时。这可能需要对配置进行更正式的定义,以便更有力地保证配置版本之间的向前和向后兼容性。为库实现透明配置还会引发如何向组件传递多组配置值的问题。
虽然从长远来看,为库提供透明配置是一项理想的功能,但我们会暂时推迟这项工作,直到此 RFC 中定义的更简单、更“私密”的配置系统投入使用。
支持组件启动后的配置更新
这种设计仅在启动组件实例时提供组件配置。如果在组件启动后修改配置,则该配置不会在下次启动之前传递给组件。另一种方法是通过 FIDL 定期将配置传递给组件。
决定重点关注组件启动符合我们对“配置”的定义(即绑定到组件生命周期),但也简化了组件作者的实现:
- 仅收到一次配置的组件实例可以在组件启动期间初始化其所有配置驱动型资源。必须定期接收新配置的组件也需要定期分配或收集资源,因为配置变更需要这样做。
- 必须定期接收新配置的组件需要执行一些定期或异步处理,否则可能没有必要执行这些处理。
- 必须定期接收新配置的组件还必须通过其诊断连接记录配置的任何更改,例如通过创建新的
MetricEventLogger
。 - 必须定期接收新配置的组件具有必须处理的其他失败模式,例如 FIDL 连接终止和超时。
目前,启动后需要进行配置更新的情况非常有限:在组件启动后,在发布版本中或通过 ChildDecl 提供的配置值无法更改。FIDL 接口可以在启动后引入更改,但最初只会在开发者工具中使用,这些工具可以轻松提供在更改组件配置后自动重启组件的方法。
未来,某些特定于产品的组件可能更倾向于实现上述复杂性,而不是重启。如果是这样,我们会考虑让组件可以选择通过 FIDL 接收更新。