Limitar los recursos requeridos por los trabajos en HTCondor usando cgroups

HTCondor divide los nodos de ejecución en “slots” que están compuestos por CPUs, RAM y disco. Cuando se lanza un trabajo, éste solicita una cantidad determinada de cada uno de esos recursos. La asignación lo que hace es buscar un slot que satisfaga la solicitud del trabajo. Sin embargo, una vez asignados, HTCondor no limita la cantidad de recursos que el trabajo usa, pudiendo un trabajo consumir más de lo que solicitó en un principio. Esto puede llevar a situaciones en las que pueda consumir toda la RAM y cuelgue el nodo, perjudicando a otros trabajos que están corriendo en el mismo nodo. Igual pasa con el caso de la CPU: puede crear más hebras que las solicitadas también perjudicando la ejecución de otros trabajos.

Hasta ahora, para evitar este comportamiento, se ha usado USER_JOB_WRAPPER con ulimit para limitar el tamaño máximo de memoria que puede usar sea igual al que ha solicitado. Para CPU, no tenía ninguna ninguna forma de limitarlo simplemente porque no se ejecutaban programas con más de 1 hebra.  Ahora, al empezar a usar programación paralela, se hace necesario controlar también este recurso.

Con ulimit, no he encontrado la manera de controlar el uso del número de CPUs. Además, con esta técnica, cuando se rebasaba la memoria, el programa era eliminado y no indicaba la causa. En el log, aparecía segmentation fault pero puede indicar también un error en el acceso a memoria.

Para paliar estos problemas, he cambiado la forma en la que se limitan los recursos de los trabajos en los nodos de ejecución, usando cgroups, que es una forma de aislar los recursos que pueden consumir un conjunto de procesos, soportado por el kernel.

Lo primero que tenemos que hacer es instalar las herramientas para poder usar esta característica, si no la tenemos. En centos, sería:

yum install libcgroup-tools.x86_64

Y configurarlo para crear el grupo de HTCondor, tal y como se explica en su documentación. Creamos un fichero en /etc/cgconfig.d/ con el siguiente contenido:

group htcondor {
cpu {}
cpuacct {}
memory {}
freezer {}
blkio {}
}

Para indicarle a Condor que use cgroups, sólo hay que añadir esta línea en la configuración de los nodos de ejecución:

BASE_CGROUP=htcondor

Además, vamos a configurar el modo en que queremos que sean tratados los trabajos si superan los límites de memoria. cgroup limita la cantidad de memoria física que puede usar un programa, pero puede aumentar su memoria virtual usando paginación. Para evitarlo, voy a usar la siguiente configuración:

CGROUP_MEMORY_LIMIT_POLICY=hard

hard indica que no puede usar más memoria de la solicitada, incluso si hay disponible en el nodo (al contrario que soft) y en la configuración de los slots, desactivo el uso de SWAP:

SLOT_TYPE_1 = cpus=100%, ram=100%, swap=0, disk=100%

 

Con esta configuración, usará todas las CPUS libres del sistema, pero si no hay, estará limitado al número de CPUs solicitadas. Por ejemplo, en un sistema con solo 4 cores libres, si nuestro programa crea 16 hebras y hemos reservado esos 4 cores, las 16 hebras correrán en esos 4 cores, compitiendo por usarlo y, a la postre, perjudicando a su rendimiento. Para la memoria, si sobrepasa su límite se pondrá en HOLD con el mensaje ”

Job has gone over memory limit”

Como funcionalidad adicional, queremos que un trabajo que haya pasado a HOLD porque haya excedido la cantidad de memoria solicitada vuelve a ejecutarse aumentando la cantidad de memoria que solicita de manera automática. Para esto, usaremos el atributo SYSTEM_PERIODIC_RELEASE del scheduler:

SYSTEM_PERIODIC_RELEASE=(JobRunCount < 5 ) && (HoldReasonCode == 34)

Con esa línea, el programa se pondrá de nuevo en IDLE si ha sido ejecutado hasta en 5 ocasiones y está en estado de HOLD por el motivo 34 (este código es el que pone Conder al pasar a HOLD por superar el límite de memoria usando cgroups).

En el submit del trabajo vamos a utilizar la siguiente configuración

ifthenelse(( NumJobStarts == 0 ),200,1.5 * 200 * ( NumJobStarts ))

Con esto, se parte de una solicitud de 200MB. Por cada vez que el trabajo pasa a HOLD y se vuelve a poner en ejecución, la solicitud se aumenta 1,5 veces.