Skip to content

Commit 7de887f

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 7de887f

14 files changed

Lines changed: 483 additions & 22 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ 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+
131133
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.
132134

133135
- `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 +150,8 @@ When stopping FUSE manager for upgrading the binary or restarting the node, you
148150
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.
149151
5. Restart the containers.
150152

153+
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.
154+
151155
### Unexpected restart handling
152156

153157
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

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

service/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,8 @@ type SnapshotterConfig struct {
7070
// NOTE: User needs to manually remove the snapshots from containerd's metadata store using
7171
// ctr (e.g. `ctr snapshot rm`).
7272
AllowInvalidMountsOnRestart bool `toml:"allow_invalid_mounts_on_restart" json:"allow_invalid_mounts_on_restart"`
73+
74+
// LazyRestoreOnRestart restores remote snapshot directories at startup but
75+
// defers remote resolution and FUSE mounting until a snapshot is first used.
76+
LazyRestoreOnRestart bool `toml:"lazy_restore_on_restart" json:"lazy_restore_on_restart"`
7377
}

0 commit comments

Comments
 (0)