Skip to content

perf(fuse/backend): PATCH on large mostly-unchanged file is slow — parallelize sequential retained-part UploadPartCopy (16s → ~1-2s) #820

Description

@qiffang

Problem

A PATCH /v1/fs/.../output/douyin/video_720.mp4 took duration_ms=16060 (~16s), status 202.

Log signal for that trace:

  • new_size≈308MB, total_parts=37, dirty_parts=1, copied_parts=36
  • i.e. only one ~8MB part actually changed; the other 36 retained parts (~288MB) must be re-included in the new multipart object.

Root cause

In pkg/backend/patch.go, the plan-execution loop that re-assembles the retained parts is sequential:

for _, p := range newParts {
    // unchanged part → server-side copy
    _, err := b.s3.UploadPartCopy(ctx, newS3Key, mpu.UploadID, p.Number, sourceKey, partStart, partEnd)
    ...
}

No goroutine / errgroup / semaphore. So the 36 UploadPartCopy calls run one after another: 16060ms / 36 ≈ 446ms per copy (normal for an ~8MB S3 server-side part-copy) → 36 sequential = ~16s. The latency is the retained-part copies serialized, not the dirty-data transfer (only 1 part).

The get_upload_reservation / get_upload_by_path not_found warns on the same trace are benign first-patch misses (~60ms), not the latency source.

The 202 is written after InitiatePatchUploadIfRevision (server.go:2444 → 2496) completes the copies — it returns a PatchPlan (presigned dirty-part upload URL + read URLs); the client then uploads the dirty part and calls confirm (ConfirmUploadWithTags) which does CompleteMultipartUpload. So 202 is not a durability signal; there is already an upload+confirm step after it.

Fix — Option A (this issue): parallelize the in-request UploadPartCopy loop

Bounded-concurrency the copy loop. Contract unchanged, pure server-side. Expected 16s → ~1–2s.

Acceptance gates (from dat9-dev1 + adversary-1 + adversary-2):

  • Bounded concurrency (semaphore, e.g. 8–16) — NOT unbounded fan-out (S3 UploadPartCopy has per-prefix rate limits).
  • Retry on 503 SlowDown.
  • Part-number + ETag ordering correct in the completed multipart (copies may finish out of order).
  • Failure atomicity: first error cancels remaining in-flight copies, AbortMultipartUpload runs exactly once, no leaked parts / MPU.
  • ctx cancellation stops subsequent copies.
  • Discriminating regression + benchmark with many copied_parts proving the serial bottleneck is broken (assert concurrency, first-error abort, no leak).

Follow-up — Option B (separate issue, not this one): async copy

Since 202 returns a plan and a confirm step already follows, the retained-part copies only need to complete before CompleteMultipartUpload at confirm-time. So the handler could return the plan immediately, run copies in a background window, and join/wait at confirm — driving the handler sub-second without changing 202 semantics. More complex (copy-vs-confirm join + failure propagation); evaluate separately.

Owner: dat9-dev1 (Option A). Reviewers: adversary-1, adversary-2. drive9 is qiffang-owned → PR + regression, merge on qiffang's signal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions