-
Notifications
You must be signed in to change notification settings - Fork 6.2k
objstore: add concurrent Azure block upload #69887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
148242f
0fcb3f8
f1f2f5e
2b09de7
f1ae5c7
be60138
a65364f
03821d9
7235876
ff6b68c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,7 @@ import ( | |
| "github.com/pingcap/tidb/pkg/objstore/storeapi" | ||
| "github.com/spf13/pflag" | ||
| "go.uber.org/zap" | ||
| "golang.org/x/sync/errgroup" | ||
| ) | ||
|
|
||
| const ( | ||
|
|
@@ -717,7 +718,7 @@ func (s *AzureBlobStorage) URI() string { | |
| const azblobChunkSize = 64 * 1024 * 1024 | ||
|
|
||
| // Create implements the StorageWriter interface. | ||
| func (s *AzureBlobStorage) Create(_ context.Context, name string, _ *storeapi.WriterOption) (objectio.Writer, error) { | ||
| func (s *AzureBlobStorage) Create(ctx context.Context, name string, wo *storeapi.WriterOption) (objectio.Writer, error) { | ||
| client := s.containerClient.NewBlockBlobClient(s.withPrefix(name)) | ||
| uploader := &azblobUploader{ | ||
| blobClient: client, | ||
|
|
@@ -730,7 +731,18 @@ func (s *AzureBlobStorage) Create(_ context.Context, name string, _ *storeapi.Wr | |
| cpkInfo: s.cpkInfo, | ||
| } | ||
|
|
||
| uploaderWriter := objectio.NewBufferedWriter(uploader, azblobChunkSize, compressedio.NoCompression, nil) | ||
| chunkSize := azblobChunkSize | ||
| if wo != nil && wo.PartSize > 0 { | ||
| chunkSize = int(wo.PartSize) | ||
| } | ||
| if wo != nil && wo.Concurrency > 1 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| eg, egCtx := errgroup.WithContext(ctx) | ||
| eg.SetLimit(wo.Concurrency) | ||
| uploader.eg = eg | ||
| uploader.egCtx = egCtx | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| uploaderWriter := objectio.NewBufferedWriter(uploader, chunkSize, compressedio.NoCompression, nil) | ||
| return uploaderWriter, nil | ||
| } | ||
|
|
||
|
|
@@ -889,28 +901,66 @@ type azblobUploader struct { | |
|
|
||
| cpkScope *blob.CPKScopeInfo | ||
| cpkInfo *blob.CPKInfo | ||
|
|
||
| // eg/egCtx drive concurrent upload; nil means Write stages blocks | ||
| // synchronously. err holds the synchronous path's stage failure; the | ||
| // concurrent path's error comes from eg.Wait(). | ||
| eg *errgroup.Group | ||
| egCtx context.Context | ||
| err error | ||
| } | ||
|
|
||
| func (u *azblobUploader) stageBlock(ctx context.Context, blockID string, data []byte) error { | ||
| _, err := u.blobClient.StageBlock(ctx, blockID, newNopCloser(bytes.NewReader(data)), &blockblob.StageBlockOptions{ | ||
| CPKScopeInfo: u.cpkScope, | ||
| CPKInfo: u.cpkInfo, | ||
| }) | ||
| if err != nil { | ||
| return errors.Annotate(err, "Failed to upload block to azure blob") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (u *azblobUploader) Write(ctx context.Context, data []byte) (int, error) { | ||
| if u.eg != nil { | ||
| if err := u.egCtx.Err(); err != nil { | ||
| return 0, err | ||
| } | ||
| } | ||
| generatedUUID, err := uuid.NewUUID() | ||
| if err != nil { | ||
| return 0, errors.Annotate(err, "Fail to generate uuid") | ||
| } | ||
| blockID := base64.StdEncoding.EncodeToString([]byte(generatedUUID.String())) | ||
| u.blockIDList = append(u.blockIDList, blockID) | ||
|
|
||
| _, err = u.blobClient.StageBlock(ctx, blockID, newNopCloser(bytes.NewReader(data)), &blockblob.StageBlockOptions{ | ||
| CPKScopeInfo: u.cpkScope, | ||
| CPKInfo: u.cpkInfo, | ||
| }) | ||
| if err != nil { | ||
| return 0, errors.Annotate(err, "Failed to upload block to azure blob") | ||
| if u.eg != nil { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3 findings on this line: 🟡 [Minor] Async Write path's defensive copy and block-order guarantee have no rationale commentImpact Scope
Evidence Change request 🟡 [Minor] Changed concurrent-upload semantics have no deterministic regression coverageImpact Scope
Evidence Change request 🟡 [Minor] Concurrent upload keeps allocating/copying the rest of the file after a block already failedImpact Scope
Evidence Change request |
||
| buf := make([]byte, len(data)) | ||
| copy(buf, data) | ||
| u.eg.Go(func() error { | ||
| return u.stageBlock(u.egCtx, blockID, buf) | ||
| }) | ||
| return len(data), nil | ||
| } | ||
| u.blockIDList = append(u.blockIDList, blockID) | ||
|
|
||
| if err := u.stageBlock(ctx, blockID, data); err != nil { | ||
| u.err = err | ||
| return 0, err | ||
| } | ||
| return len(data), nil | ||
| } | ||
|
|
||
| func (u *azblobUploader) Close(ctx context.Context) error { | ||
| err := u.err | ||
| if u.eg != nil { | ||
| err = u.eg.Wait() | ||
| } | ||
| if err != nil { | ||
| // Uncommitted blocks are garbage-collected by Azure after a week, so | ||
| // skip CommitBlockList and surface the failure instead. | ||
| return err | ||
| } | ||
|
|
||
| // the encryption scope and the access tier can not be both in the HTTP headers | ||
| options := &blockblob.CommitBlockListOptions{ | ||
| CPKScopeInfo: u.cpkScope, | ||
|
|
@@ -920,6 +970,6 @@ func (u *azblobUploader) Close(ctx context.Context) error { | |
| if len(u.accessTier) > 0 { | ||
| options.Tier = &u.accessTier | ||
| } | ||
| _, err := u.blobClient.CommitBlockList(ctx, u.blockIDList, options) | ||
| _, err = u.blobClient.CommitBlockList(ctx, u.blockIDList, options) | ||
| return errors.Trace(err) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 [Minor] Storage.Create interface doc now contradicts Azure's new WriterOption support
Impact
The
storeapi.Storage.Createdoc comment states "Currently only s3 implemented WriterOption," but this diff makesAzureBlobStorage.Createhonorwo.Concurrencyandwo.PartSizefor real, so the comment is now factually wrong.A maintainer deciding whether to pass concurrency/part-size options for the Azure backend, or evaluating parity when adding the same feature to GCS, will be misled into thinking Azure ignores these fields.
Scope
pkg/objstore/storeapi/storage.go:168—Storage.Createpkg/objstore/azblob.go:722—AzureBlobStorage.CreateEvidence
storeapi/storage.go:166-168documents the sharedCreatecontract with "Currently only s3 implemented WriterOption." This diff changesAzureBlobStorage.Createfromfunc (s *AzureBlobStorage) Create(_ context.Context, name string, _ *storeapi.WriterOption)to actually readingwo.Concurrency/wo.PartSizeand driving concurrent block staging, which the interface comment no longer reflects.Change request
Update the
Createdoc comment onstoreapi.Storageto list Azure alongside s3 (and any other backends that already implement it, e.g.s3store.KS3Storage), or drop the backend-enumeration claim and instead point readers to backend-specific docs.