-
Notifications
You must be signed in to change notification settings - Fork 5
design: unbounded storage backend for gantry #330
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Gantry: Cache-Origin Chain | ||
|
|
||
| **Status:** Draft | ||
| **Date:** 2026-06-23 | ||
|
|
||
| --- | ||
|
|
||
| ## Problem | ||
|
|
||
| Gantry fetches blobs missing from containerd directly from the upstream OCI registry. | ||
| On Unbounded-managed clusters, blobs may already be resident in unbounded-storage's | ||
| RDMA-accelerated P2P cache, but gantry ignores it and re-fetches over the internet. | ||
|
|
||
| Gantry also runs on plain Kubernetes clusters without unbounded-storage. Any integration | ||
| must be strictly opt-in with zero behavioral change when not configured. | ||
|
|
||
| --- | ||
|
|
||
| ## Approach | ||
|
|
||
| Introduce a **priority chain** of cache origins that gantry consults before falling | ||
| through to the OCI registry on the "cold fetch" path - the one pull per blob per node | ||
| that reaches the origin. Once a blob is committed to containerd it is served locally | ||
| forever; the cache chain is never consulted again for that blob. | ||
|
|
||
| The chain is a simple ordered list: try each entry in turn, fall through to the OCI | ||
| registry on any miss or failure. The OCI registry is always the mandatory final | ||
| fallback and cannot be misconfigured away. | ||
|
|
||
| **unbounded-storage** is the first supported cache origin. It runs as a daemon on each | ||
| node and is consulted over loopback (HTTP on port 8080). Cache hits are served at | ||
| local NVMe or RDMA speeds. On a cache miss, unbounded-storage closes the TCP connection | ||
| without sending an HTTP response - gantry treats this as a clean miss and falls through. | ||
|
|
||
| --- | ||
|
|
||
| ## Goals | ||
|
|
||
| - Gantry tries configured cache origins in order before the OCI registry. | ||
| - Adding a future cache origin type requires only a new implementation and one config | ||
| wiring change - no changes to the chain, config schema, or metrics. | ||
| - When no cache origins are configured, the code path is identical to today. | ||
| - Any cache origin failure or miss is transparent; gantry continues to function. | ||
|
|
||
| ## Non-Goals | ||
|
|
||
| - Writing blobs into unbounded-storage. Gantry only reads from it; cache population | ||
|
Contributor
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. IMO this one defeats the purpose of the integration. The cache should be transparent to users
Contributor
Author
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. Cache is indeed transparent to users in my mind too. For gantry, transparency is that users specify a container image in pod definition. Now whether the container image is pulled from origin from internet, or a gantry peer or unbounded storage is not visible to user nor their concern. Hydrating the unbounded-storage on cache miss is not a goal for this initial proposal. We can definitely that functionality add that later on and it will still remain transparent to user. Gantry will do the hydration behind the scenes. |
||
| is the operator's concern. | ||
| - OCI Distribution Spec support in unbounded-storage. It is treated as a plain HTTP | ||
| cache keyed by URL path. | ||
| - Circuit breaker or availability tracking in v1. Fall-through on every failure is | ||
|
Contributor
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. How important is it to support multiple origins? I'm trying to understand the use case. It would be much simpler to just support one: no need to think about circuit breakers
Contributor
Author
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. Currently, I am positioning gantry as a component that can also run on non-unbounded clusters and in that spirit that shouldnt be necessary need unbounded-storage but could use it if its available.
Contributor
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. I understand needing swappable origins, my question is about why you are proposing support for multiple chained origins. It would be simpler to fall through to a single configured origin, and I can't think of a use case where multiple are needed (maybe I'm missing something there)
Contributor
Author
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. Let's say a container image acr.foo.bar:latest manifest requires a blob with sha1234 needs to be pulled on a node A. This blob is not present on the node but it could be at multiple locations: 1). it can be on the another node's containerd cache. A single configured origin ie. any one option of above doesnt make sense to me. Chaining helps in hydrating the cache as well. Gantry can detect that its mirror failed across all the caches and went all the way to internet to pull the blob but now it push the downloaded to harbor or unbounded storage etc..
Contributor
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. (1) and (2) should be the only cases where it's possible to fall through e.g. cache misses or unavailability. But for 3-5 we should be able to try exactly one upstream server: local harbor would have a unique prefix in the image tag, same for public registries. Conceptually, I think it makes sense to decouple the concept of pull through caching from the concept of origins/upstreams. Configure any trusted origins, then configure the cache layer as a separate concern. Every image resolves back to a single origin, and is cached by gantry's p2p and/or the configured external cache e.g. unbounded storage. |
||
| sufficient. | ||
| - Auth support for cache origin backends in v1. | ||
|
|
||
| --- | ||
|
|
||
| ## Design | ||
|
|
||
| ### Data Flow | ||
|
|
||
| ``` | ||
| cold fetch (containerd miss) | ||
| --> PriorityChain | ||
| [1] unbounded-storage -- hit: done; miss or error: next | ||
| [2] ...future backends... | ||
| [N] OCI registry -- always the final fallback | ||
| ``` | ||
|
|
||
| ### Components | ||
|
|
||
| **`internal/gantry/unstore/`** - protocol shim for unbounded-storage. Implements the | ||
| existing `OriginPuller` interface. Owns all wire quirks (notably: connection close | ||
|
jveski marked this conversation as resolved.
|
||
| signals a miss, not 404) so nothing outside this package knows about them. | ||
|
|
||
| **`internal/gantry/origin/chain.go`** - `PriorityChain`, a thin ordered list of | ||
| `OriginPuller` implementations. No logic beyond iterating entries and calling the | ||
| fallback on miss. Each entry owns its own metrics. | ||
|
|
||
| ### Config | ||
|
|
||
| ```yaml | ||
| cache_origins: | ||
| - type: unbounded-storage | ||
| endpoint: http://127.0.0.1:8080 | ||
| timeout: 30s | ||
| ``` | ||
|
|
||
| An empty or absent `cache_origins` disables the feature entirely. | ||
|
|
||
| ### Observability | ||
|
|
||
| Shared Prometheus counters per backend: pull attempts, hits, misses, transient errors. | ||
| All labeled with `backend` so a single dashboard query covers all cache origin types. | ||
|
|
||
| --- | ||
|
|
||
| ## Graceful Degradation | ||
|
|
||
| | Scenario | Behavior | | ||
| |---|---| | ||
| | `cache_origins` absent or empty | Feature off; OCI registry path unchanged | | ||
| | cache origin unreachable | WARN log; falls through to OCI registry | | ||
| | cache origin miss | Falls through to next entry or OCI registry | | ||
| | cache origin hit | Blob served; OCI registry not contacted | | ||
| | all cache origins miss and OCI also fails | Same error behavior as today | | ||
|
|
||
|
|
||
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.
How would unbounded-storage's cache get filled in this case? It needs an upstream
backendto fill from.Maybe unbounded storage should implement an OCI backend that knows how to pull from registries. Or maybe Gantry exposes an HTTP endpoint on the loopback for storage to pull from on cache miss.
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.
For this initial proposal, I am treating unbounded-storage as a readonly cache ie. if the image is present in unbounded-storage then use it but if not then just pull it from internet.
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.
But how will they get into unbounded-storage in that case?
Users don't write to unbounded storage - they read from a configured frontend, and it pulls from the corresponding backend impl if the value isn't in cache. So it's always a pull through cache. In order for Gantry to use unbounded-storage it needs both a way to pull values from unbounded AND the ability for unboudned to fetch those values on misses.
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.
Can you show me an example of how it is done today?
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.
This integration test is a pretty good example: https://github.com/Azure/unbounded/blob/main/internal/orca/inttest/storageboundary_test.go
It spins up a chain of unbounded-storage -> Orca -> Garage (for fake S3 origin), then downloads some blobs through the chain by hitting the unbounded-storage S3 frontend.