资讯动态

LocalAI 分布式模型暂存状态操作的稳定性设计:以 PostgreSQL 持久化作业为主数据源的 /api/operations 合并方案

发布时间:2026/9/9 20:23:39 来源:尧图企业网站定制
LocalAI 分布式模型暂存状态操作的稳定性设计以 PostgreSQL 持久化作业为主数据源的 /api/operations 合并方案【免费下载链接】LocalAILocalAI is the open-source AI engine. Run any model - LLMs, vision, voice, image, video - on any hardware. No GPU required.项目地址: https://gitcode.com/GitHub_Trending/lo/LocalAI导读LocalAI 的分布式架构中每个前端副本通过各自进程内的内存态StagingTracker与 NATS 广播来呈现模型文件暂存staging进度但广播是瞬时的——副本一旦错过消息就会让 UI 上的操作行“闪烁”消失。本设计引入NodeRegistry对 PostgreSQLmodel_load_jobs表的查询作为稳定基线再叠加本地的 tracker 快照让/api/operations在任何副本上都返回不闪烁、不重复的暂存操作。读完本文你将理解 flicker 问题的成因、数据库行与 tracker 的合并规则、失败降级策略以及对应的源码与测试验证方法。问题背景请求均衡导致的操作行闪烁GET /api/operations是 LocalAI Web UI 用于轮询当前所有操作模型安装、后端安装、模型文件暂存等的管理员接口前端每秒钟轮询一次。在单节点模式下它完全可靠但在分布式distributed模式下存在一致性问题。问题根因是staging 进度原本只存在于发起传输的那个前端副本进程内的内存StagingTracker中。为了让其他副本也能展示这个操作副本之间通过 NATS 广播 tracker 更新StagingProgressEvent其他副本收到后用ApplyRemote将其镜像到本地。这套机制带来的隐患是消息是瞬时的fire-and-forget某副本在 staging 开始之后才启动错过全部广播某副本临时断连错过中间更新某副本因流量、GC 等原因漏掉某次广播远端镜像超过 TTLstagingRemoteTTL 60 * time.Second未刷新会被视为过期并删除。当浏览器的逐秒轮询被负载均衡到这些副本上时同一操作会出现又消失appears and disappears表现为 UI 上进度行闪烁、甚至完全丢失。相关细节可参考 NATS subject 的定义与说明core/services/messaging/subjects.go。设计基石数据库行作为集群的持久权威LocalAI 的分布式冷加载cold load已经在 PostgreSQL 的model_load_jobs表中持久化了每次加载的完整生命周期信息。这张表在每个副本可见天然具备跨副本一致性因此本设计把它确立为暂存操作的持久权威durable cluster authority与基线视图。ModelLoadJob一次冷加载的一行记录ModelLoadJob结构体定义在 core/services/nodes/registry.go关键字段包括字段说明TrackingKey主键一个模型的一次冷加载唯一对应一行该唯一性用于跨副本去重并发加载器State加载阶段取值见下方状态常量OwnerReplica/NodeID/NodeName/ReplicaIndex负责副本与目标 worker 节点的身份/位置信息BytesSent/TotalBytes/FileIndex/TotalFiles字节与文件进度计数StartedAt首次上报字节的时刻ETA 的速率基准区别于CreatedAt后者还覆盖节点选择、后端安装等不移动字节的阶段LastProgress心跳时间戳仅表示进程存活不代表字节在移动checkpoint 加载可能长时间零字节CreatedAt/UpdatedAt/LastError时间线与失败信息一次冷加载的状态机由五个常量描述core/services/nodes/model_load_job.gopending节点选择与副本分配阶段installing后端安装阶段staging模型文件传输暂存阶段——本设计只关注这一阶段loadingcheckpoint 加载阶段failed失败行保留loadJobFailureGrace15 秒后删除。终端行一律被删除而非保留因为NodeModel行已经记录了“模型已加载”这一事实保留已完成的作业会产生第二个真相源。model_load_jobs表的出现本身解决了更早的一个架构问题在此表之前整个冷加载后端安装、数 GB 的 staging、checkpoint 加载都发生在 per-model advisory lock 内其他副本对同一模型的请求会阻塞在pg_advisory_lock上长达数十分钟最终被statement_timeout杀掉。引入作业行后锁被收缩到“认领”这一个动作真正的加载工作可以在无锁状态下进行且全程可观测core/services/nodes/registry.go。NodeRegistry 提供的查询能力NodeRegistry提供了一组围绕作业行的方法core/services/nodes/model_load_job.goClaimLoadJob在 per-model advisory lock 下决定本副本是否拥有某 tracking key 的冷加载认领逻辑结合IsOrphaned判断超过loadJobOrphanWindow即 60 秒无心跳即可被其他副本接管删除重建防止“拥有者崩溃导致模型永久卡死”GetLoadJob非拥有副本轮询权威行ListActiveLoadJobs按tracking_key升序返回所有进行中的加载作业——这正是/api/operations合并逻辑调用的接口UpdateLoadJob/FailLoadJob/DeleteLoadJob阶段迁移、失败记录与终态清理。其中Progress()方法core/services/nodes/model_load_job.go实现与 tracker 相同思路的整体进度算法——单文件用字节百分比多文件则按下述公式计算确保数据库行与 tracker 对“整体进度”的语义一致filePct BytesSent / TotalBytes * 100 Progress ( (FileIndex-1)*100 filePct ) / TotalFiles // 多文件时内存侧的现状StagingTracker 与 NATS 镜像StagingStatus 数据结构StagingTracker是各前端副本进程内的内存状态位于 core/services/nodes/staging_progress.go。其中StagingStatus是暴露给 UI 的核心结构type StagingStatus struct { ModelID string json:model_id NodeName string json:node_name FileName string json:file_name BytesSent int64 json:bytes_sent TotalBytes int64 json:total_bytes Progress float64 json:progress // 0-100 overall progress Speed string json:speed FileIndex int json:file_index TotalFiles int json:total_files Message string json:message StartedAt time.Time json:started_at }生命周期 API 与两个关键常量tracker 的核心方法构成了 staging 的生命周期Start注册操作、UpdateFile刷新文件级字节进度、FileComplete标记单文件完成、Complete移除操作ApplyRemote合并远端广播GetAll/Get提供查询后者会顺带清理过期的远端镜像。源码中两个关键常量值得关注stagingBroadcastInterval time.Second // 字节级 UpdateFile 的广播去抖间隔前沿去抖 stagingRemoteTTL 60 * time.Second // 远端镜像过期 TTL广播去抖UpdateFile每秒可能触发多次只在距上次广播超过stagingBroadcastInterval时才重发而Start、FileComplete、Complete这类状态迁移事件总是立即广播保证对端不会漏掉关键状态镜像 TTLNATS 是 fire-and-forget如果错过Done事件对端的镜像行会永远残留成“幻影”。由于存活操作至少每stagingBroadcastInterval刷新一次镜像因此超过 60 秒未更新的远端镜像可安全判定为过期并剔除见 core/services/nodes/staging_progress.go 的完整注释。另外ApplyRemote有一个重要的本地权威规则本地拥有的操作不会被远端事件覆盖或删除防止回环与串扰而Complete会广播Done: true让对端删除镜像core/services/nodes/staging_progress.go。合并设计以 tracking key 为键的 baseline overlay设计文档给出的核心方案是数据库行提供基线tracker 数据作为更新鲜的覆盖层overlay。因为 tracker 里可能持有比周期性落库的作业行更新的 message 与文件名而数据库行则能保证任何副本在任何时刻都看到这条操作。合并规则在/api/operations处理逻辑中实现于 core/http/routes/ui_api.go合并流程分四步若当前副本没有分布式服务applicationInstance.Distributed() nil维持原有逻辑不进入合并分支从d.Router.StagingTracker().GetAll()取出本副本 tracker 快照为每个 entry 构造stagingOperations[modelID]其操作 id 统一为staging: modelID调用d.Registry.ListActiveLoadJobs()列出全部进行中的作业行仅对State LoadJobStateStaging的行构造数据库侧操作同样写入stagingOperations[trackingKey]若数据库行存在同 key的 tracker entry则用 tracker 的nodeName、message、progress、currentBytes、totalBytes覆盖数据库行的对应字段。合并以stagingOperationsmap 为媒介天然满足“不产生重复”的要求——tracker entry 与数据库行落在同一个 key 下tracker 字段覆盖数据库字段而非追加一条。整体排序规则保持不变先按progress升序、再按id稳定排序core/http/routes/ui_api.go。字段来源分工输出字段数据库行提供baselinetracker 覆盖overlayid/name/fullName/jobIDstaging: TrackingKey相同 key不重复nodeID/nodeNamejob.NodeID/job.NodeNamestatus.NodeName存在时message空周期性落库不含消息文案status.Message存在时progressint(job.Progress())int(status.Progress)currentBytes/totalBytesjob.BytesSent/job.TotalBytesstatus.BytesSent/status.TotalBytesphasejob.State即staging保留taskTypestaging保留三种可见性语义最终对外的可见性可以归纳为三种情形缺一不可数据库-only 行任何副本都可见 → 消除闪烁这正是本设计要修复的核心场景复现“请求落到错过所有 NATS 广播的副本”tracker-only entry保持可见 → 兼容没有持久作业行的暂存路径例如某些直接走 tracker 的 stage 流程数据库行 tracker 快照合并为一个操作 → 字段新鲜度最高且不重复。失败处理可观测性故障不得拖垮整个接口设计文档对失败场景给出了明确边界如果数据库查询失败/api/operations记录错误日志并回退到现有的 tracker-only 响应——可观测性设施Observability的故障绝不能破坏整个 operations 端点也不能隐藏无关的 gallery 操作。实现完全遵循该约定core/http/routes/ui_api.go 中ListActiveLoadJobs出错时仅调用xlog.Warn(Failed to list durable model load jobs, error, err)然后跳过合并直接使用 tracker 结果HTTP 层仍返回200与完整 payload。另一条失败语义同样关键只有存活中的staging行会被纳入。pending、installing后端安装、loading、failed行分别由它们已有的用户流程呈现绝不能被打上文件暂存的标签。实现上通过if job.State ! nodes.LoadJobStateStaging { continue }完成阶段过滤。测试策略Ginkgo 聚焦覆盖设计文档要求为合并逻辑添加聚焦的 Ginkgo 测试覆盖四种情形。它们都已落地在 core/http/routes/ui_api_operations_test.go 中数据库-only 的 staging 作业出现在 payload 中includes database-only staging jobs and excludes other load phases用ClaimLoadJobUpdateLoadJobState: staging,BytesSent: 25, TotalBytes: 100, FileIndex: 1, TotalFiles: 1构造作业再通过applicationWithDistributedServices注入 registry 与 router断言返回staging:durable-model字段完整且progress 25同时构造一条loading状态的作业断言staging:loading-model不出现在结果中——一举同时覆盖测试点 1 与 3。匹配的 tracker entry 覆盖数据库行且不重复overlays a matching tracker snapshot without duplicating the durable job数据库行上报BytesSent: 10随后 trackerStartUpdateFile上报同一模型的更新进度70/100、文件名weights.gguf断言响应中该 id 仅出现一次且展示的是 tracker 的新鲜值。非 staging 加载作业不得作为暂存操作出现已由第 1 条测试中staging:loading-model的断言覆盖。数据库读取失败时保留 tracker-only 操作且端点仍成功retains tracker-only operations when the database read fails先注册 tracker-only 操作随后直接sqlDB.Close()模拟数据库不可用断言/api/operations仍返回200且该操作携带完整进度字段。测试辅助函数applicationWithDistributedServices通过 unsafe 反射把DistributedServices{Registry, Router}注入Application使单测无需拉起完整分布式运行环境即可驱动 handler 的合并分支。按设计文档要求运行相关 Go 包测试即可无需长构建。假设模块路径为本地仓库go.mod声明模块github.com/mudler/LocalAI可执行go test ./core/http/routes/... -run TestRoutes总结与影响面该设计是对既有 UI 操作一致性的一次修正不引入任何新 API、新配置项或新用户工作流因此也不需要更新用户文档。核心价值可以概括为以数据库为基线model_load_jobs中staging阶段的行成为每个副本都能看到的不闪烁基线以 tracker 为新鲜层合并后保留更及时的文件名、消息与实时字节进度本地权威优先本地拥有的操作不被远端镜像覆盖tracker-only 操作兼容保留失败可降级数据库故障只影响“持久化补充视图”绝不破坏整个 operations 端点阶段严格过滤只有staging会被展示为文件暂存操作其余阶段交给各自原有的用户流程。延伸阅读设计文档原文docs/superpowers/specs/2026-08-21-distributed-staging-operations-design.md端到端实现core/http/routes/ui_api.go合并逻辑与 core/http/routes/ui_api_operations_test.go持久化作业模型与 CRUDcore/services/nodes/model_load_job.go、core/services/nodes/registry.go内存 tracker 与 NATS 广播core/services/nodes/staging_progress.go、core/services/messaging/subjects.go相关行为测试core/services/nodes/model_load_job_test.go、core/services/nodes/staging_progress_broadcast_test.go【免费下载链接】LocalAILocalAI is the open-source AI engine. Run any model - LLMs, vision, voice, image, video - on any hardware. No GPU required.项目地址: https://gitcode.com/GitHub_Trending/lo/LocalAI创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价