![¿Puede Linux "quedarse sin RAM"?](https://rvso.com/image/50560/%C2%BFPuede%20Linux%20%22quedarse%20sin%20RAM%22%3F.png)
Vi varias publicaciones en la web de personas que aparentemente se quejaban de que un VPS alojado cancelaba procesos inesperadamente porque usaban demasiada RAM.
¿Cómo es esto posible? Pensé que todos los sistemas operativos modernos proporcionan "RAM infinita" simplemente usando el intercambio de disco para todo lo que pasa por la RAM física. ¿Es esto correcto?
¿Qué podría estar sucediendo si un proceso "se cancela debido a poca RAM"?
Respuesta1
¿Qué podría estar sucediendo si un proceso "se cancela debido a poca RAM"?
A veces se dice que Linux, por defecto, nunca niega las solicitudes de más memoria del código de la aplicación, por ejemplo malloc()
. 1 En realidad, esto no es cierto; el valor predeterminado utiliza una heurística mediante la cual
Se rechazan los compromisos excesivos obvios del espacio de direcciones. Utilizado para un sistema típico. Garantiza que una asignación grave falle y al mismo tiempo permite un compromiso excesivo para reducir el uso de swap.
De [linux_src]/Documentation/vm/overcommit-accounting
(todas las comillas son del árbol 3.11). No se hace explícito exactamente qué se considera una "asignación realmente salvaje", por lo que tendríamos que consultar la fuente para determinar los detalles. También podríamos usar el método experimental en la nota al pie 2 (a continuación) para intentar obtener un reflejo de la heurística; en base a eso, mi observación empírica inicial es que, en circunstancias ideales (== el sistema está inactivo), si no Si no tienes ningún intercambio, se te permitirá asignar aproximadamente la mitad de tu RAM, y si tienes un intercambio, obtendrás aproximadamente la mitad de tu RAM más todo tu intercambio. eso es mas o menospor proceso(pero tenga en cuenta este límiteesdinámico y sujeto a cambios debido al estado, véanse algunas observaciones en la nota 5).
La mitad de tu RAM más el intercambio es explícitamente el valor predeterminado para el campo "CommitLimit" en /proc/meminfo
. Esto es lo que significa, y tenga en cuenta que en realidad no tiene nada que ver con el límite que acabamos de comentar (de [src]/Documentation/filesystems/proc.txt
):
Límite de confirmación:Según la tasa de sobrecommit ('vm.overcommit_ratio'), esta es la cantidad total de memoria actualmente disponible para asignaren el sistema.Este límite solo se cumple si está habilitada la contabilidad estricta de sobrecompromiso (modo 2 en 'vm.overcommit_memory'). El CommitLimit se calcula con la siguiente fórmula: CommitLimit = ('vm.overcommit_ratio' * RAM física) + Swap Por ejemplo, en un sistema con 1G de RAM física y 7G de swap con un 'vm.overcommit_ratio' de 30 produciría un límite de compromiso de 7,3G.
El documento de contabilidad excesiva de compromisos citado anteriormente establece que el valor predeterminado vm.overcommit_ratio
es 50. Entonces, si lo hace sysctl vm.overcommit_memory=2
, puede ajustar vm.covercommit_ratio (con sysctl
) y ver las consecuencias. 3 El modo predeterminado, cuando CommitLimit
no se aplica y sólo "se rechazan las confirmaciones excesivas obvias del espacio de direcciones", es cuando vm.overcommit_memory=0
.
Si bien la estrategia predeterminada tiene un límite heurístico por proceso que evita la "asignación realmente salvaje", deja al sistema en su conjunto libre para volverse seriamente salvaje en cuanto a la asignación. 4 Esto significa que en algún momento puede quedarse sin memoria y tener que declarar en quiebra algunos procesos a través delasesino.
¿Qué mata el asesino OOM? No necesariamente el proceso que pidió memoria cuando no la había, ya que ese no es necesariamente el proceso verdaderamente culpable y, más importante aún, no necesariamente el que sacará más rápidamente al sistema del problema en el que se encuentra.
Esto se cita deaquíque probablemente cita una fuente 2.6.x:
/*
* oom_badness - calculate a numeric value for how bad this task has been
*
* The formula used is relatively simple and documented inline in the
* function. The main rationale is that we want to select a good task
* to kill when we run out of memory.
*
* Good in this context means that:
* 1) we lose the minimum amount of work done
* 2) we recover a large amount of memory
* 3) we don't kill anything innocent of eating tons of memory
* 4) we want to kill the minimum amount of processes (one)
* 5) we try to kill the process the user expects us to kill, this
* algorithm has been meticulously tuned to meet the principle
* of least surprise ... (be careful when you change it)
*/
Lo cual parece una justificación decente. Sin embargo, sin ser forense, el n.° 5 (que es redundante del n.° 1) parece una implementación difícil de vender, y el n.° 3 es redundante del n.° 2. Por lo tanto, podría tener sentido considerar esto reducido a los puntos 2/3 y 4.
Busqué en una fuente reciente (3.11) y noté que este comentario ha cambiado mientras tanto:
/**
* oom_badness - heuristic function to determine which candidate task to kill
*
* The heuristic for determining which task to kill is made to be as simple and
* predictable as possible. The goal is to return the highest value for the
* task consuming the most memory to avoid subsequent oom failures.
*/
Esto es un poco más explícito sobre el punto 2:"El objetivo es [eliminar] la tarea que consume más memoria para evitar fallas posteriores".y por implicación #4 ("queremos matar la cantidad mínima de procesos (uno)).
Si desea ver al asesino de OOM en acción, consulte la nota al pie 5.
1 Un engaño del que Gilles afortunadamente me libró, ver comentarios.
2 Aquí hay una parte sencilla de C que solicita porciones de memoria cada vez más grandes para determinar cuándo fallará una solicitud de más:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#define MB 1 << 20
int main (void) {
uint64_t bytes = MB;
void *p = malloc(bytes);
while (p) {
fprintf (stderr,
"%lu kB allocated.\n",
bytes / 1024
);
free(p);
bytes += MB;
p = malloc(bytes);
}
fprintf (stderr,
"Failed at %lu kB.\n",
bytes / 1024
);
return 0;
}
Si no sabes C, puedes compilar esto gcc virtlimitcheck.c -o virtlimitcheck
y luego ejecutarlo ./virtlimitcheck
. Es completamente inofensivo, ya que el proceso no utiliza nada del espacio que solicita, es decir, nunca utiliza realmente RAM.
En un sistema 3.11 x86_64 con un sistema de 4 GB y 6 GB de intercambio, fallé en ~7400000 kB; el número fluctúa, por lo que quizás el estado sea un factor. Coincidentemente, esto está cerca de CommitLimit
in /proc/meminfo
, pero modificar esta vía vm.overcommit_ratio
no hace ninguna diferencia. Sin embargo, en un sistema ARM 3.6.11 de 32 bits y 448 MB con 64 MB de intercambio, fallo en ~230 MB. Esto es interesante ya que en el primer caso la cantidad es casi el doble de la cantidad de RAM, mientras que en el segundo es aproximadamente 1/4, lo que implica fuertemente que la cantidad de intercambio es un factor. Esto se confirmó desactivando el intercambio en el primer sistema, cuando el umbral de falla bajó a ~1,95 GB, una proporción muy similar a la pequeña caja ARM.
¿Pero esto es realmente por proceso? Parece ser. El breve programa siguiente solicita una porción de memoria definida por el usuario y, si tiene éxito, espera a que presiones regresar; de esta manera puedes probar varias instancias simultáneas:
#include <stdio.h>
#include <stdlib.h>
#define MB 1 << 20
int main (int argc, const char *argv[]) {
unsigned long int megabytes = strtoul(argv[1], NULL, 10);
void *p = malloc(megabytes * MB);
fprintf(stderr,"Allocating %lu MB...", megabytes);
if (!p) fprintf(stderr,"fail.");
else {
fprintf(stderr,"success.");
getchar();
free(p);
}
return 0;
}
Sin embargo, tenga en cuenta que no se trata estrictamente de la cantidad de RAM y el intercambio independientemente del uso; consulte la nota al pie 5 para obtener observaciones sobre los efectos del estado del sistema.
3 CommitLimit
se refiere a la cantidad de espacio de direcciones permitido parael sistemacuando vm.overcommit_memory = 2. Presumiblemente, entonces, la cantidad que puede asignar debería ser esa menos lo que ya está comprometido, que aparentemente es el Committed_AS
campo.
Un experimento potencialmente interesante que demuestra esto es agregarlo #include <unistd.h>
en la parte superior de virtlimitcheck.c (ver nota al pie 2) y fork()
justo antes del while()
bucle. No se garantiza que funcione como se describe aquí sin una sincronización tediosa, pero hay muchas posibilidades de que así sea, YMMV:
> sysctl vm.overcommit_memory=2
vm.overcommit_memory = 2
> cat /proc/meminfo | grep Commit
CommitLimit: 9231660 kB
Committed_AS: 3141440 kB
> ./virtlimitcheck 2&> tmp.txt
> cat tmp.txt | grep Failed
Failed at 3051520 kB.
Failed at 6099968 kB.
Esto tiene sentido: al observar tmp.txt en detalle, puede ver que los procesos alternan sus asignaciones cada vez mayores (esto es más fácil si agrega el pid a la salida) hasta que uno, evidentemente, ha reclamado lo suficiente como para que el otro falle. El ganador podrá entonces quedarse con todo hasta CommitLimit
menos Committed_AS
.
4 Vale la pena mencionar, en este punto, si aún no entiendes el direccionamiento virtual y la paginación por demanda, que lo que hace posible el exceso de compromiso en primer lugar es que lo que el núcleo asigna a los procesos del usuario no es memoria física en absoluto, sinoespacio de direcciones virtuales. Por ejemplo, si un proceso reserva 10 MB para algo, eso se presenta como una secuencia de direcciones (virtuales), pero esas direcciones aún no corresponden a la memoria física. Cuando se accede a dicha dirección, esto resulta en unafallo de páginay luego el núcleo intenta asignarlo a la memoria real para que pueda almacenar un valor real. Los procesos suelen reservar mucho más espacio virtual del que realmente acceden, lo que permite al kernel hacer el uso más eficiente de la RAM. Sin embargo, la memoria física sigue siendo un recurso finito y cuando toda ella se ha asignado al espacio de direcciones virtuales, es necesario eliminar parte del espacio de direcciones virtuales para liberar algo de RAM.
5 primerouna advertencia: Si intentas esto con vm.overcommit_memory=0
, asegúrate de guardar tu trabajo primero y cerrar todas las aplicaciones críticas, porque el sistema se congelará durante aproximadamente 90 segundos y algunos procesos morirán.
La idea es ejecutar unbomba de tenedoreso se agota después de 90 segundos, con las bifurcaciones asignando espacio y algunas de ellas escribiendo grandes cantidades de datos en la RAM, mientras informan a stderr.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
/* 90 second "Verbose hungry fork bomb".
Verbose -> It jabbers.
Hungry -> It grabs address space, and it tries to eat memory.
BEWARE: ON A SYSTEM WITH 'vm.overcommit_memory=0', THIS WILL FREEZE EVERYTHING
FOR THE DURATION AND CAUSE THE OOM KILLER TO BE INVOKED. CLOSE THINGS YOU CARE
ABOUT BEFORE RUNNING THIS. */
#define STEP 1 << 30 // 1 GB
#define DURATION 90
time_t now () {
struct timeval t;
if (gettimeofday(&t, NULL) == -1) {
fprintf(stderr,"gettimeofday() fail: %s\n", strerror(errno));
return 0;
}
return t.tv_sec;
}
int main (void) {
int forks = 0;
int i;
unsigned char *p;
pid_t pid, self;
time_t check;
const time_t start = now();
if (!start) return 1;
while (1) {
// Get our pid and check the elapsed time.
self = getpid();
check = now();
if (!check || check - start > DURATION) return 0;
fprintf(stderr,"%d says %d forks\n", self, forks++);
// Fork; the child should get its correct pid.
pid = fork();
if (!pid) self = getpid();
// Allocate a big chunk of space.
p = malloc(STEP);
if (!p) {
fprintf(stderr, "%d Allocation failed!\n", self);
return 0;
}
fprintf(stderr,"%d Allocation succeeded.\n", self);
// The child will attempt to use the allocated space. Using only
// the child allows the fork bomb to proceed properly.
if (!pid) {
for (i = 0; i < STEP; i++) p[i] = i % 256;
fprintf(stderr,"%d WROTE 1 GB\n", self);
}
}
}
Compila esto gcc forkbomb.c -o forkbomb
. Primero, pruébalo con sysctl vm.overcommit_memory=2
; probablemente obtendrás algo como:
6520 says 0 forks
6520 Allocation succeeded.
6520 says 1 forks
6520 Allocation succeeded.
6520 says 2 forks
6521 Allocation succeeded.
6520 Allocation succeeded.
6520 says 3 forks
6520 Allocation failed!
6522 Allocation succeeded.
En este entorno, este tipo de bomba horquilla no llega muy lejos. Tenga en cuenta que el número en "dice N bifurcaciones" no es el número total de procesos, es el número de procesos en la cadena/rama que conduce a ese.
Ahora pruébalo con vm.overcommit_memory=0
. Si redirige stderr a un archivo, puede realizar un análisis básico después, por ejemplo:
> cat tmp.txt | grep failed
4641 Allocation failed!
4646 Allocation failed!
4642 Allocation failed!
4647 Allocation failed!
4649 Allocation failed!
4644 Allocation failed!
4643 Allocation failed!
4648 Allocation failed!
4669 Allocation failed!
4696 Allocation failed!
4695 Allocation failed!
4716 Allocation failed!
4721 Allocation failed!
Sólo 15 procesos no pudieron asignar 1 GB, lo que demuestra que la heurística para overcommit_memory = 0esafectado por el estado. ¿Cuántos procesos hubo? Mirando el final de tmp.txt, probablemente> 100.000. Ahora bien, ¿cómo puedo llegar a utilizar 1 GB?
> cat tmp.txt | grep WROTE
4646 WROTE 1 GB
4648 WROTE 1 GB
4671 WROTE 1 GB
4687 WROTE 1 GB
4694 WROTE 1 GB
4696 WROTE 1 GB
4716 WROTE 1 GB
4721 WROTE 1 GB
Ocho, lo cual nuevamente tiene sentido, ya que en ese momento tenía ~3 GB de RAM libres y 6 GB de intercambio.
Eche un vistazo a los registros de su sistema después de hacer esto. Deberías ver las puntuaciones de informes del asesino de OOM (entre otras cosas); presumiblemente esto se relaciona con oom_badness
.
Respuesta2
Esto no le sucederá si solo carga 1G de datos en la memoria. ¿Y si cargas mucho, mucho más? Por ejemplo, suelo trabajar con archivos enormes que contienen millones de probabilidades que deben cargarse en R. Esto requiere alrededor de 16 GB de RAM.
Ejecutar el proceso anterior en mi computadora portátil hará que comience a intercambiarse como loco tan pronto como se llenen mis 8 GB de RAM. Eso, a su vez, ralentizará todo porque la lectura desde el disco es mucho más lenta que la lectura desde la RAM. ¿Qué pasa si tengo un portátil con 2 GB de RAM y sólo 10 GB de espacio libre? Una vez que el proceso ha consumido toda la RAM, también llenará el disco porque está escribiendo para intercambiar y no me queda más RAM ni más espacio para intercambiar (la gente tiende a limitar el intercambio a una partición dedicada en lugar de una archivo de intercambio exactamente por esa razón). Ahí es donde entra el asesino OOM y comienza a matar procesos.
Por lo tanto, el sistema puede quedarse sin memoria. Además, los sistemas que intercambian mucho pueden volverse inutilizables mucho antes de que esto suceda simplemente debido a las lentas operaciones de E/S debidas al intercambio. Generalmente se quiere evitar el intercambio tanto como sea posible. Incluso en servidores de gama alta con SSD rápidos hay una clara disminución del rendimiento. En mi computadora portátil, que tiene una unidad clásica de 7200 RPM, cualquier cambio significativo esencialmente inutiliza el sistema. Cuanto más intercambia, más lento se vuelve. Si no elimino el proceso infractor rápidamente, todo se bloquea hasta que interviene el asesino OOM.
Respuesta3
Los procesos no mueren cuando no hay más RAM, sino cuando han sido engañados de esta manera:
- El kernel de Linux comúnmente permite que los procesos asignen (es decir, reserven) una cantidad de memoria virtual que es mayor que la que realmente está disponible (parte de la RAM + toda el área de intercambio).
- Siempre que los procesos sólo accedan a un subconjunto de las páginas que han reservado, todo funcionará bien.
- Si después de un tiempo, un proceso intenta acceder a una página de su propiedad pero no hay más páginas libres, se produce una situación de falta de memoria.
- El asesino de OOM selecciona uno de los procesos, no necesariamente el que solicitó una nueva página, y simplemente lo elimina para recuperar la memoria virtual.
Esto puede suceder incluso cuando el sistema no está intercambiando activamente, por ejemplo, si el área de intercambio está llena de páginas de memoria de demonios inactivos.
Esto nunca sucede en sistemas operativos que no comprometen demasiado la memoria. Con ellos, no se elimina ningún proceso aleatorio, pero el primer proceso que solicita memoria virtual mientras está agotada hace que malloc (o similar) regrese por error. De este modo se le da la oportunidad de manejar adecuadamente la situación. Sin embargo, en estos sistemas operativos, también puede suceder que el sistema se quede sin memoria virtual mientras todavía hay RAM libre, lo cual es bastante confuso y generalmente incomprendido.
Respuesta4
Solo para agregar otro punto de vista de las otras respuestas, muchos VPS alojan varias máquinas virtuales en un servidor determinado. Cualquier VM tendrá una cantidad específica de RAM para su propio uso. Muchos proveedores ofrecen "RAM de ráfaga", en la que pueden usar RAM más allá de la cantidad designada. Esto está destinado a ser solo para uso a corto plazo, y aquellos que vayan más allá de esta cantidad de tiempo prolongado pueden ser penalizados por el host que elimina procesos para reducir la cantidad de RAM en uso para que otros no sufran. la máquina host está sobrecargada.