iii 引擎配置管理实战:config.yaml 与 configuration Worker 的双层模型、实时热加载与 Schema 校验
2026/9/15 5:57:53 网站建设 项目流程

iii 引擎配置管理实战:config.yaml 与 configuration Worker 的双层模型、实时热加载与 Schema 校验

【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii

本篇围绕 iii(Motia 引擎项目)0.21.0 版本文档using-iii/configuration展开:iii 把配置拆成「声明层」与「运行时层」——config.yaml只负责声明哪些 worker 运行、并提供一次性引导值,而configurationworker 负责管理每个 worker 的运行时设置。读完后,你将掌握「一个 worker 一个 YAML 文件」的存储布局、改配置的三种方式(改文件 / Console /configuration::set)、${VAR:default}环境变量模板的求值规则,以及如何通过configurationtrigger 对配置变更做出反应,并能结合仓库源码理解 schema 校验、外部编辑热加载与 TTL 清理的底层实现。

1. 两个配置层

iii 把配置拆成两层,职责明确分离:

  • config.yaml:声明_哪些 worker 会运行_,并为它们的设置提供引导(bootstrap)值。这是引擎启动时读取的文件,即引擎配置入口(参见 引擎配置文档)。
  • configurationworker:管理_每个 worker 的运行时设置_。它是一个带 schema 校验的、响应式的注册表,默认内置启用(enabled by default):每个 worker 以自己的 id(httpstatequeue、……)为名注册一份设置 JSON Schema,任何一次变更都会先对照该 schema 校验、再应用到运行中的 worker。大多数设置立即生效,少数在下次引擎启动时生效(见第 5 节)。

1.1config:块是「引导种子」,不是永久数据源

config.yaml中某个 worker 下的config:块是bootstrap seed(引导种子):它只会在该 worker 注册 schema 后的首次启动被读取一次,用于创建该 worker 的配置条目。从那一刻起,配置条目(entry)才是唯一的数据源(source of truth)。下一次启动时,引擎会把已消费的config:块从config.yaml中移除,并留下一行注释指向条目的新位置;- name:行以及文件中的其余内容按原样保留。

这个「种子生命周期」在源码中有直接对应:config_rewrite.rs 模块专门负责在 worker 首次把config:块写入配置存储后重写config.yaml。从源码结构看,它的编辑是刻意按行进行的——用serde_yaml往返解析会丢掉注释、重排键、并把${VAR:default}占位符展开掉,而其他 worker 块依赖这些占位符;因此它只删除一段连续的行、插入一行注释,文件中其余部分逐字节保留。替换掉种子块后留下的注释形如:

# 'iii-stream': value now lives in the configuration worker at ./config/iii-stream.yaml. Edit at runtime via 'iii config set iii-stream' (or 'configuration::set'); this block is no longer read.

E2E 测试 config_reload_e2e.rs 会等待并断言config.yaml被重写为包含iii config set提示的注释,验证这条改写路径。

两个边界情况需要注意:

  • 这只对注册了配置 schema 的 worker生效。没有注册 schema 的 worker(例如iii-worker-manager)会继续直接读取config.yaml里的块。
  • 在更早的 iii 版本中,所有 worker 设置都留在config.yaml里,配置存储默认位于./data/configuration。当运行在默认位置时,引擎会在启动时做一次一次性迁移,把旧目录中找到的条目搬到./config;显式配置directory:覆盖项会跳过这次迁移。详见 0.21.0 变更日志。源码层面,fs.rs 中定义了DEFAULT_DIRECTORY = "./config"LEGACY_DEFAULT_DIRECTORY = "./data/configuration"FsAdapter::new在解析目录恰为默认值时调用migrate_legacy_default:尽力而为地把旧目录里的*.yaml逐个rename过来,目标位置已存在同名文件则跳过并打 WARNING、I/O 出错则记录并跳过,绝不让迁移失败阻塞适配器初始化。

1.2 源码视角:configuration worker 如何落地

从源码结构看,configurationworker 是引擎内置且不可关闭的:configuration.rs 末尾通过

crate::register_worker!( "configuration", ConfigurationWorker, description = "Register, store, and watch typed configuration values for the engine.", mandatory );

注册,mandatory标记保证了它随引擎默认启用。worker 的默认配置在清单文件 iii.worker.yaml 中给出:

iii: v1 name: configuration type: engine description: Register, store, and watch typed configuration values for the engine. repo: https://github.com/iii-hq/iii config: adapter: name: fs config: directory: ./config ttl_seconds: 0

即默认使用fs适配器、目录./configttl_seconds: 0(关闭 TTL 清理,见第 6 节)。配置结构体 ConfigurationModuleConfig 只有两个字段:adapter(可选,fs为默认,bridge委托给远端引擎)和ttl_seconds(默认0表示禁用清理),且带deny_unknown_fields——写错字段名会在启动时直接报错而不是被静默忽略。

2. 一个 worker 一个文件

使用默认的fs适配器时,每个配置条目就是项目根目录./config下一个以 worker 命名的 YAML 文件:

config/ http.yaml queue.yaml state.yaml ...

每个文件承载条目的身份信息加当前值:

# config/http.yaml id: http name: HTTP description: > HTTP server settings: host/port binding, CORS, request timeout, concurrency limit, and global middleware. value: port: 3111 host: 127.0.0.1

该目录在启动时创建,之后一直被监听。注册的 JSON Schema持久化在文件里——worker 每次启动都会重新注册它;磁盘文件只保留人类会编辑的value。这一点在 fs.rs 的模块注释中被明确说明:schema 体积大、且 worker 每次启动都会重新注册,所以刻意不落盘,文件布局为<directory>/<id>.yaml,含id/name/descriptionvalue与可选metadata。加载目录时,YAML 解析失败的文件只打 WARNING 并跳过,不会阻断引擎启动。

3. 三种修改配置的方式

3.1 直接编辑文件(热加载)

编辑value:下的字段并保存。fs适配器通过notify监听目录:变更会对照 worker 注册的 schema 校验,并走与configuration::set完全相同的应用路径。校验失败的编辑会被拒绝,只在引擎日志中留下警告,上一个合法值继续生效——所以手滑打错类型不会把 worker 搞挂。

源码印证:configuration.rs 的handle_external_change对外部编辑(ExternalChange::Registered/Updated)执行「环境变量展开 + 类型强制」后再做 schema 校验;若引用了未设置且无默认值的环境变量、或展开后的值违反 schema,则记 WARNING 并丢弃编辑、保留旧值。它还特别强调不会回写文件,因此不会形成「保存→重载→保存」的循环。单元测试覆盖了这几条路径,如external_change_rejects_invalid_edit_and_keeps_previous(非法编辑被丢弃、旧值保留)与external_file_edit_hot_reloads_through_the_watcher(走完整的initialize → watcher → handle_external_change链路,模拟运维直接在磁盘上改文件,断言存储层反映新值)。引擎侧对文件增删改都会打 info 日志("Loaded new configuration ... from a file added on disk" 等),让热加载行为对操作者可见。

3.2 通过 Console

console worker 的Configuration页面列出所有已注册条目,并根据其 JSON Schema 渲染编辑表单:带类型的字段、按适配器区分的变体、保存前校验。${VAR:default}模板按原样展示和保存,因此从 Console 编辑不会覆盖由环境变量驱动的值。保存立即生效。安装 console:

iii worker add console

3.3 调用configuration::set

configuration::set替换某个条目的值,对照已注册的 schema 校验并立即应用。它可以来自 CLI、任何 SDK 或你自己的自动化:

# CLI iii trigger configuration::get --json '{"id": "http"}' iii trigger configuration::set --json '{"id": "http", "value": {"port": 8080, "host": "127.0.0.1"}}'
// Node / TypeScript await worker.trigger({ function_id: "configuration::set", payload: { id: "http", value: { port: 8080, host: "127.0.0.1" } }, });

set外,configurationworker 还暴露(见 configuration.rs 中带#[function]宏的五个函数定义):

  • configuration::get:读取单个条目;默认对${VAR:default}占位符做实时展开,传raw: true可读取原始模板。
  • configuration::list:枚举所有条目(按 id 排序),只返回 schema,从不返回存储的值——这从函数描述 "Sorted by id; never returns the stored value" 中得到源码级确认。
  • configuration::schema:返回单个 id 的 schema、name 与 description,等价于list中的一项。
  • configuration::register:以 name、description 和 JSON Schema 注册一个配置 id;幂等,重新注册会替换元数据(提供initial_value时同时替换值),并先对initial_value做校验。

失败时会返回结构化错误码,便于程序化处理。从store_error_to_failureget_fn的实现可以看到完整集合:NOT_REGISTEREDINVALID_IDSCHEMA_INVALIDSCHEMA_UNAVAILABLEADAPTER_ERRORNOT_FOUND,以及展开失败专用的EXPAND_FAILED。例如对一个从未注册的 id 执行set会得到NOT_REGISTERED,对 schema 尚为 null 的条目执行set会得到SCHEMA_UNAVAILABLE(磁盘加载的条目在 owner worker 重新注册 schema 前就是这种形态,单测set_without_available_schema_returns_schema_unavailable专门验证了这一点)。更完整的函数参考与错误码说明,可参阅仓库中的 configuration worker 说明。

4. 值中的环境变量

配置值支持与config.yaml相同的${VAR:default}语法。模板按原样存储,在每次读取时对照当前进程环境变量展开——因此改一个环境变量就能传播开,无需重写存储的值。如果一个字段整体只是单个占位符,展开后会被强制转换为 schema 声明的标量类型:port: ${HTTP_PORT:3111}校验/读取出来是整型3111,而不是字符串。对configuration::getraw: true可以读到存储的模板形态。

源码层面,这条规则由 store.rs 中的两个正则实现:ENV_VAR_RE匹配单个${VAR}/${VAR:default}引用(与引擎配置解析 config.rs 的EngineConfig::expand_env_vars以及 Console 前端的模板解析器保持同一文法);LONE_PLACEHOLDER只匹配「整串就是单个占位符」的字符串,只有这种 lone 占位符才会做标量类型强制——混排文本或含多个占位符的值保持字符串。配套的expand_leaf被设计为从不 panic:缺少且无默认值的变量会被记入missing列表、${VAR}字面保留。单测register_and_get_coerces_templated_integer_port验证了模板化整数端口的注册与读取(get返回is_i64()3111raw: true返回"${CFG_GET_TPORT:3111}");get_fails_expand_failed_when_required_var_missing则验证必填变量缺失时getEXPAND_FAILED失败而非 panic。

另一个重要的健壮性细节:get校验的是应用后(展开 + 类型强制)的值;若展开后的值违反 schema,get会记 ERROR 并返回SCHEMA_INVALID失败,而不是把一个坏值交还给消费者——消费者只会特判NOT_FOUND,其余错误码都会让它回退到「未加载」并使用自己的默认配置(单测get_fails_schema_invalid_when_applied_value_violates_schema覆盖此路径)。

5. 变更如何生效

多数设置在变更发生的瞬间生效,例如http的 CORS、timeout、port 变更。少数字段属于 restart-tier(重启级):变更会被记录、打日志,并在下次引擎启动时生效(例如state的存储适配器)。每个 worker 的具体设置清单与生效方式,可在其 worker 文档(如仓库内 engine/src/workers/ 下各 worker 目录的 README/清单文件)中查对。

6. 更改配置的存放位置

configuration worker 本身是种子生命周期的一个例外:它不能把自己的设置存进自己,所以它的config:块始终留在config.yaml中,每次启动直接读取。想把每个 worker 的文件存到别处,设置fs适配器的directory

# config.yaml workers: - name: configuration config: adapter: name: fs config: directory: ./config

configuration worker 还提供两个可选项(定义见 config.rs):

  • bridge适配器:当多个 iii 引擎需要共享同一份配置源时使用。从源码看,bridge.rs 会把configuration::*调用委托给远端实例(默认ws://localhost:49134,单次远程调用带 30 秒超时,避免远端无响应拖挂本地 worker),并把远端实例的configurationtrigger 事件转发回本地扇出,让本地订阅者也能看到远端发起的变更。
  • ttl_seconds:为「瞬时 worker」的条目提供清理。默认0表示禁用;设为非零后,当一个 id 的最后一个 trigger 被注销,且经过ttl_seconds秒内没有新的 trigger 注册,该配置条目即被删除并发出configuration:deleted事件。

TTL 机制在 trigger.rs 中实现:每个 id 维护一个 trigger 计数(ref-count)槽,计数归零且ttl_seconds > 0时启动tokio::spawn倒计时;倒计时期间若新 trigger 注册进来会 abort 掉过期任务,worker 销毁时 abort 全部倒计时。单测ttl_countdown_fires_when_last_trigger_unregisteredttl_countdown_aborts_on_re_register分别验证了「最后注销后到期清理」与「到期前重新注册则存活」两种行为。

7. 对配置变更做出反应

worker 通过绑定configurationtrigger 来订阅变更,而不是轮询:绑定的函数在每次 register、set、delete 时触发,包括外部文件编辑——这正是各 worker 热应用自己设置所用的同一机制。

从源码看,trigger 的配置结构 ConfigurationTriggerConfig 有三个可选字段:

字段类型说明
configuration_idstring精确匹配某个配置 id;省略则匹配所有 id
event_typesstring[]事件类型过滤,如["configuration:updated"];省略则匹配全部
condition_function_idstring可选条件函数,在 handler 运行前对事件求值,返回 false 则跳过

事件在扇出时会携带展开后的new_value/old_value(订阅者看到的是解析完${VAR:default}的值),事件类型 wire 值为三种:configuration:registeredconfiguration:updatedconfiguration:deleted(见 configuration.rs 中fan_outevent_type_wire映射与entry_to_event)。注册新条目时事件为registered,幂等重注册(值被替换)则发出updated,删除(含 TTL 清理)发出deleted

一个典型用法是:你的 worker 注册一个绑定自己处理函数的configurationtrigger,并设configuration_id为自己、event_types过滤configuration:updated;之后无论是 Console 保存、configuration::set还是运维手改config/<id>.yaml,处理函数都会被异步调用,worker 据此热应用新设置。

小结

iii 的配置模型可以浓缩为三条规则:

  1. 两层分离——config.yaml只声明与播种,configurationworker 是运行时数据源;种子块在首次播种后的下一次启动被替换为一行指引注释(config_rewrite.rs)。
  2. 一个 worker 一个文件——./config/<id>.yaml只存value,schema 每次启动重新注册;目录被持续监听,外部编辑与configuration::set走同一条校验→应用→触发器扇出路径(fs.rs、configuration.rs)。
  3. 变更是事件——register/set/delete(含文件编辑与 TTL 清理)都会广播configuration:*事件,worker 用configurationtrigger 订阅而非轮询(trigger.rs)。

想进一步深入,建议阅读 configuration worker 源码目录、fs 适配器实现、E2E 配置重载测试 以及 0.21.0 变更日志 中关于种子块移除的说明。

【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询