Prekill Hooks

April 26, 2021 ยท View on GitHub

Prekill hooks are an experimental generic, pluggable way to do work just before oomd kills a cgroup.

Background

Owners of an oomed process may want a heap dump or other memory statistics of the killed program at the time it died to get insight into potential misbehavior.

Prekill hooks direct oomd to collect these metrics, or do other arbitrary work, just before it kills a cgroup. It is a generic interface not tied to any particular metric collection approach or, specifically metric collection at all.

Hooks may timeout, and should not be assumed to run to completion. Process owners should know the kernel may oom kill their code separately from oomd, in which case prekill hooks will obviously not run at all.

Configuration

Prekill hooks are configured the oomd.json config json in a top-level "prekill_hooks" key, adjacent to "rulesets".

Prekill hooks are at the top level because they run on every kill oomd makes, across all rulesets.

Prekill hooks are not interchangeable with plugins but are configured in the same way, via "name" and "args". Hooks can't be used where plugins are expected, and vice versa.

{ "rulesets": [ ... ], "prekill_hooks": [ { "name": "hypothetical_prekill_hook", "args": { "cgroup": "/foo,/bar/*/baz" } } ] }

On a kill, the oomd runs the first configured prekill hook whose "cgroup" arg matches the path of the cgroup to be killed. At most one prekill hook runs per kill.

Dropins may contain prekill_hooks. Dropped-in prekill hooks get priority over those in the base configuration. Like ruleset dropins, prekill hook dropins added later get higher priority.

The "cgroup" arg is a list of comma-separated patterns. Patterns are cgroup paths, except path components may be "*". No other glob matching works except star for a single whole path component.

A cgroup path matches a pattern if it 1) exactly matches the pattern, 2) is an ancestor of a path that would match the pattern, or 3) is a descendant of a path that matches the pattern.

To run on all kills, set "cgroup": "/".

Rulesets may set a "prekill_hook_timeout" in seconds. If unset, the default is 5 seconds.

{ "rulesets": [ { "name": "memory pressure protection", "prekill_hook_timeout": "30", "detectors": [...], "actions": [...] ], "prekill_hooks": [...] }

The prekill hook timeout sets a window for all prekill hooks in an action chain to finish running. For example, consider:

  • a ruleset with two kill plugin actions and a 5s prekill hook timeout
  • the action chain fires
  • the first action targets /foo.slice and fires a prekill hook on it
  • the prekill hook finishes in 3s
  • /foo.slice fails to die, so the first action returns CONTINUE
  • the second kill plugin runs, targets /bar.slice, and fires a prekill hook

The second prekill hook only has 2s to run before it times out, since it's been 3s (or more) since the action chain started, and the action chain set a 5s max window for prekill hooks to run.

API

Prekill hook implementers should subclass PrekillHook and PrekillHookInvocation and implement these core methods:

  /* same as BasePlugin::init(args, context) */
  int PrekillHook::init(
      const Engine::PluginArgs& args,
      const PluginConstructionContext& context);

  /* main method for a hook, called just before the cgroup is killed */
  std::unique_ptr<PrekillHookInvocation> PrekillHook::fire(
        const CgroupContext&);

  /* Invocation object returned from fire() is polled to see when the hook
     has finished running, and killing may begin */
  bool PrekillHookInvocation::didFinish()

  /* Invocation object is destructed either when it finishes, or early
     if it times out */
  PrekillHookInvocation::~PrekillHookInvocation()

Hooks are kicked off with PrekillHook::fire(cgroup) with the cgroup oomd intends to kill.

Oomd is designed as a single threaded event loop, so fire() shouldn't do long work that blocks the main thread. Instead, it vends an Invocation object which will be polled every main loop tick (typically 1s) for didFinish(). The cgroup will not be killed until didFinish() returns true, or we reach a timeout.

If oomd determines a PrekillHookInvocation timed out, it is destructed and PrekillHookInvocation::~PrekillHookInvocation() called. The destructor will be called before the cgroup is killed, regardless of whether the hook timed out or didFinish() returned true.

All methods (fire, didFinish, ~PrekillHookInvocation) will be always be called on the main thread and should not block for nontrivial time. If blocking work is needed, it should be done in other threads, possibly spawned in PrekillHook::init().

Guarantees

  • At most one prekill hook will be running per ruleset at any moment. There may be multiple instances of a prekill hook running at the same time, as part of different rulesets.
  • If a prekill hook is run on a cgroup, the cgroup is not guaranteed to die. Oomd may fail to kill it. (Oomd will then pick a different cgroup to try to kill, and again call the prekill hook on its new target before trying to kill it.)
  • PrekillHooks are not guaranteed to outlive the Invocations they fire(). Invocations should encapsulate any data they need to run to completion.