From 55a8d686a98934f02ae264d021c93d9327375b5b Mon Sep 17 00:00:00 2001 From: Ido Yariv Date: Tue, 8 Dec 2015 10:33:47 -0500 Subject: [PATCH] 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 --- libcontainer/container_linux.go | 10 ++++++ libcontainer/notify_linux.go | 54 ++++++++++++++++++++-------- libcontainer/notify_linux_test.go | 58 ++++++++++++++++++++++++------- 3 files changed, 95 insertions(+), 27 deletions(-) diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index de98e97c..a895b154 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -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 != "" { diff --git a/libcontainer/notify_linux.go b/libcontainer/notify_linux.go index cf81e24d..839a50c5 100644 --- a/libcontainer/notify_linux.go +++ b/libcontainer/notify_linux.go @@ -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) +} diff --git a/libcontainer/notify_linux_test.go b/libcontainer/notify_linux_test.go index 09bdf644..9aa4f3b3 100644 --- a/libcontainer/notify_linux_test.go +++ b/libcontainer/notify_linux_test.go @@ -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) + } +}