- El Pensadero de Canek - https://aztlan.fciencias.unam.mx/~canek/pensadero -

Tirando pasajeros del avión

El otro día, xochitl (la máquina que alberga mi blog) entró en un semi coma. Eso quiere decir que no estaba muerta, pero no respondía o tardaba mucho en hacerlo. Del orden de minutos.

Teniendo mucha paciencia pude entrar por SSH, hacerme root y reiniciar la máquina, y un análisis de los logs mostró que fue un problema de MySQL que dejó de responder consultas, lo que causó que las páginas de mi blog se tardaran porque estaban esperando dichas consultas, lo que causó que apache tuviera que crear más procesos para poder servir otras peticiones de páginas porque había varios procesos atorados sirviendo las que esperaban a MySQL, lo que causó que se acabara la memoria (física y virtual).

Ya he pasado por eso antes.

¿Cómo rastrea uno un problema así? Es bastante sencillo; uno va a los logs y busca por “oom-killer” en ellos. El kernel de Linux tiene un mecanismo que le permite matar procesos cuando la memoria no alcanza; cuando el sistema tiene memoria libre negativa (ahorita explico eso), recorre la lista de procesos y busca matar alguno, esperando que eso libere memoria. Es una heurística relativamente compleja; no crean que agarra y mata el proceso que más memoria ocupe (al inicio hacía eso, y resultaba entonces que siempre mataba X… lo cual causaba que murieran un montón de aplicaciones, tal vez causándole pérdida de información al usuario). Este mecanismo es conocido como el “Out of Memory killer”, y generalmente referido como “oom-killer”.

¿Pero cómo carajo puede llegar a tener el sistema memoria negativa? Eso es bien fácil de responder: porque Linux miente cuando ciertas llamadas del sistema solicitan memoria.

El ejemplo más sencillo es fork: cuando uno hace fork, el proceso padre se copia completamente y se le transmite al proceso hijo. El problema es que el 90% de las veces, cuando uno hace fork, en el proceso hijo se ejecuta un comando completamente independiente de la copia del proceso padre, y esa copia es descartada de forma inmediata. Si uno tuviera muy poca memoria libre (menos que la que ocupa el proceso padre, pero más de la que ocupa el comando ejecutado por el proceso hijo), entonces es posible ejecutar de forma exitosa el fork. Linux entonces miente al decirle al fork que sí se puede ejecutar, esperando que todo salga bien.

Cuando todo no sale bien, se manda llamar al “oom-killer”.

Hace años había broncas con el OOM killer, y a veces se mataban procesos aunque hubiera memoria suficiente. Un desarrollador de Linux hizo una analogía muy interesante [1] de eso y del OOM killer en general:

“An aircraft company discovered that it was cheaper to fly its planes with less fuel on board. The planes would be lighter and use less fuel and money was saved. On rare occasions however the amount of fuel was insufficient, and the plane would crash. This problem was solved by the engineers of the company by the development of a special OOF (out-of-fuel) mechanism. In emergency cases a passenger was selected and thrown out of the plane. (When necessary, the procedure was repeated.) A large body of theory was developed and many publications were devoted to the problem of properly selecting the victim to be ejected. Should the victim be chosen at random? Or should one choose the heaviest person? Or the oldest? Should passengers pay in order not to be ejected, so that the victim would be the poorest on board? And if for example the heaviest person was chosen, should there be a special exception in case that was the pilot? Should first class passengers be exempted? Now that the OOF mechanism existed, it would be activated every now and then, and eject passengers even when there was no fuel shortage. The engineers are still studying precisely how this malfunction is caused.”

El OOM killer sigue en el kernel, y en el caso de lo que ocurrió con xochitl por poco tumba a la máquina porque la heurísitica trata de matar el mínimo número de procesos corriendo. Esto quiere decir que mata un montón de procesos de apache, pero no a todos, y en particular nunca al proceso principal de apache (porque eso mataría muchos procesos). Esto permite que apache siga sirviendo páginas, que se siguen quedando atoradas por MySQL, lo que hace que se tengan que crear más procesos, etc.

(MySQL se queda atorado, pero no consume mucha memoria, entonces no es asesinado aunque realmente sea el culpable.)

Por suerte ahora el OOM killer es configurable, y uno puede marcar ciertos procesos como más “asesinables” (o usando la analogía, podemos seleccionar qué pasajeros queremos que mueran primero). Con ello lo que me pasó a mí es fácil evitar que se repita.

Pero realmente (como la analogía lo muestra), es idiota; no habría porqué estar matando procesos, no importa qué tan “inteligente” sea la heurística. El OOM killer sigue en el kernel porque en general la memoria se ha hecho estúpidamente barata, y los desarrolladores han tomado la postura de que es más inteligente dar más memoria o más disco duro al swap que estarse preocupando de los casos marginales (como el mío) donde de repente se acaba toda. Y para los casos marginales, siempre se puede decidir qué pasajero matar.

Así que con su permiso voy a implementar la política en mi aerolínea de que los pasajeros cuyos nombres terminen con “SQL” y comiencen con “My” sean aventados fuera del avión a la primera señal de que el combustible está bajo.

Eso, y un cron para revivir MySQL cada que sea asesinado. Digo, hay que ser justos.