10BC0 devmapper cleanup improvements by kolyshkin · Pull Request #36307 · moby/moby · GitHub
[go: up one dir, main page]

Skip to content

Conversation

kolyshkin
Copy link
Contributor
@kolyshkin kolyshkin commented Feb 14, 2018

devmapper.shutdown: optimize

Move the "unmount and deactivate" code into a separate method, and optimize it a bit:

  1. Do not use filepath.Walk() as there's no requirement to recursively go into every directory under home/mnt; a list of directories in mnt is sufficient. With filepath.Walk(), in case some container failed to unmount, it'll go through the whole container filesystem which is excessive and useless.

  2. Do not use GetMounts() and check if a directory is mounted; just unmount it and ignore "not mounted" error. Note the same error is returned in case of wrong flags set, but as flags are hardcoded we can safely ignore such cases.

While at it, promote "can't unmount" message log level from debug to warning.

devmapper cleanup: improve error msg

  1. Make sure it's clear the error is from unmount.

  2. Simplify the code a bit to make it more readable.

Copy link
Member
@tonistiigi tonistiigi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: errors.Wrapf

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right; patch updated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this error ignored?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only errors returned are from os.Open() (other than ENOENT) or Readdir(), meaning it failed to scan through the entries in the directory (and unmount and deactivate those). Given that this is a shutdown procedure, called when the daemon is exiting, I think in this situation it makes sense to proceed with the rest of the code in Shutdown() (i.e. deactivate the base device and the pool).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems this function shouldn't return an error then if desired behavior is to always ignore them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes total sense, I overlooked it. Please see the updated commit

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errors.Wrapf(umountErr, "error unmounting %s", d.home)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is that better?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errors are supposed to be lowercase for future wrapping in the caller

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought you were asking to change using Wrap and string concatenation to Wrapf and %s format, and overlooked the lowercase part.

Patch updated

@tonistiigi
Copy link
Member

SGTM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only problem with this is, if there are many containers we will be making lots of potentially unneeded system calls.
I think it's still better to check the mount table to see if it's a mountpoint before attempting an unmount.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the /proc/self/mountinfo also involves system calls, at least three: open(), read(), close(). More importantly, it also involves the kernel going through internal structures (possibly with some locking) to get various bits of information about all the mounts, which is definitely a costly operation -- especially in the case of many containers, so the mountinfo will have many entries.

Having said that, we only perform the above thing once and when we repeat a relatively quick (albeit linear) search for the string in a relatively short array. It might be faster that way, but since we now remove the mount point upon unmounting, the chance that a given directory is a mount point is close to 100%.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good point, there shouldn't be many dirs here.

@kolyshkin
Copy link
Contributor Author

I ran some (very non-scientific) benchmarks on my laptop, here are the results:

wc -l /proc/self/mountinfo unix.Unmount() parse mountinfo linear search in mountinfo list quick search in mountinfo list
35 153 38855 43 35
76 153 150407 93 43
148 153 256400 167 47

Notes:

  1. The times in columns 2...5 are in nanoseconds.
  2. For column 3, a very simple/fast parser is used (see the code below).
  3. For the last two columns, only the time to search within a list is measured, i.e. preparing the list is not included.
  4. About 30 dummy containers had to be started to achieve number 150 in the first column.

Observations:

  1. Parsing /proc/self/mountinfo takes 103 to 104 times more than calling unix.Umount(). Note I am using an extremely trivial parser implementation; the one that is used currently is much slower.
  2. The code we currently use (linear search in mountinfo table) becomes slower than calling unix.Unmount() when the size of /proc/self/mountinfo grows to about 150 entries. This is not taking into account the time to parse /proc/self/mountinfo.
  3. Calling umount() appears to be O(1) or close to it.
  4. Reading /proc/self/mountinfo and linear search in the list is O(n).

Conclusions:

  1. It only makes sense to parse /proc/self/mountinfo if it would help to avoid calling umount more than 103... 104 times.
  2. Calling umount straight ahead is the only approach that scales well.

Testing code:

package main

import (
	"bufio"
	"os"
	"sort"
	"strings"
	"testing"

	"golang.org/x/sys/unix"
)

func BenchmarkUmount(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = unix.Unmount("nonexistent/path", unix.MNT_DETACH)
	}
}

func BenchmarkUmountCheckLinear(b *testing.B) {
	const mnt = "nonexistent/path"
	mounts, err := parseMountinfo()
	if err != nil {
		b.Fatal(err)
	}
	for i := 0; i < b.N; i++ {
		for _, m := range mounts {
			if m == mnt {
				unix.Unmount(mnt, unix.MNT_DETACH)
				break
			}
		}
	}
}

func BenchmarkUmountCheckOptimized(b *testing.B) {
	const mnt = "nonexistent/path"
	mounts, err := parseMountinfo()
	if err != nil {
		b.Fatal(err)
	}
	sort.Strings(mounts)
	for i := 0; i < b.N; i++ {
		pos := sort.SearchStrings(mounts, mnt)
		if pos < len(mounts) && mounts[pos] == mnt {
			unix.Unmount(mnt, unix.MNT_DETACH)
		}
	}
}

func parseMountinfo() (mounts []string, err error) {
	f, err := os.Open("/proc/self/mountinfo")
	if err != nil {
		return mounts, err
	}
	s := bufio.NewScanner(f)
	for s.Scan() {
		if err := s.Err(); err != nil {
			return mounts, err
		}
		text := s.Text()
		fields := strings.SplitN(text, " ", 6)
		mounts = append(mounts, fields[4])
	}
	f.Close()
	return mounts, nil
}

func BenchmarkReadMountinfo(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_, err := parseMountinfo()
		if err != nil {
			b.Fatal(err)
		}
	}
}

@codecov
Copy link
codecov bot commented Feb 19, 2018

Codecov Report

❗ No coverage uploaded for pull request base (master@8830ef8). Click here to learn what that means.
The diff coverage is 33.33%.

@@            Coverage Diff            @@
##             master   #36307   +/-   ##
=========================================
  Coverage          ?   34.64%           
=========================================
  Files             ?      613           
  Lines             ?    45387           
  Branches          ?        0           
=========================================
  Hits              ?    15723           
  Misses            ?    27601           
  Partials          ?     2063

Copy link
Member
@cpuguy83 cpuguy83 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@thaJeztah
Copy link
Member

@kolyshkin looks like this needs a rebase

@tonistiigi LGTY?

1. Make sure it's clear the error is from unmount.

2. Simplify the code a bit to make it more readable.

[v2: use errors.Wrap]
[v3: use errors.Wrapf]
[v4: lowercase the error message]

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
Move the "unmount and deactivate" code into a separate method, and
optimize it a bit:

1. Do not use filepath.Walk() as there's no requirement to recursively
   go into every directory under home/mnt; a list of directories in mnt
   is sufficient. With filepath.Walk(), in case some container will fail
   to unmount, it'll go through the whole container filesystem which is
   excessive and useless.

2. Do not use GetMounts() and check if a directory is mounted; just
   unmount it and ignore "not mounted" error. Note the same error
   is returned in case of wrong flags set, but as flags are hardcoded
   we can safely ignore such case.

While at it, promote "can't unmount" log level from debug to warning.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
@kolyshkin
Copy link
Contributor Author

@tonistiigi PTAL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants
0