¿Tus reconstrucciones tardan una eternidad con millones de subidas? ¡Prueba a añadir esta plantilla!

parallel-fs-ops.template.yml (2.8 KB)

Hablemos de un problema de escalabilidad que se vuelve dolorosamente evidente una vez que un sitio de Discourse acumula una gran biblioteca de cargas.

Lo que antes tomaba minutos para ejecutar un comando chown en un directorio de cargas enorme, ¡ahora toma segundos!

Antecedentes

Las reconstrucciones de Discourse pueden ejecutar operaciones recursivas como:

chown -R ...
chmod -R ...

Más específicamente, esta línea en templates/web.template.yml:

- chown -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp

Estos comandos recorren el sistema de archivos de forma serial, uno por uno.

Esto es perfectamente razonable para una instalación pequeña. Pero cuando shared/uploads contiene cientos de miles o incluso millones de archivos, las operaciones recursivas de propiedad y permisos pueden dominar el tiempo de despliegue. Puede haber capacidad de CPU, almacenamiento y red disponible, pero un solo proceso recorre todo el árbol un nodo (inode) a la vez.

Para comunidades con muchas cargas, el resultado puede ser:

  • Reconstrucciones extremadamente largas
  • Ventanas de mantenimiento más largas
  • Despliegues y actualizaciones de seguridad retrasados
  • Mala utilización de almacenamiento rápido o distribuido
  • Un despliegue que parece atascado mientras procesa un árbol de archivos enorme
  • Rendimiento especialmente doloroso en NFS, JuiceFS, CephFS y otros sistemas de archivos remotos

La parte frustrante es que muchos de estos archivos son independientes. Sus permisos pueden procesarse de forma concurrente.

La solución: Plantilla de Operaciones Paralelas del Sistema de Archivos

Creé una plantilla de pups que reemplaza transparentemente las operaciones recursivas de chmod y chown con pipelines paralelos de find y xargs.

Los envoltorios (wrappers) se anuncian a sí mismos cada vez que interceptan una operación recursiva:

echo "[parallel-fs-ops] chmod -R override active: $*" >&2

y:

echo "[parallel-fs-ops] chown -R override active: $*" >&2

Ese prefijo entre corchetes hace que la optimización sea fácil de detectar en el registro de despliegue.

Cómo se ve un despliegue

Cerca del inicio del despliegue, la plantilla confirma qué binarios manejarán las operaciones posteriores del sistema de archivos:

[parallel-fs-ops] chmod -> /usr/local/bin/chmod
[parallel-fs-ops] chown -> /usr/local/bin/chown

Cuando una plantilla de nivel superior ejecuta más tarde un cambio de permisos recursivo, la salida del despliegue incluye una línea similar a:

[parallel-fs-ops] chmod -R override active: -R 0755 /var/www/discourse/public

Un cambio de propiedad recursivo produce:

[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp

Para una instalación con muchas cargas, podrías ver algo parecido a:

[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp

Las rutas y argumentos exactos dependen de las plantillas utilizadas, pero la parte importante es el marcador visible:

[parallel-fs-ops]

Sin la plantilla, el despliegue puede parecer pausado durante mucho tiempo durante una operación recursiva del sistema de archivos. Con la plantilla, el registro te indica que:

  1. El envoltorio se instaló correctamente.
  2. Se detectó una operación recursiva.
  3. La implementación paralela está activa.
  4. Los argumentos originales que se están procesando son visibles.

Esto es particularmente valioso durante la solución de problemas, ya que distingue una exploración paralela lenta del sistema de archivos de una compilación colgada.

Después de que la operación termina, la implementación continúa con su salida normal de pups. El envoltorio en sí mismo no imprime una línea por archivo, por lo que incluso un árbol que contiene millones de cargas no inunda el registro de despliegue.

La plantilla

run:
  - file:
      path: /usr/local/bin/chmod
      chmod: "+x"
      contents: |
        #!/bin/bash
        if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
          echo "[parallel-fs-ops] chmod -R override active: $*" >&2
          args=()
          for arg in "$@"; do
            [[ "$arg" != "-R" ]] && args+=("$arg")
          done
          mode="${args[0]}"
          targets=("${args[@]:1}")
          [[ ${#targets[@]} -eq 0 ]] && targets=(".")
          find "${targets[@]}" -print0 |
            xargs -0 -n 32 -P 128 /bin/chmod "$mode"
        else
          exec /bin/chmod "$@"
        fi

  - file:
      path: /usr/local/bin/chown
      chmod: "+x"
      contents: |
        #!/bin/bash
        if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
          echo "[parallel-fs-ops] chown -R override active: $*" >&2
          args=()
          for arg in "$@"; do
            [[ "$arg" != "-R" ]] && args+=("$arg")
          done
          owner="${args[0]}"
          targets=("${args[@]:1}")
          [[ ${#targets[@]} -eq 0 ]] && targets=(".")
          find "${targets[@]}" -print0 |
            xargs -0 -n 32 -P 128 /bin/chown "$owner"
        else
          exec /bin/chown "$@"
        fi

  - exec:
      cmd: |
        echo "[parallel-fs-ops] chmod -> $(command -v chmod)"
        echo "[parallel-fs-ops] chown -> $(command -v chown)"

La plantilla instala envoltorios en /usr/local/bin, que normalmente aparece antes de /bin en PATH.

Cuando se solicita una operación normal, no recursiva, el envoltorio delega directamente en la utilidad estándar:

exec /bin/chmod "$@"

Cuando está presente -R, elimina la bandera recursiva, enumera los objetivos de forma segura con delimitadores nulos y procesa lotes de forma concurrente:

find "${targets[@]}" -print0 |
  xargs -0 -n 32 -P 128 /bin/chmod "$mode"

Esto también funciona cuando pups invoca comandos a través de /bin/sh. El shebang de Bash del envoltorio se respeta cuando se lanza el ejecutable, aunque el shell que lo llama sea Dash.

Por qué esto importa más cuando tienes muchas cargas

Las comunidades con muchas cargas son exactamente donde el comportamiento del despliegue necesita escalar con gracia.

Un foro de larga data puede contener:

  • Imágenes incrustadas a lo largo de años de publicaciones
  • Avatares y fondos de perfil
  • Variantes de imagen originales y optimizadas
  • Adjuntos de video y audio
  • Documentos y archivos
  • Cargas seguras
  • Medios gestionados por plugins
  • Árboles de carga multisitio

La cantidad de código de la aplicación puede permanecer relativamente estable mientras que el número de objetos del sistema de archivos cargados continúa creciendo. La exploración del sistema de archivos, no la compilación o la creación de contenedores, puede convertirse eventualmente en el costo dominante del despliegue.

Este es un problema de escalabilidad inusual: cuanto más exitosa y rica en contenido se vuelve la comunidad, más costoso puede volverse el trabajo operativo rutinario.

Por qué se necesita una plantilla

Cambiar .bashrc o establecer BASH_ENV no resuelve esto de manera confiable. pups ejecuta los comandos run a través de /bin/sh, y Dash ni carga la configuración de Bash ni entiende las funciones específicas de Bash.

Una plantilla proporciona una forma repetible de instalar los envoltorios lo suficientemente pronto para que las operaciones recursivas posteriores, incluidas las de plantillas de nivel superior, se resuelvan a través de la implementación paralela:

templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  - "containers/parallel-fs-ops.template.yml"

Opciones configurables

Opciones de procesamiento paralelo

La plantilla utiliza:

find "${targets[@]}" -print0 |
  xargs -0 -n 32 -P 128 /bin/chmod "$mode"

Los parámetros relevantes de xargs son:

Opción Propósito
-0 Lee rutas delimitadas por nulos producidas por find -print0. Esto maneja de forma segura nombres de archivos que contienen espacios, comillas, tabulaciones o saltos de línea.
-n 32 Pasa como máximo 32 rutas a cada invocación de chmod o chown. Este es el tamaño del lote.
-P 128 Permite que hasta 128 procesos de chmod o chown se ejecuten de forma concurrente. Este es el nivel de paralelismo.

Juntos, -n 32 -P 128 significa que hasta 128 procesos pueden ejecutarse simultáneamente, con cada proceso manejando un lote de hasta 32 rutas. Por lo tanto, aproximadamente 4,096 rutas pueden estar siendo distribuidas activamente en lotes de comandos a la vez.

Elegir -n

-n controla cuánto trabajo se asigna a cada comando:

  • Valores más bajos proporcionan una distribución de trabajo más fina pero inician más procesos.
  • Valores más altos reducen la sobrecarga de inicio de procesos pero crean lotes más grandes y menos uniformemente distribuidos.
  • -n 1 ejecuta un comando chmod o chown por ruta.
  • -n 32 es un punto de partida razonable para equilibrar el loteo y el paralelismo.
  • Valores muy grandes pueden reducir la efectividad de -P porque se crean menos lotes en total.

Elegir -P

-P controla cuántos comandos pueden ejecutarse al mismo tiempo:

  • Valores más bajos reducen la carga en la CPU y el sistema de archivos.
  • Valores más altos pueden mejorar el rendimiento en almacenamiento rápido o distribuido.
  • Un paralelismo excesivo puede abrumar a los discos, saturar un servidor de metadatos o empeorar el rendimiento.
  • -P 1 es efectivamente ejecución serial.
  • -P 8 o -P 16 es un punto de partida conservador.
  • -P 32 puede ser adecuado para almacenamiento respaldado por SSD rápidos.
  • -P 128 debe usarse solo cuando el sistema de archivos y el host puedan sostener esa concurrencia.

Los mejores valores dependen de la latencia del sistema de archivos, el rendimiento de los metadatos, la capacidad de la CPU y el número de archivos. Idealmente, ambos deberían ser configurables y estar benchmarked para la instalación específica.

Demasiado paralelismo puede abrumar un sistema de archivos, saturar servidores de metadatos o degradar el rendimiento del despliegue. Por lo tanto, el tamaño del lote y la concurrencia deberían ser configurables.

Esta plantilla es una solución práctica, pero la propuesta más amplia es más amplia:

¿Podría Discourse admitir oficialmente la paralelización configurable para grandes operaciones recursivas del sistema de archivos durante los despliegues?

Una implementación de nivel superior podría:

  • Paralelizar solo árboles de directorios grandes conocidos
  • Evitar recorrer innecesariamente árboles de carga sin cambios
  • Hacer la concurrencia configurable
  • Detectar sistemas de archivos locales frente a respaldados por red
  • Preservar la semántica completa de los argumentos de chmod y chown
  • Emitir progreso periódico para árboles muy grandes
  • Registrar tiempos para que los administradores puedan identificar cuellos de botella en el despliegue

Precaución importante

El envoltorio anterior se centra en las formas de comando recursivo utilizadas por nuestro proceso de compilación. No es una reimplementación completa de todas las posibles combinaciones de opciones de chmod o chown.

Debe probarse contra los comandos exactos generados por las plantillas de un sitio antes de su uso en producción. Los operadores deberían comenzar con un paralelismo conservador y medir el efecto en su almacenamiento.

Pero el problema subyacente es real: las operaciones recursivas de metadatos seriales no escalan bien cuando una comunidad ha acumulado un árbol de carga masivo.

Buena suerte, y agradezco cualquier comentario o sugerencia (¡incluso si duplicé los esfuerzos de otra persona, también agradecería que me indicaran a dónde mirar)!

¡Saludos!

2 Me gusta