Migrando da DigitalOcean para a Hetzner
(isayeter.com)- Uma infraestrutura de produção de US$ 1.432/mês foi migrada para um servidor dedicado de US$ 233/mês, com troca até do sistema operacional e ainda assim mantendo a continuidade do serviço sem downtime
- Após recriar no novo servidor 30 bancos de dados MySQL e 34 hosts virtuais do Nginx, além de GitLab EE, Neo4J, Supervisor e Gearman, a migração foi concluída com replicação em tempo real e sincronização incremental final
- O ponto central da migração de banco de dados foi a combinação de processamento paralelo com mydumper·myloader e replicação do MySQL, incluindo a correção de problemas de schema
syse permissões surgidos ao atualizar do MySQL 5.7 para o 8.0 - O cutover foi feito na sequência de redução do TTL do DNS, conversão do Nginx do servidor antigo em reverse proxy e alteração em massa dos registros A, de modo que requisições ao IP antigo continuassem sendo encaminhadas ao novo servidor durante a propagação do DNS
- Como resultado, o caso alcançou economia de US$ 1.199 por mês, US$ 14.388 por ano, além de upgrade de CPU, memória e armazenamento com 0 minuto de downtime
Contexto da migração
- Em um ambiente de operação de uma empresa de software na Turquia, a combinação de inflação acelerada e desvalorização da lira turca aumentou fortemente o peso dos custos de infraestrutura em dólar
- O custo mensal do servidor existente na DigitalOcean era de US$ 1.432, com configuração de 192GB de RAM, 32 vCPU, 600GB SSD, 2 volumes em bloco de 1TB e backups inclusos
- O novo destino foi um servidor dedicado Hetzner AX162-R, com AMD EPYC 9454P de 48 núcleos e 96 threads, 256GB DDR5 e 1,92TB NVMe Gen4 em RAID1
- O custo mensal caiu para US$ 233, com economia de US$ 1.199 por mês e US$ 14.388 por ano
- Não havia insatisfação com a confiabilidade do servidor anterior nem com a experiência de desenvolvedor, mas em workloads de estado estável o custo-benefício já não fazia mais sentido
Ambiente operacional existente
- A stack em operação não era um ambiente simples de teste, e sim uma configuração real de produção
- 30 bancos de dados MySQL, totalizando 248GB de dados
- 34 hosts virtuais do Nginx operando em vários domínios
- GitLab EE com backup de 42GB
- Neo4J Graph DB com 30GB
- Supervisor gerenciando dezenas de workers em background
- Uso de fila de tarefas Gearman
- Operação de app mobile em produção para centenas de milhares de usuários
- O sistema operacional do servidor antigo era CentOS 7, já fora de suporte
- O sistema operacional do novo servidor é AlmaLinux 9.7, uma distribuição compatível com RHEL 9 e sucessora natural do CentOS
- Esta migração serviu não só para reduzir custos, mas também para sair de um sistema operacional que já não recebia atualizações de segurança havia anos
Estratégia sem downtime
- Em vez de aceitar um simples cambio de DNS com reinício de serviços, a migração sem downtime foi executada em 6 etapas
-
Etapa 1: instalar toda a stack no novo servidor
- Instalação do Nginx compilado do código-fonte com as mesmas flags do servidor antigo
- Instalação do PHP via Remi repo, aplicando os mesmos arquivos de configuração
.inido servidor antigo - Instalação de MySQL 8.0, Neo4J Graph DB, GitLab EE, Node.js, Supervisor e Gearman, configurados para corresponder ao funcionamento anterior
- Antes de mexer nos registros DNS, todos os serviços já estavam ajustados para funcionar de forma idêntica ao servidor antigo
- Os certificados SSL foram tratados copiando com rsync todo o diretório
/etc/letsencrypt/do servidor antigo - Depois que todo o tráfego foi transferido para o novo servidor, foi executado
certbot renew --force-renewalpara renovação forçada em massa dos certificados
-
Etapa 2: replicação dos arquivos web com rsync
- Todo o diretório
/var/www/html, cerca de 65GB e 1,5 milhão de arquivos, foi replicado comrsyncvia SSH - A opção
--checksumfoi usada para verificação de integridade - Logo antes do cutover, foi feita uma sincronização incremental final para refletir os arquivos alterados
- Todo o diretório
-
Etapa 3: replicação master-slave do MySQL
- Em vez de derrubar os bancos para fazer dump e restore, foi configurada replicação em tempo real
- O servidor antigo foi definido como master e o novo servidor como slave em modo somente leitura
- A carga inicial de grande volume foi feita com
mydumper, e depois a replicação começou exatamente da posição do binlog registrada nos metadados do dump - Até o momento do cutover, os dois bancos permaneceram sincronizados em tempo real
-
Etapa 4: reduzir o TTL do DNS
- Um script chamou a API da DigitalOcean DNS para reduzir o TTL de todos os registros A/AAAA de 3600 segundos para 300 segundos
- Os registros MX e TXT não foram alterados
- A mudança de TTL dos registros de e-mail foi excluída para evitar possíveis problemas de entregabilidade
- Depois de esperar 1 hora para o TTL antigo expirar globalmente, o ambiente ficou pronto para o cutover em até 5 minutos
-
Etapa 5: converter o Nginx do servidor antigo em reverse proxy
- Um script em Python fez parsing dos blocos
server {}nas 34 configurações de sites do Nginx - A configuração antiga foi salva em backup e substituída por configuração de proxy apontando para o novo servidor
- Assim, mesmo durante a propagação do DNS, requisições que chegassem ao IP antigo eram encaminhadas silenciosamente ao novo servidor
- Do ponto de vista do usuário, não havia interrupção visível
- Um script em Python fez parsing dos blocos
-
Etapa 6: cutover do DNS e desligamento do servidor antigo
- Um script em Python chamou a API da DigitalOcean para alterar todos os registros A para o IP do novo servidor em poucos segundos
- O servidor antigo foi mantido por 1 semana em cold standby antes de ser desligado
- Durante todo o processo, o serviço continuou respondendo diretamente ou via proxy, sem qualquer janela de indisponibilidade
Migração do MySQL
- A etapa mais complexa de todo o trabalho foi a migração do MySQL
-
Dump dos dados
- Em vez do
mysqldumppadrão, foi usado mydumper - Aproveitando os 48 núcleos de CPU do novo servidor para exportação/importação paralelas, uma tarefa que levaria dias com
mysqldumpsingle-thread foi reduzida para poucas horas - Entre as principais opções usadas estavam
--threads 32,--compress,--trx-consistency-only,--skip-definer,--chunk-filesize 256 - O arquivo
metadatado dump principal registrou a posição do binlog no momento do snapshotFile: mysql-bin.000004Position: 21834307
- Esses valores foram usados depois como ponto inicial da replicação
- Em vez do
-
Transferência do dump
- Após a conclusão do dump, ele foi transferido para o novo servidor com rsync via SSH
- Foram transmitidos ao todo 248GB em chunks comprimidos
- A opção
--compressdomydumperajudou a melhorar a velocidade de transmissão na rede
-
Carga dos dados
- Foi usado
myloader - As principais opções foram
--threads 32,--overwrite-tables,--ignore-errors 1062,--skip-definer
- Foi usado
-
Problemas na transição do MySQL 5.7 para 8.0
- Por conta do ambiente em CentOS 7, o servidor antigo estava preso ao MySQL 5.7
- Antes da migração, foi executado
mysqlcheck --check-upgradepara verificar a compatibilidade dos dados com o MySQL 8.0, e o resultado não apontou problemas - No novo servidor foi instalada a versão mais recente do MySQL 8.0 Community
- Em todo o projeto, o tempo de execução das consultas caiu de forma significativa; no texto original isso é atribuído ao optimizer melhorado e aos avanços no InnoDB do MySQL 8.0
- Ainda assim, houve problemas por causa do salto de versão
- Após o import, a estrutura de colunas da tabela
mysql.usertinha 45 colunas, e não as 51 esperadas - Como resultado,
mysql.infoschemaestava ausente e houve falha na autenticação de usuários
- Após o import, a estrutura de colunas da tabela
- A primeira tentativa de correção usou os comandos abaixo
systemctl stop mysqldmysqld --upgrade=FORCE --user=mysql &
- A primeira tentativa falhou com o erro
ERROR: 'sys.innodb_buffer_stats_by_schema' is not VIEW - A causa foi que o schema
syshavia sido importado como tabela comum, e não como view - A solução foi executar
DROP DATABASE sys;e repetir o upgrade - Depois disso, tudo foi concluído normalmente
Configuração da replicação do MySQL
- Depois que a carga do dump terminou nos dois servidores, o novo servidor foi configurado como replica do servidor antigo
- Na instrução
CHANGE MASTER TOforam definidos o IP do servidor antigo, o usuário de replicação, a porta 3306,MASTER_LOG_FILE='mysql-bin.000004'eMASTER_LOG_POS=21834307 - Em seguida, foi executado
START SLAVE; - Quase imediatamente, a replicação parou com error 1062 Duplicate Key
- A causa foi que o dump foi feito em duas partes, e nesse intervalo houve gravações em algumas tabelas; ao reproduzir o binlog, tentou-se inserir novamente linhas que já estavam presentes no dump importado
- Para resolver, foi aplicada a configuração abaixo
SET GLOBAL slave_exec_mode = 'IDEMPOTENT';START SLAVE;
- O modo IDEMPOTENT ignora silenciosamente erros de chave duplicada e de linha ausente
- Todos os bancos principais foram sincronizados sem erro, e em poucos minutos o valor de
Seconds_Behind_Mastercaiu para 0
Verificação antes do cutover
- Antes de mexer nos registros DNS, era necessário verificar se todos os serviços estavam funcionando corretamente no novo servidor
- O método de validação foi editar temporariamente o arquivo
/etc/hostsda máquina local para mapear os domínios para o IP do novo servidor - Navegador e Postman enviavam requisições ao novo servidor, enquanto os usuários externos continuavam acessando o antigo
- Foram verificados endpoints de API, painel administrativo e o estado de resposta de cada serviço
- Após confirmar tudo, foi feito o cutover real
Problema com privilégio SUPER
- Depois que a replicação master-slave ficou totalmente sincronizada, foi observado que no novo servidor comandos INSERT continuavam funcionando mesmo com
read_only = 1 - A causa era que todos os usuários PHP da aplicação tinham o privilégio SUPER
- No MySQL, o privilégio SUPER contorna
read_only - O resultado de
SHOW GRANTS FOR 'some_db_user'@'localhost';confirmou que o privilégioSUPERestava presente - Foi executado repetidamente
REVOKE SUPER ON *.* FROM 'some_db_user'@'localhost';para um total de 24 usuários de aplicação - Depois disso, foi executado
FLUSH PRIVILEGES; - A partir daí,
read_only = 1passou a bloquear corretamente as escritas dos usuários da aplicação, enquanto a replicação continuou permitida
Preparação do DNS
- Todos os domínios eram gerenciados no DigitalOcean DNS, com os nameservers conectados via GoDaddy
- O trabalho de redução do TTL foi automatizado com script contra a API da DigitalOcean
- O alvo da mudança foi limitado apenas aos registros A e AAAA
- Os registros MX e TXT não foram alterados
- A redução do TTL dos registros de e-mail foi excluída por possível impacto na entregabilidade do Google Workspace
- Após esperar 1 hora pela expiração do TTL anterior, o ambiente ficou pronto para o cutover
Conversão do Nginx do servidor antigo em reverse proxy
- Em vez de editar manualmente 34 arquivos de configuração, a conversão foi automatizada com um script em Python
- O script faz parsing dos blocos
server {}em todos os arquivos de configuração, identifica o bloco principal de conteúdo e o substitui por configuração de proxy - A configuração original é salva em arquivos
.backup - No exemplo de configuração, foram aplicados
proxy_pass https://NEW_SERVER_IP;,proxy_set_header Host $host;,proxy_set_header X-Real-IP $remote_addr;,proxy_read_timeout 150; - A opção essencial foi
proxy_ssl_verify off- Isso porque o certificado SSL do novo servidor é válido para o domínio, mas não para o endereço IP
- Como o ambiente controla as duas pontas, desativar a verificação foi considerado aceitável nesse caso
Procedimento de cutover
- As condições imediatamente antes do cutover eram atraso de replicação em
Seconds_Behind_Master: 0e reverse proxy pronto - A ordem de execução foi a seguinte
- No novo servidor,
STOP SLAVE; - No novo servidor,
SET GLOBAL read_only = 0; - No novo servidor,
RESET SLAVE ALL; - No novo servidor,
supervisorctl start all - No servidor antigo, executar
nginx -t && systemctl reload nginxpara ativar o proxy - No servidor antigo,
supervisorctl stop all - No Mac local, executar
python3 do_cutover.pypara alterar todos os registros A do DNS para o IP do novo servidor - Esperar cerca de 5 minutos pela propagação
- No servidor antigo, comentar todas as entradas do crontab
- No novo servidor,
- O script de cutover do DNS chamou a API da DigitalOcean e alterou todos os registros A em cerca de 10 segundos
Trabalho adicional após o cutover
- Depois da migração, foi constatado que vários webhooks de projetos GitLab ainda apontavam para o IP do servidor antigo
- Foi então criado e aplicado um script que varria todos os projetos via API do GitLab e atualizava os webhooks em lote
Resultado final
- O custo mensal caiu de US$ 1.432 para US$ 233
- A economia anual foi de US$ 14.388
- Em desempenho, também houve ganho com um servidor mais forte
- A CPU passou de 32 vCPU para 96 CPUs lógicas
- A RAM passou de 192GB para 256GB DDR5
- O armazenamento mudou de uma composição mista de cerca de 2,6TB para 2TB NVMe RAID1
- O downtime foi de 0 minuto
- O tempo total da migração foi de aproximadamente 24 horas
- Não houve impacto para os usuários
Principais lições
- Replicação do MySQL é o principal recurso para migrações sem downtime
- A abordagem é configurá-la cedo, deixar sincronizar bem e só então fazer o cutover
- As permissões dos usuários do MySQL precisam ser verificadas antes da migração
- Se houver privilégio SUPER,
read_onlyé contornado e o ambiente slave deixa de ser realmente somente leitura
- Se houver privilégio SUPER,
- Atualizações de DNS, mudanças de configuração do Nginx e correções de webhook devem ser automatizadas com scripts
- Fazer isso manualmente em mais de 34 sites consome tempo e aumenta o risco de erro
- A combinação mydumper + myloader é muito mais rápida que
mysqldumpem datasets grandes- Com dump e restore paralelos em 32 threads, um trabalho de dias caiu para poucas horas
- Em workloads de estado estável, provedores de cloud podem sair caros, e um servidor dedicado pode entregar mais desempenho por menos custo
Scripts no GitHub
- Todos os scripts em Python usados na migração foram publicados no GitHub
- Lista de scripts incluídos
do_list_domains_ttl.py- Consulta todos os registros A, IP e TTL de todos os domínios na DigitalOcean
do_ttl_update.py- Reduz em massa o TTL de todos os registros A/AAAA para 300 segundos
do_to_hetzner_bulk_dns_records_import.py- Migra todas as zonas DNS da DigitalOcean para o Hetzner DNS
do_cutover_to_new_ip.py- Troca todos os registros A do IP do servidor antigo para o IP do novo servidor
nginx_reverse_proxy_update.py- Converte todas as configurações de sites nginx em configuração de reverse proxy
mysql_compare.py- Compara a contagem de linhas de todas as tabelas entre dois servidores MySQL
final_gitlab_webhook_update.py- Atualiza todos os webhooks de projetos GitLab para o IP do novo servidor
mydumper- biblioteca mydumper
- Todos os scripts suportam o modo
DRY_RUN = True, permitindo uma prévia segura antes da aplicação real
1 comentários
Comentários no Hacker News
Há alguns meses, migrei dois servidores da Linode e da DO para a Hetzner e consegui uma grande redução de custos, quase no mesmo nível. O mais impressionante é que era uma pilha caótica com dezenas de sites, linguagens diferentes, bibliotecas antigas, MySQL e Redis todos misturados. Mesmo assim, o Claude Code migrou tudo e, quando faltavam bibliotecas, reescreveu partes do código para resolver. Agora esse tipo de migração complexa ficou muito mais fácil, então imagino que a mobilidade entre provedores vai aumentar no futuro
Estou planejando uma migração da AWS para a Hetzner. A Amazon às vezes cobra 20 vezes mais do que a concorrência, força compromissos de longo prazo para chegar a um preço minimamente razoável e também torna a saída de dados muito cara, o que me parece extremamente hostil ao cliente. Eles podem achar que prendem as pessoas com taxa de egress, mas na prática isso vira uma pressão para mover tudo de uma vez assim que você leva uma parte para um concorrente. Ainda assim, como eu não construí minha plataforma em cima de serviços exclusivos da Amazon, a migração ficou um pouco mais fácil
Sempre que vejo textos assim, acho curioso como quase ninguém fala de redundância ou load balancer. Se um servidor morrer, vários serviços podem cair junto, então fico pensando se as pessoas realmente consideram isso aceitável. Pode até economizar dinheiro, mas talvez se gaste mais tempo de manutenção e mais dor de cabeça no futuro
Nós da lithus.eu já migramos muitos clientes de várias clouds para a Hetzner. Normalmente montamos ambientes multisservidor, às vezes multi-AZ, e distribuímos os workloads com Kubernetes para fornecer HA. Em nó único o Kubernetes pode ser exagero, mas com vários nós faz muito mais sentido. Para backup, usamos Velero junto com backups em nível de aplicação; por exemplo, no Postgres levamos até PITR com backup de WAL. Os dados com estado ficam em pelo menos dois nós para garantir HA. Em desempenho, bare metal também costuma ser melhor, e muitas vezes já vimos latência cair pela metade em comparação com a AWS. Acho que isso se deve menos à virtualização em si e mais a fatores ao redor, como NVMe, menor latência de rede e menos cache contention. Escrevi mais sobre isso num post no HN
Esse texto foi bem difícil de ler. Parecia que o Claude fez a migração e depois eu estava lendo um relatório escrito pelo Claude. Se foi graças a LLM que se economizou tudo isso, ótimo, mas se vai publicar, o mínimo seria revisar o texto para limpar repetições e o estilo narrativo de LLM
Acho que é preciso ter cuidado com a Hetzner. Eu gostava muito deles antes, mas migrei para fora recentemente. Eles derrubaram cerca de 30 VMs que usávamos no pipeline de CI/CD por causa de uma única disputa de cobrança de 36 dólares. Enviei prova de pagamento integral, incluindo registros bancários, e mesmo assim não quiseram olhar. Enquanto tentávamos contato urgente, acabaram bloqueando todo o acesso. Agora estamos na Scaleway
Há alguns meses, enquanto procurava uma alternativa à AWS para um pequeno SaaS paralelo, considerei seriamente a Hetzner no começo, tanto pela economia quanto por apoiar uma cloud da UE. Eu estava disposto a aceitar ter mais trabalho manual, mas o fator decisivo acabou sendo a reputação de IP. Uma das regras de firewall gerenciado da AWS que usamos na empresa bloqueava muitos IPs da Hetzner, talvez até todos, e no meu notebook corporativo sites hospedados em IPs da Hetzner também não abriam por causa da política de TI. Talvez usando algo como Cloudflare isso pese menos, mas também vi comentários de que a proteção contra DDoS é fraca. No fim, escolhi a DO App Platform na região da UE, e as opções de banco gerenciado também foram uma grande vantagem
Achei bem útil e agradeço por compartilhar essa experiência de migração. Eu vejo a comparação entre DO e Hetzner como o trade-off entre abrir DoorDash ou UberEats e fazer o jantar você mesmo. A proporção de custo me parece parecida. Eu lido com as três grandes clouds e também com on-prem, mas para tarefas pequenas ou testes de PoC ainda corro para o console da DigitalOcean. A conveniência de subir um servidor ou bucket com alguns cliques, ter sane defaults e ativar backup marcando uma caixinha realmente tem valor quando se pensa no custo do tempo
Fiquei curioso sobre como fazem backup do banco. Queria saber se existe replica ou standby, ou se é só backup por hora mesmo. Num tipo de configuração de servidor único assim, se houver falha de hardware como SSD, o app pode parar na hora, e especialmente se o SSD morrer imagino que possa haver horas ou dias de downtime até configurar tudo de novo
A imagem de meme no cabeçalho fui eu que fiz. Eu a tinha usado neste texto, então foi divertido vê-la sendo usada duas vezes