Skip to content

Commit f1d79d3

Browse files
author
Kern Walster
committed
Discard blocks when removing a thin device
dmsetup does not discard blocks when removing a thin device. The unused blocks are reused by the thin-pool, but will remain allocated in the underlying device indefinitely. For loop device backed thin-pools, this results in "lost" disk space in the underlying file system as the blocks remain allocated in the loop device's backing file. This change adds an option, discard_blocks, to the devmapper snapshotter which causes the snapshotter to issue blkdiscard ioctls on the thin device before removal. With this option enabled, loop device setups will see disk space return to the underlying filesystem immediately on exiting a container. Fixes containerd#5691 Signed-off-by: Kern Walster <walster@amazon.com>
1 parent b88bf1e commit f1d79d3

File tree

7 files changed

+114
-4
lines changed

7 files changed

+114
-4
lines changed

snapshots/devmapper/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The following configuration flags are supported:
2626
should be the same as in `/dev/mapper/` directory
2727
* `base_image_size` - defines how much space to allocate when creating the base device
2828
* `async_remove` - flag to async remove device using snapshot GC's cleanup callback
29+
* `discard_blocks` - whether to discard blocks when removing a device. This is especially useful for returning disk space to the filesystem when using loopback devices.
2930

3031
Pool name and base image size are required snapshotter parameters.
3132

@@ -93,6 +94,7 @@ cat << EOF
9394
pool_name = "${POOL_NAME}"
9495
root_path = "${DATA_DIR}"
9596
base_image_size = "10GB"
97+
discard_blocks = true
9698
EOF
9799
```
98100

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// +build linux
2+
3+
/*
4+
Copyright The containerd Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package blkdiscard
20+
21+
import "os/exec"
22+
23+
// Version returns the output of "blkdiscard --version"
24+
func Version() (string, error) {
25+
return blkdiscard("--version")
26+
}
27+
28+
// BlkDiscard discards all blocks of a device.
29+
// devicePath is expected to be a fully qualified path.
30+
// BlkDiscard expects the caller to verify that the device is not in use.
31+
func BlkDiscard(devicePath string) (string, error) {
32+
return blkdiscard(devicePath)
33+
}
34+
35+
func blkdiscard(args ...string) (string, error) {
36+
output, err := exec.Command("blkdiscard", args...).CombinedOutput()
37+
if err != nil {
38+
return "", err
39+
}
40+
return string(output), nil
41+
}

snapshots/devmapper/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ type Config struct {
4343

4444
// Flag to async remove device using Cleanup() callback in snapshots GC
4545
AsyncRemove bool `toml:"async_remove"`
46+
47+
// Whether to discard blocks when removing a thin device.
48+
DiscardBlocks bool `toml:"discard_blocks"`
4649
}
4750

4851
// LoadConfig reads devmapper configuration file from disk in TOML format

snapshots/devmapper/dmsetup/dmsetup.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
limitations under the License.
1717
*/
1818

19+
// Copyright 2012-2017 Docker, Inc.
20+
1921
package dmsetup
2022

2123
import (
@@ -26,6 +28,7 @@ import (
2628
"strconv"
2729
"strings"
2830

31+
blkdiscard "github.com/containerd/containerd/snapshots/devmapper/blkdiscard"
2932
"github.com/pkg/errors"
3033
"golang.org/x/sys/unix"
3134
)
@@ -37,6 +40,9 @@ const (
3740
SectorSize = 512
3841
)
3942

43+
// ErrInUse represents an error mutating a device because it is in use elsewhere
44+
var ErrInUse = errors.New("device is in use")
45+
4046
// DeviceInfo represents device info returned by "dmsetup info".
4147
// dmsetup(8) provides more information on each of these fields.
4248
type DeviceInfo struct {
@@ -345,6 +351,24 @@ func BlockDeviceSize(path string) (int64, error) {
345351
return size, nil
346352
}
347353

354+
// DiscardBlocks discards all blocks for the given thin device
355+
// ported from https://github.com/moby/moby/blob/7b9275c0da707b030e62c96b679a976f31f929d3/pkg/devicemapper/devmapper.go#L416
356+
func DiscardBlocks(deviceName string) error {
357+
inUse, err := isInUse(deviceName)
358+
if err != nil {
359+
return err
360+
}
361+
if inUse {
362+
return ErrInUse
363+
}
364+
path := GetFullDevicePath(deviceName)
365+
_, err = blkdiscard.BlkDiscard(path)
366+
if err != nil {
367+
return err
368+
}
369+
return nil
370+
}
371+
348372
func dmsetup(args ...string) (string, error) {
349373
data, err := exec.Command("dmsetup", args...).CombinedOutput()
350374
output := string(data)
@@ -406,3 +430,14 @@ func parseDmsetupError(output string) string {
406430
str = strings.ToLower(str)
407431
return str
408432
}
433+
434+
func isInUse(deviceName string) (bool, error) {
435+
info, err := Info(deviceName)
436+
if err != nil {
437+
return true, err
438+
}
439+
if len(info) != 1 {
440+
return true, errors.New("could not get device info")
441+
}
442+
return info[0].OpenCount != 0, nil
443+
}

snapshots/devmapper/dmsetup/dmsetup_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func TestDMSetup(t *testing.T) {
8383
t.Run("ActivateDevice", testActivateDevice)
8484
t.Run("DeviceStatus", testDeviceStatus)
8585
t.Run("SuspendResumeDevice", testSuspendResumeDevice)
86+
t.Run("DiscardBlocks", testDiscardBlocks)
8687
t.Run("RemoveDevice", testRemoveDevice)
8788

8889
t.Run("RemovePool", func(t *testing.T) {
@@ -169,6 +170,11 @@ func testSuspendResumeDevice(t *testing.T) {
169170
assert.NilError(t, err)
170171
}
171172

173+
func testDiscardBlocks(t *testing.T) {
174+
err := DiscardBlocks(testDeviceName)
175+
assert.NilError(t, err, "failed to discard blocks")
176+
}
177+
172178
func testRemoveDevice(t *testing.T) {
173179
err := RemoveDevice(testPoolName)
174180
assert.Assert(t, err == unix.EBUSY, "removing thin-pool with dependencies shouldn't be allowed")

snapshots/devmapper/pool_device.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ import (
2929
"golang.org/x/sys/unix"
3030

3131
"github.com/containerd/containerd/log"
32+
blkdiscard "github.com/containerd/containerd/snapshots/devmapper/blkdiscard"
3233
"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
3334
)
3435

3536
// PoolDevice ties together data and metadata volumes, represents thin-pool and manages volumes, snapshots and device ids.
3637
type PoolDevice struct {
37-
poolName string
38-
metadata *PoolMetadata
38+
poolName string
39+
metadata *PoolMetadata
40+
discardBlocks bool
3941
}
4042

4143
// NewPoolDevice creates new thin-pool from existing data and metadata volumes.
@@ -51,6 +53,15 @@ func NewPoolDevice(ctx context.Context, config *Config) (*PoolDevice, error) {
5153

5254
log.G(ctx).Infof("using dmsetup:\n%s", version)
5355

56+
if config.DiscardBlocks {
57+
blkdiscardVersion, err := blkdiscard.Version()
58+
if err != nil {
59+
log.G(ctx).Error("blkdiscard is not available")
60+
return nil, err
61+
}
62+
log.G(ctx).Infof("using blkdiscard:\n%s", blkdiscardVersion)
63+
}
64+
5465
dbpath := filepath.Join(config.RootPath, config.PoolName+".db")
5566
poolMetaStore, err := NewPoolMetadata(dbpath)
5667
if err != nil {
@@ -64,8 +75,9 @@ func NewPoolDevice(ctx context.Context, config *Config) (*PoolDevice, error) {
6475
}
6576

6677
poolDevice := &PoolDevice{
67-
poolName: config.PoolName,
68-
metadata: poolMetaStore,
78+
poolName: config.PoolName,
79+
metadata: poolMetaStore,
80+
discardBlocks: config.DiscardBlocks,
6981
}
7082

7183
if err := poolDevice.ensureDeviceStates(ctx); err != nil {
@@ -422,6 +434,16 @@ func (p *PoolDevice) DeactivateDevice(ctx context.Context, deviceName string, de
422434

423435
if err := p.transition(ctx, deviceName, Deactivating, Deactivated, func() error {
424436
return retry(ctx, func() error {
437+
if !deferred && p.discardBlocks {
438+
err := dmsetup.DiscardBlocks(deviceName)
439+
if err != nil {
440+
if err == dmsetup.ErrInUse {
441+
log.G(ctx).Warnf("device %q is in use, skipping blkdiscard", deviceName)
442+
} else {
443+
return err
444+
}
445+
}
446+
}
425447
if err := dmsetup.RemoveDevice(deviceName, opts...); err != nil {
426448
return errors.Wrap(err, "failed to deactivate device")
427449
}

snapshots/devmapper/pool_device_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func TestPoolDevice(t *testing.T) {
8585
RootPath: tempDir,
8686
BaseImageSize: "16mb",
8787
BaseImageSizeBytes: 16 * 1024 * 1024,
88+
DiscardBlocks: true,
8889
}
8990

9091
pool, err := NewPoolDevice(ctx, config)

0 commit comments

Comments
 (0)