De la vida con Kubernetes: cómo eliminamos DBMS (y no solo) de entornos de revisión a estáticos



Nota : este artículo no pretende ser una mejor práctica. Describe la experiencia de una implementación específica de una tarea de infraestructura en términos de uso de Kubernetes y Helm, que puede ser útil para resolver problemas relacionados.

El uso de entornos de revisión en CI / CD puede ser muy útil, tanto para desarrolladores como para ingenieros de sistemas. Primero sincronicemos las ideas generales sobre ellos:

  1. Los entornos de revisión se pueden crear desde ramas separadas en repositorios Git definidos por los desarrolladores (las denominadas ramas de características).
  2. Pueden tener instancias DBMS separadas, procesadores de cola, servicios de almacenamiento en caché, etc. - en general, todo para la reproducción completa del entorno de producción.
  3. Permiten el desarrollo paralelo, acelerando significativamente el lanzamiento de nuevas características en la aplicación. Al mismo tiempo, se pueden requerir docenas de tales entornos todos los días, por lo que la velocidad de su creación es crítica.

En la intersección del segundo y el tercer punto, a menudo surgen dificultades: dado que la infraestructura es muy diferente, sus componentes se pueden implementar durante mucho tiempo. Este tiempo empleado, por ejemplo, incluye restaurar la base de datos desde una copia de seguridad ya preparada *. El artículo trata sobre la forma fascinante en la que una vez fuimos para resolver este problema.

* Por cierto, específicamente sobre volcados de bases de datos grandes en este contexto, ya escribimos en el material sobre la aceleración de la base de datos de bootstrap ).

El problema y la forma de resolverlo.


En uno de los proyectos, se nos asignó la tarea de "crear un único punto de entrada para desarrolladores e ingenieros de control de calidad". Esta formulación oculta técnicamente lo siguiente:

  1. Para simplificar el trabajo de los ingenieros de control de calidad y algunos otros empleados, elimine todas las bases de datos (y los correspondientes vhosts) utilizados en la revisión, en un entorno separado, estático. Por las razones que prevalecen en el proyecto, esta forma de interactuar con ellos fue óptima.
  2. Reduzca el tiempo que lleva crear un entorno de revisión. Todo el proceso de su creación desde cero está implícito, es decir incluyendo clonación de bases de datos, migraciones, etc.

Desde el punto de vista de la implementación, el problema principal se reduce a garantizar la idempotencia al crear y eliminar entornos de revisión. Para lograr esto, cambiamos el mecanismo para crear entornos de revisión migrando primero los servicios PostgreSQL, MongoDB y RabbitMQ a un entorno estático. Estático se refiere a un entorno "permanente" que no se creará a petición del usuario (como es el caso de los entornos de revisión).

¡Importante! El enfoque con un entorno estático está lejos de ser ideal ; para conocer sus deficiencias específicas, consulte el final del artículo. Sin embargo, compartimos esta experiencia en detalle, ya que puede ser más o menos aplicable en otras tareas, y al mismo tiempo sirve como argumento cuando se discuten problemas de diseño de infraestructura.

Entonces, la secuencia de acciones en la implementación:

  • Al crear un entorno de revisión, lo siguiente debería suceder una vez: la creación de bases de datos en dos DBMS (MongoDB y PostgreSQL), la restauración de bases de datos desde una copia de seguridad / plantilla y la creación de vhost en RabbitMQ. Esto requerirá una forma conveniente de cargar los volcados actuales. (Si antes tenía entornos de revisión, lo más probable es que ya tenga una solución preparada para esto).
  • Después de completar el entorno de revisión, debe eliminar la base de datos y el host virtual en RabbitMQ.

En nuestro caso, la infraestructura opera dentro del marco de Kubernetes (usando Helm). Por lo tanto, para la implementación de las tareas anteriores, los ganchos Helm fueron excelentes . Se pueden realizar tanto antes de la creación de todos los demás componentes en la versión de Helm como después de su eliminación. Por lo tanto:

  • para la tarea de inicialización, usaremos un enlace pre-installpara iniciarlo antes de crear todos los recursos en el lanzamiento;
  • para la tarea de eliminación, un gancho post-delete.

Pasemos a los detalles de implementación.

Implementación práctica


En la versión original, este proyecto utilizaba solo un trabajo, que constaba de tres contenedores. Por supuesto, esto no es del todo conveniente, ya que el resultado es un gran manifiesto que es curiosamente difícil de leer. Por lo tanto, lo dividimos en tres pequeños trabajos.

La siguiente es una lista de PostgreSQL, y los otros dos (MongoDB y RabbitMQ) son idénticos en estructura de manifiesto:

{{- if .Values.global.review }}
---
apiVersion: batch/v1
kind: Job
metadata:
  name: db-create-postgres-database
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "5"
spec:
  template:
    metadata:
      name: init-db-postgres
    spec:
      volumes:
      - name: postgres-scripts
        configMap:
          defaultMode: 0755
          name: postgresql-configmap
      containers:
      - name: init-postgres-database
        image: private-registry/postgres 
        command: ["/docker-entrypoint-initdb.d/01-review-load-dump.sh"]
        volumeMounts:
        - name: postgres-scripts
          mountPath: /docker-entrypoint-initdb.d/01-review-load-dump.sh
          subPath: review-load-dump.sh
        env:
{{- include "postgres_env" . | indent 8 }}
      restartPolicy: Never
{{- end }}

Comentarios sobre el contenido del manifiesto:

  1. Job review-. review CI/CD Helm- (. if .Values.global.review ).
  2. Job — , ConfigMap. , , . , hook-weight.
  3. cURL , PostgreSQL, .
  4. PostgreSQL : , shell- .

PostgreSQL


Lo más interesante está en el script de shell ( review-load-dump.sh) ya mencionado en el listado . ¿Cuáles son las opciones generales para restaurar una base de datos en PostgreSQL?

  1. Recuperación "estándar" de la copia de seguridad;
  2. Recuperación utilizando plantillas .

En nuestro caso, la diferencia entre los dos enfoques está principalmente en la velocidad de crear una base de datos para el nuevo entorno. En el primero, cargamos el volcado de la base de datos y lo restauramos pg_restore. Y con nosotros esto sucede más lentamente que el segundo método, por lo que se hizo la elección correspondiente.

Usando la segunda opción ( recuperación con plantillas) puede clonar la base de datos a nivel físico sin enviarle datos de forma remota desde el contenedor en otro entorno; esto reduce el tiempo de recuperación. Sin embargo, hay una limitación: no puede clonar una base de datos a la que permanecen las conexiones activas. Dado que usamos stage como el entorno estático (y no como un entorno de revisión separado), necesitamos crear una segunda base de datos y convertirla en una plantilla, actualizándola diariamente (por ejemplo, en la mañana). Se preparó un pequeño CronJob para esto:

---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: update-postgres-template
spec:
  schedule: "50 4 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  startingDeadlineSeconds: 600
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          imagePullSecrets:
          - name: registrysecret
          volumes:
          - name: postgres-scripts
            configMap:
              defaultMode: 0755
              name: postgresql-configmap-update-cron
          containers:
          - name: cron
            command: ["/docker-entrypoint-initdb.d/update-postgres-template.sh"]
          image: private-registry/postgres 
            volumeMounts:
            - name: postgres-scripts
              mountPath: /docker-entrypoint-initdb.d/update-postgres-template.sh
              subPath: update-postgres-template.sh
            env:
{{- include "postgres_env" . | indent 8 }}

El manifiesto completo de ConfigMap que contiene el script probablemente no tenga mucho sentido (informe en los comentarios si este no es el caso). En cambio, daré lo más importante: un script bash:

#!/bin/bash -x

CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"

psql -d "${CREDENTIALS}" -w -c "REVOKE CONNECT ON DATABASE ${POSTGRES_DB_TEMPLATE} FROM public"
psql -d "${CREDENTIALS}" -w -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${POSTGRES_DB_TEMPLATE}'"

curl --fail -vsL ${HOST_FORDEV}/latest_${POSTGRES_DB_STAGE}.psql -o /tmp/${POSTGRES_DB}.psql

psql -d "${CREDENTIALS}" -w -c "ALTER DATABASE ${POSTGRES_DB_TEMPLATE} WITH is_template false allow_connections true;"
psql -d "${CREDENTIALS}" -w -c "DROP DATABASE ${POSTGRES_DB_TEMPLATE};" || true
psql -d "${CREDENTIALS}" -w -c "CREATE DATABASE ${POSTGRES_DB_TEMPLATE};" || true
pg_restore -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -w -j 4 -d ${POSTGRES_DB_TEMPLATE} /tmp/${POSTGRES_DB}.psql

psql -d "${CREDENTIALS}" -w -c "ALTER DATABASE ${POSTGRES_DB_TEMPLATE} WITH is_template true allow_connections false;"

rm -v /tmp/${POSTGRES_DB}.psql

Puede restaurar varias bases de datos a la vez desde una plantilla sin ningún conflicto. Lo principal es que las conexiones a la base de datos deberían estar prohibidas, y la base de datos debería ser una plantilla. Esto se hace en el penúltimo paso.

El manifiesto que contiene el script de shell para restaurar la base de datos resultó así:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-configmap
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  review-load-dump.sh: |
    #!/bin/bash -x
    
 
 
    CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"

    if [ "$( psql -d "${CREDENTIALS}" -tAc "SELECT CASE WHEN EXISTS (SELECT * FROM pg_stat_activity WHERE datname = '${POSTGRES_DB}' LIMIT 1) THEN 1 ELSE 0 END;" )" = '1' ]
      then
          echo "Open connections has been found in ${POSTGRES_DB} database, will drop them"
          psql -d "${CREDENTIALS}" -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${POSTGRES_DB}' -- AND pid <> pg_backend_pid();"
      else
          echo "No open connections has been found ${POSTGRES_DB} database, skipping this stage"
    fi

    psql -d "${CREDENTIALS}" -c "DROP DATABASE ${POSTGRES_DB}"

    if [ "$( psql -d "${CREDENTIALS}" -tAc "SELECT 1 FROM pg_database WHERE datname='${POSTGRES_DB}'" )" = '1' ]
      then
          echo "Database ${POSTGRES_DB} still exists, delete review job failed"
          exit 1
      else
          echo "Database ${POSTGRES_DB} does not exist, skipping"
    fi


    psql ${CREDENTIALS} -d postgres -c 'CREATE DATABASE ${POSTGRES_DB} TEMPLATE "loot-stage-copy"'

Aparentemente, están involucrados aquí hook-delete-policy. Los detalles sobre la aplicación de estas políticas están escritos aquí . En el manifiesto dado usamos los before-hook-creation,hook-succeededque permiten cumplir los siguientes requisitos: eliminar el objeto anterior antes de crear un nuevo enlace y eliminar solo cuando el enlace fue exitoso.

Eliminaremos la base de datos en este mapa de configuración:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-configmap-on-delete
  annotations:
    "helm.sh/hook": "post-delete, pre-delete"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  review-delete-db.sh: |
    #!/bin/bash -e

    CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"

    psql -d "${CREDENTIALS}" -w postgres -c "DROP DATABASE ${POSTGRES_DB}"

Aunque lo trasladamos a un ConfigMap separado, se puede colocar en un ConfigMap normal command. Después de todo, se puede hacer una línea sin complicar la apariencia del manifiesto en sí.

Si la opción con las plantillas de PostgreSQL por alguna razón no se ajusta o no se ajusta, puede volver a la ruta de recuperación "estándar" mencionada anteriormente utilizando la copia de seguridad . El algoritmo será trivial:

  1. Todas las noches, se realiza una copia de seguridad de la base de datos para que se pueda descargar desde la red local del clúster.
  2. En el momento de la creación del entorno de revisión, la base de datos se carga y se restaura desde el volcado.
  3. Cuando se despliega el volcado, se realizan todas las demás acciones.

En este caso, el script de recuperación se convertirá aproximadamente de la siguiente manera:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-configmap
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  review-load-dump.sh: |
    #!/bin/bash -x

    CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"
    psql -d "${CREDENTIALS}" -w -c "DROP DATABASE ${POSTGRES_DB}" || true
    psql -d "${CREDENTIALS}" -w -c "CREATE DATABASE ${POSTGRES_DB}"

    curl --fail -vsL ${HOST_FORDEV}/latest_${POSTGRES_DB_STAGE}.psql -o /tmp/${POSTGRES_DB}.psql

    psql psql -d "${CREDENTIALS}" -w -c "CREATE EXTENSION ip4r;"
    pg_restore -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -w -j 4 -d ${POSTGRES_DB} /tmp/${POSTGRES_DB}.psql
    rm -v /tmp/${POSTGRES_DB}.psql

El procedimiento corresponde a lo que ya se ha descrito anteriormente. El único cambio es la eliminación del archivo psql después de que se haya agregado todo el trabajo.

Nota : tanto en el script de recuperación como en el script de desinstalación, la base de datos se elimina cada vez. Esto se hace para evitar posibles conflictos durante la recreación de la revisión: debe asegurarse de que la base de datos se elimine realmente. Además, este problema puede resolverse potencialmente agregando un indicador --cleanen la utilidad pg_restore, pero tenga cuidado: este indicador borra los datos de solo aquellos elementos que están en el volcado, por lo que en nuestro caso esta opción no funciona.

Como resultado, obtuvimos un mecanismo de trabajo que requiere mejoras adicionales (hasta reemplazar los scripts de Bash con un código más elegante). Los dejaremos fuera del alcance del artículo (aunque los comentarios sobre el tema, por supuesto, son bienvenidos).

Mongodb


El siguiente componente es MongoDB. La principal dificultad es que para este DBMS, la opción de copiar la base de datos (como en PostgreSQL) existe de manera bastante nominal, porque:

  1. Él está en un estado desaprobado .
  2. Según los resultados de nuestras pruebas, no encontramos una gran diferencia en el tiempo de recuperación de la base de datos en comparación con el habitual mongo_restore. Sin embargo, observo que las pruebas se llevaron a cabo como parte de un proyecto; en su caso, los resultados pueden ser completamente diferentes.

Resulta que en el caso de un gran volumen de base de datos, puede surgir un problema grave: ahorramos tiempo en la restauración de la base de datos en PgSQL, pero al mismo tiempo restauramos el volcado en Mongo durante mucho tiempo. Al momento de escribir, y dentro del marco de la infraestructura existente, vimos tres formas (por cierto, se pueden combinar):

  1. La recuperación puede llevar mucho tiempo, por ejemplo, si su DBMS está ubicado en un sistema de archivos de red (para casos no con entorno de producción). Luego, simplemente puede transferir el DBMS del escenario a un nodo separado y usar el almacenamiento local. Como esto no es producción, la velocidad de crear una revisión es más crítica para nosotros.
  2. Puede llevar cada recuperación de trabajo a un pod separado, lo que le permite ejecutar previamente migraciones y otros procesos que dependen del funcionamiento del DBMS. Así que ahorramos tiempo al completarlos por adelantado.
  3. A veces puede reducir el tamaño del volcado eliminando datos antiguos / irrelevantes, hasta el punto de que es suficiente dejar solo la estructura de la base de datos. Por supuesto, esto no es para aquellos casos en que se requiere un volcado completo (por ejemplo, para tareas de prueba de control de calidad).

Si no necesita crear rápidamente entornos de revisión, puede ignorar todas las dificultades descritas.

Nosotros, al no poder copiar la base de datos de manera similar a PgSQL, iremos por el primer camino, es decir recuperación estándar de respaldo. El algoritmo es el mismo que con PgSQL. Esto es fácil de ver si nos fijamos en los manifiestos:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb-scripts-on-delete
  annotations:
    "helm.sh/hook": "post-delete, pre-delete"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  review-delete-db.sh: |
    #!/bin/bash -x

    mongo ${MONGODB_NAME} --eval "db.dropDatabase()" --host ${MONGODB_REPLICASET}/${MONGODB_HOST}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb-scripts
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  review-load-dump.sh: |
    #!/bin/bash -x

    curl --fail -vsL ${HOST_FORDEV}/latest_${MONGODB_NAME_STAGE}.gz -o /tmp/${MONGODB_NAME}.gz

    mongo ${MONGODB_NAME} --eval "db.dropDatabase()" --host ${MONGODB_REPLICASET}/${MONGODB_HOST}
    mongorestore --gzip --nsFrom "${MONGODB_NAME_STAGE}.*" --nsTo "${MONGODB_NAME}.*" --archive=/tmp/${MONGODB_NAME}.gz --host ${MONGODB_REPLICASET}/${MONGODB_HOST}

Hay un detalle importante aquí. En nuestro caso, MongoDB está en el clúster y debe asegurarse de que la conexión siempre le ocurra al nodo primario . Si especifica, por ejemplo, el primer host en el quórum, luego de un tiempo puede cambiar de Primario a Secundario, lo que evitará la creación de una base de datos. Por lo tanto, debe conectarse no a un host, sino inmediatamente a ReplicaSet , enumerando todos los hosts en él. Solo por esta razón, debe hacer MongoDB como StatefulSet para que los nombres de host sean siempre los mismos (sin mencionar que MongoDB es una aplicación con estado por naturaleza). En esta opción, tiene la garantía de conectarse al nodo primario.

Para MongoDB, también eliminamos la base de datos antes de crear la revisión, esto se hace por las mismas razones que en PostgreSQL.

Último matiz: dado que la base de datos para revisión está en el mismo entorno que la etapa, se requiere un nombre diferente para la base de datos clonada. Si el volcado no es un archivo BSON, se producirá el siguiente error:

the --db and --collection args should only be used when restoring from a BSON file. Other uses are deprecated and will not exist in the future; use --nsInclude instead

Por lo tanto, en el ejemplo anterior, --nsFromy se utilizan --nsTo.

No encontramos otros problemas con la recuperación. Al final, solo agregaré que la documentación de copyDatabaseMongoDB está disponible aquí , en caso de que desee probar esta opción.

Rabbitmq


La última aplicación en nuestra lista de requisitos fue RabbitMQ. Es simple: necesita crear un nuevo vhost en nombre del usuario con el que se conectará la aplicación. Y luego bórralo.

Manifiesto para crear y eliminar vhosts:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-configmap
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  rabbitmq-setup-vhost.sh: |
    #!/bin/bash -x

    /usr/local/bin/rabbitmqadmin -H ${RABBITMQ_HOST} -u ${RABBITMQ_USER} -p ${RABBITMQ_PASSWORD} declare vhost name=${RABBITMQ_VHOST}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-configmap-on-delete
  annotations:
    "helm.sh/hook": "post-delete, pre-delete"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  rabbitmq-delete-vhost.sh: |
    #!/bin/bash -x

    /usr/local/bin/rabbitmqadmin -H ${RABBITMQ_HOST} -u ${RABBITMQ_USER} -p ${RABBITMQ_PASSWORD} delete vhost name=${RABBITMQ_VHOST}

Con grandes dificultades en RabbitMQ nosotros (¿hasta ahora?) No nos hemos encontrado. En general, el mismo enfoque puede aplicarse a cualquier otro servicio que no tenga un vínculo crítico para los datos.

desventajas


¿Por qué esta decisión no pretende ser "mejores prácticas"?

  1. Resulta un solo punto de falla en la forma de un entorno de escenario.
  2. Si una aplicación en un entorno de etapa se ejecuta solo en una réplica, dependemos aún más del host en el que se ejecuta esta aplicación. En consecuencia, con un aumento en el número de entornos de revisión, la carga en el nodo aumenta proporcionalmente sin la capacidad de equilibrar esta carga.

No fue posible resolver completamente estos dos problemas, teniendo en cuenta las capacidades de la infraestructura de un proyecto en particular, sin embargo, el daño potencial se puede minimizar mediante la agrupación (agregando nuevos nodos) y el escalado vertical.

Conclusión


A medida que la aplicación se desarrolla y con el aumento en el número de desarrolladores, tarde o temprano, la carga en los entornos de revisión aumenta y se les agregan nuevos requisitos. Es importante que los desarrolladores entreguen los próximos cambios en la producción lo más rápido posible, pero para que esto sea posible, necesitamos entornos de revisión dinámicos que hagan que el desarrollo sea "paralelo". Como resultado, la carga en la infraestructura está creciendo y el tiempo para crear tales entornos está aumentando.

Este artículo fue escrito en base a experiencias reales y bastante específicas. Solo en casos excepcionales aislamos los servicios en entornos estáticos, y aquí se trataba específicamente de él. Tal medida necesaria nos permitió acelerar el desarrollo y la depuración de la aplicación, gracias a la capacidad de crear rápidamente entornos de revisión desde cero.

Cuando comenzamos a hacer esta tarea, parecía muy simple, pero a medida que trabajábamos, encontramos muchos matices. Fueron ellos los que se reunieron en el artículo final: aunque no son universales, pueden servir como ejemplo para la base / inspiración de sus propias decisiones sobre entornos de revisión acelerados.

PD


Lea también en nuestro blog:


All Articles