首页
/ iii 引擎(Engine)深度解析:启动流程、调用路由、断连清理与配置热重载

iii 引擎(Engine)深度解析:启动流程、调用路由、断连清理与配置热重载

2026-09-13 23:59:43作者:毕习沙Eudora

本文基于 docs/0-17-0/understanding-iii/engine.mdx(及仓库中最新版 docs/understanding-iii/engine.mdx)编写,并以 engine/ 目录下的 Rust 源码、engine/config.yaml 配置与相关测试用例作为实现级佐证。目标是让读者完整理解 iii 引擎在运行时做了什么、如何与 Worker 协作,以及如何通过配置热重载与发现机制驱动"任何语言、任何运行时"的统一调用面。

导读

iii 引擎是整个系统的"薄层"(thin layer):它不执行业务逻辑,而是让 Workers、Triggers 与 Functions 的价值得以发生——接受 Worker 的 WebSocket 连接、维护实时注册表、在 Worker 之间路由调用,并在 Worker 断连时自动清理现场。读完本文,你将掌握引擎的启动顺序与配置文件结构、引擎的三大运行时职责、Worker 断连时引擎的清理语义(含 invocation_stopped 取消错误码与发现事件)、config.yaml 热重载的工作方式,以及如何通过 engine::*::list 快照调用与 engine::workers-available / engine::functions-available 订阅事件观察系统拓扑。

引擎在 iii 中的定位

官方文档对引擎的定位是一句话:docs/understanding-iii/engine.mdx 的 frontmatter 描述写道:"The iii engine is the thin layer that allows the benefits of Workers, Triggers, and Functions to happen."(iii 引擎是让 Workers、Triggers、Functions 的各种好处得以发生的薄层。)

引擎本身是 Rust 实现的可执行程序(engine/ 目录,入口为 engine/src/main.rs),但"引擎"更多是一个运行时角色:任何通过 WebSocket 接入、向引擎注册 Functions 与 Triggers 并接受调用分发的进程,都处于引擎的管辖之下。引擎对外提供的是一套统一、语言无关的调用与发现面。

启动流程(Startup flow)

文档将引擎启动描述为四步序列,仓库源码可以逐一对上:

  1. 解析命令行参数。引擎二进制使用 clap 定义 CLI(见 engine/src/main.rsCli 结构体)。关键的引擎级参数是 --config <path>:指定配置文件路径,默认值为 config.yaml。不带任何子命令直接运行 iii 即进入 serve 模式启动引擎(None => run_serve(&cli_args),见 engine/src/main.rs)。

  2. 加载配置文件(通常为 config.yamlrun_serve 中通过 EngineConfig::config_file(config_path) 加载配置,再用 EngineBuilder::new().with_config(config).with_config_path(config_path).build().await 构建引擎实例(见 engine/src/main.rs)。一个值得注意的细节:如果 config.yaml 不存在,ensure_config_file 会提示"创建并启动引擎吗?";在非交互会话(容器、CI、服务管理器)中则自动创建一个仅含空 workers 列表的起始配置,保证引擎可以无人值守启动(见 engine/src/main.rs)。

  3. 应用配置中的 Worker 声明(启动每个声明的 Worker 进程)。配置里的 workers: 列表声明了随引擎生命周期启动的 Worker,例如仓库根目录的 engine/config.yaml 声明了 iii-streamconfiguration 两个引擎级 Worker:

    registration_namespace_grace_ms: 5000
    
    # Only workers that are part of the engine lifecycle belong here. Project
    # workers such as http, state, cron, queue, pubsub, and bridge belong in
    # worker-compose.yaml.
    workers:
      - name: iii-stream
        config:
          port: ${STREAM_PORT:3112}
          host: 127.0.0.1
          adapter:
            name: redis
            config:
              redis_url: redis://localhost:6379
    
      - name: configuration
        config:
          adapter:
            name: fs
            config:
              directory: ./config
          ttl_seconds: 0
    

    注意:只有属于引擎生命周期的 Worker 才放在 config.yaml;项目级 Worker(http、state、cron、queue、pubsub、bridge 等)属于 worker-compose.yaml,由 iii compose 管理(engine/config.yaml 中注释即说明这一点)。${STREAM_PORT:3112} 这类占位符表示支持环境变量插值与默认值。

  4. 开始服务连接engine.serve().await 启动 WebSocket 监听,接受来自各 Worker 的连接。

完成上述序列后,引擎即可接受 Worker 的 WebSocket 连接,并在它们之间路由调用。从源码结构看,这一就绪状态对应 engine/src/engine/mod.rsEngine 结构体持有的一组运行时注册表:worker_registry(Worker 连接注册表)、functions(函数注册表)、trigger_registry(触发器注册表)、service_registry(服务注册表)、invocations(调用处理器)与 channel_manager(通道管理器)。

引擎的三大运行时职责

文档将引擎职责概括为三个运行时关注点,下面结合源码逐一展开。

1. 接受 Worker 连接并维护实时注册表

引擎通过 WebSocket 接受 Worker 连接,并维护"当前有哪些 Worker 在线"的实时注册表。对应实现是 WorkerConnectionRegistryengine.worker_registry 字段),每个连接对应一个 WorkerConnection。Worker 进程本身可以部署在网络的任何可达位置——连接字符串是 Worker 与 iii 实例之间唯一的耦合。

一个从源码可以看到的实现细节是命名空间(namespace)解析:连接建立时,Worker 的命名空间并非随 WebSocket 握手到达,而是随 engine::workers::register 引擎调用到来。引擎为每个连接维护一个命名空间状态机(Pending → Draining → Resolved,见 engine/src/engine/mod.rs),期间到达的注册消息按到达顺序缓冲,避免注册乱序;若 Worker 迟迟不声明命名空间,引擎在 REGISTRATION_NAMESPACE_GRACE(默认 5 秒,可通过 registration_namespace_grace_ms 配置或 III_NAMESPACE_GRACE_MS 环境变量调整,见 engine/config.yamlengine/src/engine/mod.rs)之后将其归入 default 命名空间。

2. 跟踪每个 Worker 注册的 Functions 与 Triggers,暴露统一系统级表面

每个已连接 Worker 会注册它提供的 Functions 与 Triggers:RegisterFunctionRegisterTriggerRegisterTriggerTypeRegisterService 等消息由引擎写入全局注册表(FunctionsRegistryTriggerRegistryServicesRegistry),形成跨所有 Worker 的统一、系统级调用表面。这也解释了"任何 Worker 注册的 Function 都可用同一个 function_id 被系统内任何地方调用"这一性质。

注册的所有权(ownership)被精细管理:function_owners 表以 (namespace, function_id) 为键记录当前持有该函数的 WS Worker 与注册类型(普通调用注册或 HTTP 调用注册),保证快速重启、断连清理等并发场景下注册不会被误删(见 engine/src/engine/mod.rs)。

3. 路由调用:找到提供目标 Function 的 Worker 并分发

当 Trigger 触发或 Function 被调用时,引擎负责解析目标并分发调用。核心链路在 engine/src/engine/mod.rs 中:

  • resolve_function(约 L652-L692):在恰好一个命名空间中解析 function_id——Some(ns) 只在 ns 中解析,None 只在 default 中解析;刻意不做跨命名空间搜索。文档与源码都强调:命名空间是调用方显式寻址的路由维度,而不是环境性作用域——否则"调用的目标取决于谁在问",这正是命名空间要消除的歧义。解析失败返回 function_not_found 错误,并附带该 id 实际存在的命名空间列表,方便排障。
  • remember_invocation + spawn_invoke_function(约 L762-L891):以后台任务的方式走完"记住调用 → 通过 InvocationHandler 处理调用 → 把 InvocationResult 回传给调用方"的完整流程;同时把调用方的 traceparent / baggage 上下文透传给下游,让调用链 Span 直接嵌套在调用方 Span 之下。

引擎内部也通过 EngineTrait 暴露 call / call_with_metadata_ns 等接口(见 engine/src/engine/mod.rs),供内置 Worker、钩子与中间件以进程内方式发起调用。

Worker 断连清理(Worker disconnect cleanup)

当一个 Worker 断开连接时,引擎会自动清理该 Worker 在实时注册表中的痕迹:其 Functions 与 Triggers 被移除,针对这些 Function 的进行中(in-flight)调用被取消,系统其余部分继续对外服务

对应实现是 cleanup_worker(见 engine/src/engine/mod.rs),其清理步骤按序执行:

  1. 只清理一次:通过 cleanup_claimed 原子标记保证并发退出路径上只有一个清理方执行完整 teardown,其余调用方等待完成,避免重复的 worker_disconnected 事件与重复的死亡指标。
  2. 终止命名空间排空abort_namespace_resolution 防止正在排空的注册队列继续给已断开的 Worker 注册函数。
  3. 释放函数所有权:对普通函数走 release_function_if_owner(CAS 释放,若所有权已被另一个仍存活的 Worker 接管则跳过——这是"快速重启竞态"的保护);对外部(HTTP 调用)函数则先快照所有权、经 HttpFunctionsWorker.unregister_http_function 注销后再 CAS 释放,防止清理中途被并发注册抢占导致误删新主人的状态。
  4. 取消进行中的调用:遍历该 Worker 的 in-flight invocation,调用 invocations.halt_invocation(invocation_id) 终止它们。
  5. 注销注册表条目trigger_registry.unregister_workerchannel_manager.remove_channels_by_worker、释放 worker-name 租约(release_worker_name_if_owner,同样 CAS,避免误伤已重连的同名 Worker)、最后 worker_registry.unregister_worker

进行中的调用:invocation_stopped 错误

文档明确指出:断连时进行中的请求会收到 invocation_stopped 错误,应将其视为取消;在拥有该 Function 的 Worker 重连之前,重试都会失败。完整语义与三语言示例见 Workers 文档「Handling Worker disconnects」,这里给出一个最小化的 Node 示例:

import { IIIInvocationError } from "iii-sdk";

try {
  const result = await worker.trigger({
    function_id: "math::add",
    payload: { a: 1, b: 2 },
  });
} catch (err) {
  if (err instanceof IIIInvocationError && err.code === "invocation_stopped") {
    // Worker disconnected mid-invocation。订阅 engine::functions-available
    // 以获知何时可以重试。
    return;
  }
  throw err;
}

优雅关闭

SDK 的 shutdown 会干净地关闭 WebSocket:引擎移除该 Worker 的 Functions 与 Triggers、以 worker_disconnected 触发 engine::workers-available,并以 invocation_stopped 取消指向它们的 in-flight 调用。若进程被强杀,引擎在发现 socket 断开后也会达到同样的状态,只是优雅关闭更确定、更快。对一次性/短生命周期 Worker(Kubernetes Job、Serverless 容器、定时脚本)而言,shutdown() 尤其有用——它们可以像普通 Worker 一样连接、干活、干净退出(详见 Workers 文档「Shutting down a worker」)。

配置热重载(Config hot-reload)

config.yaml 在运行时被监听。文件变更后,引擎执行 解析(parse)→ 差异(diff)→ 校验(validate)→ 提交(commit) 四步:

  • 未在差异中变化的 Worker 保持运行,只有新增、删除或变更的 Worker 被重启。这意味着调整一个 Worker 的声明不会影响其他 Worker 的在线状态。
  • 无效配置(解析错误或校验失败)导致引擎退出,而不是进入不确定状态(indeterminate state)。这是文档明确强调的失败语义:宁可退出,也不要在半应用的状态下继续服务。

源码侧的证据:EngineBuilder::build 在配置了文件路径时设置 config file watcher(见 engine/src/workers/config.rs,watcher 观察父目录以兼容编辑器的原子写入);纯内存配置(无文件路径)则关闭文件监听并记录 "reload: no config file to watch"。reload 的差异、作用域与管理逻辑集中在 engine/src/workers/reload/ 模块,仓库还带有 reload_diff_unit.rsreload_manager_unit.rsreload_scope_unit.rs 等单测(engine/tests/),以及配置重载相关的 e2e 测试 config_reload_e2e.rs

Worker settings 是独立分层(较新版本的补充)

在仓库最新版文档 docs/understanding-iii/engine.mdx 中,热重载章节还补充了一层 Worker settings:注册了配置 schema 的 Worker,在首次启动时读取一次自己的 config: 块,用于引导其在 configuration Worker 中的条目;此后其 settings 由该 Worker 动态管理并更新,无需引擎 reload。settings 变更会依据 Worker 的 schema 校验:无效变更被拒绝并保留旧值,因此一次错误的 settings 编辑永远不会把引擎搞挂。这与 config.yaml 的"无效即退出"形成对照:引擎级配置变更走"重载并严格校验"路径,Worker 级 settings 变更走"独立、可回滚"路径。

架构无关路由(Architecture-agnostic routing)

路由与语言、运行时、位置无关。引擎对以下场景应用同一条路由路径:

  • 笔记本上运行的 Python Agent 托管的 Function;
  • 浏览器标签页里运行的 TypeScript Worker 托管的 Function;
  • 微虚拟机(microVM)中运行的 Rust 二进制 托管的 Function;
  • Kubernetes 上运行的 OCI 镜像 托管的 Function。

这就是文档所说的:"任何语言、任何运行时"(any language, any runtime)是 iii 的一个具体属性(concrete property),而非愿景。

这一性质的根基在于 Worker 与引擎之间唯一的耦合就是连接字符串。Worker 通过 III_URL 环境变量约定引擎地址(也可显式传给 register_worker),例如 Workers 文档「Connecting to the engine」 中的三语言写法:

import { registerWorker } from "iii-sdk";

const url = process.env.III_URL;
if (!url) throw new Error("III_URL must be set");
const worker = registerWorker(url, {
  workerName: "my-worker",
});
import os
from iii import register_worker, InitOptions

worker = register_worker(
    os.environ.get("III_URL"),
    InitOptions(worker_name="my-worker"),
)
use iii_sdk::{InitOptions, WorkerMetadata, register_worker};

let url = std::env::var("III_URL").expect("III_URL must be set");
let worker = register_worker(
    &url,
    InitOptions {
        metadata: Some(WorkerMetadata {
            name: "my-worker".into(),
            ..Default::default()
        }),
        ..Default::default()
    },
);

Worker 连接后经历的状态机为 connecting → connected → available / busy → disconnected(详见 Workers 文档「Worker lifecycle」),引擎跟踪这些迁移并通过发现函数(discovery functions)向其他 Worker 与工具暴露,使系统可以响应拓扑变化。

发现与实时注册表(Discovery and the live registry)

引擎维护的注册表包含三类信息:每个已连接 Worker每个 Worker 注册的 Functions绑定到这些 Functions 的 Triggers。其他 Worker 与工具既可以按需读取注册表的当前快照,也可以订阅其演化过程。

快照调用:engine::*::list

按需读取注册表快照的方式是调用以下 engine::*::list 引擎函数(见 Workers 文档「Inspecting the live registry」):

函数 返回内容
engine::workers::list 每个已连接 Worker 及其指标(可传 worker_id 查询单个 Worker)
engine::functions::list 每个已注册 Function,可通过 include_internal 过滤内置函数
engine::triggers::list 每个已注册 Trigger,可通过 include_internal 过滤
engine::trigger-types::list 每个已通告的 Trigger 类型及其配置与调用 schema

一个 Node 示例:

const { workers } = await worker.trigger({
  function_id: "engine::workers::list",
  payload: {},
});

const { functions } = await worker.trigger({
  function_id: "engine::functions::list",
  payload: { include_internal: false },
});

订阅事件:engine::workers-available / engine::functions-available

实时响应拓扑变化,可以把 Trigger 绑定到引擎的发现事件:

Trigger 触发时机
engine::workers-available 有 Worker 连接或断开
engine::functions-available 有 Function 被注册或注销

这对"等 Worker 重新上线后继续工作"尤其有用。以 Node 为例,注册一个针对 engine::workers-available 的处理器:

worker.registerFunction(
  "discovery::on-workers",
  async (data: { event: string; worker_id: string }) => {
    if (data.event === "worker_connected") {
      // 一个 Worker 刚加入注册表,它的 Functions 现在可调用了。
    }
  },
);
worker.registerTrigger({
  type: "engine::workers-available",
  function_id: "discovery::on-workers",
  config: {},
});

可观测性

查询 traces、logs 与 metrics 的能力由 iii-observability Worker 提供(关联文档末尾的 Note 明确说明这一点)。换言之:注册表/拓扑相关的发现由引擎自身暴露,而时序数据类可观测性则委托给独立的 observability Worker,引擎保持"薄层"定位。

小结

iii 引擎的运行时心智模型可以浓缩为四句话:

  • 启动:解析参数 → 加载 config.yaml → 按声明启动引擎级 Worker → 开始服务 WebSocket 连接;
  • 职责:维护在线 Worker 注册表、聚合所有 Worker 的 Functions/Triggers 成统一表面、按命名空间精确解析并路由每次调用;
  • 容错:Worker 断连时自动清理注册、取消 in-flight 调用(invocation_stopped);配置变更走"解析-差异-校验-提交"热重载,无效配置宁可退出也不半应用;Worker settings 走独立可回滚路径;
  • 开放:路由与语言、运行时、位置无关,任何通过 III_URL 接入的进程都获得同等的一等公民地位;系统拓扑通过 engine::*::list 与发现事件对任何 Worker/工具开放。

如果你想进一步深入,推荐继续阅读:

登录后查看全文
热门项目推荐
相关项目推荐