Da vida com o Kubernetes: como removemos o DBMS (e não apenas) dos ambientes de revisão para os estáticos



Nota : este artigo não afirma ser uma prática recomendada. Ele descreve a experiência de uma implementação específica de uma tarefa de infraestrutura em termos de uso do Kubernetes e Helm, que pode ser útil na solução de problemas relacionados.

O uso de ambientes de revisão no CI / CD pode ser muito útil, tanto para desenvolvedores quanto para engenheiros de sistema. Vamos primeiro sincronizar as idéias gerais sobre eles:

  1. Os ambientes de revisão podem ser criados a partir de ramificações separadas nos repositórios Git definidos pelos desenvolvedores (as chamadas ramificações de recursos).
  2. Eles podem ter instâncias DBMS separadas, processadores de filas, serviços de cache, etc. - em geral, tudo para a reprodução completa do ambiente de produção.
  3. Eles permitem o desenvolvimento paralelo, acelerando significativamente o lançamento de novos recursos no aplicativo. Ao mesmo tempo, dezenas desses ambientes podem ser necessários todos os dias, e é por isso que a velocidade de sua criação é crítica.

Na interseção do segundo e terceiro pontos, surgem frequentemente dificuldades: como a infraestrutura é muito diferente, seus componentes podem ser implantados por um longo tempo. Esse tempo gasto, por exemplo, inclui a restauração do banco de dados a partir de um backup já preparado *. O artigo é sobre a maneira fascinante como resolvemos resolver esse problema.

* A propósito, especificamente sobre grandes despejos de banco de dados nesse contexto, já escrevemos no material sobre como acelerar o banco de dados de autoinicialização .)

O problema e a maneira de resolvê-lo


Em um dos projetos, recebemos a tarefa de "criar um único ponto de entrada para desenvolvedores e engenheiros de controle de qualidade". Esta formulação escondeu tecnicamente o seguinte:

  1. Para simplificar o trabalho dos engenheiros de controle de qualidade e de alguns outros funcionários, retire todos os bancos de dados (e vhosts correspondentes) usados ​​na revisão, em um ambiente estático separado. Pelas razões prevalecentes no projeto, essa maneira de interagir com elas foi ótima.
  2. Reduza o tempo necessário para criar um ambiente de revisão. Todo o processo de sua criação a partir do zero está implícito, ou seja, incluindo clonagem de banco de dados, migrações etc.

Do ponto de vista da implementação, o principal problema se resume a garantir a idempotência ao criar e excluir ambientes de revisão. Para isso, alteramos o mecanismo de criação de ambientes de revisão migrando primeiro os serviços PostgreSQL, MongoDB e RabbitMQ para um ambiente estático. Estático refere-se a um ambiente "permanente" que não será criado a pedido do usuário (como é o caso dos ambientes de revisão).

Importante! A abordagem com um ambiente estático está longe de ser ideal - por suas deficiências específicas, consulte o final do artigo. No entanto, compartilhamos essa experiência em detalhes, pois ela pode ser mais ou menos aplicável em outras tarefas e, ao mesmo tempo, serve de argumento ao discutir questões de design de infraestrutura.

Então, a sequência de ações na implementação:

  • Ao criar um ambiente de revisão, o seguinte deve ocorrer uma vez: a criação de bancos de dados em dois DBMSs (MongoDB e PostgreSQL), a restauração de bancos de dados de um backup / modelo e a criação de vhost no RabbitMQ. Isso exigirá uma maneira conveniente de carregar dumps atuais. (Se você já tinha revisado ambientes antes, provavelmente já possui uma solução pronta para isso.)
  • Após a conclusão do ambiente de revisão, você deve excluir o banco de dados e o host virtual no RabbitMQ.

No nosso caso, a infraestrutura opera dentro da estrutura do Kubernetes (usando Helm). Portanto, para a implementação das tarefas acima, os ganchos Helm eram excelentes . Eles podem ser executados antes da criação de todos os outros componentes na versão Helm e / ou após a remoção. Portanto:

  • para a tarefa de inicialização, usaremos um gancho pre-installpara iniciá-lo antes de criar todos os recursos no release;
  • para a tarefa de exclusão, um gancho post-delete.

Vamos passar aos detalhes da implementação.

Implementação prática


Na versão original, este projeto usava apenas um trabalho, composto por três contêineres. Obviamente, isso não é totalmente conveniente, pois o resultado é um grande manifesto que é muito difícil de ler. Portanto, dividimos em três pequenos trabalhos.

A seguir, é apresentada uma lista do PostgreSQL, e os outros dois (MongoDB e RabbitMQ) são idênticos na estrutura do manifesto:

{{- 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 }}

Comentários sobre o conteúdo do manifesto:

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

PostgreSQL


O mais interessante está no script de shell ( review-load-dump.sh) já mencionado na lista . Quais são as opções gerais para restaurar um banco de dados no PostgreSQL?

  1. Recuperação "padrão" do backup;
  2. Recuperação usando modelos .

No nosso caso, a diferença entre as duas abordagens está principalmente na velocidade de criação de um banco de dados para o novo ambiente. No primeiro - carregamos o dump do banco de dados e o restauramos pg_restore. E conosco isso acontece mais lentamente que o segundo método, portanto, a escolha correspondente foi feita.

Usando a segunda opção ( recuperação com modelos), você pode clonar o banco de dados no nível físico sem enviar dados para ele remotamente do contêiner em outro ambiente - isso reduz o tempo de recuperação. No entanto, há uma limitação: você não pode clonar um banco de dados no qual as conexões ativas permanecem. Como usamos o estágio como o ambiente estático (e não um ambiente de revisão separado), precisamos criar um segundo banco de dados e convertê-lo em um modelo, atualizando-o diariamente (por exemplo, pela manhã). Um pequeno CronJob foi preparado para isso:

---
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 }}

O manifesto completo do ConfigMap que contém o script provavelmente não faz muito sentido (relate nos comentários se esse não for o caso). Em vez disso, darei a coisa mais importante - um 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

Você pode restaurar vários bancos de dados ao mesmo tempo a partir de um modelo sem conflitos. O principal é que as conexões com o banco de dados devem ser proibidas e o próprio banco de dados deve ser um modelo. Isso é feito na penúltima etapa.

O manifesto que contém o script de shell para restaurar o banco de dados ficou assim:

---
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, eles estão envolvidos aqui hook-delete-policy. Detalhes sobre a aplicação dessas políticas estão escritos aqui . No manifesto fornecido, usamos o before-hook-creation,hook-succeededque permite atender aos seguintes requisitos: exclua o objeto anterior antes de criar um novo gancho e exclua somente quando o gancho tiver êxito.

Excluiremos o banco de dados neste ConfigMap:

---
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}"

Embora o tenhamos movido para um ConfigMap separado, ele pode ser colocado em um ConfigMap regular command. Afinal, ele pode ser transformado em uma linha sem complicar a aparência do próprio manifesto.

Se a opção com os modelos do PostgreSQL, por algum motivo, não se adequar ou não se encaixar, você poderá retornar ao caminho de recuperação "padrão" mencionado acima usando o backup . O algoritmo será trivial:

  1. Todas as noites, é feito um backup do banco de dados para que possa ser baixado da rede local do cluster.
  2. No momento da criação do ambiente de revisão, o banco de dados é carregado e restaurado a partir do dump.
  3. Quando o dump é implantado, todas as outras ações são executadas.

Nesse caso, o script de recuperação se tornará aproximadamente da seguinte maneira:

---
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

O procedimento corresponde ao que já foi descrito acima. A única alteração é a remoção do arquivo psql após todo o trabalho ter sido adicionado.

Nota : no script de recuperação e no script de desinstalação, o banco de dados é excluído todas as vezes. Isso é feito para evitar possíveis conflitos durante a recriação da revisão: você precisa garantir que o banco de dados seja realmente excluído. Além disso, esse problema pode ser resolvido com a adição de um sinalizador --cleanno utilitário pg_restore, mas tenha cuidado: esse sinalizador limpa os dados apenas dos elementos que estão no próprio despejo, portanto, no nosso caso, essa opção não funciona.

Como resultado, obtivemos um mecanismo funcional que requer melhorias adicionais (até a substituição de scripts Bash por um código mais elegante). Vamos deixá-los fora do escopo do artigo (embora os comentários sobre o tópico, é claro, sejam bem-vindos).

Mongodb


O próximo componente é o MongoDB. A principal dificuldade é que, para este DBMS, a opção de copiar o banco de dados (como no PostgreSQL) existe de maneira bastante nominal, porque:

  1. Ele está em um estado obsoleto .
  2. De acordo com os resultados de nossos testes, não encontramos uma grande diferença no tempo de recuperação do banco de dados em comparação com o usual mongo_restore. No entanto, observo que o teste foi realizado como parte de um projeto - no seu caso, os resultados podem ser completamente diferentes.

Acontece que, no caso de um grande volume de banco de dados, um sério problema pode surgir: economizamos tempo ao restaurar o banco de dados no PgSQL, mas, ao mesmo tempo, restauramos o despejo no Mongo por muito tempo. No momento da redação deste artigo, e dentro da estrutura da infraestrutura existente, vimos três maneiras (a propósito, elas podem ser combinadas):

  1. A recuperação pode demorar, por exemplo, se o DBMS estiver localizado em um sistema de arquivos de rede (para casos que não estejam no ambiente de produção). Em seguida, você pode simplesmente transferir o DBMS do palco para um nó separado e usar o armazenamento local. Como isso não é produção, a velocidade de criação de uma revisão é mais crítica para nós.
  2. Você pode levar cada recuperação de tarefa para um pod separado, permitindo pré-executar migrações e outros processos que dependem da operação do DBMS. Assim, economizamos tempo preenchendo-os com antecedência.
  3. Às vezes, você pode reduzir o tamanho do dump excluindo dados antigos / irrelevantes - até o ponto em que é suficiente deixar apenas a estrutura do banco de dados. Obviamente, isso não ocorre nos casos em que é necessário um dump completo (por exemplo, para tarefas de teste de controle de qualidade).

Se você não precisar criar rapidamente ambientes de revisão, todas as dificuldades descritas poderão ser ignoradas.

Não sendo possível copiar o banco de dados de maneira semelhante ao PgSQL, seguiremos o primeiro caminho, ou seja, recuperação padrão do backup. O algoritmo é o mesmo que com o PgSQL. É fácil ver se você olha para os manifestos:

---
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}

Há um detalhe importante aqui. No nosso caso, o MongoDB está no cluster e você precisa ter certeza de que a conexão sempre acontece com o nó Primário . Se você especificar, por exemplo, o primeiro host no quorum, depois de algum tempo poderá mudar de Primário para Secundário, o que impedirá a criação de um banco de dados. Portanto, você precisa se conectar não a um host, mas imediatamente ao ReplicaSet , listando todos os hosts nele. Por esse motivo, é necessário tornar o MongoDB como um StatefulSet para que os nomes de host sejam sempre os mesmos (sem mencionar que o MongoDB é um aplicativo com estado por natureza). Nesta opção, você garante a conexão com o nó Primário.

Para o MongoDB, também excluímos o banco de dados antes de criar a revisão - isso é feito pelos mesmos motivos que no PostgreSQL.

Última nuance: como o banco de dados para revisão está no mesmo ambiente que o estágio, é necessário um nome separado para o banco de dados clonado. Se o despejo não for um arquivo BSON, ocorrerá o seguinte erro:

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

Portanto, no exemplo acima, --nsFrome são usados --nsTo.

Não encontramos outros problemas com a recuperação. No final, acrescentarei apenas que a documentação do copyDatabaseMongoDB está disponível aqui - caso você queira experimentar esta opção.

Rabbitmq


A última aplicação em nossa lista de requisitos foi o RabbitMQ. É simples: você precisa criar um novo vhost em nome do usuário com o qual o aplicativo se conectará. E depois exclua.

Manifesto para criar e remover 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}

Com grandes dificuldades no RabbitMQ nós (até agora?) Não encontramos. Em geral, a mesma abordagem pode ser aplicada a qualquer outro serviço que não tenha um vínculo crítico com os dados.

desvantagens


Por que essa decisão não afirma ser “melhores práticas”?

  1. Acontece um único ponto de falha na forma de um ambiente de palco.
  2. Se um aplicativo em um ambiente de estágio for executado apenas em uma réplica, ficaremos ainda mais dependentes do host no qual esse aplicativo é executado. Consequentemente, com um aumento no número de ambientes de revisão, a carga no nó aumenta proporcionalmente sem a capacidade de equilibrar essa carga.

Não foi possível resolver completamente esses dois problemas, levando em consideração os recursos da infraestrutura de um projeto específico; no entanto, os danos em potencial podem ser minimizados com o agrupamento (adição de novos nós) e o dimensionamento vertical.

Conclusão


À medida que o aplicativo se desenvolve e com o aumento do número de desenvolvedores, mais cedo ou mais tarde, a carga nos ambientes de revisão aumenta e novos requisitos são adicionados a eles. É importante que os desenvolvedores entreguem as próximas alterações na produção o mais rápido possível, mas para tornar isso possível, precisamos de ambientes de revisão dinâmica que tornem o desenvolvimento "paralelo". Como resultado, a carga na infraestrutura está aumentando e o tempo para a criação desses ambientes está aumentando.

Este artigo foi escrito com base em experiências reais e bastante específicas. Somente em casos excepcionais isolamos quaisquer serviços em ambientes estáticos, e aqui era especificamente sobre ele. Uma medida tão necessária nos permitiu acelerar o desenvolvimento e a depuração do aplicativo - graças à capacidade de criar rapidamente ambientes de revisão do zero.

Quando começamos a fazer essa tarefa, parecia muito simples, mas, ao trabalharmos, descobrimos muitas nuances. Foram eles que foram reunidos no artigo final: embora não sejam universais, eles podem servir de exemplo para a base / inspiração de suas próprias decisões sobre a aceleração dos ambientes de revisão.

PS


Leia também no nosso blog:


All Articles