Add option to auto-configure blkdev for devmapper

Instead of forcing users to manually configure a block device to use
with devmapper, this gives the user the option to let the devmapper
driver configure a device for them.

Adds several new options to the devmapper storage-opts:

- dm.directlvm_device="" - path to the block device to configure for
  direct-lvm
- dm.thinp_percent=95 - sets the percentage of space to use for
  storage from the passed in block device
- dm.thinp_metapercent=1 - sets the percentage of space to for metadata
  storage from the passed in block device
- dm.thinp_autoextend_threshold=80 - sets the threshold for when `lvm`
  should automatically extend the thin pool as a percentage of the total
  storage space
- dm.thinp_autoextend_percent=20 - sets the percentage to increase the
  thin pool by when an autoextend is triggered.

Defaults are taken from
[here](https://docs.docker.com/engine/userguide/storagedriver/device-mapper-driver/#/configure-direct-lvm-mode-for-production)

The only option that is required is `dm.directlvm_device` for docker to
set everything up.

Changes to these settings are not currently supported and will error
out.
Future work could support allowing changes to these values.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff 2017-02-16 15:33:03 -05:00
parent dab6c7d4f2
commit 5ef07d79c4
5 changed files with 435 additions and 3 deletions

View file

@ -5,7 +5,9 @@
The device mapper graphdriver uses the device mapper thin provisioning The device mapper graphdriver uses the device mapper thin provisioning
module (dm-thinp) to implement CoW snapshots. The preferred model is module (dm-thinp) to implement CoW snapshots. The preferred model is
to have a thin pool reserved outside of Docker and passed to the to have a thin pool reserved outside of Docker and passed to the
daemon via the `--storage-opt dm.thinpooldev` option. daemon via the `--storage-opt dm.thinpooldev` option. Alternatively,
the device mapper graphdriver can setup a block device to handle this
for you via the `--storage-opt dm.directlvm_device` option.
As a fallback if no thin pool is provided, loopback files will be As a fallback if no thin pool is provided, loopback files will be
created. Loopback is very slow, but can be used without any created. Loopback is very slow, but can be used without any

View file

@ -0,0 +1,247 @@
package devmapper
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"github.com/Sirupsen/logrus"
"github.com/pkg/errors"
)
type directLVMConfig struct {
Device string
ThinpPercent uint64
ThinpMetaPercent uint64
AutoExtendPercent uint64
AutoExtendThreshold uint64
}
var (
errThinpPercentMissing = errors.New("must set both `dm.thinp_percent` and `dm.thinp_metapercent` if either is specified")
errThinpPercentTooBig = errors.New("combined `dm.thinp_percent` and `dm.thinp_metapercent` must not be greater than 100")
errMissingSetupDevice = errors.New("must provide device path in `dm.setup_device` in order to configure direct-lvm")
)
func validateLVMConfig(cfg directLVMConfig) error {
if reflect.DeepEqual(cfg, directLVMConfig{}) {
return nil
}
if cfg.Device == "" {
return errMissingSetupDevice
}
if (cfg.ThinpPercent > 0 && cfg.ThinpMetaPercent == 0) || cfg.ThinpMetaPercent > 0 && cfg.ThinpPercent == 0 {
return errThinpPercentMissing
}
if cfg.ThinpPercent+cfg.ThinpMetaPercent > 100 {
return errThinpPercentTooBig
}
return nil
}
func checkDevAvailable(dev string) error {
lvmScan, err := exec.LookPath("lvmdiskscan")
if err != nil {
logrus.Debug("could not find lvmdiskscan")
return nil
}
out, err := exec.Command(lvmScan).CombinedOutput()
if err != nil {
logrus.WithError(err).Error(string(out))
return nil
}
if !bytes.Contains(out, []byte(dev)) {
return errors.Errorf("%s is not available for use with devicemapper", dev)
}
return nil
}
func checkDevInVG(dev string) error {
pvDisplay, err := exec.LookPath("pvdisplay")
if err != nil {
logrus.Debug("could not find pvdisplay")
return nil
}
out, err := exec.Command(pvDisplay, dev).CombinedOutput()
if err != nil {
logrus.WithError(err).Error(string(out))
return nil
}
scanner := bufio.NewScanner(bytes.NewReader(bytes.TrimSpace(out)))
for scanner.Scan() {
fields := strings.SplitAfter(strings.TrimSpace(scanner.Text()), "VG Name")
if len(fields) > 1 {
// got "VG Name" line"
vg := strings.TrimSpace(fields[1])
if len(vg) > 0 {
return errors.Errorf("%s is already part of a volume group %q: must remove this device from any volume group or provide a different device", dev, vg)
}
logrus.Error(fields)
break
}
}
return nil
}
func checkDevHasFS(dev string) error {
blkid, err := exec.LookPath("blkid")
if err != nil {
logrus.Debug("could not find blkid")
return nil
}
out, err := exec.Command(blkid, dev).CombinedOutput()
if err != nil {
logrus.WithError(err).Error(string(out))
return nil
}
fields := bytes.Fields(out)
for _, f := range fields {
kv := bytes.Split(f, []byte{'='})
if bytes.Equal(kv[0], []byte("TYPE")) {
v := bytes.Trim(kv[1], "\"")
if len(v) > 0 {
return errors.Errorf("%s has a filesystem already, use dm.directlvm_device_force=true if you want to wipe the device", dev)
}
return nil
}
}
return nil
}
func verifyBlockDevice(dev string, force bool) error {
if err := checkDevAvailable(dev); err != nil {
return err
}
if err := checkDevInVG(dev); err != nil {
return err
}
if force {
return nil
}
if err := checkDevHasFS(dev); err != nil {
return err
}
return nil
}
func readLVMConfig(root string) (directLVMConfig, error) {
var cfg directLVMConfig
p := filepath.Join(root, "setup-config.json")
b, err := ioutil.ReadFile(p)
if err != nil {
if os.IsNotExist(err) {
return cfg, nil
}
return cfg, errors.Wrap(err, "error reading existing setup config")
}
// check if this is just an empty file, no need to produce a json error later if so
if len(b) == 0 {
return cfg, nil
}
err = json.Unmarshal(b, &cfg)
return cfg, errors.Wrap(err, "error unmarshaling previous device setup config")
}
func writeLVMConfig(root string, cfg directLVMConfig) error {
p := filepath.Join(root, "setup-config.json")
b, err := json.Marshal(cfg)
if err != nil {
return errors.Wrap(err, "error marshalling direct lvm config")
}
err = ioutil.WriteFile(p, b, 0600)
return errors.Wrap(err, "error writing direct lvm config to file")
}
func setupDirectLVM(cfg directLVMConfig) error {
pvCreate, err := exec.LookPath("pvcreate")
if err != nil {
return errors.Wrap(err, "error lookuping up command `pvcreate` while setting up direct lvm")
}
vgCreate, err := exec.LookPath("vgcreate")
if err != nil {
return errors.Wrap(err, "error lookuping up command `vgcreate` while setting up direct lvm")
}
lvCreate, err := exec.LookPath("lvcreate")
if err != nil {
return errors.Wrap(err, "error lookuping up command `lvcreate` while setting up direct lvm")
}
lvConvert, err := exec.LookPath("lvconvert")
if err != nil {
return errors.Wrap(err, "error lookuping up command `lvconvert` while setting up direct lvm")
}
lvChange, err := exec.LookPath("lvchange")
if err != nil {
return errors.Wrap(err, "error lookuping up command `lvchange` while setting up direct lvm")
}
if cfg.AutoExtendPercent == 0 {
cfg.AutoExtendPercent = 20
}
if cfg.AutoExtendThreshold == 0 {
cfg.AutoExtendThreshold = 80
}
if cfg.ThinpPercent == 0 {
cfg.ThinpPercent = 95
}
if cfg.ThinpMetaPercent == 0 {
cfg.ThinpMetaPercent = 1
}
out, err := exec.Command(pvCreate, "-f", cfg.Device).CombinedOutput()
if err != nil {
return errors.Wrap(err, string(out))
}
out, err = exec.Command(vgCreate, "docker", cfg.Device).CombinedOutput()
if err != nil {
return errors.Wrap(err, string(out))
}
out, err = exec.Command(lvCreate, "--wipesignatures", "y", "-n", "thinpool", "docker", "--extents", fmt.Sprintf("%d%%VG", cfg.ThinpPercent)).CombinedOutput()
if err != nil {
return errors.Wrap(err, string(out))
}
out, err = exec.Command(lvCreate, "--wipesignatures", "y", "-n", "thinpoolmeta", "docker", "--extents", fmt.Sprintf("%d%%VG", cfg.ThinpMetaPercent)).CombinedOutput()
if err != nil {
return errors.Wrap(err, string(out))
}
out, err = exec.Command(lvConvert, "-y", "--zero", "n", "-c", "512K", "--thinpool", "docker/thinpool", "--poolmetadata", "docker/thinpoolmeta").CombinedOutput()
if err != nil {
return errors.Wrap(err, string(out))
}
profile := fmt.Sprintf("activation{\nthin_pool_autoextend_threshold=%d\nthin_pool_autoextend_percent=%d\n}", cfg.AutoExtendThreshold, cfg.AutoExtendPercent)
err = ioutil.WriteFile("/etc/lvm/profile/docker-thinpool.profile", []byte(profile), 0600)
if err != nil {
return errors.Wrap(err, "error writing docker thinp autoextend profile")
}
out, err = exec.Command(lvChange, "--metadataprofile", "docker-thinpool", "docker/thinpool").CombinedOutput()
return errors.Wrap(err, string(out))
}

View file

@ -5,7 +5,6 @@ package devmapper
import ( import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -13,6 +12,7 @@ import (
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"reflect"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -29,6 +29,7 @@ import (
"github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/mount"
"github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/parsers"
units "github.com/docker/go-units" units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/opencontainers/runc/libcontainer/label" "github.com/opencontainers/runc/libcontainer/label"
) )
@ -50,6 +51,7 @@ var (
enableDeferredDeletion = false enableDeferredDeletion = false
userBaseSize = false userBaseSize = false
defaultMinFreeSpacePercent uint32 = 10 defaultMinFreeSpacePercent uint32 = 10
lvmSetupConfigForce bool
) )
const deviceSetMetaFile string = "deviceset-metadata" const deviceSetMetaFile string = "deviceset-metadata"
@ -123,6 +125,7 @@ type DeviceSet struct {
gidMaps []idtools.IDMap gidMaps []idtools.IDMap
minFreeSpacePercent uint32 //min free space percentage in thinpool minFreeSpacePercent uint32 //min free space percentage in thinpool
xfsNospaceRetries string // max retries when xfs receives ENOSPC xfsNospaceRetries string // max retries when xfs receives ENOSPC
lvmSetupConfig directLVMConfig
} }
// DiskUsage contains information about disk usage and is used when reporting Status of a device. // DiskUsage contains information about disk usage and is used when reporting Status of a device.
@ -1730,8 +1733,36 @@ func (devices *DeviceSet) initDevmapper(doInit bool) error {
return err return err
} }
// Set the device prefix from the device id and inode of the docker root dir prevSetupConfig, err := readLVMConfig(devices.root)
if err != nil {
return err
}
if !reflect.DeepEqual(devices.lvmSetupConfig, directLVMConfig{}) {
if devices.thinPoolDevice != "" {
return errors.New("cannot setup direct-lvm when `dm.thinpooldev` is also specified")
}
if !reflect.DeepEqual(prevSetupConfig, devices.lvmSetupConfig) {
if !reflect.DeepEqual(prevSetupConfig, directLVMConfig{}) {
return errors.New("changing direct-lvm config is not supported")
}
logrus.WithField("storage-driver", "devicemapper").WithField("direct-lvm-config", devices.lvmSetupConfig).Debugf("Setting up direct lvm mode")
if err := verifyBlockDevice(devices.lvmSetupConfig.Device, lvmSetupConfigForce); err != nil {
return err
}
if err := setupDirectLVM(devices.lvmSetupConfig); err != nil {
return err
}
if err := writeLVMConfig(devices.root, devices.lvmSetupConfig); err != nil {
return err
}
}
devices.thinPoolDevice = "docker-thinpool"
logrus.WithField("storage-driver", "devicemapper").Debugf("Setting dm.thinpooldev to %q", devices.thinPoolDevice)
}
// Set the device prefix from the device id and inode of the docker root dir
st, err := os.Stat(devices.root) st, err := os.Stat(devices.root)
if err != nil { if err != nil {
return fmt.Errorf("devmapper: Error looking up dir %s: %s", devices.root, err) return fmt.Errorf("devmapper: Error looking up dir %s: %s", devices.root, err)
@ -2605,6 +2636,7 @@ func NewDeviceSet(root string, doInit bool, options []string, uidMaps, gidMaps [
} }
foundBlkDiscard := false foundBlkDiscard := false
var lvmSetupConfig directLVMConfig
for _, option := range options { for _, option := range options {
key, val, err := parsers.ParseKeyValueOpt(option) key, val, err := parsers.ParseKeyValueOpt(option)
if err != nil { if err != nil {
@ -2699,11 +2731,60 @@ func NewDeviceSet(root string, doInit bool, options []string, uidMaps, gidMaps [
return nil, err return nil, err
} }
devices.xfsNospaceRetries = val devices.xfsNospaceRetries = val
case "dm.directlvm_device":
lvmSetupConfig.Device = val
case "dm.directlvm_device_force":
lvmSetupConfigForce, err = strconv.ParseBool(val)
if err != nil {
return nil, err
}
case "dm.thinp_percent":
per, err := strconv.ParseUint(strings.TrimSuffix(val, "%"), 10, 32)
if err != nil {
return nil, errors.Wrapf(err, "could not parse `dm.thinp_percent=%s`", val)
}
if per >= 100 {
return nil, errors.New("dm.thinp_percent must be greater than 0 and less than 100")
}
lvmSetupConfig.ThinpPercent = per
case "dm.thinp_metapercent":
per, err := strconv.ParseUint(strings.TrimSuffix(val, "%"), 10, 32)
if err != nil {
return nil, errors.Wrapf(err, "could not parse `dm.thinp_metapercent=%s`", val)
}
if per >= 100 {
return nil, errors.New("dm.thinp_metapercent must be greater than 0 and less than 100")
}
lvmSetupConfig.ThinpMetaPercent = per
case "dm.thinp_autoextend_percent":
per, err := strconv.ParseUint(strings.TrimSuffix(val, "%"), 10, 32)
if err != nil {
return nil, errors.Wrapf(err, "could not parse `dm.thinp_autoextend_percent=%s`", val)
}
if per > 100 {
return nil, errors.New("dm.thinp_autoextend_percent must be greater than 0 and less than 100")
}
lvmSetupConfig.AutoExtendPercent = per
case "dm.thinp_autoextend_threshold":
per, err := strconv.ParseUint(strings.TrimSuffix(val, "%"), 10, 32)
if err != nil {
return nil, errors.Wrapf(err, "could not parse `dm.thinp_autoextend_threshold=%s`", val)
}
if per > 100 {
return nil, errors.New("dm.thinp_autoextend_threshold must be greater than 0 and less than 100")
}
lvmSetupConfig.AutoExtendThreshold = per
default: default:
return nil, fmt.Errorf("devmapper: Unknown option %s\n", key) return nil, fmt.Errorf("devmapper: Unknown option %s\n", key)
} }
} }
if err := validateLVMConfig(lvmSetupConfig); err != nil {
return nil, err
}
devices.lvmSetupConfig = lvmSetupConfig
// By default, don't do blk discard hack on raw devices, its rarely useful and is expensive // By default, don't do blk discard hack on raw devices, its rarely useful and is expensive
if !foundBlkDiscard && (devices.dataDevice != "" || devices.thinPoolDevice != "") { if !foundBlkDiscard && (devices.dataDevice != "" || devices.thinPoolDevice != "") {
devices.doBlkDiscard = false devices.doBlkDiscard = false

View file

@ -322,6 +322,60 @@ not use loopback in production. Ensure your Engine daemon has a
$ sudo dockerd --storage-opt dm.thinpooldev=/dev/mapper/thin-pool $ sudo dockerd --storage-opt dm.thinpooldev=/dev/mapper/thin-pool
``` ```
##### `dm.directlvm_device`
As an alternative to providing a thin pool as above, Docker can setup a block
device for you.
###### Example:
```bash
$ sudo dockerd --storage-opt dm.directlvm_device=/dev/xvdf
```
##### `dm.thinp_percent`
Sets the percentage of passed in block device to use for storage.
###### Example:
```bash
$ sudo dockerd --storage-opt dm.thinp_percent=95
```
##### `dm.thinp_metapercent`
Sets the percentage of the passed in block device to use for metadata storage.
###### Example:
```bash
$ sudo dockerd --storage-opt dm.thinp_metapercent=1
```
##### `dm.thinp_autoextend_threshold`
Sets the value of the percentage of space used before `lvm` attempts to
autoextend the available space [100 = disabled]
###### Example:
```bash
$ sudo dockerd --storage-opt dm.thinp_autoextend_threshold=80
```
##### `dm.thinp_autoextend_percent`
Sets the value percentage value to increase the thin pool by when when `lvm`
attempts to autoextend the available space [100 = disabled]
###### Example:
```bash
$ sudo dockerd --storage-opt dm.thinp_autoextend_percent=20
```
##### `dm.basesize` ##### `dm.basesize`
Specifies the size to use when creating the base device, which limits the Specifies the size to use when creating the base device, which limits the

View file

@ -418,6 +418,54 @@ Example use:
$ dockerd \ $ dockerd \
--storage-opt dm.thinpooldev=/dev/mapper/thin-pool --storage-opt dm.thinpooldev=/dev/mapper/thin-pool
#### dm.directlvm_device
As an alternative to manually creating a thin pool as above, Docker can
automatically configure a block device for you.
Example use:
$ dockerd \
--storage-opt dm.directlvm_device=/dev/xvdf
##### dm.thinp_percent
Sets the percentage of passed in block device to use for storage.
###### Example:
$ sudo dockerd \
--storage-opt dm.thinp_percent=95
##### `dm.thinp_metapercent`
Sets the percentage of the passed in block device to use for metadata storage.
###### Example:
$ sudo dockerd \
--storage-opt dm.thinp_metapercent=1
##### dm.thinp_autoextend_threshold
Sets the value of the percentage of space used before `lvm` attempts to
autoextend the available space [100 = disabled]
###### Example:
$ sudo dockerd \
--storage-opt dm.thinp_autoextend_threshold=80
##### dm.thinp_autoextend_percent
Sets the value percentage value to increase the thin pool by when when `lvm`
attempts to autoextend the available space [100 = disabled]
###### Example:
$ sudo dockerd \
--storage-opt dm.thinp_autoextend_percent=20
#### dm.basesize #### dm.basesize
Specifies the size to use when creating the base device, which limits Specifies the size to use when creating the base device, which limits