Systemd-run — Limitar Recursos

Introducción

systemd-run permite lanzar cualquier comando dentro de una unidad transitoria de systemd, es decir, una unidad que se crea al vuelo y desaparece cuando el proceso termina, sin necesidad de escribir un archivo .service. Como cada unidad tiene su propio cgroup, se le pueden aplicar las mismas directrices de control de recursos que a un servicio normal mediante la opción -p (--property).

Requisitos:

  • cgroups v2, que es lo predeterminado en cualquier distribución moderna. Se comprueba con stat -fc %T /sys/fs/cgroup, que debe devolver cgroup2fs.
  • Con --user el comando corre bajo el gestor de usuario, sin sudo, pero solo se pueden usar los controladores que systemd delega al usuario. Por defecto son cpu, memory y pids:
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers

Para delegar más controladores (por ejemplo cpuset para limitar núcleos o io para disco):

sudo systemctl edit user@.service
[Service]
Delegate=cpu cpuset io memory pids

Después hay que cerrar sesión y volver a entrar, o reiniciar.


Creando procesos como scope

systemd-run puede crear dos tipos de unidades transitorias:

Tipo Opción Comportamiento
.service Por defecto Lo lanza systemd en segundo plano, con un entorno limpio, y la salida va al journal
.scope --scope Lo lanza la propia terminal en primer plano, hereda el entorno y la salida se ve en la terminal

Para programas interactivos o gráficos lo más cómodo es --scope, ya que el programa se comporta exactamente igual que si se lanzara directamente, solo que dentro de su propio cgroup:

systemd-run --user --scope -p MemoryMax=2G firefox

Opciones útiles:

  • -u nombre / --unit=nombre: nombre de la unidad. Si no se indica, se genera uno tipo run-r3f2a....scope.
  • --slice=nombre.slice: agrupa varias unidades bajo un mismo slice, de modo que el límite se reparte entre todas.
  • -p se puede repetir tantas veces como propiedades se quieran aplicar.
  • -q / --quiet: no muestra el mensaje con el nombre de la unidad.
  • Solo para .service: --pty conecta la terminal al proceso, --wait espera a que termine y --collect descarga la unidad aunque falle.

Consultar el consumo y modificar los límites en caliente:

systemctl --user status firefox.scope    # Estado, consumo y límites
systemd-cgtop                            # Consumo por cgroup en tiempo real
systemctl --user set-property --runtime firefox.scope MemoryMax=1G

Para no escribir todo cada vez, se puede añadir una función al .zshrc / .bashrc:

limitar() { systemd-run --user --scope -q "$@"; }

limitar -p MemoryMax=1G -p CPUQuota=50% programa

Limitar CPU

Limitar uso de CPU

  • CPUQuota=: Límite duro de tiempo de CPU, en porcentaje de un núcleo: 50% es medio núcleo y 200% son dos núcleos completos. Aunque el sistema esté libre, el proceso nunca pasa de ese límite.
  • CPUWeight= : Prioridad relativa, de 1 a 10000 (por defecto 100). Solo tiene efecto cuando hay competencia por la CPU; si el sistema está libre, el proceso usa toda la que necesite. CPUWeight=idle le da la mínima prioridad posible.
  • --nice= : Ajusta el nice clásico (de -20 a 19). Los valores negativos requieren root.
# Compilar sin pasar de 2 núcleos
systemd-run --user --scope -p CPUQuota=200% make -j8

# Tarea de fondo que solo usa la CPU sobrante
systemd-run --user --scope -p CPUWeight=idle ffmpeg -i entrada.mkv salida.mp4

Limitar núcleos

A diferencia de CPUQuota, aquí no se limita cuánto tiempo de CPU usa el proceso sino en qué núcleos puede ejecutarse. Los núcleos disponibles se ven con lscpu -e. Hay tres formas:

  • AllowedCPUs= : Usa el controlador cpuset del cgroup. Acepta rangos y listas: 0-3, 0,2,4. Funciona con --scope, pero en modo --user requiere haber delegado cpuset (ver Introducción); si no, la propiedad simplemente se ignora.
  • CPUAffinity= : Usa sched_setaffinity(), por lo que no necesita cgroups ni delegación, pero solo funciona en unidades .service (sin --scope), ya que es systemd quien lanza el proceso.
  • taskset : Dentro de un scope, sin delegación ni servicios.
# Con cpuset delegado
systemd-run --user --scope -p AllowedCPUs=0-3 blender

# Sin delegación, como servicio
systemd-run --user --pty -p CPUAffinity=0-3 ./script.sh

# Sin delegación, como scope
systemd-run --user --scope taskset -c 0-3 blender

Para comprobar en qué núcleos corre un proceso: taskset -cp <PID>.


Limitar Ram

Directriz Descripción
MemoryMax= Límite duro. Si se supera, el OOM killer mata el proceso
MemoryHigh= Límite blando. Al superarlo el kernel ralentiza el proceso y lo obliga a liberar memoria (moviéndola a swap), pero no lo mata
MemorySwapMax= Swap máxima que puede usar. 0 le desactiva la swap
MemoryLow=, MemoryMin= Lo contrario: memoria protegida que el kernel intenta no reclamarle

Los valores aceptan sufijos K, M, G y T (base 1024), un porcentaje de la RAM física (MemoryMax=25%) o infinity.

Lo recomendable es combinar MemoryHigh un poco por debajo de MemoryMax, de manera que el proceso se ralentice antes de llegar al límite duro y que el OOM killer sea solo el último recurso:

systemd-run --user --scope -p MemoryHigh=3G -p MemoryMax=4G -p MemorySwapMax=1G chromium

Si se usa MemorySwapMax=0 junto a MemoryMax, al llegar al límite el proceso muere directamente sin pasar por la swap.


Limitar red

cgroups v2 no tiene controlador de red, así que systemd no puede limitar el ancho de banda, pero sí controlar a dónde puede conectarse un proceso. Estas directrices funcionan mediante BPF, por lo que requieren el gestor del sistema (sudo, sin --user); en el gestor de usuario no están disponibles. Para no ejecutar el programa como root se usa --uid:

Directriz Descripción
IPAddressDeny=any Bloquea todo el tráfico IP
IPAddressAllow= Excepciones a lo anterior: localhost, link-local, multicast, 192.168.1.0/24...
RestrictNetworkInterfaces= Solo permite ciertas interfaces, o las prohíbe si se antepone ~
PrivateNetwork=yes El proceso solo ve su propia interfaz lo. Solo en .service
IPAccounting=yes No limita, pero contabiliza los bytes enviados y recibidos (se ven en systemctl status)
# Sin acceso a internet, solo red local
sudo systemd-run --scope --uid=$USER \
  -p IPAddressDeny=any \
  -p IPAddressAllow=localhost \
  -p IPAddressAllow=192.168.1.0/24 \
  programa

# Que solo pueda salir por la VPN
sudo systemd-run --scope --uid=$USER -p "RestrictNetworkInterfaces=lo wg0" programa

RestrictNetworkInterfaces también afecta a lo, por eso hay que incluirlo; si no, deja de funcionar la resolución DNS a través de systemd-resolved (127.0.0.53).

Al pasar por sudo el entorno se resetea, por lo que las aplicaciones gráficas pueden perder DISPLAY / WAYLAND_DISPLAY. En ese caso hay que usar sudo -E o pasarlas con -E DISPLAY=$DISPLAY.

Ancho de banda

Para limitar la velocidad hay que recurrir a herramientas externas:

  • trickle: funciona en espacio de usuario mediante LD_PRELOAD, así que no sirve con binarios estáticos (por ejemplo, los compilados en Go). Las velocidades van en KB/s:
systemd-run --user --scope trickle -s -d 500 -u 100 programa
  • nftables + tc: la opción robusta. Se marcan los paquetes del cgroup con la expresión socket cgroupv2 de nftables y se limitan con tc. Es bastante más complejo de configurar.

Resumen

Recurso Propiedad --user --scope
Uso de CPU CPUQuota, CPUWeight Sí Sí
Núcleos AllowedCPUs Delegando cpuset Sí
Núcleos CPUAffinity Sí No
RAM MemoryMax, MemoryHigh, MemorySwapMax Sí Sí
Red IPAddressDeny, IPAddressAllow, RestrictNetworkInterfaces No, requiere sudo Sí
Red PrivateNetwork No, requiere sudo No

https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html

https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html