Skip to content

Commit 2302135

Browse files
committed
snapshot: restore remote mounts on demand after restart
Add lazy_restore_on_restart to skip remote resolution and FUSE mount restoration during snapshotter startup. When an unregistered remote layer is first used, restore its mount from the snapshot labels. Coalesce concurrent requests for the same mountpoint and support both in-process and fuse-manager modes. This allows the snapshotter to start while the registry is unavailable. Signed-off-by: Wang Xingxing <wangxingxing@siliconflow.cn>
1 parent 4daea59 commit 2302135

15 files changed

Lines changed: 576 additions & 27 deletions

File tree

cmd/containerd-stargz-grpc/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ func main() {
190190
log.G(ctx).WithError(err).Fatalf("failed to configure fusemanager")
191191
}
192192
flags := []snbase.Opt{snbase.AsynchronousRemove}
193+
if config.LazyRestoreOnRestart {
194+
flags = append(flags, snbase.LazyRestoreOnRestart)
195+
}
193196
// "managerNewlyStarted" being true indicates that the FUSE manager is newly started. To
194197
// fully recover the snapshotter and the FUSE manager's state, we need to restore
195198
// all snapshot mounts. If managerNewlyStarted is false, the existing FUSE manager maintains

docs/overview.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ When you stop Stargz Sanpshotter on the node, it takes the following behaviour d
128128

129129
killing containerd-stargz-grpc will result in unmounting all snapshot mounts managed by Stargz Snapshotter.
130130
When containerd-stargz-grpc is restarted, all those snapshots are mounted again by lazy pulling all layers.
131+
If `lazy_restore_on_restart = true`, containerd-stargz-grpc restores the local snapshot directories but doesn't resolve remote blobs or create FUSE mounts at startup. The first `Prepare`, `View`, or `Mounts` request that uses a remote snapshot mounts it on demand. If the registry is still unavailable, that request fails with an unavailable error while the snapshotter keeps running; a later request retries the mount. Metadata-only operations such as `Stat`, `Usage`, and `Walk` don't trigger an on-demand mount.
132+
When `lazy_restore_on_restart` and `allow_invalid_mounts_on_restart` are both enabled, lazy restoration takes precedence during startup, so `allow_invalid_mounts_on_restart` isn't consulted unless lazy restoration is disabled.
133+
131134
If the snapshotter fails to mount one of the snapshots (e.g. because of lazy pulling failure) during this step, the behaviour differs depending on `allow_invalid_mounts_on_restart` flag in the config TOML.
132135

133136
- `allow_invalid_mounts_on_restart = true`: containerd-stargz-grpc leaves the failed snapshots as empty directories. The user needs to manually remove those snapshot via containerd (e.g. using `ctr snapshot rm` command). The name of those snapshots can be seen in the log with `failed to restore remote snapshot` message.
@@ -148,6 +151,8 @@ When stopping FUSE manager for upgrading the binary or restarting the node, you
148151
4. Restart the containerd-stargz-grpc process. This restores all snapshot mounts by lazy pulling them. `allow_invalid_mounts_on_restart` (described in the above) can still be used for controlling the behaviour of the error cases.
149152
5. Restart the containers.
150153

154+
If `lazy_restore_on_restart` is enabled, step 4 restores only the local snapshot directories. Remote FUSE mounts are deferred until the snapshots are first used.
155+
151156
### Unexpected restart handling
152157

153158
When Stargz Snapshotter is killed unexpectedly (e.g., by OOM killer or system crash), the process doesn't get a chance to perform graceful cleanup. In such cases, the snapshotter can successfully restart and restore remote snapshots, but this may lead to the temporary cache directories having duplicating cached data.

fs/fs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ func (fs *filesystem) Check(ctx context.Context, mountpoint string, labels map[s
377377
fs.layerMu.Unlock()
378378
if l == nil {
379379
log.G(ctx).Debug("layer not registered")
380-
return fmt.Errorf("layer not registered")
380+
return snapshot.ErrLayerNotRegistered
381381
}
382382

383383
if l.Info().FetchedSize < l.Info().Size {

fs/fs_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ package fs
2424

2525
import (
2626
"context"
27+
"errors"
2728
"fmt"
2829
"testing"
2930
"time"
@@ -33,6 +34,7 @@ import (
3334
"github.com/containerd/stargz-snapshotter/fs/layer"
3435
"github.com/containerd/stargz-snapshotter/fs/remote"
3536
"github.com/containerd/stargz-snapshotter/fs/source"
37+
"github.com/containerd/stargz-snapshotter/snapshot"
3638
"github.com/containerd/stargz-snapshotter/task"
3739
fusefs "github.com/hanwen/go-fuse/v2/fs"
3840
digest "github.com/opencontainers/go-digest"
@@ -59,6 +61,10 @@ func TestCheck(t *testing.T) {
5961
if err := fs.Check(context.TODO(), "test", nil); err == nil {
6062
t.Errorf("connection succeeded; wanted to fail")
6163
}
64+
65+
if err := fs.Check(context.TODO(), "missing", nil); !errors.Is(err, snapshot.ErrLayerNotRegistered) {
66+
t.Fatalf("missing layer error = %v; want ErrLayerNotRegistered", err)
67+
}
6268
}
6369

6470
type breakableLayer struct {

fusemanager/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import (
2626
"github.com/containerd/log"
2727
"google.golang.org/grpc"
2828
"google.golang.org/grpc/backoff"
29+
"google.golang.org/grpc/codes"
2930
"google.golang.org/grpc/credentials/insecure"
31+
"google.golang.org/grpc/status"
3032

3133
pb "github.com/containerd/stargz-snapshotter/fusemanager/api"
3234
"github.com/containerd/stargz-snapshotter/snapshot"
@@ -120,6 +122,9 @@ func (cli *Client) Check(ctx context.Context, mountpoint string, labels map[stri
120122
_, err := cli.client.Check(ctx, req)
121123
if err != nil {
122124
log.G(ctx).WithError(err).Errorf("failed to call Check")
125+
if status.Code(err) == codes.NotFound {
126+
return fmt.Errorf("%w: %v", snapshot.ErrLayerNotRegistered, err)
127+
}
123128
return err
124129
}
125130

fusemanager/fusemanager_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package fusemanager
1919
import (
2020
"context"
2121
"encoding/json"
22+
"errors"
2223
"fmt"
2324
"net"
2425
"os"
@@ -27,6 +28,7 @@ import (
2728

2829
pb "github.com/containerd/stargz-snapshotter/fusemanager/api"
2930
"github.com/containerd/stargz-snapshotter/service"
31+
"github.com/containerd/stargz-snapshotter/snapshot"
3032
"google.golang.org/grpc"
3133
)
3234

@@ -229,7 +231,49 @@ func TestFuseManager(t *testing.T) {
229231
if !mockFs.unmountCalled {
230232
t.Error("Unmount() was not called on filesystem")
231233
}
234+
235+
err = client.Check(context.Background(), tc.mountpoint, tc.labels)
236+
if !errors.Is(err, snapshot.ErrLayerNotRegistered) {
237+
t.Errorf("Check() after unmount error = %v; want ErrLayerNotRegistered", err)
238+
}
232239
}
233240
})
234241
}
235242
}
243+
244+
func TestRestoreFuseInfoRespectsRestartMode(t *testing.T) {
245+
ctx := context.Background()
246+
fuseStorePath := filepath.Join(t.TempDir(), "fusestore.db")
247+
fm, err := NewFuseManager(ctx, nil, grpc.NewServer(), fuseStorePath, "")
248+
if err != nil {
249+
t.Fatalf("failed to create fuse manager: %v", err)
250+
}
251+
defer fm.Close(ctx)
252+
253+
mockFs := newMockFileSystem(t)
254+
fm.curFs = mockFs
255+
fm.config = &Config{Config: service.Config{
256+
SnapshotterConfig: service.SnapshotterConfig{LazyRestoreOnRestart: true},
257+
}}
258+
if err := fm.storeFuseInfo(&fuseInfo{
259+
Mountpoint: "/snapshot/1/fs",
260+
Labels: map[string]string{"test": "label"},
261+
}); err != nil {
262+
t.Fatalf("failed to store fuse info: %v", err)
263+
}
264+
265+
if err := fm.restoreFuseInfo(ctx); err != nil {
266+
t.Fatalf("lazy fuse restore failed: %v", err)
267+
}
268+
if mockFs.mountCalled {
269+
t.Fatal("lazy fuse restore mounted a remote layer")
270+
}
271+
272+
fm.config.Config.LazyRestoreOnRestart = false
273+
if err := fm.restoreFuseInfo(ctx); err != nil {
274+
t.Fatalf("eager fuse restore failed: %v", err)
275+
}
276+
if !mockFs.mountCalled {
277+
t.Fatal("eager fuse restore didn't mount the remote layer")
278+
}
279+
}

fusemanager/fusestore.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"encoding/json"
2222

23+
"github.com/containerd/log"
2324
bolt "go.etcd.io/bbolt"
2425

2526
"github.com/containerd/stargz-snapshotter/service"
@@ -80,6 +81,11 @@ func (fm *Server) removeFuseInfo(fuseInfo *fuseInfo) error {
8081
// restoreFuseInfo restores fuseInfo when Init is called, it will skip mounted
8182
// layers whose mountpoint can be found in fsMap
8283
func (fm *Server) restoreFuseInfo(ctx context.Context) error {
84+
if fm.config.Config.LazyRestoreOnRestart {
85+
log.G(ctx).Debug("deferred fuse-manager mount restoration until first use")
86+
return nil
87+
}
88+
8389
return fm.ms.View(func(tx *bolt.Tx) error {
8490
bucket := tx.Bucket(fuseInfoBucket)
8591
if bucket == nil {

fusemanager/service.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package fusemanager
1919
import (
2020
"context"
2121
"encoding/json"
22+
"errors"
2223
"fmt"
2324
"net"
2425
"os"
@@ -30,6 +31,8 @@ import (
3031
"github.com/moby/sys/mountinfo"
3132
bolt "go.etcd.io/bbolt"
3233
"google.golang.org/grpc"
34+
"google.golang.org/grpc/codes"
35+
"google.golang.org/grpc/status"
3336

3437
pb "github.com/containerd/stargz-snapshotter/fusemanager/api"
3538
"github.com/containerd/stargz-snapshotter/service"
@@ -261,15 +264,18 @@ func (fm *Server) Check(ctx context.Context, req *pb.CheckRequest) (*pb.Response
261264

262265
obj, found := fm.fsMap.Load(req.Mountpoint)
263266
if !found {
264-
err := fmt.Errorf("failed to find filesystem of mountpoint %s", req.Mountpoint)
267+
err := fmt.Errorf("%w: failed to find filesystem of mountpoint %s", snapshot.ErrLayerNotRegistered, req.Mountpoint)
265268
log.G(ctx).WithError(err).Errorf("failed to check filesystem")
266-
return &pb.Response{}, err
269+
return &pb.Response{}, status.Error(codes.NotFound, err.Error())
267270
}
268271

269272
fs := obj.(snapshot.FileSystem)
270273
err := fs.Check(ctx, req.Mountpoint, req.Labels)
271274
if err != nil {
272275
log.G(ctx).WithError(err).Errorf("failed to check filesystem")
276+
if errors.Is(err, snapshot.ErrLayerNotRegistered) {
277+
return &pb.Response{}, status.Error(codes.NotFound, err.Error())
278+
}
273279
return &pb.Response{}, err
274280
}
275281

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/opencontainers/go-digest v1.0.0
2424
github.com/opencontainers/image-spec v1.1.1
2525
github.com/opencontainers/runtime-spec v1.3.0
26+
github.com/pelletier/go-toml/v2 v2.2.4
2627
github.com/prometheus/client_golang v1.23.2
2728
github.com/rs/xid v1.6.0
2829
github.com/sirupsen/logrus v1.9.4
@@ -79,7 +80,6 @@ require (
7980
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
8081
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
8182
github.com/opencontainers/selinux v1.13.1 // indirect
82-
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
8383
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
8484
github.com/pkg/errors v0.9.1 // indirect
8585
github.com/pmezard/go-difflib v1.0.0 // indirect

script/demo/config.stargz.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ insecure = true
99
direct = true
1010
[snapshotter]
1111
allow_invalid_mounts_on_restart = true
12+
# Set to true to avoid registry access while restoring snapshots at startup.
13+
# lazy_restore_on_restart = true

0 commit comments

Comments
 (0)