What happened?
本地 chunked upload 存在两个相互反压的文件 I/O 阶段:
- Chunk PUT 阶段:读取 HTTP payload,创建临时 chunk 文件,写入并 flush,通过 hard-link 无覆盖发布,更新
received_count;
- 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
- 使用本地文件系统存储和 SQLite 启动隔离实例。
- 创建 8 个可登录 benchmark 用户,避免所有 VU 共享同一 quota 行成为干扰变量。
- 运行 8 VU、每个文件 10 MiB、每个文件 2 个 5 MiB chunk、持续 20 秒的 chunked upload。
- 记录 Chunk PUT、complete 和 flow latency。
- 给以下阶段增加 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;
- 观察多个 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
Checklist
What happened?
本地 chunked upload 存在两个相互反压的文件 I/O 阶段:
received_count;在小文件分片数较少、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 更新:
同一时间窗口内还捕获到:
这说明 Chunk PUT 的孤立长尾来自 complete assembly 对本地文件系统的反压,而不是请求体读取或 SQLite 单 writer。
Steps to reproduce
可用脚本:
Expected behavior
Actual behavior
users.storage_used行。在清理上传目录后,使用相同构建参数、相同 64 KiB buffer、相同 8 VU/10 秒参数进行 limiter A/B:
单并发 limiter 同时降低 complete 极端长尾并提高吞吐,说明 unrestricted assembly concurrency 当前是负优化。
AsterDrive version
Commit
4f09eb51fe00(0.4.0-beta.2development tree)Installation method
Built from source
Database backend
SQLite
Storage backend involved
Local filesystem
Environment
/tmp; generated upload data was removed between clean A/B runsError messages / logs
Relevant configuration
Proposed implementation and acceptance criteria
assemble_local_chunks_to_temp_file.Checklist