De repente, un sistema de recolección de basura solo no es suficiente

Aquí hay una breve historia sobre fallas misteriosas en el servidor que tuve que depurar hace un año (artículo fechado el 5 de diciembre de 2018, aproximadamente por año). Los servidores funcionaron bien por un tiempo, y luego en algún momento comenzaron a fallar. Después de esto, los intentos de ejecutar casi cualquier programa que estuviera en los servidores fallaron con los errores "No hay espacio en el dispositivo", aunque el sistema de archivos informó solo unos pocos gigabytes ocupados en discos de ~ 20 GB.

Resultó que el problema fue causado por el sistema de registro. Esta era una aplicación de Ruby que toma archivos de registro, envía datos a un servidor remoto y elimina archivos antiguos. El error fue que los archivos de registro abiertos no se cerraron explícitamente. En cambio, la aplicación permitió que el recolector de basura automático de Ruby limpiara los objetos File. El problema es que los objetos File no consumen mucha memoria, por lo que, en teoría, un sistema de registro podría mantener abiertos millones de registros antes de que se requiera la recolección de basura.

* Los sistemas de archivos Nix separan nombres de archivos y datos en archivos. Los datos en un disco pueden tener varios nombres de archivos apuntando a ellos (es decir, enlaces duros), y los datos se eliminan solo cuando se elimina el último enlace. Un descriptor de archivo abierto se considera un enlace, por lo que si el archivo se elimina mientras el programa está leyendo, el nombre del archivo desaparece del directorio, pero los datos del archivo permanecen vivos hasta que el programa lo cierra. Esto es lo que le pasó al registrador. El comando du ("uso de disco") busca archivos utilizando una lista de directorio, por lo que no vio gigabytes de datos de archivo para los miles de archivos de registro que aún estaban abiertos. Estos archivos se descubrieron solo después de ejecutar lsof ("lista de archivos abiertos").

Por supuesto, se produce un error similar en otros casos similares. Hace un par de meses, tuve que encontrarme con una aplicación Java que se descompuso después de unos días debido a una fuga en las conexiones de red.

Solía ​​escribir la mayor parte de mi código en C, y luego en C ++. En aquellos días, pensé que la gestión manual de recursos era suficiente. ¿Qué tan complicado fue eso? Cada malloc () necesita la función free (), y cada open () necesita close (). Simplemente. Excepto que no todos los programas son simples, por lo que la gestión manual de recursos con el tiempo se ha convertido en una camisa de fuerza. Entonces, un día descubrí el conteo de enlaces y la recolección de basura. Pensé que resuelve todos mis problemas y dejé de preocuparme por la administración de recursos. Nuevamente, para programas simples, esto era normal, pero no todos los programas son simples.

No puede contar con la recolección de basura, ya que solo resuelve el problema de la administración de la memoria, y los programas complejos tienen que lidiar con mucho más que solo la memoria. Hay un meme popular que responde a esto con el hecho de que la memoria es el 95% de los problemas de recursos . Incluso podría decir que todos los recursos son el 0% de sus problemas, hasta que se quede sin uno de ellos. Entonces este recurso se convierte en el 100% de tus problemas.

Pero tal pensamiento todavía percibe los recursos como un caso especial. Un problema más profundo es que a medida que los programas se vuelven más complejos, todo tiende a convertirse en un recurso. Por ejemplo, tome un programa de calendario. El sofisticado programa de calendario permite a múltiples usuarios administrar múltiples calendarios compartidos, y con eventos que pueden compartirse en múltiples calendarios. Cualquier parte de los datos afectará en última instancia a varias partes del programa, y ​​debe ser relevante y correcta. Por lo tanto, para todos los datos dinámicos, necesita un propietario, y no solo para la administración de la memoria. A medida que se agregan nuevas funciones, será necesario actualizar cada vez más partes del programa. Si está sano, solo le permitirá actualizar datos de una parte del programa a la vez,para que el derecho y la responsabilidad de actualizar los datos se conviertan en sí mismos en un recurso limitado. Modelar datos mutados utilizando estructuras inmutables no conduce a la desaparición de estos problemas, sino que solo los traduce a otro paradigma.

La planificación de la propiedad y la vida útil de los recursos es una parte inevitable del diseño de software complejo. Esto es más fácil si usa algunos patrones comunes. Uno de los patrones son los recursos intercambiables. Un ejemplo es la cadena inmutable "foo", que es semánticamente igual que cualquier otro "foo" inmutable. Este tipo de recurso no necesita una vida o posesión predeterminada. De hecho, para que el sistema sea lo más simple posible, es mejor no tener una vida útil o propiedad predeterminada (hola Rust, aprox. Por persona). Otro patrón son los recursos que no son intercambiables, pero que tienen una vida útil determinada. Esto incluye conexiones de red, así como conceptos más abstractos, como el derecho a controlar parte de los datos.Lo más razonable es garantizar explícitamente la vida útil de tales cosas al codificar.

Tenga en cuenta que la recolección automática de basura es realmente buena para implementar el primer patrón, pero no el segundo, mientras que las técnicas manuales de administración de recursos (como RAII) son excelentes para implementar el segundo patrón, pero terrible para el primero. Estos dos enfoques se vuelven complementarios en programas complejos.

Source: https://habr.com/ru/post/undefined/


All Articles