libcontainer: Add support for memcg pressure notifications

It may be desirable to receive memory pressure levels notifications
before the container depletes all memory. This may be useful for
handling cases where the system thrashes when reaching the container's
memory limits.

Signed-off-by: Ido Yariv <ido@wizery.com>
This commit is contained in:
Ido Yariv 2015-12-08 10:33:47 -05:00
parent d97d5e8b00
commit 55a8d686a9
3 changed files with 95 additions and 27 deletions

View File

@ -103,6 +103,12 @@ type Container interface {
// errors:
// Systemerror - System error.
NotifyOOM() (<-chan struct{}, error)
// NotifyMemoryPressure returns a read-only channel signaling when the container reaches a given pressure level
//
// errors:
// Systemerror - System error.
NotifyMemoryPressure(level PressureLevel) (<-chan struct{}, error)
}
// ID returns the container's unique ID
@ -368,6 +374,10 @@ func (c *linuxContainer) NotifyOOM() (<-chan struct{}, error) {
return notifyOnOOM(c.cgroupManager.GetPaths())
}
func (c *linuxContainer) NotifyMemoryPressure(level PressureLevel) (<-chan struct{}, error) {
return notifyMemoryPressure(c.cgroupManager.GetPaths(), level)
}
// XXX debug support, remove when debugging done.
func addArgsFromEnv(evar string, args *[]string) {
if e := os.Getenv(evar); e != "" {

View File

@ -12,31 +12,32 @@ import (
const oomCgroupName = "memory"
// notifyOnOOM returns channel on which you can expect event about OOM,
// if process died without OOM this channel will be closed.
// s is current *libcontainer.State for container.
func notifyOnOOM(paths map[string]string) (<-chan struct{}, error) {
dir := paths[oomCgroupName]
if dir == "" {
return nil, fmt.Errorf("There is no path for %q in state", oomCgroupName)
}
oomControl, err := os.Open(filepath.Join(dir, "memory.oom_control"))
type PressureLevel uint
const (
LowPressure PressureLevel = iota
MediumPressure
CriticalPressure
)
func registerMemoryEvent(cgDir string, evName string, arg string) (<-chan struct{}, error) {
evFile, err := os.Open(filepath.Join(cgDir, evName))
if err != nil {
return nil, err
}
fd, _, syserr := syscall.RawSyscall(syscall.SYS_EVENTFD2, 0, syscall.FD_CLOEXEC, 0)
if syserr != 0 {
oomControl.Close()
evFile.Close()
return nil, syserr
}
eventfd := os.NewFile(fd, "eventfd")
eventControlPath := filepath.Join(dir, "cgroup.event_control")
data := fmt.Sprintf("%d %d", eventfd.Fd(), oomControl.Fd())
eventControlPath := filepath.Join(cgDir, "cgroup.event_control")
data := fmt.Sprintf("%d %d %s", eventfd.Fd(), evFile.Fd(), arg)
if err := ioutil.WriteFile(eventControlPath, []byte(data), 0700); err != nil {
eventfd.Close()
oomControl.Close()
evFile.Close()
return nil, err
}
ch := make(chan struct{})
@ -44,7 +45,7 @@ func notifyOnOOM(paths map[string]string) (<-chan struct{}, error) {
defer func() {
close(ch)
eventfd.Close()
oomControl.Close()
evFile.Close()
}()
buf := make([]byte, 8)
for {
@ -61,3 +62,28 @@ func notifyOnOOM(paths map[string]string) (<-chan struct{}, error) {
}()
return ch, nil
}
// notifyOnOOM returns channel on which you can expect event about OOM,
// if process died without OOM this channel will be closed.
func notifyOnOOM(paths map[string]string) (<-chan struct{}, error) {
dir := paths[oomCgroupName]
if dir == "" {
return nil, fmt.Errorf("path %q missing", oomCgroupName)
}
return registerMemoryEvent(dir, "memory.oom_control", "")
}
func notifyMemoryPressure(paths map[string]string, level PressureLevel) (<-chan struct{}, error) {
dir := paths[oomCgroupName]
if dir == "" {
return nil, fmt.Errorf("path %q missing", oomCgroupName)
}
if level > CriticalPressure {
return nil, fmt.Errorf("invalid pressure level %d", level)
}
levelStr := []string{"low", "medium", "critical"}[level]
return registerMemoryEvent(dir, "memory.pressure_level", levelStr)
}

View File

@ -13,24 +13,25 @@ import (
"time"
)
func TestNotifyOnOOM(t *testing.T) {
memoryPath, err := ioutil.TempDir("", "testnotifyoom-")
type notifyFunc func(paths map[string]string) (<-chan struct{}, error)
func testMemoryNotification(t *testing.T, evName string, notify notifyFunc, targ string) {
memoryPath, err := ioutil.TempDir("", "testmemnotification-"+evName)
if err != nil {
t.Fatal(err)
}
oomPath := filepath.Join(memoryPath, "memory.oom_control")
evFile := filepath.Join(memoryPath, evName)
eventPath := filepath.Join(memoryPath, "cgroup.event_control")
if err := ioutil.WriteFile(oomPath, []byte{}, 0700); err != nil {
if err := ioutil.WriteFile(evFile, []byte{}, 0700); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(eventPath, []byte{}, 0700); err != nil {
t.Fatal(err)
}
var eventFd, oomControlFd int
paths := map[string]string{
"memory": memoryPath,
}
ooms, err := notifyOnOOM(paths)
ch, err := notify(paths)
if err != nil {
t.Fatal("expected no error, got:", err)
}
@ -40,7 +41,14 @@ func TestNotifyOnOOM(t *testing.T) {
t.Fatal("couldn't read event control file:", err)
}
if _, err := fmt.Sscanf(string(data), "%d %d", &eventFd, &oomControlFd); err != nil {
var eventFd, evFd int
var arg string
if targ != "" {
_, err = fmt.Sscanf(string(data), "%d %d %s", &eventFd, &evFd, &arg)
} else {
_, err = fmt.Sscanf(string(data), "%d %d", &eventFd, &evFd)
}
if err != nil || arg != targ {
t.Fatalf("invalid control data %q: %s", data, err)
}
@ -63,9 +71,9 @@ func TestNotifyOnOOM(t *testing.T) {
}
select {
case <-ooms:
case <-ch:
case <-time.After(100 * time.Millisecond):
t.Fatal("no notification on oom channel after 100ms")
t.Fatal("no notification on channel after 100ms")
}
// simulate what happens when a cgroup is destroyed by cleaning up and then
@ -79,18 +87,42 @@ func TestNotifyOnOOM(t *testing.T) {
// give things a moment to shut down
select {
case _, ok := <-ooms:
case _, ok := <-ch:
if ok {
t.Fatal("expected no oom to be triggered")
t.Fatal("expected no notification to be triggered")
}
case <-time.After(100 * time.Millisecond):
}
if _, _, err := syscall.Syscall(syscall.SYS_FCNTL, uintptr(oomControlFd), syscall.F_GETFD, 0); err != syscall.EBADF {
t.Error("expected oom control to be closed")
if _, _, err := syscall.Syscall(syscall.SYS_FCNTL, uintptr(evFd), syscall.F_GETFD, 0); err != syscall.EBADF {
t.Error("expected event control to be closed")
}
if _, _, err := syscall.Syscall(syscall.SYS_FCNTL, uintptr(eventFd), syscall.F_GETFD, 0); err != syscall.EBADF {
t.Error("expected event fd to be closed")
}
}
func TestNotifyOnOOM(t *testing.T) {
f := func(paths map[string]string) (<-chan struct{}, error) {
return notifyOnOOM(paths)
}
testMemoryNotification(t, "memory.oom_control", f, "")
}
func TestNotifyMemoryPressure(t *testing.T) {
tests := map[PressureLevel]string{
LowPressure: "low",
MediumPressure: "medium",
CriticalPressure: "critical",
}
for level, arg := range tests {
f := func(paths map[string]string) (<-chan struct{}, error) {
return notifyMemoryPressure(paths, level)
}
testMemoryNotification(t, "memory.pressure_level", f, arg)
}
}