Skip to content

Commit fc85b8d

Browse files
authored
Add a diff service to Overlaybd snapshotter (#21)
This diff service identifies whether the Compare function is operating on overlaybd devices, and if so, attempts to call overlaybd-commit on the writable_data and writable_index of the upper layer, efficiently generating a blob for the writable layer. This is much faster than the default diff service, which mounts the block devices and compares the filesystem's file-by-file, which generates an OCI tar which we then convert to overlaybd.
1 parent ae0c90b commit fc85b8d

2 files changed

Lines changed: 296 additions & 2 deletions

File tree

cmd/overlaybd-snapshotter/main.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,16 @@ import (
3232
"github.com/containerd/accelerated-container-image/pkg/metrics"
3333
overlaybd "github.com/containerd/accelerated-container-image/pkg/snapshot"
3434
"github.com/containerd/accelerated-container-image/pkg/tracing"
35+
"github.com/containerd/errdefs"
3536

37+
diffapi "github.com/containerd/containerd/api/services/diff/v1"
3638
snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1"
39+
"github.com/containerd/containerd/v2/contrib/diffservice"
3740
"github.com/containerd/containerd/v2/contrib/snapshotservice"
41+
"github.com/containerd/containerd/v2/core/diff"
42+
"github.com/containerd/containerd/v2/core/mount"
3843
"github.com/containerd/log"
44+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3945
"github.com/pkg/errors"
4046
"github.com/sirupsen/logrus"
4147
"golang.org/x/sys/unix"
@@ -145,6 +151,7 @@ func main() {
145151
grpc.UnaryInterceptor(requestIDInterceptor),
146152
)
147153
snapshotsapi.RegisterSnapshotsServer(srv, tracing.WithTracing(snapshotservice.FromSnapshotter(sn)))
154+
diffapi.RegisterDiffServer(srv, diffservice.FromApplierAndComparer(notImplementedApplier{}, sn))
148155

149156
address := strings.TrimSpace(pconfig.Address)
150157

@@ -240,3 +247,9 @@ func setLogLevel(level string) error {
240247
logrus.SetLevel(logLevel)
241248
return nil
242249
}
250+
251+
type notImplementedApplier struct{}
252+
253+
func (notImplementedApplier) Apply(ctx context.Context, desc ocispec.Descriptor, mount []mount.Mount, opts ...diff.ApplyOpt) (ocispec.Descriptor, error) {
254+
return ocispec.Descriptor{}, errdefs.ErrNotImplemented
255+
}

pkg/snapshot/overlay.go

Lines changed: 283 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ package snapshot
1919
import (
2020
"bytes"
2121
"context"
22+
"crypto/rand"
23+
"encoding/base64"
2224
"fmt"
25+
"io"
2326
"os"
2427
"os/exec"
28+
"path"
2529
"path/filepath"
30+
"slices"
2631
"strings"
2732
"sync"
2833
"syscall"
@@ -32,6 +37,8 @@ import (
3237
"github.com/containerd/accelerated-container-image/pkg/label"
3338
"github.com/containerd/accelerated-container-image/pkg/snapshot/diskquota"
3439
"github.com/containerd/accelerated-container-image/pkg/tracing"
40+
"github.com/containerd/accelerated-container-image/pkg/utils"
41+
"github.com/containerd/accelerated-container-image/pkg/version"
3542
"go.opentelemetry.io/otel/attribute"
3643
"go.opentelemetry.io/otel/trace"
3744

@@ -40,14 +47,20 @@ import (
4047
"github.com/data-accelerator/zdfs"
4148
"github.com/sirupsen/logrus"
4249

43-
"github.com/containerd/containerd/v2/core/mount"
50+
"github.com/containerd/containerd/v2/client"
51+
"github.com/containerd/containerd/v2/core/content"
4452
"github.com/containerd/containerd/v2/core/snapshots"
4553
"github.com/containerd/containerd/v2/core/snapshots/storage"
54+
"github.com/containerd/containerd/v2/pkg/namespaces"
4655
"github.com/containerd/continuity/fs"
4756
"github.com/containerd/errdefs"
4857
"github.com/containerd/log"
4958
"github.com/moby/locker"
5059
"github.com/pkg/errors"
60+
61+
"github.com/containerd/containerd/v2/core/diff"
62+
"github.com/containerd/containerd/v2/core/mount"
63+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
5164
)
5265

5366
// storageType is used to indicate that what kind of the snapshot it is.
@@ -215,8 +228,13 @@ type snapshotter struct {
215228
quotaSize string
216229
}
217230

231+
type SnapshotterAndComparer interface {
232+
snapshots.Snapshotter
233+
diff.Comparer
234+
}
235+
218236
// NewSnapshotter returns a Snapshotter which uses block device based on overlayFS.
219-
func NewSnapshotter(bootConfig *BootConfig, opts ...Opt) (snapshots.Snapshotter, error) {
237+
func NewSnapshotter(bootConfig *BootConfig, opts ...Opt) (SnapshotterAndComparer, error) {
220238
config := defaultConfig
221239
for _, opt := range opts {
222240
if err := opt(&config); err != nil {
@@ -1198,6 +1216,261 @@ func (o *snapshotter) Remove(ctx context.Context, key string) (err error) {
11981216
return nil
11991217
}
12001218

1219+
// Reverses a map, returning a map that maps value to key. This is lossy, in that if
1220+
// multiple keys map to the same value, the returned map will map that value to the
1221+
// last key in iteration order.
1222+
func ReverseMapLossy[K comparable, V comparable](src map[K]V) map[V]K {
1223+
reversed := make(map[V]K)
1224+
for k, v := range src {
1225+
reversed[v] = k
1226+
}
1227+
return reversed
1228+
}
1229+
1230+
type snapshotIdAndInfo struct {
1231+
snapshots.Info
1232+
Id string
1233+
}
1234+
1235+
// Returns a map of block device path (`/dev/XXX`) to snapshot Info.
1236+
func (o *snapshotter) resolveBlockDevices(ctx context.Context) (map[string]*snapshotIdAndInfo, error) {
1237+
ctx, t, err := o.ms.TransactionContext(ctx, false)
1238+
if err != nil {
1239+
return nil, err
1240+
}
1241+
defer t.Rollback()
1242+
1243+
// Build a reverse-lookup map so that we can recover the ID from the snapshot Name
1244+
ids, err := storage.IDMap(ctx)
1245+
if err != nil {
1246+
return nil, err
1247+
}
1248+
keyToId := ReverseMapLossy(ids)
1249+
1250+
// Construct a lookup map of block device path to info by walking the snapshots
1251+
results := make(map[string]*snapshotIdAndInfo)
1252+
storage.WalkInfo(ctx, func(ctx context.Context, info snapshots.Info) error {
1253+
id, ok := keyToId[info.Name]
1254+
if !ok {
1255+
return nil
1256+
}
1257+
// Read the backstore mark file, which containts the `/dev/XXX` device path.
1258+
// If the read fails, this is likely not an overlaybd snapshot, so skip it.
1259+
blockDev, err := os.ReadFile(o.overlaybdBackstoreMarkFile(id))
1260+
if err != nil {
1261+
return nil
1262+
}
1263+
results[strings.TrimSpace(string(blockDev))] = &snapshotIdAndInfo{
1264+
Id: id,
1265+
Info: info,
1266+
}
1267+
return nil
1268+
})
1269+
return results, nil
1270+
}
1271+
1272+
func getBlockDeviceMount(m []mount.Mount, mode string) (string, bool) {
1273+
if len(m) == 1 && strings.HasPrefix(m[0].Source, "/dev/") && slices.Contains(m[0].Options, mode) {
1274+
return m[0].Source, true
1275+
}
1276+
return "", false
1277+
}
1278+
1279+
// Validates that the lower and upper mounts are compatible with this Differ and returns the upper mount info if they are.
1280+
func (o *snapshotter) validateDiffCompatibility(ctx context.Context, lower, upper []mount.Mount) (*snapshotIdAndInfo, bool) {
1281+
// Only handle requests whose upper mount is a RW block device.
1282+
upperDev, ok := getBlockDeviceMount(upper, "rw")
1283+
if !ok {
1284+
return nil, false
1285+
}
1286+
1287+
// Only handle requests whose lower mount is a RO block device.
1288+
lowerDev, ok := getBlockDeviceMount(lower, "ro")
1289+
if !ok {
1290+
return nil, false
1291+
}
1292+
1293+
// Create a block device path to info lookup map.
1294+
lookup, err := o.resolveBlockDevices(ctx)
1295+
if err != nil {
1296+
logrus.Errorln("failed to create block device lookup map:", err)
1297+
return nil, false
1298+
}
1299+
1300+
// Validate the upper device properties.
1301+
upperInfo, ok := lookup[upperDev]
1302+
if !ok {
1303+
logrus.Warnf("upper dev %q is not managed by overlaybd", upperDev)
1304+
return nil, false
1305+
}
1306+
if upperInfo.Kind != snapshots.KindActive {
1307+
logrus.Warnf("upper snapshot %s must be active", upperInfo.Name)
1308+
return nil, false
1309+
}
1310+
1311+
// Validate the lower device properties.
1312+
lowerInfo, ok := lookup[lowerDev]
1313+
if !ok {
1314+
logrus.Warnf("lower dev %q is not managed by overlaybd", lowerDev)
1315+
return nil, false
1316+
}
1317+
switch lowerInfo.Kind {
1318+
case snapshots.KindCommitted:
1319+
if lowerInfo.Name != upperInfo.Parent {
1320+
logrus.Warnf("upper snapshot %s must be direct child of lower snapshot %s", upperInfo.Name, lowerInfo.Name)
1321+
return nil, false
1322+
}
1323+
case snapshots.KindView:
1324+
// A View is a RO snapshot on top of a committed snapshot, so we compare parents to determine ancestry.
1325+
if lowerInfo.Parent != upperInfo.Parent {
1326+
logrus.Warnf("upper snapshot %s must be direct child of lower snapshot %s", upperInfo.Name, lowerInfo.Name)
1327+
return nil, false
1328+
}
1329+
default:
1330+
logrus.Warnf("lower snapshot %s must be a RO layer", lowerInfo.Name)
1331+
return nil, false
1332+
}
1333+
1334+
return upperInfo, true
1335+
}
1336+
1337+
const uncompressedOciLayerMediaType = "application/vnd.oci.image.layer.v1.tar"
1338+
1339+
func (o *snapshotter) Compare(ctx context.Context, lower, upper []mount.Mount, opts ...diff.Opt) (ocispec.Descriptor, error) {
1340+
// Inflate our config from the passed in options.
1341+
var config diff.Config
1342+
for _, opt := range opts {
1343+
if err := opt(&config); err != nil {
1344+
return ocispec.Descriptor{}, err
1345+
}
1346+
}
1347+
1348+
logrus.Infoln("Compare")
1349+
logrus.Infof("-> lower is %+v", lower)
1350+
logrus.Infof("-> upper is %+v", upper)
1351+
logrus.Infof("-> config is %+v", config)
1352+
1353+
// MediaType must be uncompressed OCI.
1354+
if config.MediaType == "" {
1355+
config.MediaType = uncompressedOciLayerMediaType
1356+
} else if config.MediaType != uncompressedOciLayerMediaType {
1357+
return ocispec.Descriptor{}, errdefs.ErrNotImplemented
1358+
}
1359+
1360+
// If we are not given a reference ID, make our own.
1361+
newReference := false
1362+
if config.Reference == "" {
1363+
config.Reference = uniqueRef()
1364+
newReference = true
1365+
}
1366+
1367+
// Make sure we can handle this Diff, as we only want to handle the case where
1368+
// the Diff is between an overlaybd writable layer and its parent.
1369+
upperInfo, compatible := o.validateDiffCompatibility(ctx, lower, upper)
1370+
if !compatible {
1371+
// ErrNotImplemented instructs containerd to try the next Differ.
1372+
return ocispec.Descriptor{}, errdefs.ErrNotImplemented
1373+
}
1374+
1375+
// Create a temp directory to store the results of overlaybd-commit.
1376+
tempDir, err := os.MkdirTemp("", fmt.Sprintf("%s-*", upperInfo.Id))
1377+
if err != nil {
1378+
logrus.Errorln("failed to create tmpdir:", err)
1379+
return ocispec.Descriptor{}, err
1380+
}
1381+
defer os.RemoveAll(tempDir)
1382+
1383+
// Call overlaybd-commit to create the overlaybd.commit file. Use zfile compression
1384+
// and wrap it in a TAR file for compatibility.
1385+
blockPath := o.blockPath(upperInfo.Id)
1386+
err = utils.Commit(ctx, blockPath, tempDir, false, "-z", "-t")
1387+
if err != nil {
1388+
logrus.Errorf("failed to commit %q for snapshot %s: %v", blockPath, upperInfo.Name, err)
1389+
return ocispec.Descriptor{}, err
1390+
}
1391+
1392+
overlaybdCommitFile := path.Join(tempDir, "overlaybd.commit")
1393+
logrus.Infof("overlaybd-commit of snapshot %s succeeded: %s", upperInfo.Name, overlaybdCommitFile)
1394+
1395+
// Write the commit file into containerd's content store.
1396+
1397+
containerdClient, err := client.New("/run/containerd/containerd.sock")
1398+
if err != nil {
1399+
logrus.Errorln("failed to create client to containerd:", err)
1400+
return ocispec.Descriptor{}, err
1401+
}
1402+
defer containerdClient.Close()
1403+
1404+
cs := containerdClient.ContentStore()
1405+
1406+
// HACK: Hardcode the namespace, for some reason it is not propagated into our DiffService.
1407+
ctx = namespaces.WithNamespace(ctx, "k8s.io")
1408+
1409+
// Grab a new content store writer using a unique ref to ensure it is new.
1410+
writer, err := cs.Writer(ctx, content.WithRef(config.Reference), content.WithDescriptor(ocispec.Descriptor{MediaType: config.MediaType}))
1411+
if err != nil {
1412+
logrus.Errorln("failed to get content store writer:", err)
1413+
return ocispec.Descriptor{}, err
1414+
}
1415+
var errWrite error
1416+
defer func() {
1417+
writer.Close()
1418+
// Only abort on an error and if we owned this reference.
1419+
if errWrite != nil && newReference {
1420+
if errAbort := cs.Abort(ctx, config.Reference); errAbort != nil {
1421+
logrus.Errorf("failed to abort content store write %q: %v", config.Reference, errAbort)
1422+
}
1423+
}
1424+
}()
1425+
1426+
if !newReference {
1427+
// If we are given a reference, make sure we rewind to the beginning.
1428+
if errWrite = writer.Truncate(0); errWrite != nil {
1429+
logrus.Errorf("failed to truncate existing writer with ref %s: %v", config.Reference, errWrite)
1430+
return ocispec.Descriptor{}, errWrite
1431+
}
1432+
}
1433+
1434+
// Write the commit file into the content store.
1435+
src, errWrite := os.Open(overlaybdCommitFile)
1436+
if errWrite != nil {
1437+
logrus.Errorf("failed to open %q: %v", overlaybdCommitFile, errWrite)
1438+
return ocispec.Descriptor{}, errWrite
1439+
}
1440+
defer src.Close()
1441+
size, errWrite := io.Copy(writer, src)
1442+
if errWrite != nil {
1443+
logrus.Errorln("failed to write overlaybd commit file to content store:", errWrite)
1444+
return ocispec.Descriptor{}, errWrite
1445+
}
1446+
var commitopts []content.Opt
1447+
if config.Labels != nil {
1448+
commitopts = append(commitopts, content.WithLabels(config.Labels))
1449+
}
1450+
if errWrite = writer.Commit(ctx, size, "", commitopts...); errWrite != nil {
1451+
if !errdefs.IsAlreadyExists(errWrite) {
1452+
logrus.Errorln("failed to commit overlaybd layer to content store:", errWrite)
1453+
return ocispec.Descriptor{}, errWrite
1454+
}
1455+
errWrite = nil
1456+
}
1457+
1458+
digest := writer.Digest()
1459+
desc := ocispec.Descriptor{
1460+
MediaType: config.MediaType,
1461+
Digest: digest,
1462+
Size: size,
1463+
Annotations: map[string]string{
1464+
label.OverlayBDVersion: version.OverlayBDVersionNumber,
1465+
label.OverlayBDBlobDigest: digest.String(),
1466+
label.OverlayBDBlobSize: fmt.Sprintf("%d", size),
1467+
label.OverlayBDBlobFsType: "ext4",
1468+
},
1469+
}
1470+
logrus.Infof("Diff created with spec %+v", desc)
1471+
return desc, nil
1472+
}
1473+
12011474
// Walk the snapshots.
12021475
func (o *snapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) (err error) {
12031476
log.G(ctx).Infof("Walk (fs: %s)", fs)
@@ -1746,3 +2019,11 @@ func isOverlaybdFileHeader(header []byte) bool {
17462019

17472020
return zfileFormat || lsmtFormat
17482021
}
2022+
2023+
func uniqueRef() string {
2024+
t := time.Now()
2025+
var b [3]byte
2026+
// Ignore read failures, just decreases uniqueness
2027+
rand.Read(b[:])
2028+
return fmt.Sprintf("%d-%s", t.UnixNano(), base64.URLEncoding.EncodeToString(b[:]))
2029+
}

0 commit comments

Comments
 (0)