Conversation
线上表现为 KV 与 D1 后端同时明显变慢。两者底层实现与网络路径完全不同, 同时退化说明瓶颈不在存储后端,而在其上的调用层:loadDb() 在一次请求内被 反复完整执行数十次。 根因:getDb() 对无参调用直接 return loadDb(envCtx),绕过了 dbCache / dbInflight 两个缓存。而仓库中存在约 30 处无参 getDb() 调用(storage.ts 的 驱动回调,以及 getSettings/getUsers/getStorages/getMetas/getPlugins), 于是每次无参调用都触发一次完整冷加载:后端全量读取 + JSON.parse + 逐字段 AES 解密 + 5 次 ensureDefault*。一次 WebDAV PROPFIND 或一次页面加载 即触发数十次,KV 被读数十次、D1 被查数十乘 N 张表。 本次修复: 1. getDb():无参调用回退到请求级 globalEnvCtx 作为缓存键,复用同一套 缓存(原为直接落盘)。仅在连 globalEnvCtx 都不存在时才退化为直读。 2. saveDb():同步刷新无参路径的缓存键,保证写后读不读到 TTL 内的旧快照。 3. getJwtSecret():KV 命中分支补写 cachedJwtSecret。修复前该分支只 return, 导致每请求 2-4 次调用各自回源一次 KV。 4. getKvBinding():新增按 env 身份的结果缓存(WeakMap),消除每次调用的 重复绑定探测与热路径 console.log 噪音。 5. unsealDb():改为收集任务后 Promise.all 并行解密,消除逐字段串行 await。 为便于测试注入统计型后端,新增仅供测试使用的 __resetDbCacheForTest / __setStoreBackendLoaderForTest 钩子,并将后端解析 统一收敛到 storeBackendLoader 间接层(生产默认行为不变)。 新增回归测试(8 个用例): - db_cache.test.ts:验证重复/并发无参调用只 load 一次、saveDb 写后读一致、 五个无参 getter 合并为一次 load、TTL 过期后允许重载。 - jwt_secret_cache.test.ts:验证 KV 命中后不再回源、resetJwtSecretCache 后 重新回源、env.JWT_SECRET 优先级最高且不读 KV。 注:getJwtSecret 用例在修复前会失败(已本地验证),具备真实回归防护能力。
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-work | 1a6cf7e | Sep 14 2026, 07:20 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-tsworkers | 1a6cf7e | Sep 14 2026, 07:20 AM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
perf(db): 修复 getDb() 缓存旁路导致的 KV/D1 全量重复加载
fix/db-cache-bypass-perfmain@5cac7eb1a6cf7e一、问题现象
线上(Cloudflare Workers)反馈:页面返回明显变慢,WebDAV 载入变慢,相比此前明显退化。
关键线索:KV 与 D1 两种后端同时变慢。
KV 与 D1 的底层实现、网络路径与延迟特性完全不同。若瓶颈在存储后端本身,两者不可能同时同向退化。这一现象本身即指向共同的上层调用层。
二、根因定位
2.1 直接根因:
getDb()的缓存旁路src/backend/internal/model/db.ts:dbCache/dbInflight均以调用方传入的envCtx对象身份为键。而代码库中存在大量无参getDb()调用:src/backend/internal/op/storage.ts(WebDAV / fs 热路径)db.ts末尾 5 个 getter:getSettings/getUsers/getStorages/getMetas/getPluginsawait getDb()server/seed.ts、assets.ts、public.ts等于是:每一次无参
getDb()= 一次完整的冷加载。2.2 单次冷加载(
loadDb)的真实开销一次 WebDAV
PROPFIND或一次页面加载会触发数十次上述全流程:2.3 三个叠加放大的次生缺陷
server/middlewares.tsgetJwtSecretreturn kvSecret,从不写cachedJwtSecret,导致每次调用都回源 KV。该函数每请求被getUserFromContext/csrfProtection/checkAdminAuth调用 2–4 次。store/json.tsgetKvBindingenv.KV/globalThis.KV、尝试初始化 Blob SDK,并在热路径console.log。被readPersistedSecret/writePersistedSecret/logAudit频繁调用。db.tsunsealDbawait解密,字段越多线性叠加。2.4 回归引入时间线
85639e8getDb,引入if (!envCtx) return loadDb(envCtx)缓存旁路 ← 主因3c5f30d1ab882e758f233/52b947a/2eb07ce三、修复内容
3.1
getDb():无参调用复用请求级缓存(核心)globalEnvCtx作为缓存键,复用同一套dbCache/dbInflight;globalEnvCtx都不存在时才退化为直读 —— 不影响数据正确性,仅作性能兜底。3.2
saveDb():同步刷新无参缓存键无参路径现在也会命中缓存,因此写入后必须同步刷新该键,否则「写后读」可能读到 TTL(1s)内的旧快照:
3.3
getJwtSecret():补上 KV 命中缓存3.4
getKvBinding():新增按 env 身份的结果缓存env对象身份相关(同一请求内env为同一对象);env对象,env释放后条目自动回收,无跨请求内存泄漏;console.log噪音。3.5
unsealDb():并行解密先同步收集待解密任务,再
Promise.all并行执行,消除逐字段串行await的线性叠加。3.6 测试可注入性(生产行为不变)
新增后端解析间接层
storeBackendLoader(生产默认getStoreBackend),并导出两个仅供测试的钩子:__resetDbCacheForTest():重置模块级缓存与内存快照,保证用例隔离;__setStoreBackendLoaderForTest(loader):注入统计型后端,用于断言load次数。四、验证
4.1 新增回归测试(8 个用例,全部通过)
src/backend/internal/model/db_cache.test.tsgetDb()→load === 1;有参调用共享同一缓存Promise.all5 次并发 →load === 1saveDb后getDb()不额外 load 且读到新值load === 1(修复前为 5)load === 2(缓存非永久固化)src/backend/server/jwt_secret_cache.test.tsresetJwtSecretCache后重新回源env.JWT_SECRET优先级最高4.2 类型检查
4.3 全量后端测试
2 个失败为既有问题,已在原始
main(stash 掉本次改动后)复现,与本 PR 无关:Security(F-11): ADMIN_PASS still forces an explicit resetCAS codec matches casmeta base64 JSON field names五、兼容性与风险
envCtx」扩展为「envCtx或请求级globalEnvCtx」;saveDb主动刷新保证写后读一致;TTL 1s 保持不变resetJwtSecretCache语义保留;getKvBinding仅缓存已解析结果,不缓存异常WeakMap以env为键,随env释放自动回收风险点:无参缓存的键为「最近一次请求的
env」。在单 isolate 串行处理请求的运行时下行为正确;若运行时在同一 isolate 内并发处理多请求,理论上不同请求的env会相互覆盖缓存键。考虑到DB_CACHE_TTL_MS = 1000且saveDb主动刷新,该风险与修复前(envCtx传参路径)处于同一量级,不构成新增回归。建议后续将 5 个 getter 与storage.ts的 24 处调用的envCtx显式透传,作为进一步的收敛(见「后续工作」)。六、建议的后续工作(本 PR 不含)
envCtx显式透传进 5 个 getter 与storage.ts的 24 处无参调用,彻底消除对「最近一次 env」的隐式依赖。loadDb调用次数与耗时建立 CI 门禁。七、Review 指引
db.ts的getDb/saveDb/unsealDb,建议优先审阅;middlewares.ts的getJwtSecret与json.ts的getKvBinding为独立的小改动;*.test.ts为新增回归测试,可直接运行npx tsx --test src/backend/internal/model/db_cache.test.ts src/backend/server/jwt_secret_cache.test.ts复核。