RFC-0127:结构化配置

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

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

问题
  • 52436
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 工程委员会,这是负责扩大 RFC 范围(即组件框架和软件交付)以及开发可以实现改进的流程(PDK 和安全)的团队。

系统的潜在客户也很重要,但由于此 RFC 不建议通用配置系统,因此不应该满足所有潜在客户的所有配置需求。

教员:abarth

审核者: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 未指定政策。但是,出于安全考虑,除非明确请求和审核,否则很可能不允许在生产环境中(例如“用户”)版本进行运行时变更。

  • 开发者可以在系统运行工程版本时测试该功能。在系统运行期间可以更改哪些配置键是一个政策问题,此 RFC 未指定政策。但是,为了简化开发、测试和调试,可能允许对工程中的大多数配置键(例如“eng”)版本。 开发者可以使用 ffx 为本地设备启用该功能。例如(使用概念语法):ffx target config set timekeeper.cml enable_frequency=true。如果政策允许,开发者还可以启用该功能,使其在设备重启后持续有效。

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

  • 构建和组装工具可以作为平台边界的一部分来控制该功能。构建和组装工具目前正在通过 DPISPAC 工作进行开发。这项工作的结果将使平台维护人员能够控制是否向产品公开每项平台功能。根据政策和功能的风险,平台可以控制功能的发布或将功能的发布委托给产品。对于更复杂的情况,平台可以在系统运行时启用功能标志的变更,即使是在生产环境中(例如,“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 行代码引入这个可定制的参数,并收到相同的实用属性。在此用例中,清单不提供默认值(因为组件作者不了解振荡器的优缺点),因此如果没有其他输入提供该值,则组装过程将失败并显示信息性错误。

其他用例

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

  • Inhibit-for-test 标志:有些组件会自然地表现出给集成测试造成困难的行为(例如,时间源故障后系统有 5 分钟的冷却时间)。测试专用标志可用于在集成期间或端到端测试期间禁止这些行为。

  • 组件实例创建时配置。有时,在创建组件实例之前无法确定组件的适当配置。可以定义配置键,以允许创建新组件实例的父组件在创建时提供配置值。这可以取代 CFv1 中的 launch args,并支持为不同角色定制多个组件实例(例如,为每个活跃帐号启动一个 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]。如果嵌套或分组配置键将来被认为有价值,则可以使用以英文句点或斜杠分隔的语法支持并引用这些键。

某些运行时(如投射和网络)会自动生成 CFv2 ComponentDecl,而不是从组件清单生成。这些运行时最初不支持结构化配置。

如果清单中包含配置部分,组件构建规则会生成一个库,其中包含该配置的 FIDL 表定义,以及在运行时通过其运行程序提供给组件的输入填充此表所需的代码。然后,组件的实现可以导入此库,并使用表中的字段控制其行为。

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

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

构建、组装和发布

构建、组装和发布流程负责为每个可配置组件生成两个文件。这两个文件都作为组件放在同一软件包中(将来,我们会进行扩展,以支持通过不同的软件包提供值,请参阅此备用软件包)。

配置定义文件

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

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

这些信息在组件清单中提供,因此该文件可能会在组件构建流程中生成。此外,建议您针对配置定义文件中的所有信息进行计算并添加哈希值,以将其用作配置版本 ID。

请注意,我们将配置定义描述为“文件”,以简化关于其用法的讨论,但实现可能会在已编译的组件清单(即*.cm 文件)而非单独的文件。

配置值文件

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

  • FIDL 字段编号
  • 配置值
  • 可由 ChildDecl 更改(布尔值)
  • 可通过替换更改(布尔值)

此 RFC 未定义该文件的格式。

在成熟且可扩缩的汇编系统中,几个不同的操作者可能希望为一个或多个密钥指定或限制此信息。例如:组件作者、董事会启动工程师、平台边界所有者、产品集成商或安全审核人员。实现此功能所需的一些工具目前正在 DPISPAC 平台路线图条目中开发,本 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 和字段编号缓存在数据库中。

值选择摘要

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

  1. 如果键因替换而可变,并且配置替换服务会返回匹配的替换,请使用此值。
  2. 否则,如果 ChildDecl 键可变,且 ChildDecl 中提供了值,则使用此值。
  3. 否则,请使用配置值文件中的值。

配置 FIDL 接口和替换数据库

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

  1. 一项服务可以:
    • 读取所有配置替换值。
    • 创建和删除保存在内存中但在配置替换管理器重启后不会保留的配置替换项。
  2. 一项服务可以:
    • 读取所有配置替换值
    • 删除所有配置替换值
    • 创建保存在内存中,但在配置替换管理器重启后不会保留的配置替换值
    • 创建存储在磁盘上的配置替换值,并在配置替换管理器重启后保留下来

第一项服务不能对配置进行长期更改,并且可能有助于自动化端到端测试。这两项服务都比较敏感,因此它们的使用已列入许可名单。

我们将引入一个 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 调用和额外的文件读取操作而可变,在启动该组件时,计算配置值会造成短暂延迟。我们会监控此费用,并在必要时优化配置替换管理器的实现。

安全性

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

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

此设计包括多项旨在防范此类攻击的功能:

  • 可配置数据的范围有限,并且每个元素都明确定义,使它们更易于审核。
  • 在为版本签名时,会为每个配置键定义一个值,这些值在验证执行功能的涵盖范围内。
  • 在对版本进行签名时,可单独为每个键和每种变更机制设置在系统运行时修改配置键的功能。已验证执行涵盖这些可变性。
  • 可变性和默认值在组件级别(而不是组件实例级别)定义。仅当为配置键设置了可通过 ChildDecl 可变或可通过替换实现可变时,才能创建具有不同配置的组件的新实例。
  • 临时和永久更改配置的功能作为单独的 FIDL 服务公开。
  • 用于更改配置的 FIDL 服务由许可名单控制。
  • 配置数据由经过审核的现有 FIDL 绑定进行解析,而不是由应用专用逻辑进行解析。
  • 配置替换管理器会是一个赚钱的目标,因此我们将申请对其实现进行安全审核。

隐私权

结构化配置并非旨在存储用户设置(这些用户设置具有不同的稳定性、持久性、访问权限和时间需求),因此配置值不应包含用户生成的数据。我们会在面向开发者的指南中明确记录这一点。

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

测试

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

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

您可以使用多种不同的选项来避免对封闭式集成测试中的配置替换服务隐式依赖。例如,可以使用不允许替换的配置值文件构建集成测试软件包,而构建测试领域可以避免路由配置替换服务。

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

文档

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

一旦语法稳定,并且结构化配置实现可供广泛采用,我们就会发布开发者指南和参考文档。

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

已考虑的替代方案

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

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

这种设计会缩小组件管理器的范围,但组件管理器和配置管理器的范围定义得比较不清晰:

  • 组件管理器需要对配置有充分的了解,知道何时调用配置管理器,但不知道如何合并值。
  • 除了数据库维护之外,配置管理器还将负责一些业务逻辑,以向组件提供配置。

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

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

此 RFC 负责在新组件(配置替换管理器)中维护配置替换数据库。另一种方法是在组件管理器内执行此功能。

这种替代方案可以消除 FIDL 调用和一些故障模式,但会增加组件管理器的复杂性,并且会成为组件管理器首次需要保留自己的数据,而不是简单地分配存储空间以供其他组件使用。使用存储空间会增加安全问题,并且需要采用新的 CF 基础架构。

通过中央软件包提供配置

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

我们倾向于采用分散式方法而不是集中式方法,主要有两个原因:

  1. 将来,Fuchsia 需要运行基础映像无法识别的组件(例如,由于应用更新而产生的组件)。这些未知组件的配置不能分发到中央软件包中,因此需要其他解决方案。
  2. 以原子方式提供二进制文件及其配置可让我们对其一致性做出更强的声明,尤其是当软件包可能会独立于基础映像进行更新时。导致组件可用但其配置不可用的故障模式更少,或导致组件及其配置不兼容的故障模式更少。

如 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)的决定主要取决于开发者的体验:

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

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

cmc 已经根据 JSON 组件清单构建 FIDL 表,解析从清单中的配置所需的额外工作不多。

支持组件之间的配置路由

组件框架中的许多资源都支持从一个组件实例路由到另一个组件实例,例如协议、目录和存储功能。支持在组件之间路由配置很自然,也就是说,子级可以使用其父级中的某些配置值。

此初始设计不支持配置路由,以避免引入新的版本控制挑战。如果我们支持在一个组件中定义配置并在不同时间点打包的另一个组件中使用该配置(由于这些组件是在不同的代码库中定义,或者由于未作为单体式应用分发到设备),我们无法再保证编译到组件中的配置定义与用于配置值的定义一致。我们将在这两个组件之间引入一个新的 ABI,但由于此接口将在 PDK(而非 IDK)中表达,因此我们无法使用 RFC-0002 中定义的流程来管理版本兼容性。

随着 PDK 和树组装工作的继续进行,我们很可能对组件 Surface 和版本控制进行了更多讨论,我们会将组件之间的配置路由推迟到更成熟一些。同时,如果需要,汇编工具可用于在多个软件包中提供一致的配置值。

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

支持对可重复使用的库进行透明配置。

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

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

与组件之间的路由(如上文的替代方案所述)一样,这将涉及针对不同软件工件之间的配置键达成更广泛的协议,例如,当树外组件使用平台库时。这有助于定义更正式的配置定义,以更有力地保证配置版本之间的向前和向后兼容性。库的透明配置也会引发一个问题,即如何向一个组件提供多组配置值。

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

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

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

专注于组件启动的决定符合我们的定义,“配置”受组件生命周期约束,但也简化了组件作者的实现:

  1. 只接收一次配置的组件实例可以在组件启动期间初始化其所有配置驱动型资源。必须定期接收新配置的组件也需要定期分配或收集资源,因为配置变更可确保这一点。
  2. 必须定期接收新配置的组件需要进行定期或异步处理,否则可能就没有必要进行一些处理。
  3. 必须定期接收新配置的组件还必须通过其诊断连接记录所有配置变更,例如通过创建新的 MetricEventLogger
  4. 对于必须定期接收新配置的组件,该组件还有其他必须处理的故障模式,如终止 FIDL 连接和超时。

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

将来,某些特定于产品的组件可能更倾向于实现上述复杂性,而不是重启。如果是,我们将考虑允许组件通过 FIDL 接收更新。

早期技术和参考资料