Dicas e truques do Kubernetes: acelerando a inicialização de grandes bancos de dados

Com este artigo, abrimos uma série de publicações com instruções práticas sobre como facilitar a vida para nós (a operação) e desenvolvedores em várias situações que acontecem literalmente todos os dias. Todos eles são coletados da experiência real na solução de problemas dos clientes e melhoraram com o tempo, mas ainda não reivindicam o ideal - considere-os mais como idéias e espaços em branco.

Começarei com um "truque" na preparação de grandes despejos de banco de dados como MySQL e PostgreSQL para sua rápida implementação para diversas necessidades - antes de tudo, nas plataformas para desenvolvedores. O contexto das operações descritas abaixo é o nosso ambiente típico, que inclui um cluster Kubernetes em funcionamento e o uso do GitLab (e dapp ) para CI / CD. Vamos lá!



O principal problema do Kubernetes ao usar a ramificação de recursos são os bancos de dados grandes, quando os desenvolvedores desejam testar / demonstrar suas alterações em um banco de dados completo (ou quase completo) da produção. Por exemplo:

  • Há um aplicativo com um banco de dados no MySQL para 1 TB e 10 desenvolvedores que desenvolvem seus próprios recursos.
  • Os desenvolvedores desejam loops de teste individuais e mais alguns loops específicos para testes e / ou demos.
  • Além disso, é necessário restaurar o despejo noturno da base de produção em seu circuito de teste por um tempo sensato - para reproduzir o problema com o cliente ou bug.
  • Por fim, é possível diminuir o tamanho do banco de dados em pelo menos 150 GB - não muito, mas ainda economizando espaço. I.e. ainda precisamos de alguma forma preparar o despejo.

Nota : Geralmente, fazemos backup dos bancos de dados MySQL usando o innobackupex da Percona, o que nos permite salvar todos os bancos de dados e usuários ... - em resumo, tudo o que for necessário. Esse exemplo é considerado mais adiante no artigo, embora, no caso geral, não importe exatamente como você faz os backups.

Então, digamos que temos um backup do banco de dados. O que fazer depois?

Etapa 1: preparando um novo banco de dados a partir do dump


Primeiro, criaremos no Kubernetes Deployment , que consistirá em dois contêineres init (ou seja, contêineres especiais que são executados antes dos fornos de aplicativos e permitem executar a pré-configuração) e um recuperador.

Mas onde colocá-lo? Temos um banco de dados grande (1 TB) e queremos aumentar dez de suas instâncias - precisamos de um servidor com um disco grande (10+ TB). Pedimos separadamente para esta tarefa e marcamos o nó com este servidor com um rótulo especial dedicated: non-prod-db . Ao mesmo tempo, usaremos a mancha homônima, que o Kubernetes dirá que apenas aplicativos que são resistentes (com tolerâncias ) a ele podem rolar para esse nó, ou seja, traduzindo o Kubernetes para o idioma, dedicated Equal non-prod-db .

Usando nodeSelector e tolerations selecione o nó desejado (localizado em um servidor com um disco grande):

  nodeSelector: dedicated: non-prod-db tolerations: - key: "dedicated" operator: "Equal" value: "non-prod-db" effect: "NoExecute" 

... e pegue a descrição do conteúdo deste nó.

Recipientes de inicialização: get-bindump


O primeiro contêiner init que chamaremos de get-bindump . Ele monta emptyDir (em /var/lib/mysql ), onde o dump do banco de dados recebido do servidor de backup será adicionado. Para fazer isso, o contêiner possui tudo o que você precisa: chaves SSH, endereços de servidor de backup. Esta etapa no nosso caso leva cerca de 2 horas.

A descrição desse contêiner no Deployment é a seguinte:

  - name: get-bindump image: db-dumps imagePullPolicy: Always command: [ "/bin/sh", "-c", "/get_bindump.sh" ] resources: limits: memory: "5000Mi" cpu: "1" requests: memory: "5000Mi" cpu: "1" volumeMounts: - name: dump mountPath: /dump - name: mysqlbindir mountPath: /var/lib/mysql - name: id-rsa mountPath: /root/.ssh 

O script get_bindump.sh usado no contêiner:

 #!/bin/bash date if [ -f /dump/version.txt ]; then echo "Dump file already exists." exit 0 fi rm -rf /var/lib/mysql/* borg extract --stdout user@your.server.net:somedb-mysql::${lastdump} stdin | xbstream -x -C /var/lib/mysql/ echo $lastdump > /dump/version.txt 

Containers de inicialização: prepare-bindump


Após o download do backup, o segundo contêiner de inicialização é iniciado - prepare-bindump . Ele executa innobackupex --apply-log (já que os arquivos já estão disponíveis em /var/lib/mysql - graças ao emptyDir de get-bindump ) e o servidor MySQL é iniciado.

É nesse contêiner init que fazemos todas as conversões necessárias no banco de dados, preparando-o para o aplicativo selecionado: limpamos as tabelas para as quais é permitido, alteramos acessos dentro do banco de dados etc. Em seguida, desligamos o servidor MySQL e simplesmente arquivamos o arquivo /var/lib/mysql inteiro em um arquivo tar.gz. Como resultado, o dump se encaixa em um arquivo de 100 GB, que já é uma ordem de magnitude menor que o 1 TB original. Esta etapa leva cerca de 5 horas.

Descrição do segundo contêiner init no Deployment :

  - name: prepare-bindump image: db-dumps imagePullPolicy: Always command: [ "/bin/sh", "-c", "/prepare_bindump.sh" ] resources: limits: memory: "5000Mi" cpu: "1" requests: memory: "5000Mi" cpu: "1" volumeMounts: - name: dump mountPath: /dump - name: mysqlbindir mountPath: /var/lib/mysql - name: debian-cnf mountPath: /etc/mysql/debian.cnf subPath: debian.cnf 

O script prepare_bindump.sh usado nele se parece com isso:

 #!/bin/bash date if [ -f /dump/healthz ]; then echo "Dump file already exists." exit 0 fi innobackupex --apply-log /var/lib/mysql/ chown -R mysql:mysql /var/lib/mysql chown -R mysql:mysql /var/log/mysql echo "`date`: Starting mysql" /usr/sbin/mysqld --character-set-server=utf8 --collation-server=utf8_general_ci --innodb-data-file-path=ibdata1:200M:autoextend --user=root --skip-grant-tables & sleep 200 echo "`date`: Creating mysql root user" echo "update mysql.user set Password=PASSWORD('password') WHERE user='root';" | mysql -uroot -h 127.0.0.1 echo "delete from mysql.user where USER like '';" | mysql -uroot -h 127.0.0.1 echo "delete from mysql.user where user = 'root' and host NOT IN ('127.0.0.1', 'localhost');" | mysql -uroot -h 127.0.0.1 echo "FLUSH PRIVILEGES;" | mysql -uroot -h 127.0.0.1 echo "truncate somedb.somedb_table_one;" | mysql -uroot -h 127.0.0.1 -ppassword somedb /usr/bin/mysqladmin shutdown -uroot -ppassword cd /var/lib/mysql/ tar -czf /dump/mysql_bindump.tar.gz ./* touch /dump/healthz rm -rf /var/lib/mysql/* 

Sob


O acorde final é o lançamento da lareira principal, que ocorre após a execução dos contêineres init. No pod, temos um nginx simples e, por meio do emtpyDir dump compactado e cortado de 100 GB é emtpyDir . A função desse nginx é fornecer esse despejo.

Configuração da lareira:

  - name: nginx image: nginx:alpine resources: requests: memory: "1500Mi" cpu: "400m" lifecycle: preStop: exec: command: ["/usr/sbin/nginx", "-s", "quit"] livenessProbe: httpGet: path: /healthz port: 80 scheme: HTTP timeoutSeconds: 7 failureThreshold: 5 volumeMounts: - name: dump mountPath: /usr/share/nginx/html - name: nginx-config mountPath: /etc/nginx/nginx.conf subPath: nginx.conf readOnly: false volumes: - name: dump emptyDir: {} - name: mysqlbindir emptyDir: {} 

É assim que o Deployment se parece com seus initContainers ...
 --- apiVersion: apps/v1beta1 kind: Deployment metadata: name: db-dumps spec: strategy: rollingUpdate: maxUnavailable: 0 revisionHistoryLimit: 2 template: metadata: labels: app: db-dumps spec: imagePullSecrets: - name: regsecret nodeSelector: dedicated: non-prod-db tolerations: - key: "dedicated" operator: "Equal" value: "non-prod-db" effect: "NoExecute" initContainers: - name: get-bindump image: db-dumps imagePullPolicy: Always command: [ "/bin/sh", "-c", "/get_bindump.sh" ] resources: limits: memory: "5000Mi" cpu: "1" requests: memory: "5000Mi" cpu: "1" volumeMounts: - name: dump mountPath: /dump - name: mysqlbindir mountPath: /var/lib/mysql - name: id-rsa mountPath: /root/.ssh - name: prepare-bindump image: db-dumps imagePullPolicy: Always command: [ "/bin/sh", "-c", "/prepare_bindump.sh" ] resources: limits: memory: "5000Mi" cpu: "1" requests: memory: "5000Mi" cpu: "1" volumeMounts: - name: dump mountPath: /dump - name: mysqlbindir mountPath: /var/lib/mysql - name: log mountPath: /var/log/mysql - name: debian-cnf mountPath: /etc/mysql/debian.cnf subPath: debian.cnf containers: - name: nginx image: nginx:alpine resources: requests: memory: "1500Mi" cpu: "400m" lifecycle: preStop: exec: command: ["/usr/sbin/nginx", "-s", "quit"] livenessProbe: httpGet: path: /healthz port: 80 scheme: HTTP timeoutSeconds: 7 failureThreshold: 5 volumeMounts: - name: dump mountPath: /usr/share/nginx/html - name: nginx-config mountPath: /etc/nginx/nginx.conf subPath: nginx.conf readOnly: false volumes: - name: dump emptyDir: {} - name: mysqlbindir emptyDir: {} - name: log emptyDir: {} - name: id-rsa secret: defaultMode: 0600 secretName: somedb-id-rsa - name: nginx-config configMap: name: somedb-nginx-config - name: debian-cnf configMap: name: somedb-debian-cnf --- apiVersion: v1 kind: Service metadata: name: somedb-db-dump spec: clusterIP: None selector: app: db-dumps ports: - name: http port: 80 

Notas adicionais:

  1. No nosso caso, preparamos um novo despejo toda noite usando o trabalho agendado no GitLab. I.e. todas as noites, essa implantação é lançada automaticamente, o que gera um novo despejo e o prepara para distribuição em todos os ambientes de desenvolvedor de teste.
  2. Por que também estamos lançando volume /dump nos contêineres init (e no script há uma verificação da existência de /dump/version.txt )? Isso é feito caso o servidor em que ele é executado seja reiniciado. Os contêineres serão reiniciados e, sem essa verificação, o dump começará a baixar novamente. Se já preparamos um dump uma vez, na próxima inicialização (no caso de uma reinicialização do servidor), o /dump/version.txt sinalizador /dump/version.txt informará sobre isso.
  3. Qual é a imagem db-dumps ? Nós o coletamos com o dapp e seu Dappfile fica assim:

     dimg: "db-dumps" from: "ubuntu:16.04" docker: ENV: TERM: xterm ansible: beforeInstall: - name: "Install percona repositories" apt: deb: https://repo.percona.com/apt/percona-release_0.1-4.xenial_all.deb - name: "Add repository for borgbackup" apt_repository: repo="ppa:costamagnagianfranco/borgbackup" codename="xenial" update_cache=yes - name: "Add repository for mysql 5.6" apt_repository: repo: deb http://archive.ubuntu.com/ubuntu trusty universe state: present update_cache: yes - name: "Install packages" apt: name: "{{`{{ item }}`}}" state: present with_items: - openssh-client - mysql-server-5.6 - mysql-client-5.6 - borgbackup - percona-xtrabackup-24 setup: - name: "Add get_bindump.sh" copy: content: | {{ .Files.Get ".dappfiles/get_bindump.sh" | indent 8 }} dest: /get_bindump.sh mode: 0755 - name: "Add prepare_bindump.sh" copy: content: | {{ .Files.Get ".dappfiles/prepare_bindump.sh" | indent 8 }} dest: /prepare_bindump.sh mode: 0755 

Etapa 2: iniciando o banco de dados em um ambiente de desenvolvedor


Ao lançar o banco de dados MySQL no ambiente de teste do desenvolvedor, ele possui um botão no GitLab que inicia a reimplantação da Implantação no MySQL com a estratégia RollingUpdate.maxUnavailable: 0 :



Como isso é implementado?
No GitLab, quando você clica em recarregar db , a implantação com a seguinte especificação é implantada:

 spec: strategy: rollingUpdate: maxUnavailable: 0 

I.e. pedimos ao Kubernetes para atualizar o Deployment (criar um novo abaixo) e garantir que pelo menos um abaixo esteja ativo. Como ao criar uma nova lareira, ela possui contêineres init enquanto eles estão trabalhando, a nova não entra no status Em execução , o que significa que a antiga continua a funcionar. E somente no momento em que o próprio MySQL foi iniciado (e a sonda de prontidão funcionou), o tráfego muda para ele e o antigo (com o banco de dados antigo) é excluído.

Detalhes sobre esse esquema podem ser encontrados nos seguintes materiais:


A abordagem escolhida nos permite esperar até que um novo dump seja baixado, descompactado e iniciado, e somente depois disso o antigo será excluído do MySQL. Assim, enquanto preparamos um novo depósito, estamos trabalhando silenciosamente com a base antiga.

O contêiner init desta implantação usa o seguinte comando:

 curl "$DUMP_URL" | tar -C /var/lib/mysql/ -xvz 

I.e. baixamos o dump do banco de dados compactado que foi preparado na etapa 1, descompacte-o em /var/lib/mysql e, em seguida, inicia-se em Deployment , no qual o MySQL é iniciado com os dados já preparados. Tudo isso leva cerca de 2 horas.

E a implantação é a seguinte ...
 apiVersion: apps/v1beta1 kind: Deployment metadata: name: mysql spec: strategy: rollingUpdate: maxUnavailable: 0 template: metadata: labels: service: mysql spec: imagePullSecrets: - name: regsecret nodeSelector: dedicated: non-prod-db tolerations: - key: "dedicated" operator: "Equal" value: "non-prod-db" effect: "NoExecute" initContainers: - name: getdump image: mysql-with-getdump command: ["/usr/local/bin/getdump.sh"] resources: limits: memory: "6000Mi" cpu: "1.5" requests: memory: "6000Mi" cpu: "1.5" volumeMounts: - mountPath: /var/lib/mysql name: datadir - mountPath: /etc/mysql/debian.cnf name: debian-cnf subPath: debian.cnf env: - name: DUMP_URL value: "http://somedb-db-dump.infra-db.svc.cluster.local/mysql_bindump.tar.gz" containers: - name: mysql image: mysql:5.6 resources: limits: memory: "1024Mi" cpu: "1" requests: memory: "1024Mi" cpu: "1" lifecycle: preStop: exec: command: ["/etc/init.d/mysql", "stop"] ports: - containerPort: 3306 name: mysql protocol: TCP volumeMounts: - mountPath: /var/lib/mysql name: datadir - mountPath: /etc/mysql/debian.cnf name: debian-cnf subPath: debian.cnf env: - name: MYSQL_ROOT_PASSWORD value: "password" volumes: - name: datadir emptyDir: {} - name: debian-cnf configMap: name: somedb-debian-cnf --- apiVersion: v1 kind: Service metadata: name: mysql spec: clusterIP: None selector: service: mysql ports: - name: mysql port: 3306 protocol: TCP --- apiVersion: v1 kind: ConfigMap metadata: name: somedb-debian-cnf data: debian.cnf: | [client] host = localhost user = debian-sys-maint password = password socket = /var/run/mysqld/mysqld.sock [mysql_upgrade] host = localhost user = debian-sys-maint password = password socket = /var/run/mysqld/mysqld.sock 

Sumário


Acontece que sempre temos o Deployment , que é lançado todas as noites e faz o seguinte:

  • Obtém um novo despejo de banco de dados
  • de alguma forma, ele o prepara para a operação correta em um ambiente de teste (por exemplo, trankeytit algumas tabelas, substitui dados reais do usuário, torna os usuários necessários, etc.);
  • fornece a cada desenvolvedor a oportunidade de lançar um banco de dados preparado para seu namespace no Deployment pressionando um botão no CI - graças ao Serviço disponível nele, o banco de dados estará disponível no mysql (por exemplo, pode ser o nome do serviço no namespace).

Para o exemplo que examinamos, a criação de um despejo a partir de uma réplica real leva cerca de 6 horas, a preparação de uma "imagem base" leva 7 horas e a atualização do banco de dados no ambiente do desenvolvedor leva 2 horas. Como as duas primeiras ações são executadas “em segundo plano” e são invisíveis para os desenvolvedores, na verdade, eles podem implantar uma versão de produção do banco de dados (com um tamanho de 1 TB) pelas mesmas 2 horas .

Perguntas, críticas e correções ao esquema proposto e seus componentes são bem-vindas nos comentários!

PS Obviamente, entendemos que, no caso do VMware e de algumas outras ferramentas, seria possível criar um instantâneo de uma máquina virtual e lançar um novo virusalka a partir de um instantâneo (que é ainda mais rápido), mas essa opção não inclui a preparação da base, levando em consideração o que será o mesmo tempo ... Sem mencionar o fato de que nem todos têm a oportunidade ou o desejo de usar produtos comerciais.

PPS


Outro do ciclo de dicas e truques do K8s:


Leia também em nosso blog:

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


All Articles