Skip to content

Commit 3b7a3d5

Browse files
authored
Merge pull request #5756 from Kern--/discard-blocks
Discard blocks when removing a thin device
2 parents f6fa3fd + f1d79d3 commit 3b7a3d5

7 files changed

Lines changed: 114 additions & 4 deletions

File tree

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)