Skip to content

Commit

Permalink
Add util::nofile_cap_limits() to manage maximum number of file descri…
Browse files Browse the repository at this point in the history
…ptors

The main purpose of this function is to disarm excessive memory use on systems
with very large limits for open file descriptors, where libkqueue currently
wastes gigabytes of memory by allocating state for every possible file
descriptor. This particularly triggers in Docker environments until libkqueue
gets fixed or containerd changes its default-uncapped fd limit behavior.

The function caps the maximum to a default of 1M open fds. The user can override
or disable the behavior via the ZEEK_NOFILE_MAX environment variable.

References:
mheily/libkqueue#153
moby/moby#38814
  • Loading branch information
ckreibich committed Nov 22, 2024
1 parent 12ba35b commit 1e7c7ad
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 0 deletions.
44 changes: 44 additions & 0 deletions src/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2578,6 +2578,50 @@ TEST_CASE("util approx_equal") {
*/
bool approx_equal(double a, double b, double tolerance) { return std::abs(a - b) < std::abs(tolerance); }

NofileUpdates nofile_cap_limits() {
struct rlimit rl;

if ( getrlimit(RLIMIT_NOFILE, &rl) < 0 ) {
// We likely don't yet have a reporter when running this.
fprintf(stderr, "nofile_cap_limits(): getrlimit failed, %s\n", strerror(errno));
exit(1);
}

rlim_t orig_cur = rl.rlim_cur;
rlim_t orig_max = rl.rlim_max;
rlim_t safe_max = 1024 * 1024;

const char* nofile_max_str = getenv("ZEEK_NOFILE_MAX");

if ( nofile_max_str ) {
char* end = nullptr;
unsigned long nofile_max = strtoul(nofile_max_str, &end, 10);

if ( nofile_max_str[0] != '\0' && (end == nofile_max_str || end[0] != '\0') ) {
fprintf(stderr, "ZEEK_NOFILE_MAX must be a non-negative integer\n");
exit(1);
}

safe_max = nofile_max;
}

if ( safe_max > 0 && safe_max < rl.rlim_max ) {
rl.rlim_max = safe_max;

if ( safe_max < rl.rlim_cur )
rl.rlim_cur = safe_max;

if ( setrlimit(RLIMIT_NOFILE, &rl) < 0 ) {
fprintf(stderr, "nofile_cap_limits(): setrlimit to %lu/$%lu failed, %s\n", rl.rlim_cur, rl.rlim_max,
strerror(errno));
exit(1);
}
}

return {orig_cur, orig_max, rl.rlim_cur, rl.rlim_max, nofile_max_str != nullptr};
}


} // namespace zeek::util

extern "C" void out_of_memory(const char* where) {
Expand Down
31 changes: 31 additions & 0 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,37 @@ int memory_size_align(size_t offset, size_t size);
// handed out by malloc.
extern void get_memory_usage(uint64_t* total, uint64_t* malloced);

// File descriptor limits.

struct NofileUpdates {
uint64_t orig_cur = 0;
uint64_t orig_max = 0;
uint64_t new_cur = 0;
uint64_t new_max = 0;

// Whether the ZEEK_NOFILE_MAX env variable affected fd limit adjustment:
bool user_configured = false;

// Predicate that indicates whether a limit adjustment occurred by default,
// without the user customizing via ZEEK_NOFILE_MAX.
bool did_default_adjustment() { return ! user_configured && (orig_cur > new_cur || orig_max > new_max); }
};

// Checks for an unreasonably large maximum allowable number of open file
// descriptors (as in "ulimit -n -H"), and caps that limit if excessive. The
// default "sane" limit is 1M (1024*1024) fds. If the currently effective limit
// (as in "ulimit -n") exceeds this limit, it too gets reduced.

// You can override the limit by setting the ZEEK_NOFILE_MAX environment
// variable to the desired number. Setting it to an empty string or 0 disables
// the capping mechanism.
//
// Exits Zeek with an error if this adjustment procedure fails.
//
// Returns a NofileUpdates struct summarizing the outcome.
NofileUpdates nofile_cap_limits();


// Class to be used as a third argument for STL maps to be able to use
// char*'s as keys. Otherwise the pointer values will be compared instead of
// the actual string values.
Expand Down

0 comments on commit 1e7c7ad

Please sign in to comment.