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