cvmts: Only target QEMU vCPU threads by default

Previous behaviour was to limit the whole QEMU process; this only limits the vCPU threads. A bit (very) hacky how I did this but it does work.
This commit is contained in:
modeco80
2024-11-02 07:46:59 -04:00
parent 4344b233bc
commit 86f1345a2d
4 changed files with 47 additions and 9 deletions

View File

@@ -61,6 +61,9 @@ snapshots = true
# #
# cpuUsageMax specifies CPU usage limits in the common top notation, so 200% means 2 CPUs, so on so forth. # cpuUsageMax specifies CPU usage limits in the common top notation, so 200% means 2 CPUs, so on so forth.
# runOnCpus specifies what CPUs the VM is allowed to run on. # runOnCpus specifies what CPUs the VM is allowed to run on.
# limitProcess is optional (default false) and determines if only qemu vCPU threads are put into the cgroup,
# or the entire QEMU process (incl. all its threads). This is rarely what you want, so the example does not
# provide it.
# #
# Either can be omitted or specified; however, if you want to disable it entirely, # Either can be omitted or specified; however, if you want to disable it entirely,
# it's a better idea to just comment this inline table out, # it's a better idea to just comment this inline table out,

View File

@@ -29,6 +29,7 @@ export class CGroup {
// Configure this "root" cgroup to provide cpu and cpuset controllers to the leaf // Configure this "root" cgroup to provide cpu and cpuset controllers to the leaf
// QEMU cgroups. A bit iffy but whatever. // QEMU cgroups. A bit iffy but whatever.
writeFileSync(path.join(this.path, 'cgroup.subtree_control'), '+cpu +cpuset'); writeFileSync(path.join(this.path, 'cgroup.subtree_control'), '+cpu +cpuset');
//writeFileSync(path.join(this.path, 'cgroup.subtree_control'), '+cpu');
} }
GetController(controller: string) { GetController(controller: string) {
@@ -63,6 +64,11 @@ export class CGroup {
appendFileSync(path.join(this.path, 'cgroup.procs'), pid.toString()); appendFileSync(path.join(this.path, 'cgroup.procs'), pid.toString());
} }
// Attaches a thread to this cgroup. (The CGroup is a threaded one. See above)
AttachThread(tid: number) {
appendFileSync(path.join(this.path, 'cgroup.threads'), tid.toString());
}
// Returns a CGroup instance for the process' current cgroup, prepared for subgroup usage. // Returns a CGroup instance for the process' current cgroup, prepared for subgroup usage.
// This will only fail if you are not using systemd or elogind, // This will only fail if you are not using systemd or elogind,
// since even logind user sessions are run inside of a user@[UID] slice. // since even logind user sessions are run inside of a user@[UID] slice.

View File

@@ -6,29 +6,31 @@ import { VncDisplay } from '../display/vnc.js';
import pino from 'pino'; import pino from 'pino';
import { CgroupLimits, QemuResourceLimitedLauncher } from './qemu_launcher.js'; import { CgroupLimits, QemuResourceLimitedLauncher } from './qemu_launcher.js';
// shim over superqemu because it diverges from the VM interface // shim over superqemu because it diverges from the VM interface
export class QemuVMShim implements VM { export class QemuVMShim implements VM {
private vm; private vm;
private display: VncDisplay | null = null; private display: VncDisplay | null = null;
private logger; private logger;
private cg_launcher: QemuResourceLimitedLauncher | null = null;
private resource_limits: CgroupLimits | null = null;
constructor(def: QemuVmDefinition, resourceLimits?: CgroupLimits) { constructor(def: QemuVmDefinition, resourceLimits?: CgroupLimits) {
this.logger = pino({ name: `CVMTS.QemuVMShim/${def.id}` }); this.logger = pino({ name: `CVMTS.QemuVMShim/${def.id}` });
if (resourceLimits) { if (resourceLimits) {
if (process.platform == 'linux') { if (process.platform == 'linux') {
this.vm = new QemuVM(def, new QemuResourceLimitedLauncher(def.id, resourceLimits)); this.resource_limits = resourceLimits;
this.cg_launcher = new QemuResourceLimitedLauncher(def.id, resourceLimits);
this.vm = new QemuVM(def, this.cg_launcher);
} else { } else {
// Just use the default Superqemu launcher on non-Linux platforms, // Just use the default Superqemu launcher on non-Linux platforms,
// .. regardless of if resource control is (somehow) enabled. // .. regardless of if resource control is (somehow) enabled.
this.logger.warn({platform: process.platform}, 'Resource control is not supported on this platform. Please remove or comment it out from your configuration.'); this.logger.warn({ platform: process.platform }, 'Resource control is not supported on this platform. Please remove or comment it out from your configuration.');
this.vm = new QemuVM(def); this.vm = new QemuVM(def);
} }
} else { } else {
this.vm = new QemuVM(def); this.vm = new QemuVM(def);
} }
} }
Start(): Promise<void> { Start(): Promise<void> {
@@ -54,7 +56,24 @@ export class QemuVMShim implements VM {
return this.vm.MonitorCommand(command); return this.vm.MonitorCommand(command);
} }
async PlaceVCPUThreadsIntoCGroup() {
if (this.cg_launcher) {
if (!this.resource_limits?.limitProcess) {
// Get all vCPUs and pin them to the CGroup.
let cpu_res = await this.vm.QmpCommand('query-cpus-fast', {});
for (let cpu of cpu_res) {
this.logger.info(`Placing vCPU thread with TID ${cpu['thread-id']} to cgroup`);
this.cg_launcher.group.AttachThread(cpu['thread-id']);
}
}
}
}
StartDisplay(): void { StartDisplay(): void {
// HACK: We should probably use another subscribed eventemitter for this. For now,
// this "works". I guess.
this.PlaceVCPUThreadsIntoCGroup();
// boot it up // boot it up
let info = this.vm.GetDisplayInfo(); let info = this.vm.GetDisplayInfo();

View File

@@ -7,6 +7,7 @@ import { CGroup } from '../util/cgroup.js';
export interface CgroupLimits { export interface CgroupLimits {
cpuUsageMax?: number; cpuUsageMax?: number;
runOnCpus?: number[]; runOnCpus?: number[];
limitProcess?: boolean;
} }
interface CGroupValue { interface CGroupValue {
@@ -47,10 +48,15 @@ class CGroupLimitedProcess extends EventEmitter implements IProcess {
stdout: Readable | null = null; stdout: Readable | null = null;
stderr: Readable | null = null; stderr: Readable | null = null;
private cgroup: CGroup; private cgroup: CGroup;
private limits;
constructor(cg: CGroup, command: string, opts?: ProcessLaunchOptions) { constructor(cg: CGroup, limits: CgroupLimits, command: string, opts?: ProcessLaunchOptions) {
super(); super();
this.cgroup = cg; this.cgroup = cg;
this.limits = limits;
if(!this.limits.limitProcess)
this.limits.limitProcess = false;
this.process = execaCommand(command, opts); this.process = execaCommand(command, opts);
@@ -60,8 +66,10 @@ class CGroupLimitedProcess extends EventEmitter implements IProcess {
let self = this; let self = this;
this.process.on('spawn', () => { this.process.on('spawn', () => {
// it should have one! if(self.limits.limitProcess) {
self.cgroup.AttachProcess(self.process.pid!); // it should have one!
self.cgroup.AttachProcess(self.process.pid!);
}
self.emit('spawn'); self.emit('spawn');
}); });
@@ -85,11 +93,13 @@ class CGroupLimitedProcess extends EventEmitter implements IProcess {
} }
export class QemuResourceLimitedLauncher implements IProcessLauncher { export class QemuResourceLimitedLauncher implements IProcessLauncher {
private group; public group;
private limits;
constructor(name: string, limits: CgroupLimits) { constructor(name: string, limits: CgroupLimits) {
let root = CGroup.Self(); let root = CGroup.Self();
this.group = root.GetSubgroup(name); this.group = root.GetSubgroup(name);
this.limits = limits;
// Set cgroup keys. // Set cgroup keys.
for(const val of MakeValuesFromLimits(limits)) { for(const val of MakeValuesFromLimits(limits)) {
@@ -99,6 +109,6 @@ export class QemuResourceLimitedLauncher implements IProcessLauncher {
} }
launch(command: string, opts?: ProcessLaunchOptions | undefined): IProcess { launch(command: string, opts?: ProcessLaunchOptions | undefined): IProcess {
return new CGroupLimitedProcess(this.group, command, opts); return new CGroupLimitedProcess(this.group, this.limits, command, opts);
} }
} }