OSS Backend Design#
Overview#
The object-storage backend extends Mooncake Store’s client-owned offload path
to key-based storage services, such as OSS and S3, through service-specific
adapters. It uses the same LOCAL_DISK replica model as local-file and NVMe KV
backends: the master records an owner and the requesting client reads through
that owner. Object storage is not a separate first-class replica type here.
This document describes the OSS adapter implementation. Its signing protocol
is OSS-specific. S3-compatible services use the separate S3 adapter
(S3ObjectStorageAdapter, AWS Signature V4); see “S3-compatible
configuration” in
Object Storage Offload.
For prerequisites, configuration, and startup examples, see Object Storage Offload.
Design Goals#
Reuse
FileStorageoffload coordination and existing Store APIs.Issue concurrent object GETs and PUTs within a synchronous batch.
Give each batch its own libcurl multi handle, without a shared batch lock.
Avoid a full-object intermediate string on the direct GET path and avoid concatenating upload slices into a full-object buffer.
Architecture#
flowchart TB
Master["Mooncake master<br/>LOCAL_DISK replicas and owners"]
Requester["Requesting Mooncake client"]
subgraph Owner["Offload owner process"]
RealClient["RealClient"]
FileStorage["FileStorage"]
Backend["DistributedStorageBackend"]
Adapter["OssObjectStorageAdapter<br/>libcurl and OSS signing"]
RealClient --> FileStorage --> Backend --> Adapter
end
OSS["OSS bucket / owner prefix"]
Requester <-->|"metadata query"| Master
RealClient <-->|"offload heartbeat and completion"| Master
Requester <-->|"offload RPC and Transfer Engine"| RealClient
Adapter <-->|"HTTP"| OSS
Layer Responsibilities#
Layer |
Responsibilities |
|---|---|
Master |
Schedule offload tasks and track completed |
|
Obtain source slices, call the selected backend, report successful writes, and manage read staging buffers. |
|
Convert |
|
Map keys, sign HTTP requests, run libcurl transfers, and implement GET/PUT/HEAD/LIST/DELETE. |
OSS |
Store object payloads under the configured bucket and prefix. |
One FileStorage instance selects one backend. Selecting OSS does not also
enable the local-file or NVMe KV backend, or create an SSD-to-OSS cache hierarchy.
Physical Keys and Object Layout#
The adapter maps each storage key supplied by FileStorage to one OSS object:
physical_key = owner_prefix + "/" + URIEncode(storage_key)
object_body = concatenated payload slices
The prefix separator is omitted when the prefix is empty. LIST decodes matching
object keys back into logical storage keys; ScanMeta uses these keys and object
sizes for metadata registration.
There is no shard/offset allocation, per-object UUID descriptor, root manifest, or adapter-level checksum envelope. A PUT targets the mapped key, not a store-if-not-exists operation. Separate owner prefixes prevent owners from overwriting each other’s objects.
Write Path#
sequenceDiagram
participant M as Master
participant F as Owner FileStorage
participant B as DistributedStorageBackend
participant A as OSS adapter
participant O as OSS
F->>M: OffloadObjectHeartbeat (default: every 10 s)
M-->>F: Offload tasks for this owner
F->>B: BatchOffload(keys and source slices)
B->>A: PutBatch(key, iov, iovcnt)
A->>O: Concurrent object PUTs
O-->>A: Per-request completion
A-->>B: Per-object results
B-->>F: Completion callback for successful objects
F->>M: NotifyOffloadSuccess
Note over M: Register completed LOCAL_DISK replicas
The application’s memory Put is not a synchronous OSS write. Offload is
scheduled separately; only successful uploads are reported as completed
replicas. A batch is not a transaction: successful objects remain in OSS when
another upload fails.
Read Path#
sequenceDiagram
participant R as Requesting client
participant M as Master
participant F as Owner RealClient / FileStorage
participant B as Backend / OSS adapter
participant O as OSS
R->>M: Query key
M-->>R: LOCAL_DISK owner and object size
R->>F: batch_get_offload_object
F->>B: BatchLoad into staging buffers
B->>O: Concurrent GETs (GetBatch)
O-->>B: Object bytes
B-->>F: Validate results and lengths
F-->>R: Staging buffer descriptors
R->>F: Fetch bytes through Transfer Engine
R->>F: Release staging buffers after transfer
The RPC coroutine posts blocking work to the existing blocking pool. It can
yield while a worker executes BatchLoad; the worker waits until the HTTP batch
finishes. No dedicated OSS worker pool is introduced.
Get, GetRange, and GetBatch copy incoming libcurl chunks directly into the
destination buffer. This removes the full-object response string and its final
copy, not all copies. GetV still reads into a contiguous temporary buffer and
then scatters. PutV / PutBatch consume iovec arrays through the upload
callback; copies into libcurl’s upload buffer remain.
Batch Execution#
Each GetBatch or PutBatch call creates a temporary CURLM and one easy handle
per prepared request. It admits up to MOONCAKE_OSS_MAX_CONNECTIONS requests
(default: 64), advances them together, and admits more as requests complete.
Requests waiting for admission stay outside CURLM so that their transfer
timeout is not consumed in libcurl’s connection queue.
Connections are reused within a batch, not across batches. Different caller threads can execute separate batches concurrently; the limit is per batch, not per adapter or process. Completion order may differ from input order, but the returned result vector preserves input order and cardinality.
Concurrency and Ownership#
Adapter APIs are synchronous: they return after batch processing and cleanup.
Each batch owns its request contexts and libcurl handles; there is no shared
CURLMor adapter-wide lock serializing complete batches.Download buffers, upload iovec arrays, and upload payloads must remain alive until the call returns. Upload descriptors and payloads must not be modified while in use.
A failed read may have partially modified its destination; do not consume that buffer as valid data. A failed
BatchLoaddoes not expose the batch as a successful Store read, even if some underlying GETs completed.
Failure Semantics#
Condition |
Behavior |
|---|---|
Invalid buffer or iovec arguments detected before batch setup |
Return |
GET reports a missing object |
Return |
GET returns an unexpected status or length |
Return a read failure; a nonempty range GET requires HTTP 206 and the requested length. |
PUT returns a non-success HTTP status |
Return a write failure; do not report that object as successfully offloaded. |
Transport error or timeout |
Return a request error; a timed-out PUT does not prove that OSS stored nothing. |
Request setup or multi-handle failure |
Return errors for requests that could not complete; do not roll back successful cloud writes. |
One object fails in a batch |
Preserve per-object results; successful uploads are not automatically deleted. |
Bounded Object I/O Component#
ObjectStorageBackend provides synchronous I/O for caller-selected object names,
independently of the client-owned offload path above. Construct it with an
initialized ObjectStorageAdapter and ObjectStorageBackendConfig:
max_object_bytesdefaults to 256 MiB and limits the declared object size.max_staging_bytesdefaults to 1 GiB and bounds concurrent payload staging allocations within one backend instance. It does not include iovec metadata, caller buffers or the adapter’s own HTTP buffers.
Write validates and gathers all slices into a full-object host buffer, then
invokes the adapter’s PutOnce. It requires enough staging budget for the
entire object. The adapter must advertise SupportsSinglePutAttempt(); otherwise
writes return NOT_SUPPORTED before staging or issuing PUT. Both OSS and S3
support this contract. The S3 single-attempt entry points disable retries for
that call only; its legacy offload API is unchanged.
The optional upload_started output becomes true immediately
before the adapter call; an error with this flag set can have an ambiguous
remote outcome. The backend does not retry PUT.
WriteV is the opt-in path for borrowing host buffers. The caller supplies an
IsHostMemory predicate; the backend does not detect pointer address spaces.
Nonempty slices classified as host are passed directly to the adapter’s
PutVOnce and must remain valid and unchanged until the synchronous call returns.
Other slices are gathered through to_host, reserving only their combined
size from the staging budget. The predicate is evaluated once per nonempty
slice. An all-host write needs neither payload staging nor a copy callback,
even if the instance’s staging budget is occupied by another operation.
For borrowed buffers this budget is not an upload concurrency limit; the
caller remains responsible for request admission and its own buffer budget.
This path requires SupportsSinglePutVAttempt(), separately from the
PutOnce capability. OSS and S3 support both single-attempt entry points.
S3 uses a per-request retry policy: PutOnce and PutVOnce never retry,
including empty uploads, while existing Put, PutV and batch calls retain
their retry behavior. Concurrent calls do not change shared retry settings.
All validation and staging complete before the single PutVOnce invocation;
upload_started and failure/exception cleanup follow the same rules as Write.
This removes the backend’s gathering copy for borrowed slices, not copies
inside the adapter or network stack. Existing Write calls retain their
full-object staging behavior.
For input known by the caller to be entirely host-accessible:
backend.WriteV(object_key, object_size, slices,
[](const Slice&) { return true; }, {}, &upload_started);
Read validates the requested range against the caller-supplied object size,
then reads consecutive chunks independently of destination slice boundaries
and scatters each chunk into the slices. GETs remain serial. Each staging allocation
is at most 4 MiB, or the configured staging limit if smaller. Reads fail on a
short response. OSS and the REST adapter used by S3 also validate Content-Range
against the requested offset and length, for both individual and batch reads.
Adapters that do not implement
the optional generic GetRange method return NOT_SUPPORTED.
The six-argument Read overload accepts an IsHostMemory predicate. With
exactly one nonempty slice classified as host, GetRange writes directly to
that destination, skipping from_host and payload staging. Empty slices are
ignored. GETs retain the same serial chunking, including the configured limit
when it is below 4 MiB. This path can run while other operations occupy the
staging budget; the caller remains responsible for request admission.
Multiple nonempty slices or a non-host destination use the staged path and
require from_host. The original five-argument API always uses staging.
backend.Read(object_key, object_size, {{host_buffer, length}}, offset, {},
[](const Slice&) { return true; });
A direct-read destination must remain valid, writable and exclusively owned until return. On failure, it may contain partial data or an HTTP error body, including when response range validation fails. Only use the result after a successful return. Direct reads remove the backend’s staging copy, not copies inside the adapter or network stack.
Copy callbacks allow callers to stage device buffers. They must finish copying
before returning, and report failure with false. Invalid arguments return
INVALID_PARAMS; staging exhaustion returns NO_AVAILABLE_HANDLE; copy
failure returns TRANSFER_FAIL. Reservations are released on return or stack
unwinding. A failed read can leave destination buffers partially written.
This component does not allocate immutable names, manage read leases, publish
replicas, or decide when deletion is safe. Those responsibilities remain with
the caller, which can use the adapter’s existing Delete operation. Sharing an
adapter across concurrent operations requires a thread-safe adapter.
This component does not add a first-class replica type or enable owner-independent Store reads. Those guarantees require Master/Client integration with durable attempt identity, commit, recovery and lease-aware reclamation. See RFC #4296 for context.
Current Limitations#
Readers depend on a reachable owner and valid Master metadata, not just the existence of an OSS object. This is different from a first-class DFS replica.
Master capacity accounting uses each owner’s configured capacity and live replica sizes. It neither queries OSS free space nor enforces a bucket quota.
The backend does not implement capacity eviction or automatic cloud-object garbage collection. Removing Master metadata does not issue an OSS DELETE; metadata usage can therefore differ from actual bucket usage.
Multipart uploads, automatic parallel range splitting, and automatic credential refresh are not implemented.
The object upload and cleanup recovery records describe the separate catalog review unit for first-class object replicas.