cgroups - Grupos de control con systemd

Veíamos en una entrada anterior los cgroups o grupos de control, que nos permitían limitar y monitorear el uso de varios tipos de recursos. Hoy en día, y desde la llegada del sistema de inicio systemd, los cgroups son una parte integral del sistema operativo, y cada proceso se ejecuta en su propio grupo de control.

De forma predeterminada systemd crea automáticamente una jerarquía de unidades slicescopeservice para proporcionar una estructura unificada para el árbol de cgroups:

  • Service Un proceso o un grupo de procesos, que systemd inició en función de un archivo de configuración de unidad. Las unidades service encapsulan los procesos especificados para que puedan iniciarse y detenerse como un solo conjunto
  • Scope Grupo de procesos creados externamente. Las unidades scope encapsulan procesos que son iniciados y detenidos por procesos arbitrarios a través de la función fork() y luego registrados por systemd en tiempo de ejecución (como sesiones de usuario, los contenedores o las máquinas virtuales)
  • Slice Grupo de unidades organizadas jerárquicamente. Las unidades slice no contienen procesos, estos organizan una jerarquía en la que se colocan las unidades scopeservice. Los procesos reales están contenidos en unidades scopeservice

Las unidades servicescopeslice se asignan directamente a los objetos del árbol cgroup. Cuando se activan estas unidades, se asignan directamente a las rutas de cgroup construidas a partir de los nombres de las unidades. Estas unidades se pueden crear manualmente por el administrador del sistema o dinámicamente por programas.

Por defecto, systemd monta automáticamente las jerarquías para los controladores de recursos del kernel importantes en el directorio /sys/fs/cgroup/.

Podemos ver los subsistemas montados por defecto en la jerarquía existente usamos el comando lssubsys:

❯ lssubsys -am
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
blkio /sys/fs/cgroup/blkio
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
net_cls,net_prio /sys/fs/cgroup/net_cls,net_prio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/pids
rdma /sys/fs/cgroup/rdma

Uso de cgroups con systemd

Con el comando systemctl podemos modificar la estructura creando sectores personalizados.

Para ver el funcionamiento vamos a utilizar el mismo programa de la entrada anterior sobre cgroup que crea procesos que consumen CPU, pero quitamos las líneas iniciales donde nos muestra el PID y espera que pulsemos ENTER:

gasta_cpu2.c
/*  
 * (c) Author: David Quiroga  
 * e-mail: david [at] clibre [dot] io  
 *  
 * SPDX-License-Identifier: GPL-3.0  
 */  
 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/types.h> 
#include <unistd.h> 
#include <sys/wait.h> 
 
#define PRI_LIMIT 200000 
#define NUM_PROC 7 
 
static void genera_primos(int limit) 
{ 
    int i, j, num = 2, isprime; 
 
    for (i = 4; i <= limit; i++) { 
        isprime = 1; 
        for (j = 2; j < limit / 2; j++) { 
         if ((i != j) && (i % j == 0)) { 
            isprime = 0; 
            break; 
         } 
        } 
        if (isprime) {  
         num++; 
         // printf("Primo: %d\n", i); 
        }  
    } 
} 
 
int main (int argc, char *argv[])  
{ 
    pid_t childPid; 
     
    printf("Gastando CPU...\n"); 
     
    for (int i=1; i<=NUM_PROC; i++) { 
        switch (childPid = fork()) { 
        case -1: 
         fprintf (stderr, "Error en fork()\n"); 
         break; 
        case 0: // child 
         genera_primos(PRI_LIMIT); 
         exit(EXIT_SUCCESS); 
        default: 
         printf("Creado proceso %d PID=%ld\n", i, (long) childPid);  
         break; 
        } 
    }  
     
    printf("Esperando que terminen los procesos...\n"); 
 
    while (waitpid(-1, NULL, 0) > 0) 
        continue; 
 
    printf("Todos los procesos finalizaron\n");  
     
    exit (EXIT_SUCCESS); 
} 

Construimos:

❯ gcc -Wall gasta_cpu2.c -o gasta_cpu2

Iguan que en el caso anterior, corremos el programa en una VM con 8 CPUs asignadas. En el código le indicamos que cree 7 procesos. Si ejecutamos el programa:

❯ ./gasta_cpu2
Gastando CPU...
Creado proceso 1 PID=23469
Creado proceso 2 PID=23470
Creado proceso 3 PID=23471
Creado proceso 4 PID=23472
Creado proceso 5 PID=23473
Creado proceso 6 PID=23474
Creado proceso 7 PID=23475
Esperando que terminen los procesos...

Podemos ver con top el uso de las CPUs:

Procesos sin cgroup

Como vemos el sistema distribuye los procesos por las CPUs de tal forma que tenemos 7 CPUs al 100% (o cercanas) durante el tiempo de proceso.

Vamos a imitarle el uso de CPU  al proceso usando cgroups. En este caso vamos a indicarle un porcentaje del total de CPU que puede consumir (le indicamos 400% que como tenemos 8 CPUs, sería el 50% del computo de todas las CPU juntas).

Modo transitorio

Con el comando systemd-run podemos crear una unidad service o una unidad scope. Si lo ejecutamos como scope el comando se iniciará directamente desde el proceso systemd-run y la ejecución será sincrónica:

❯ sudo systemd-run --scope -p CPUQuota=400% ./gasta_cpu2

En cambio si lo ejecutamos como unidad service el comando se invocará por systemd y su ejecución será de forma asincrónica:

❯ sudo systemd-run --unit=gasta_CPU --slice=gCPU -p CPUQuota=400% ./gasta_cpu2 -b

En el primer caso no le hemos indicado la opción --slice por lo que correrá en el slice por defecto system.slice. Podemos indicarle un slice como en el segundo caso, si no existe lo creará:

❯ systemctl -t slice
  UNIT                         LOAD   ACTIVE SUB    DESCRIPTION                 
  -.slice                      loaded active active Root Slice                  
  gCPU.slice                   loaded active active gCPU.slice                  
  system-getty.slice           loaded active active system-getty.slice          
  system-modprobe.slice        loaded active active system-modprobe.slice       
  system-systemd\x2dfsck.slice loaded active active system-systemd\x2dfsck.slice
  system.slice                 loaded active active System Slice                
  user-1000.slice              loaded active active User Slice of UID 1000      
  user.slice                   loaded active active User and Session Slice      

LOAD   = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
SUB    = The low-level unit activation state, values depend on unit type.

8 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.

En ambos casos podemos ver con top que el proceso se ejecuta con el límite establecido:

Límite CPU con systemd

Modo persistente

En los casos anteriores que hemos visto las limitaciones a los procesos persistirán durante la sesión (o hasta que finalice el programa). Pero es frecuente, sobretodo en el caso de servicios, que estos se ejecuten en el arranque de forma automática y queramos que los ajustes prevalezcan entre sesiones. En estos casos tenemos que crear una unidad service en la que podemos añadir los parámetros de configuración para la gestión de recursos, en nuestro caso la limitación de CPU:

❯ cat /etc/systemd/system/gasta_cpu.service
[Unit]
Description=Cosumir CPU

[Service]
Type=Simple
ExecStart=/home/david/Projects/gasta_cpu2
CPUQuota=400%

Ahora podemos arrancar es servicio igual que el resto de servicios:

❯ sudo systemctl start gasta_cpu

Si vemos los procesos con top comprobaremos que resultado es el mismo que en los casos anteriores y el proceso corre limitado según la cuota que le hemos asignado.

Supervisión del consumo de recursos

Podemos ver una instantánea de la jerarquía de cgroup en un momento dato con systemd-cgls. Si lo ejecutamos cuando esta corriendo el servicio que acabamos de crear podemos ver nuestro servicio dentro del árbol de system.slice que como he indicado antes es el slice por defecto que se asigna si no especificamos ninguno:

└─system.slice 
  ├─
  ├─gasta_cpu.service
  │ ├─5253 /home/david/Projects/gasta_cpu2
  │ ├─5254 /home/david/Projects/gasta_cpu2
  │ ├─5255 /home/david/Projects/gasta_cpu2
  │ ├─5256 /home/david/Projects/gasta_cpu2
  │ ├─5258 /home/david/Projects/gasta_cpu2
  │ ├─5259 /home/david/Projects/gasta_cpu2
  │ ├─5260 /home/david/Projects/gasta_cpu2
  │ └─5261 /home/david/Projects/gasta_cpu2

Con systemd-cgtop podemos ver de forma interactiva (similar al comando top) los consumos de las diferentes unidades slice:

Control Group                               Tasks   %CPU   Memory  Input/s Output/s
/                                             731  426.5     2.3G        -        -
system.slice                                  157  395.7     1.1G        -        -
system.slice/gasta_cpu.service                  8  395.6   784.0K        -        -
system.slice/wpa_supplicant.service             1    0.0     4.0M        -        -
system.slice/accounts-daemon.service            3    0.0     7.5M        -        -
system.slice/apache2.service                   55    0.0     9.6M        -        -
gCPU.slice                                      -      -    20.0K        -        -
init.scope                                      1      -    12.5M        -        -
system.slice/ModemManager.service               3      -     7.0M        -        -
system.slice/NetworkManager.service             3      -    15.3M        -        -
system.slice/acpid.service                      1      -   448.0K        -        -
system.slice/avahi-daemon.service               2      -     1.6M        -        -
system.slice/boot-efi.mount                     -      -    20.0K        -        -
system.slice/colord.service                     3      -    22.1M        -        -
system.slice/cron.service                       1      -     2.7M        -        -

Vemos como el consumo de nuestro servicio gasta_cpu.service se mantiene dentro del porcentaje asignado de CPU.

(1 Voto)

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.