Skip to main content
IO 访问子系统是 Worker 进程内的一层资源治理代码。Builder、Executor 中的用户函数(经 Python SDK 和 UDS 回送)、Writer plugin 的所有 BlockDB / RPC / 能力后端访问都经过它,再由各 backend adapter 发起真实网络调用。它在系统中的位置见 架构总览 它不按 core / adapter 的事件-命令模式组织,而是采用 Scoped Resource Context:Worker 级共享资源(WorkerIOScope)里嵌套 task 级作用域(TaskIOScope),请求生命周期嵌套在 task 生命周期里。

职责与边界

负责:
  • 提供 TaskIOScope.Read / Write 这一个同步入口,供 Builder、Executor IO 回送、Plugin 共用。
  • 请求路径固定为:Worker 级读缓存 → task 内 singleflight → task IO 窗口 → backend admission → adapter。
  • 持有 Worker 级共享读缓存(IO cache)和独立的 system cache。
  • 对带 Builder / Result retry scope 的请求执行有界重试。
  • 把请求生命周期绑定到 task:scope 关闭后取消在途请求、拒绝新请求、丢弃迟到结果。
  • 统一错误模型(model.IOError)与 per-task IO 统计。
不负责:
  • 决定 call 是否被调度(那是 Dispatcher 的事,见 Call 执行子系统)。
  • 真实协议调用、连接池、错误分类的具体逻辑——这些在 internal/sdk/* 的 adapter 里,见 后端适配器
  • call result cache(归 Call 执行子系统)。
  • 跨 task 的写幂等或事务恢复。
核心不变量:
  • 每个 backend kind 只有一个 admission controller,由 assembly.Build 强制包装(adaptive.WrapBackend);TaskIOScope 前面不再叠第二层共享 quota。
  • 写请求不进缓存、不做 singleflight,但同样经过 task 窗口和 backend admission。
  • req.Timeout() 只约束获得 permit 之后的真实 adapter 调用;task 窗口和 admission 排队没有本地超时,只受请求 context 与 scope 生命周期约束。
  • CacheKey() 表示”不可缓存”,跳过缓存和 singleflight,直接走窗口 + adapter。
  • TaskIOScope.Close 幂等;关闭后不得再回填缓存或 singleflight。
  • IO cache key 由 (backend, operation) 命名空间 + 请求归一化 key 组成,不同后端 / 接口不会碰撞。

代码位置

核心类型与接口

  • model.BackendAdapterinternal/io/core/model/adapter.go):adapter 必须实现的最小接口。Write 返回 (*WriteResult, error)
  • model.ReadReq / model.WriteReq(同文件):请求接口。ReadReqBackend() BackendKindOperation() stringCacheKey() stringTimeout() time.DurationWriteReq 没有 CacheKey。具体请求类型由各 SDK 包定义(如 internal/sdk/blockdb/types.go),Executor 回送的请求由 executor.IORequest 实现。
  • model.ErrorClassifier:adapter 可选实现 ClassifyError(err) *IOError;没实现时未知错误一律归 retryable_io
  • model.CachedReadValidator:可缓存读的可选能力 AcceptCachedRead(data []byte) bool,返回 false 时旁路缓存值并进入同 key 的 singleflight 刷新(当前使用者是 internal/sdk/iceberg/io_adapter.go)。
  • model.IOError / model.IOErrorKind:统一错误模型。Kind 取值 system_errorparam_errorretryable_ionon_retryabletimeoutscope_closedCode 是细粒度可观测码(如 io:no_adapter_registeredcontext:timeout);StructuredError() 生成回给 Python SDK 的 errorKind / retryable / detailCode
  • model.BackendRegistrationKindAdapterSystemCache bool(读走 system cache)、ReadOnly boolWrite 直接以 io:write_not_supported 拒绝)。
  • core.WorkerIOScopeworker_scope.go):NewWorkerIOScope(WorkerIOScopeParams)NewTaskScope(*commontypes.TaskCtx)Close()Snapshot()WorkerIOScopeParams.DisableSharedReadCache 供 Sync Invoker 关闭跨 call 结果复用。
  • core.TaskIOScopetask_scope.go):ReadWriteClose(reason)Snapshot()IOStats()BackendHits()AddCallIO / FinalizeCallIOBackendIOStats()DrainIOErrorClusters()
  • core.RetryScope / core.WithRetryScoperetry.go):只有 RetryScopeBuilderRetryScopeResult 两个框架标签能开启 IO-local retry;未知标签 fail-closed。
  • adaptive.Backend / adaptive.WrapBackend(next, Config)adaptive/backend.go):给任意 adapter 外包一个独占 Limiter;同时实现 ErrorClassifier,把 admission 错误映射为 canonical 的 context:timeout / context:canceled / io:temporarily_unavailable
  • adaptive.Limiter / adaptive.Permit / adaptive.Outcomeadaptive/limiter.go):Acquire(ctx) (*Permit, error)Permit.Done(outcome)(幂等);Outcome 三值 OutcomeSuccess / OutcomeOverloaded / OutcomeIgnore
  • adaptive.OutcomeClassifier:adapter 可选实现 ClassifyAdaptiveOutcome(err) Outcome;没实现时成功记 Success,错误记 Ignore。实现者见 internal/sdk/noderpc/adaptive.gointernal/sdk/blockdb/adaptive.gointernal/sdk/logicaltypes/adaptive.gointernal/sdk/meta/adaptive.go
  • adaptive.AIMDConfigadaptive/config.go):运维面的 AIMD 配置;ResolveAdmission(initialLimit)Enabled=false 时退化为 FixedConfig
  • assembly.Module / assembly.Buildassembly/assembly.go):见”扩展点”。
  • capabilitywire.DecodeBinaryRequest(kind, req) (method, payload, error):见”扩展点”。

数据流 / 执行流程

一次来自 Executor 的读请求(Python SDK → UDS → Worker → backend): 要点:
  • 入口是 internal/worker/adapters/executor/handlers.goprocessWaitRequest:按 CallWaitingPayloadMode / Operation / Backend / CacheKey / TimeoutMs / Request 构造 executor.IORequest(带二进制 sidecar 时用 NewIORequestWithBinaryDeriveCacheKeyFromBinary=true 时以 sidecar 字节直接做 cache key),再调 IOHandler.HandleIO
  • Orchestrator.HandleIOorchestrator_dispatch.go)按 taskID 找到 taskRuntime,把 instance id 写进 ctx(供公平 lane 用),Executor 来源的 blockdb 写在这里被拒绝,然后调 rt.io.Read / Write;成功后把 BackendLatencyMs 记进 per-call IO 统计。
  • 回程:ResumeCall.BudgetUsedMsReadResult.BudgetChargedMs()——缓存 / singleflight 命中为 0,真实读为 adapter 测得的后端时长;admission 和窗口排队时间不计入 budget。请求经二进制 sidecar 到达的,结果 Data 也走 sidecar 回去(binary-in → binary-out)。
  • Builder 与 Writer 不经 UDS,直接持有 TaskIOScope(Builder 拿的是 cbtypes.TaskIOReader,plugin 拿 types.TaskIO),ctx 分别带 RetryScopeBuilder / RetryScopeResult
  • 写路径与读路径的差别只在:跳过缓存与 singleflight、先检查 ReadOnly;其余(窗口、admission、重试、错误分类)相同。

缓存与命名空间

WorkerIOScope 持有两个 cache.Cache[ioCacheKey] 实例:
  • IO cache:默认 4096 entries / 16 MiB,TTL 固定 1 分钟(workerIOCacheTTL),跨 task 共享,Worker 关闭时 Purge
  • system cache:默认 64 entries / 512 MiB / 20 分钟,只服务注册时声明 SystemCache: true 的 backend(当前是 icebergresolve_data_files)。
ioCacheKey{namespace, request} 中的 namespace(backend, operation) 的数字 ID:BlockDB 两个 kind、logicalTypesmetarouterlocaltestserviceiceberg 的已知 operation 在 knownIOCacheNamespace 里有编译期 ID;NodeRPC 的 JSON-RPC 方法是开放集合,与其他未知组合一起走有界(4096)动态注册表,注册表耗尽时该请求退化为不可缓存读。第三类缓存 call result cache 不在本子系统内。

自适应准入

adaptive.Backend 是每个 backend 唯一的进程级准入门。Limiter 实现窗口化 AIMD:
  • 每个采样窗口(默认 1s)内首次 Overloaded 立即乘法下降(BackoffRatio 默认 0.5,下限 MinLimit);同窗口再次下降要求新代际样本达到 RepeatBackoffMinSamples 且过载比例达到 RepeatBackoffOverloadRatio,且不超过 MaxDecreasesPerWindow
  • Ignore 比例达到 IgnoreRatioThreshold(0.10)时窗口失去增长资格,达到 IgnoreBackoffRatioThreshold(0.30)时窗口结算时收缩一次。
  • 增长只在窗口结束时发生,要求无过载、样本够、饱和(最大 in-flight 达到 limit)、连续健康窗口达到 IncreaseAfterHealthyWindows;步长 IncreaseStep(0 表示 initial 的 1%,至少 1)。
  • 超过已学习的 softLimit 时增长变成探测:探测代际过载则精确退回 probeBaseLimit,连续失败按窗口数指数退避(上限 ProbeBackoffMaxWindows)。
  • limit 降到 in-flight 以下时不取消存量请求,用 shrink debt 偿还。
  • 不起 goroutine,窗口由 Acquire / Done 事件惰性推进。
Config.LaneKeyFromCtx 非空时进入公平 lane 模式(adaptive/lanes.go):等待者按 key 分道,空出的容量给 in-flight 最少的 lane;AIMD 策略不变。Worker 通过 IOFairQueueBackends 配置对指定 backend 开启,key 取 clientid.FromContext observed.WrapBackendRegistration 在包装的同时按 BackendKind 注册 blockx_io_adaptive_limit / _pressure / _overloads_total / _ignored_total / _limit_transitions_total / _admission_waits_total / _admission_wait_seconds_total。IO 层其余指标是 blockx_task_io_ops_totalblockx_task_io_backend_duration_millisecondsblockx_task_io_cache_hits_totalblockx_task_io_singleflight_hits_total,定义在 internal/obs/metrics.gointernal/obs/metrics_io_adaptive.go

状态与生命周期

TaskIOScope 只有”活跃”和”closing”两个状态:
  • 创建:task 激活时 OrchestratorioScope.NewTaskScope(ioTaskCtx)orchestrator_phases.go),失败记为 task_activation_failed。scope 持有派生自 task ctx 的 scopeCtx
  • 请求:每次 Read / Write 派生 ioCtxcontext.AfterFunc(scopeCtx, ioCancel) 使 scope 关闭能取消在途 adapter 调用;每个 attempt 单独 Acquire / Release task 窗口,退避期间不占窗口和 permit。
  • 关闭:task 终态时 rt.io.Close("task_terminal")CloseCompareAndSwap 保证幂等,然后依次:取消 scopeCtxsfTable.DrainAll(scope_closed) 唤醒所有 waiter、从 WorkerIOScope.activeScopes 移除。
  • 迟到结果:leader 在 runIORetry 成功返回后再检查一次 closing,已关闭则返回 scope_closed,不写缓存也不广播;sflight.Table.CompleteDrainAll 在同一把锁下做幂等 close,避免 double-close panic。
  • Worker 关闭:WorkerIOScope.Close 先关所有活跃 task scope,Purge 两个缓存,再逐个 adapter.Close()(adaptive wrapper 先关 limiter 唤醒 waiter,再关 raw adapter)。
错误分类与重试:
  • 等待期错误由 classifyWaitContextError 归一:scope 已 closing → scope_closed / io:temporarily_unavailableDeadlineExceededtimeout / context:timeoutCanceledscope_closed / context:canceled;其他 → system_error。没有独立的”排队超时”错误。
  • backend 错误由 classifyBackendError 归一:*IOError 原样透传 → adapter 的 ClassifyError → 否则 retryable_io
  • shouldRetryIO 只在 ioErr.Retryable()、未用完 RetryMaxRetries、ctx 带已知 retry scope 且 ctx 未结束时放行。退避用 backoff/v5:默认三次窗口 250–500ms、500ms–1s、1–2s,单次上限 3s。
  • 写重试是 at-least-once:IO core 只重放当前 typed write request。BlockDB 全部 API(含 BundleWrite 的 InitWriteJob / CommitWriteJob / CancelWriteJob)保证幂等,因此 Result scope 内的 BlockDB 写保持可重试;此前的 WithoutRetryScope 退出机制已在 commit aae6b77a 删除。接入不满足幂等契约的写 API 时必须先调整 retry scope 或 backend 契约。
  • singleflight leader 拥有整个 retry 循环,waiter 只等最终结果;缓存只回填最终成功值。

配置

Worker 侧 IO 配置在 WorkerFullConfig.IO(类型 iocore.WorkerIOConfig,定义在 internal/io/core/model/adapter.gointernal/io/core/types.go 只做别名;默认值来自 internal/worker/app/config.goDefaultIOConfig): backend admission 配置在 WorkerFullConfig 顶层: Worker 与 Sync Invoker 的装配表注册的 backend kind 是 rpcblockdbblockdb_executorlogicalTypesiceberglocaltestservicerouterinternal/sdk/meta 定义了 BackendMeta,但两处装配表都没有注册它。 Sync Invoker(cmd/syncinvoker/io.go)只设 TaskMaxInflightIO: 1024,并以 DisableSharedReadCache: true 构造 WorkerIOScope

扩展点

新增一个 capability backend

internal/sdk/localtestservice/ 为模板(docs/capability-backend-guide.md 是逐文件手册),平台代码零改动。Go 侧需要:
1

定义 kind 与 wire 契约

internal/sdk/<kind>/ 下声明 const Backend<X> iocore.BackendKind = "<kind>"(不得复用存量 kind),写 proto/<kind>.proto 作为 gRPC 服务定义,生成代码进 gen/
2

实现 service 与 adapter

service.go 实现生成的 server 接口,业务逻辑全在这里。adapter.go 实现 model.BackendAdapterReadcapabilitywire.DecodeBinaryRequest(string(Backend<X>), req) 拆出 (grpc_method, protobuf 字节)proto.Unmarshal 后调 service,响应 proto.MarshalReadResult.Data;未知方法返回 IOErrParam + <kind>:unsupported_method,信封 / proto 不合形返回 <kind>:bad_request。只读能力的 Write 不会被调到(module 声明 ReadOnly: true)。外部依赖型后端应再实现 model.ErrorClassifieradaptive.OutcomeClassifier
3

提供 assembly.Module

导出 Module()(可带依赖入参)返回 assembly.Module{Kind, ReadOnly, Build, Stub, Admission}。进程内轻后端用 adaptive.FixedConfig(n);外部依赖型后端用 AIMDConfig.ResolveAdmissionEnabled 判断配置是否齐全,Stub 给 dev 回退(不给则未配置时拒绝启动)。需要借用其他 backend 时按 localtestservice.BlockDBReader 的方式注入 raw adapter:借用不持有(Close 不关它)、准入自负、本地 ctx 结束的错误先原样放行再委托依赖分类。
4

注册到两个 binary

internal/worker/app/config.go 加开关字段,internal/worker/app/app.go 加 env override 并 backendModules = append(backendModules, <kind>.Module(...))cmd/syncinvoker/io.go 同样 append。deploy/env/worker.env.example 补一行。
5

测试

adapter_test.go 覆盖:正常路径、参数默认值、未知 operation、malformed 信封、malformed proto、Write 拒绝、Module() 声明;组合后端再加 typed 请求转发、依赖未注入干净失败、依赖错误分类透传。e2e 在 cmd/worker/worker_process_localtestservice_test.go 写正例(FUNCTION_CODE_AUDIT_MODE=enforce 下 task SUCCEEDED)和 <KIND>_DISABLED=true 负例(task FAILED);devstub 函数注册在 internal/worker/app/function_code.go
Python 侧(blockx-py 类、connection.py channel getter、executor BridgeChannel 接线、python/blockx_audit/tables.py 五处表增量)见 Python Executordocs/capability-backend-guide.md §3–§4。 assembly.Module 的字段:
OuterWrap 是唯一允许放在 admission 外侧的层(当前只有 NodeRPC 语义合批 noderpc.NewBatchingBackend,由 NODE_RPC_SEMANTIC_BATCH_MODE 门控,默认 off)。

其他常见改动

  • 给热点 (backend, operation) 加编译期 cache 命名空间:改 worker_scope.goknownIOCacheNamespace 和对应枚举,并更新 TestKnownIOCacheNamespacesAreDistinctEnums
  • 调整某个 backend 的 AIMD 反馈:改该 SDK 包的 adaptive.goClassifyAdaptiveOutcome),不要动 internal/io/adaptive;判断传输层错误用 adaptive.IsTransportFailure / IsTransportFailureMessage
  • 新增可缓存但需要请求级校验的读:让请求类型实现 model.CachedReadValidator

测试

测试组织:
  • internal/io/core/core_test.go:请求路径主干——缓存命中 / 跨 task 共享(TestWorkerScope_CacheIsSharedAcrossTaskLifetimes)、singleflight(TestTaskScope_ReadSingleflight)、task 窗口获取 / 释放(TestTaskScope_TaskWindowWaitsUntilCapacityIsReleasedTestTaskScope_IOBackendAttemptReleasesTaskWindow)、scope 关闭与迟到结果(TestTaskScope_ActiveBackendContextLifecycleTestTaskScope_ReadAfterCloseTestTaskScope_CloseIdempotentTestWorkerScope_CloseCascades)、只读后端拒写、空 cacheKey 旁路(task_scope_bypass_test.go)。
  • internal/io/core/retry_test.go:retry scope 门控、单个 leader 拥有重试、attempt 间释放资源、退避边界。
  • internal/io/core/task_scope_drain_test.goCompleteDrainAll 并发不 double-close。
  • internal/io/adaptive/limiter_test.golanes_test.gobackend_test.gobackend_budget_test.go:AIMD 各条规则、探测、公平 lane、admission 排队不计入 budget、超时在 admission 后才开始。
  • internal/io/assembly/assembly_test.go:stub 回退、重复 kind、失败时关闭已建 adapter、OuterWrap
  • internal/io/capabilitywire/wire_test.go:信封三方绑定、shape 错误 fail-closed。
  • internal/io/sflight/sflight_test.gointernal/io/cache/cache_test.go:singleflight 与缓存的独立单测。
  • 基准:internal/io/core/bench_test.goadaptive/lanes_bench_test.go
  • 与 Executor 联动的进程级 e2e 在 cmd/worker/worker_process_*_test.go(如 worker_process_blockdb_bridge_test.goworker_process_localtestservice_test.go)。更多见 测试组织与命令

相关文档

blockx 仓库中的 spec 与设计文档:
  • docs/specs/io-subsystem.md:本子系统的行为规范(Scoped Resource Context、adapter 职责、AIMD 反馈分类、Close 语义、缓存策略与 fork 风险、配置项)。
  • docs/io-backend-module-design.md:backend module 装配层的设计动机与不变量。
  • docs/capability-backend-guide.md:新增能力后端的逐文件操作手册与检查单。
  • docs/specs/architecture.md §4.2.5 / §4.2.6:IO 访问子系统与共享状态缓存在总体架构中的定位。
  • docs/specs/noderpc-semantic-batching.md:NodeRPC 语义合批(OuterWrap 的使用者)。
  • docs/deploy.md:IO / AIMD 相关环境变量的部署说明。
站内相关页面:

Worker

WorkerIOScope 的宿主,task scope 的创建与关闭时机。

后端适配器

BlockDB / NodeRPC / logical-types / Iceberg 等 adapter 的具体实现与错误分类。

Python Executor

IO 请求在 Python 侧如何被拦截并经 UDS 回送。

Call 执行子系统

HandleIO 所在的 Orchestrator 与 call result cache。