Skip to content

feat(temp-upload): hour-bucketed shared uploads with best-effort cleanup - #4479

Draft
zhoujh01 wants to merge 1 commit into
mainfrom
feat/temp-upload-paged-cleanup
Draft

feat(temp-upload): hour-bucketed shared uploads with best-effort cleanup#4479
zhoujh01 wants to merge 1 commit into
mainfrom
feat/temp-upload-paged-cleanup

Conversation

@zhoujh01

Copy link
Copy Markdown
Collaborator

Description

对 shared 临时上传(shared TempUpload)做三件事:(1) 按 UTC 小时分桶存储
(2) 把清理与本地磁盘 IO 移出请求路径(异步化)(3) 重写清理为有界、best-effort 的方式

改造前的问题:

  • 所有 upload 平铺在 viking://upload/ 下,该目录会膨胀到数十万条子目录;清理任务一次性枚举
    全量目录、耗时数分钟,线上曾观测到 listed 达 70 万+、单轮清理 250s~45min,并伴随大量
    "already pending"。
  • _save_shared 在请求路径内同步执行清理,且上传/解析过程中的本地磁盘读写是阻塞调用,会拖慢
    Core worker 的事件循环。

本 PR 通过小时分桶把单层目录规模控制在有界范围,并将清理与本地 IO 移出请求路径,从根本上解决
上述问题。

Human Involvement

  • A human participated in the implementation or review loop

Related Issue

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • Performance improvement

Changes Made

1. 存储分桶

  • 新上传写入 viking://upload/<YYYYMMDDHH>/<uuid>/{content,meta}(UTC 小时)。upload_id
    <YYYYMMDDHH>-<uuid>,10 位小时前缀既是存储桶也是新格式标记;桶内子目录只用 <uuid>,不重复
    前缀。对外 temp_file_id(shared_<upload_id>)契约保持不变。
  • 消费兼容:遗留的扁平 <13位ms>-<uuid> 上传在 TTL 内仍可读;_read_shared_meta 根据
    upload_id 格式直接选定唯一路径读取,不做回退探测(新格式走桶、老格式走扁平)。

2. 异步化 / 移出请求路径

  • 清理不再在请求路径内同步执行:_save_shared 写完后仅调用 _schedule_shared_cleanup(ctx)
    入队,不等待;实际清理在专用后台线程 ov-shared-upload-cleanup(单 worker、daemon、通过
    asyncio.run 执行)中运行。
  • 调度节流:模块级 due_at(epoch 秒)+ pending 集合 + 有界队列 Queue(maxsize=100)
    同一 account 已在 pending、或 due_at 未到时跳过入队;队列满时释放 pending 并 WARNING;worker
    finally 无条件释放 pending。(TempUploadStore 非单例,故节流状态放在模块级。)
  • 请求路径本地 IO offload:_stream_upload_to_local_temp / _save_local / _resolve_local
    / _resolve_shared / ResolvedTempUpload.cleanup 中的本地磁盘读写(open/write/close/unlink/
    read_bytes/mkstemp/json 写入)统一用 asyncio.to_thread 执行,避免慢本地存储阻塞 Core worker
    事件循环。

3. 清理重写(best-effort、oldest-first、按名称升序 ls)

  • YYYYMMDDHH 桶在 bucket_start + 3600 + ttl 后过期,整桶 rm -r 删除,扫描遇到首个未过期桶
    即停止并把该桶过期时刻记为 due_at;
  • 遗留扁平上传按 created_at + ttl 单个删除;
  • 其它/非法目录仅在 temp_upload.cleanup_invalid_dirs=true 时删除,且遗留扁平上传永远不会
    被当作非法目录
    ;删除失败不中断本轮扫描;
  • 每次删除输出 kind/uri/elapsed_ms 日志,清理完成输出 listed/scanned/removed/next_cleanup_at
    汇总;list/删除失败为 WARNING;
  • list 失败或删除失败会清除该 account 的 due_at,让下一次请求重试而不是被陈旧节流卡住。

4. pathlock 与配置

  • shared 上传的写入、失败回滚、清理删除均使用 auto_pathlock=False,避免清理与 pathlock 相互
    阻塞;VikingFS.rm/write_file/write_file_bytes 新增 auto_pathlock 参数(默认 True,其它
    调用方行为不变)。
  • 新增 temp_upload.cleanup_invalid_dirs 配置开关(默认 False)。

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows

tests/server/test_temp_upload_store_async_io.py(17 passed)覆盖:

  • 请求路径 IO offload:_save_local / _resolve_local 关键磁盘操作确实走 asyncio.to_thread;
  • 调度节流:pending 去重、due_at 未到跳过、到期提交、队列满释放 pending;
  • 清理:桶过期整桶删除并停在存活桶、遗留扁平按 TTL 删除、非法目录默认跳过 / 开关开启后删除 /
    删除失败不中断扫描、遗留扁平永不被当非法、list/删除失败清 due_at;
  • 消费路径:新旧 upload_id 格式判定,新格式读桶路径、老格式读扁平路径,各只读一次。

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

Additional Notes

  • TTL 语义:按桶整体过期,单个 upload 的最长存活为 ttl + 不到 1 小时(清理本就是
    best-effort,以此换取按桶删除带来的数量级性能提升)。
  • 清理列举:清理仍使用全量 ls,依赖“分桶后顶层目录很小(约 TTL 小时数 + 1 个桶)”这一前提。
    若存在大量历史遗留的扁平目录,首轮清理仍会较慢,属历史数据的一次性开销。
  • 一次性运维:历史上已膨胀的旧 upload/ 目录,即使被清空,目录 inode 也不会自动收缩;
    建议在服务空闲时对旧目录做一次重建
    (mv upload upload.old && mkdir upload && rm -rf upload.old)。
  • cleanup_invalid_dirs 无每轮删除上限,启用时会在一轮内清理全部非法目录;若存在大量历史非法
    目录,首轮清理开销会较大。
  • 清理进程为进程内后台线程,due_at/pending 为进程内状态;多进程/重启后状态丢失只会导致额外
    的清理触发,不影响正确性。

@zhoujh01
zhoujh01 force-pushed the feat/temp-upload-paged-cleanup branch from 06ff745 to 5a9d709 Compare August 29, 2026 07:49
@zhoujh01
zhoujh01 marked this pull request as ready for review August 29, 2026 09:21
@zhoujh01
zhoujh01 marked this pull request as draft August 29, 2026 09:48
@zhoujh01
zhoujh01 force-pushed the feat/temp-upload-paged-cleanup branch from 5a9d709 to 0b9f89a Compare August 29, 2026 10:37
Group shared uploads into hourly buckets so the upload root never becomes one
huge flat directory (which made cleanup enumerate hundreds of thousands of
entries and take minutes), and rework cleanup to be bounded and best-effort.

Storage layout:
- New uploads: viking://upload/<YYYYMMDDHH>/<uuid>/{content,meta}, UTC hour.
  upload_id is <YYYYMMDDHH>-<uuid>; the 10-digit hour prefix is the bucket and a
  format marker, and the in-bucket directory is just the uuid (no repeated
  prefix). temp_file_id stays shared_<upload_id> (external contract unchanged).
- Legacy flat <13-digit-ms>-<uuid> uploads stay readable; _read_shared_meta
  picks exactly one path by id format, no fallback probe.

Cleanup (best-effort, off the request path, oldest-first via name-ascending ls):
- module-level due_at (epoch) + pending throttle so requests don't pile up
  duplicate jobs;
- YYYYMMDDHH bucket expires at bucket_start + 3600 + ttl and is removed whole
  (rm -r); scan stops at the first live bucket;
- legacy flat uploads expire by created_at + ttl and are removed individually;
- other/malformed dirs are removed only when temp_upload.cleanup_invalid_dirs is
  enabled, and legacy flat uploads are never treated as invalid;
- every deletion logs kind/uri/elapsed_ms.

Shared upload writes, failure rollback, and cleanup deletes use
auto_pathlock=False; VikingFS.rm/write_file/write_file_bytes gain an
auto_pathlock parameter (default True, no behavior change for other callers).

Add temp_upload.cleanup_invalid_dirs config flag (default False).

Co-authored-by: TRAE CLI <traecli@bytedance.com>
@zhoujh01
zhoujh01 force-pushed the feat/temp-upload-paged-cleanup branch from 0b9f89a to 320dc67 Compare August 29, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant