Skip to content

[Performance]: 限制本地 chunk assembly 并发,避免 PUT/complete 两阶段 I/O 反压 #402

Description

@AptS-1547

What happened?

本地 chunked upload 存在两个相互反压的文件 I/O 阶段:

  1. Chunk PUT 阶段:读取 HTTP payload,创建临时 chunk 文件,写入并 flush,通过 hard-link 无覆盖发布,更新 received_count
  2. Complete 阶段:重新打开全部 chunk,按顺序读取,并写出一份完整 assembled 文件,然后才进入 storage stage 和数据库 finalize。

在小文件分片数较少、complete 频率很高的负载下,多个 complete 会同时重写 assembled 文件。并发 assembly 不仅没有提升吞吐,反而形成文件系统写回拥塞,并反压正在同一临时目录中创建和写入文件的 Chunk PUT。

典型路径:

  • src/services/files/upload/chunk.rs:Chunk PUT 临时文件写入、发布和进度更新;
  • src/services/files/upload/complete/chunked.rs:完整读取所有 chunk 并重新写出 assembled 文件;
  • src/services/workspace/storage_core/finalize.rs:创建文件、更新 quota、标记 session completed。

分段计时表明主长尾来自文件 assembly,而不是 SQLite quota 更新:

complete 内部阶段 p50 p99 p999 Max
assembly 总计 29.1 ms 87.0 ms 823.5 ms 840.3 ms
assembly read 9.4 ms 38.3 ms 52.4 ms 60.9 ms
assembly write 13.6 ms 61.4 ms 789.9 ms 820.1 ms
storage stage 0 ms 6 ms 9 ms 13 ms
DB finalize 总计 0 ms 8 ms 23 ms 40 ms
quota update 0 ms 0 ms 1 ms 1 ms
mark session completed 0 ms 0 ms 1 ms 3 ms

同一时间窗口内还捕获到:

complete A assembly write: 820.121 ms
complete B assembly write: 811.184 ms
complete C assembly write: 789.943 ms
concurrent Chunk PUT create temp file: 768.743 ms
Chunk PUT payload wait: 0.015 ms

这说明 Chunk PUT 的孤立长尾来自 complete assembly 对本地文件系统的反压,而不是请求体读取或 SQLite 单 writer。

Steps to reproduce

  1. 使用本地文件系统存储和 SQLite 启动隔离实例。
  2. 创建 8 个可登录 benchmark 用户,避免所有 VU 共享同一 quota 行成为干扰变量。
  3. 运行 8 VU、每个文件 10 MiB、每个文件 2 个 5 MiB chunk、持续 20 秒的 chunked upload。
  4. 记录 Chunk PUT、complete 和 flow latency。
  5. 给以下阶段增加 debug timing:
    • Chunk PUT:temp create、payload wait、write、flush、publish、DB increment、final SELECT;
    • complete:assembly create/open/read/write/flush、storage stage、DB finalize;
  6. 观察多个 complete 同时进入 assembly 时,Chunk PUT temp create/write 是否同步出现长尾。

可用脚本:

ASTER_BENCH_BASE_URL=http://127.0.0.1:PORT \
ASTER_BENCH_DIAG_USER_COUNT=8 \
ASTER_BENCH_DIAG_VUS=8 \
ASTER_BENCH_DIAG_DURATION=20s \
ASTER_BENCH_DIAG_TOTAL_BYTES=10485760 \
k6 run tmp/performance-k6-diagnostics/upload-chunked-users.js

Expected behavior

  • 本地 complete assembly 使用有界并发,避免多个完整文件复制任务同时打满本地文件系统;
  • Chunk PUT 不会因为其他 session 同时 complete 而出现数百毫秒到秒级的 temp file create/write 停顿;
  • limiter 只覆盖本地 assembly,storage stage、DB finalize、audit 和 cleanup 不占用 permit;
  • limiter 由 runtime/AppState 所有,不使用散落的进程级全局可变状态;
  • 暴露 assembly queue wait、active assembly 和 assembly duration 指标;
  • 保持 chunk 幂等发布、session 恢复、失败清理和跨数据库行为。

Actual behavior

  • 每次 10 MiB/2 chunks complete 使用 64 KiB buffer,约产生 162 次 read 和 160 次 write;
  • 8 VU 高频 complete 会形成并发 assembled 文件写入风暴;
  • complete assembly write 最大达到约 820 ms;
  • 同期 Chunk PUT 创建临时文件可等待约 769 ms;
  • PostgreSQL 并未消除该长尾;独立用户对照也未改善,因此根因不是 SQLite 单 writer 或共享 users.storage_used 行。

在清理上传目录后,使用相同构建参数、相同 64 KiB buffer、相同 8 VU/10 秒参数进行 limiter A/B:

指标 无 assembly limiter assembly concurrency=1 变化
完成 flows 640 808 +26.3%
Chunk PUT p99 61.8 ms 48.7 ms -21.1%
Chunk PUT Max 193.2 ms 154.0 ms -20.3%
Complete p99 86.7 ms 80.4 ms -7.2%
Complete p999 648.1 ms 170.2 ms -73.7%
Complete Max 714.9 ms 177.3 ms -75.2%

单并发 limiter 同时降低 complete 极端长尾并提高吞吐,说明 unrestricted assembly concurrency 当前是负优化。

AsterDrive version

Commit 4f09eb51fe00 (0.4.0-beta.2 development tree)

Installation method

Built from source

Database backend

SQLite

Storage backend involved

Local filesystem

Environment

  • OS / distro: macOS / APFS
  • Browser or WebDAV client: k6
  • Reverse proxy: none
  • Deployment notes: runtime, database, uploads, logs and diagnostic build were isolated under /tmp; generated upload data was removed between clean A/B runs

Error messages / logs

local chunk assembly phases:
  read_elapsed_us=10833
  write_elapsed_us=820121
  total_elapsed_us=840300

concurrent local chunk temp write phases:
  create_elapsed_us=768743
  payload_elapsed_us=15
  write_elapsed_us=9368
  total_elapsed_us=778925

Relevant configuration

[database]
url = "sqlite://DATABASE?mode=rwc"
pool_size = 10

[server]
upload_temp_dir = ".uploads"

[rate_limit]
enabled = false

Proposed implementation and acceptance criteria

  • Add an AppState/runtime-owned semaphore for local chunk assembly.
  • Start with a default concurrency of 1, based on the clean A/B result.
  • Acquire immediately before assemble_local_chunks_to_temp_file.
  • Release immediately after assembly, before storage stage and DB finalize.
  • Preserve cancellation safety through RAII permit ownership.
  • Add unit tests for the limiter and a concurrent local chunk completion integration test.
  • Add metrics or structured timing for queue wait and assembly duration.
  • Verify 1 VU latency does not regress materially.
  • Verify 8 VU Chunk PUT p99/max and complete p999/max improve under an isolated local-filesystem k6 run.
  • Run SQLite tests and ensure PostgreSQL/MySQL compilation and behavior remain valid.
  • Keep longer-term direct-to-assembled offset writing as a separate design discussion; do not mix that larger state-machine change into the limiter fix.

Checklist

  • I have searched existing issues for duplicates
  • I have checked the documentation and FAQ
  • I have removed secrets and private credentials from this report

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

BugSomething isn't workingPriority: HighHigh priority issueRustPull requests that update Rust codeScope: StorageStorage policies, connectors, drivers, provider capabilities, and storage backends

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions