Skip to content

Commit 81bd8e5

Browse files
committed
Fix aide-worker memory growth caused by cgroup page cache accumulation
AIDE scans the entire host filesystem, and the resulting kernel page cache is charged to the container's cgroup. Without reclamation, reported memory grows toward the resource limit after each scan cycle. Use cgroup v2 memory.reclaim to evict file-backed page cache after each AIDE scan and database initialization. This reduced aide-worker memory from ~570 MiB to ~11 MiB in testing on OCP 4.18.22. Use raw syscalls (syscall.Open/Write/Close) for memory.reclaim instead of os.OpenFile, because Go's runtime registers fds with its epoll poller and the cgroup v2 file's poll support causes the goroutine to hang waiting for write-readiness that never arrives. Additional fixes: - Close leaked file descriptor in getNonEmptyFile when file is empty - Pre-compile regex patterns used in log parsing - Handle AlreadyExists on ConfigMap creation to avoid unnecessary retries - Call runtime.GC and debug.FreeOSMemory after scan to return heap to OS - Update outdated GODEBUG comment (madvdontneed=1 is default since Go 1.16)
1 parent c93c0e2 commit 81bd8e5

4 files changed

Lines changed: 110 additions & 6 deletions

File tree

cmd/manager/daemon_util.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616
package manager
1717

1818
import (
19+
"bufio"
1920
"context"
2021
"fmt"
2122
"io"
@@ -25,8 +26,11 @@ import (
2526
"os/exec"
2627
"path"
2728
"path/filepath"
29+
"runtime"
30+
"runtime/debug"
2831
"sort"
2932
"strings"
33+
"syscall"
3034
"time"
3135

3236
backoff "github.com/cenkalti/backoff/v4"
@@ -38,6 +42,70 @@ import (
3842
"github.com/openshift/file-integrity-operator/pkg/common"
3943
)
4044

45+
// reclaimCgroupPageCache asks the kernel to reclaim file-backed (page cache)
46+
// memory charged to this container's cgroup. AIDE scans the entire host
47+
// filesystem, and the resulting page cache pages are charged to the container's
48+
// cgroup, causing reported memory to grow toward the limit over scan cycles.
49+
//
50+
// We use raw syscalls instead of os.OpenFile because Go's runtime registers
51+
// opened fds with its epoll-based poller. The cgroup v2 memory.reclaim file
52+
// supports poll (via cgroup_file_poll), so Go treats it as a pollable fd and
53+
// waits for write-readiness before issuing the write. That readiness event
54+
// never arrives, hanging the goroutine permanently.
55+
func reclaimCgroupPageCache() {
56+
cgroupPath, err := getOwnCgroupPath()
57+
if err != nil {
58+
LOG("could not determine own cgroup path (page cache not reclaimed): %v", err)
59+
return
60+
}
61+
62+
reclaimFile := path.Join(cgroupPath, "memory.reclaim")
63+
fd, err := syscall.Open(reclaimFile, syscall.O_WRONLY, 0)
64+
if err != nil {
65+
LOG("memory.reclaim not available at %s (page cache not reclaimed): %v", reclaimFile, err)
66+
return
67+
}
68+
69+
_, err = syscall.Write(fd, []byte("500M"))
70+
closeErr := syscall.Close(fd)
71+
if err != nil && err != syscall.EAGAIN {
72+
LOG("memory.reclaim write returned (non-fatal): %v", err)
73+
}
74+
if closeErr != nil {
75+
LOG("memory.reclaim close error: %v", closeErr)
76+
}
77+
LOG("reclaimed cgroup page cache after AIDE scan")
78+
}
79+
80+
// getOwnCgroupPath reads /proc/self/cgroup (cgroup v2 unified format) and
81+
// returns the sysfs path for this process's cgroup.
82+
func getOwnCgroupPath() (string, error) {
83+
f, err := os.Open("/proc/self/cgroup")
84+
if err != nil {
85+
return "", err
86+
}
87+
defer f.Close()
88+
89+
scanner := bufio.NewScanner(f)
90+
for scanner.Scan() {
91+
line := scanner.Text()
92+
// cgroup v2: "0::<path>"
93+
parts := strings.SplitN(line, ":", 3)
94+
if len(parts) == 3 && parts[0] == "0" {
95+
return "/sys/fs/cgroup" + parts[2], nil
96+
}
97+
}
98+
return "", fmt.Errorf("no cgroup v2 entry found in /proc/self/cgroup")
99+
}
100+
101+
// releaseMemoryAfterScan forces the Go GC to run and returns freed memory to
102+
// the OS. Combined with reclaimCgroupPageCache, this minimizes the container's
103+
// memory footprint between scan cycles.
104+
func releaseMemoryAfterScan() {
105+
runtime.GC()
106+
debug.FreeOSMemory()
107+
}
108+
41109
func aideReadDBPath(c *daemonConfig) string {
42110
return path.Join(c.FileDir, aideReadDBFileName)
43111
}
@@ -345,6 +413,9 @@ func getNonEmptyFile(filename string) *os.File {
345413
return file
346414
}
347415

416+
if err := file.Close(); err != nil {
417+
LOG("warning: error closing empty/unreadable file %s: %v", cleanFileName, err)
418+
}
348419
return nil
349420
}
350421

cmd/manager/logcollector_util.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,19 @@ import (
3030
"github.com/spf13/cobra"
3131

3232
corev1 "k8s.io/api/core/v1"
33+
kerr "k8s.io/apimachinery/pkg/api/errors"
3334
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3435
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3536

3637
"github.com/openshift/file-integrity-operator/pkg/common"
3738
)
3839

40+
var (
41+
reFilesAdded = regexp.MustCompile(`\s+Added entries:\s+(?P<num_added>\d+)`)
42+
reFilesChanged = regexp.MustCompile(`\s+Changed entries:\s+(?P<num_changed>\d+)`)
43+
reFilesRemoved = regexp.MustCompile(`\s+Removed entries:\s+(?P<num_removed>\d+)`)
44+
)
45+
3946
const (
4047
crdGroup = "fileintegrity.openshift.io"
4148
crdAPIVersion = "v1alpha1"
@@ -59,8 +66,7 @@ func getValidStringArg(cmd *cobra.Command, name string) string {
5966
return val
6067
}
6168

62-
func matchFileChangeRegex(contents string, regex string) string {
63-
re := regexp.MustCompile(regex)
69+
func matchFileChangeRegex(contents string, re *regexp.Regexp) string {
6470
match := re.FindStringSubmatch(contents)
6571
if len(match) < 2 {
6672
return "0"
@@ -70,9 +76,9 @@ func matchFileChangeRegex(contents string, regex string) string {
7076
}
7177

7278
func annotateFileChangeSummary(contents string, annotations map[string]string) {
73-
annotations[common.IntegrityLogFilesAddedAnnotation] = matchFileChangeRegex(contents, `\s+Added entries:\s+(?P<num_added>\d+)`)
74-
annotations[common.IntegrityLogFilesChangedAnnotation] = matchFileChangeRegex(contents, `\s+Changed entries:\s+(?P<num_changed>\d+)`)
75-
annotations[common.IntegrityLogFilesRemovedAnnotation] = matchFileChangeRegex(contents, `\s+Removed entries:\s+(?P<num_removed>\d+)`)
79+
annotations[common.IntegrityLogFilesAddedAnnotation] = matchFileChangeRegex(contents, reFilesAdded)
80+
annotations[common.IntegrityLogFilesChangedAnnotation] = matchFileChangeRegex(contents, reFilesChanged)
81+
annotations[common.IntegrityLogFilesRemovedAnnotation] = matchFileChangeRegex(contents, reFilesRemoved)
7682
DBG("added %s changed %s removed %s",
7783
annotations[common.IntegrityLogFilesAddedAnnotation],
7884
annotations[common.IntegrityLogFilesChangedAnnotation],
@@ -209,6 +215,12 @@ func reportOK(ctx context.Context, conf *daemonConfig, rt *daemonRuntime) error
209215
fi := rt.GetFileIntegrityInstance()
210216
confMap := newInformationalConfigMap(fi, conf.LogCollectorConfigMapName, conf.LogCollectorNode, nil)
211217
_, err := rt.clientset.CoreV1().ConfigMaps(conf.Namespace).Create(ctx, confMap, metav1.CreateOptions{})
218+
if kerr.IsAlreadyExists(err) {
219+
if delErr := rt.clientset.CoreV1().ConfigMaps(conf.Namespace).Delete(ctx, confMap.Name, metav1.DeleteOptions{}); delErr != nil {
220+
return delErr
221+
}
222+
_, err = rt.clientset.CoreV1().ConfigMaps(conf.Namespace).Create(ctx, confMap, metav1.CreateOptions{})
223+
}
212224
return err
213225
}, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries))
214226
}
@@ -222,6 +234,12 @@ func reportError(ctx context.Context, msg string, conf *daemonConfig, rt *daemon
222234
}
223235
confMap := newInformationalConfigMap(fi, conf.LogCollectorConfigMapName, conf.LogCollectorNode, annotations)
224236
_, err := rt.clientset.CoreV1().ConfigMaps(conf.Namespace).Create(ctx, confMap, metav1.CreateOptions{})
237+
if kerr.IsAlreadyExists(err) {
238+
if delErr := rt.clientset.CoreV1().ConfigMaps(conf.Namespace).Delete(ctx, confMap.Name, metav1.DeleteOptions{}); delErr != nil {
239+
return delErr
240+
}
241+
_, err = rt.clientset.CoreV1().ConfigMaps(conf.Namespace).Create(ctx, confMap, metav1.CreateOptions{})
242+
}
225243
return err
226244
}, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries))
227245
}
@@ -232,6 +250,12 @@ func uploadLog(ctx context.Context, contents, compressedContents []byte, conf *d
232250
fi := rt.GetFileIntegrityInstance()
233251
confMap := newLogConfigMap(fi, conf.LogCollectorConfigMapName, common.IntegrityLogContentKey, conf.LogCollectorNode, contents, compressedContents)
234252
_, err := rt.clientset.CoreV1().ConfigMaps(conf.Namespace).Create(ctx, confMap, metav1.CreateOptions{})
253+
if kerr.IsAlreadyExists(err) {
254+
if delErr := rt.clientset.CoreV1().ConfigMaps(conf.Namespace).Delete(ctx, confMap.Name, metav1.DeleteOptions{}); delErr != nil {
255+
return delErr
256+
}
257+
_, err = rt.clientset.CoreV1().ConfigMaps(conf.Namespace).Create(ctx, confMap, metav1.CreateOptions{})
258+
}
235259
return err
236260
}, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries))
237261
}

cmd/manager/loops.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ func aideLoop(ctx context.Context, rt *daemonRuntime, conf *daemonConfig, errCha
7272
// All done. Send the result.
7373
rt.SendResult(aideResult)
7474
rt.UnlockAideFiles("aideLoop")
75+
76+
// AIDE reads the entire host filesystem, and the resulting page
77+
// cache is charged to this container's cgroup. Reclaim it so
78+
// reported memory drops back to the daemon's actual working set.
79+
reclaimCgroupPageCache()
80+
releaseMemoryAfterScan()
7581
}
7682
time.Sleep(time.Second * time.Duration(conf.Interval))
7783
}
@@ -215,6 +221,8 @@ func handleAIDEInit(ctx context.Context, rt *daemonRuntime, conf *daemonConfig,
215221
}
216222

217223
LOG("initialization finished")
224+
reclaimCgroupPageCache()
225+
releaseMemoryAfterScan()
218226
return nil
219227
}
220228

pkg/controller/fileintegrity/fileintegrity_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,8 @@ func aideDaemonset(dsName string, fi *v1alpha1.FileIntegrity, operatorImage stri
909909
},
910910
},
911911
{
912-
// Needed for friendlier memory reporting as long as we are on golang < 1.16
912+
// MADV_DONTNEED is already the default on Go >= 1.16. Kept for
913+
// documentation; harmless no-op on current toolchain (Go 1.22).
913914
Name: "GODEBUG",
914915
Value: "madvdontneed=1",
915916
},

0 commit comments

Comments
 (0)