Files
nomad/client/lib/proclib/wrangler.go
Seth Hoenig 2e1974a574 client: refactor cpuset partitioning (#18371)
* client: refactor cpuset partitioning

This PR updates the way Nomad client manages the split between tasks
that make use of resources.cpus vs. resources.cores.

Previously, each task was explicitly assigned which CPU cores they were
able to run on. Every time a task was started or destroyed, all other
tasks' cpusets would need to be updated. This was inefficient and would
crush the Linux kernel when a client would try to run ~400 or so tasks.

Now, we make use of cgroup heirarchy and cpuset inheritence to efficiently
manage cpusets.

* cr: tweaks for feedback
2023-09-12 09:11:11 -05:00

90 lines
2.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package proclib
import (
"fmt"
"sync"
)
// Task records the unique coordinates of a task from the perspective of a Nomad
// client running the task, that is to say (alloc_id, task_name). Also indicates
// whether the task is making use of reserved cpu cores.
type Task struct {
AllocID string
Task string
Cores bool
}
func (task Task) String() string {
return fmt.Sprintf("%s/%s", task.AllocID[0:8], task.Task)
}
type create func(Task) ProcessWrangler
// Wranglers keeps track of the ProcessWrangler created for each task. Some
// operating systems may implement ProcessWranglers to ensure that all of the
// processes created by a Task are killed, going a step beyond trusting the
// task drivers to properly clean things up. (Well, on Linux anyway.)
//
// This state must be restored on Client agent startup.
type Wranglers struct {
configs *Configs
create create
lock sync.Mutex
m map[Task]ProcessWrangler
}
// Setup any process management technique relevant to the operating system and
// its particular configuration.
func (w *Wranglers) Setup(task Task) error {
w.configs.Logger.Trace("setup client process management", "task", task)
// create process wrangler for task
pw := w.create(task)
// perform any initialization if necessary (e.g. create cgroup)
// if this doesn't work just keep going; it's up to each task driver
// implementation to decide if this is a failure mode
_ = pw.Initialize()
w.lock.Lock()
defer w.lock.Unlock()
// keep track of the process wrangler for task
w.m[task] = pw
return nil
}
// Destroy any processes still running that were spawned by task. Ideally the
// task driver should be implemented well enough for this to not be necessary,
// but we protect the Client as best we can regardless.
//
// Note that this is called from a TR.Stop which must be idempotent.
func (w *Wranglers) Destroy(task Task) error {
w.configs.Logger.Trace("destroy and cleanup remnant task processes", "task", task)
w.lock.Lock()
defer w.lock.Unlock()
if pw, exists := w.m[task]; exists {
pw.Kill()
pw.Cleanup()
delete(w.m, task)
}
return nil
}
// A ProcessWrangler "owns" a particular Task on a client, enabling the client
// to kill and cleanup processes created by that Task, without help from the
// task driver. Currently we have implementations only for Linux (via cgroups).
type ProcessWrangler interface {
Initialize() error
Kill() error
Cleanup() error
}