2 pontos por GN⁺ 2023-12-14 | 1 comentários | Compartilhar no WhatsApp
  • A Knock definiu um procedimento para migrar o Postgres, repositório central do seu motor de workflows de notificações, do AWS RDS Aurora 11.9 para o 15.3 sem impacto para os clientes
  • Como a data de descontinuação do Postgres 11.9 no Amazon RDS era 29 de fevereiro de 2024, não agir antes disso significaria correr o risco de upgrade forçado e downtime
  • Como upgrade in-place e pg_dump/pg_restore exigiam longos períodos de interrupção, a empresa escolheu configurar replicação lógica baseada em PUBLICATION/SUBSCRIPTION em um novo banco
  • A estratégia de replicação foi dividida conforme o tamanho das tabelas e o padrão de escrita: tabelas pequenas foram replicadas diretamente, enquanto tabelas grandes append-only combinaram copy_data = false com backfill por snapshot
  • A troca final terminou em poucos segundos, mantendo conexão com os dois bancos, mudando flags e dando 500ms para queries em andamento, depois pausando por 1 segundo as requisições ao novo banco para reduzir o risco de stale reads

Objetivos e restrições do upgrade

  • A Knock depende de Postgres para seu motor de workflows de notificações e usa o banco para configurações de workflow, templates de mensagens, coleta de milhões de logs e enfileiramento de jobs em background
  • Como banco de dados relacional, o Postgres exige no mínimo um reinício durante upgrades, e upgrades de versão major podem exigir desligamento completo por vários minutos ou mais devido a mudanças na forma de armazenar dados e índices em disco
  • O Postgres 11.9, usado desde o início da empresa, estava prestes a ser descontinuado no Amazon RDS, e sem ação adicional havia risco de upgrade forçado e downtime forçado
  • As condições do upgrade foram definidas com foco em reduzir o risco operacional
    • Pular para a versão mais recente possível, Postgres 15.3 no Aurora
    • Não aceitar downtime acima de 60 segundos e, idealmente, 0 de downtime do sistema
    • Concluir antes do prazo da Amazon em 2024
    • Minimizar o impacto nos clientes, por exemplo com 0 respostas de erro na API
    • Transformar o procedimento em um runbook reutilizável para o próximo upgrade
  • Ir do 11.9 ao 15.3 significava um upgrade de 4 versões major, então repetir o processo de upgrade in-place quatro vezes foi descartado

Preparação prévia: redução de risco e observabilidade

  • A abordagem para o upgrade do Postgres começou com uma lista de riscos, reduzindo primeiro os que tinham maior impacto e eram mais fáceis de eliminar antecipadamente
    • downtime prolongado
    • perda de dados
    • mudança no desempenho do banco sob a carga da aplicação
    • mudanças na frequência ou no comportamento do VACUUM
    • necessidade ou não de migrar replication slots
  • As notas de release do Postgres foram usadas para verificar mudanças entre versões e identificar riscos como alterações no comportamento do VACUUM ou necessidade de reindexação em upgrades específicos
  • Durante o upgrade, era necessário acompanhar continuamente métricas do sistema e do banco de dados
    • Max TXN ID para prevenção de wraparound de transações
    • uso de CPU do banco
    • sessões em espera na instância writer
    • latência de queries
    • latência de resposta da API da aplicação
  • A Knock também monitorou métricas específicas da aplicação, como o tempo entre uma requisição de API e sua conversão em notificação
  • Sem métricas que pudessem ser verificadas no momento certo, o upgrade aconteceria às cegas

Abordagens descartadas: upgrade in-place e dump/restore

  • O upgrade in-place do AWS RDS é executado pelo console da AWS, interrompendo o banco, rodando scripts de upgrade e colocando-o novamente online
  • Esse processo pode levar de alguns minutos a várias horas, dependendo do volume de dados e da amplitude das mudanças entre versões
  • Mesmo depois de o banco voltar ao ar, tarefas de manutenção como VACUUM ou REINDEX podem ser necessárias, então ele pode não ficar totalmente utilizável de imediato
  • O método com pg_dump e pg_restore exige desconectar todas as aplicações do banco antigo para obter um backup confiável, e em bancos grandes o próprio dump e restore demora bastante
  • Como as duas abordagens tinham grande chance de ultrapassar o limite de downtime da Knock, ambas foram descartadas

Abordagem escolhida: upgrade com replicação lógica

  • A escolha final foi a replicação lógica do Postgres usando PUBLICATION e SUBSCRIPTION
  • O fluxo básico foi o seguinte
    • subir um novo banco na versão alvo do Postgres
    • migrar configurações, extensões, estrutura das tabelas, usuários etc.
    • criar uma publication no banco antigo e configurar uma subscription no banco novo
    • adicionar tabelas à publication
    • quando a replicação terminasse, executar testes para verificar os riscos remanescentes
    • quando a configuração do novo banco estivesse validada, mudar a aplicação para ele
    • remover o banco antigo
  • Isso permitiu avançar em etapas graduais em vez de fazer um upgrade grande de uma vez, além de testar o novo banco com dados e carga reais
  • Depois que o novo banco ficou pronto, a troca em si levou apenas alguns segundos, dando mais controle sobre o momento e a forma do cutover

Pontos centrais da configuração de replicação

  • A replicação lógica do Postgres usa parâmetros necessários para configurar replication slots, e em aplicações simples a principal mudança pode ser definir wal_level como logical
  • Se você já usa replication slots para réplicas de leitura, failover do banco ou sincronização com data warehouse, parâmetros relacionados como max_replication_slots precisam ser ajustados conforme a documentação
  • A estrutura das tabelas no novo banco precisa ser idêntica, mas ele deve estar vazio
  • Um snapshot de schema pode ser gerado com pg_dumpall usando --schema-only e --no-role-passwords, e depois comparado com o SQL do novo banco para corrigir diferenças
  • Ao criar a publication no banco antigo e a subscription no banco novo, algumas opções principais foram configuradas
    • enabled = false: evita iniciar a sincronização imediatamente
    • create_slot = true: faz o Postgres gerenciar o replication slot
    • copy_data = true: copia por padrão o conteúdo das tabelas
    • disable_on_error = true: pausa a subscription em caso de erro inesperado para permitir correção e retomada
  • Colocar todas as tabelas de uma vez em uma publication com FOR ALL TABLES pode causar problemas de desempenho em bancos grandes, então a Knock usou ALTER PUBLICATION ... ADD TABLE para adicionar as tabelas uma a uma

Classificação das tabelas e estratégia de replicação

  • A Knock dividiu as tabelas com base no tamanho em disco e no número de tuplas
    • tabelas pequenas, sincronizáveis em poucos minutos
    • tabelas grandes, mas quase append-only
    • tabelas grandes cujas rows são atualizadas com frequência
  • Para a Knock, uma tabela “pequena” era uma tabela com menos de 50GB e menos de 10 milhões de tuplas
  • No Postgres, tupla é a unidade em que inserts e updates são armazenados; mesmo com poucas rows, muitas tuplas não limpas podem alongar o tempo de replicação
  • Rodar VACUUM antes da replicação pode ajudar a reduzir o número de tuplas que o banco de origem precisará copiar para o banco de destino
  • O tempo de sincronização de uma tabela está diretamente ligado ao tamanho em disco e ao número de tuplas, e sincronizações longas podem atrapalhar o VACUUM no banco primário, levando a degradação de desempenho e risco de wraparound de transaction ID

Replicação de tabelas pequenas

  • Tabelas pequenas foram tratadas adicionando a tabela à publication no banco antigo e atualizando a subscription no banco novo
  • O Postgres ficou responsável por copiar a tabela, sincronizar e aplicar mudanças posteriores
  • Tabelas muito pequenas podem sincronizar em menos de 1 segundo

Replicação de tabelas grandes append-only

  • Tabelas grandes sem updates, ou com updates apenas em rows recentes, podem usar uma publication/subscription separada com copy_data = false
  • A Knock usou o sufixo _nocopy no nome para diferenciar esse caso da replicação normal
  • Primeiro são replicadas apenas as mudanças novas, e os dados históricos são depois preenchidos separadamente por backfill a partir de backup ou snapshot
  • No AWS RDS Aurora, o procedimento usado foi o seguinte
    • criar um snapshot do banco de produção
    • restaurar o snapshot em uma nova instância de banco
    • adicionar um sufixo como _snapshot ao nome das tabelas na instância de snapshot que seriam replicadas
    • criar no banco de destino tabelas de snapshot com o mesmo schema
    • configurar publication/subscription da instância de snapshot para o banco de destino
    • monitorar o progresso da replicação
    • quando a replicação alcançasse o estado atual, mesclar na tabela real de destino com INSERT ... ON CONFLICT DO NOTHING
  • Tabelas muito grandes podem levar dias nesse processo, mas como ele acontece em background, não deveria afetar o ambiente de produção
  • Depois da mesclagem, a consistência foi verificada comparando a contagem de rows, e então foram removidas as tabelas de snapshot no banco de destino, a subscription de snapshot e a instância de snapshot

Tabelas grandes e frequentemente atualizadas

  • Tabelas grandes em que a maioria das rows recebe updates frequentes são o caso mais difícil, e replicações longas podem atrapalhar a execução do AUTOVACUUM
  • Algumas medidas possíveis
    • verificar se tarefas de housekeeping podem reduzir o tamanho da tabela
    • conferir se houve VACUUM recentemente
    • avaliar se a tabela pode ser particionada em partes menores
    • verificar se os updates param depois de algum tempo para decidir se ela pode ser tratada como append-only
  • Se o banco de origem estiver abaixo do PG 15, as opções são limitadas, e pode ser necessário replicar no mesmo modelo das tabelas pequenas e monitorar se há degradação de serviço
  • Se necessário, é possível fazer rollback removendo a tabela da publication e atualizando a subscription
  • Para tabelas grandes demais, a replicação pode começar em horários de tráfego baixo para reduzir o impacto da carga e da atividade de escrita

Replicação particionada de tabelas grandes no PG 15+

  • Se o banco de origem estiver em PG 15 ou superior, a replicação pode ser dividida em várias publications para mover tabelas grandes em partes menores
  • Isso funciona de forma parecida com particionamento ou sharding, em troca de usar mais replication slots
  • Como a Knock migrou do 11.9 para o 15.3, ela não podia usar essa abordagem e não a testou diretamente
  • O exemplo dado usa hash da primary key e cláusulas WHERE para dividir as rows em várias publications
  • O tamanho de parte considerado administrável pela Knock era algo em torno de 100GB de dados, sem contar os índices

Verificação do estado da replicação e interrupção

  • Depois que uma tabela é adicionada à subscription, o estado pode ser verificado em pg_subscription_rel.srsubstate no banco de destino
    • i: inicialização
    • d: cópia do conteúdo da tabela
    • f: cópia concluída, aguardando sincronização final
    • s: finalização da sincronização inicial
    • r: replicação normal em execução
  • A etapa d precisa manter transaction IDs antigos do Postgres e pode bloquear efetivamente o VACUUM, levando a problemas de desempenho ou wraparound de transaction ID
  • Se o wraparound estiver próximo, é melhor interromper a migração e dividir em partes menores
  • Para parar a replicação de uma tabela específica, basta removê-la da publication no banco antigo e atualizar a subscription no banco novo
  • Apenas desabilitar a subscription pode não resolver o problema de desempenho, porque o banco de origem pode continuar retendo transaction IDs antigos
  • Em emergências, é possível apagar publication e subscription por completo e recomeçar do zero, e o Postgres limpará os replication slots relacionados

Limitações na migração de replication slots

  • Replication slots do Postgres armazenam logs de atividade do banco que podem ser consumidos por outros bancos ou aplicações
  • O progresso do slot é rastreado por Log Sequence Number, ou LSN, que é exclusivo do banco Postgres primário
  • Não é possível copiar diretamente o LSN de um replication slot do banco antigo para o novo banco
  • Aplicações que consomem replication slots, como ferramentas de data warehouse, precisam definir uma estratégia de migração conforme a documentação de cada ferramenta
  • Se sua própria aplicação usa replication slots, um mecanismo de idempotência para eliminar transações duplicadas entre o banco antigo e o novo pode ajudar

Validação final

  • Depois que todas as tabelas forem adicionadas à publication e a subscription alcançar o estado atual, é preciso validar se as tabelas realmente batem
  • Por causa da latência da replicação lógica, o banco antigo e o novo dificilmente estarão 100% idênticos no mesmo instante, mas a comparação de contagem de rows permite verificar se estão próximos o suficiente
  • A Knock escreveu um script para contar rows em cada tabela no banco antigo e no novo
  • Para tabelas com coluna inserted_at, foram comparadas apenas rows com mais de 10 segundos, assumindo que os 10 segundos mais recentes ainda seriam replicados em seguida
  • Em algumas tabelas, samples aleatórios de rows também foram comparados para confirmar que o conteúdo batia

Como foi feita a troca da aplicação

  • Para o cutover final, a aplicação pode ser alterada para se conectar aos dois bancos
  • Em bancos com pouco tráfego, a migração pode ser feita de forma simples, alterando a configuração para o novo banco e reiniciando a aplicação
  • Em aplicações com muita atividade simultânea, era necessário evitar escritas conflitantes entre o banco antigo e o novo
  • O script de cutover da Knock seguiu esta ordem
    • instruir todas as instâncias da aplicação a enviar novas queries para o novo banco
    • dar 500ms para conclusão das queries de banco em andamento e, depois disso, cancelá-las à força
    • nos primeiros 1 segundo após a troca da flag, pausar artificialmente as requisições ao novo banco para dar tempo de transações pendentes serem replicadas
    • depois disso, normalizar a atividade do banco já apontando para o novo banco
    • alguns workloads especiais de banco foram interrompidos e reiniciados para se reconectar ao novo banco
  • A Knock confirmou que 500ms era bem mais tempo do que a maioria das suas queries de banco precisava, e que não houve erros por encerramento forçado de conexão

Tratamento de sequences

  • A replicação lógica do Postgres não sincroniza sequence
  • Mesmo que valores de sequence sejam consumidos no banco antigo, os valores de sequence do novo banco não avançam
  • A Knock executou um script conectado aos dois bancos logo antes da troca da feature flag
    • para cada sequence do banco antigo, buscou o próximo valor com SELECT nextval('sequence_name')
    • no banco novo, adiantou a sequence com SELECT setval('sequence_name', value::int4 + 100000)
  • Essa abordagem cria gaps nas sequences, mas como as sequences da Knock eram bigint, pular 100 mil valores representava na prática quase 0% do espaço disponível
  • O tamanho do gap deve ser ajustado conforme o volume de valores de sequence que pode ser consumido durante o cutover real

O que verificar antes do cutover

  • Os itens de validação antes da troca final cobrem de forma ampla a prontidão operacional
    • se a contagem de rows de todas as tabelas bate com o esperado
    • se todas as subscriptions estão habilitadas e rodando sem erros
    • se o schema está igual e se é possível congelar os releases de migration
    • se o novo banco foi dimensionado para a carga de trabalho
    • se serão necessárias read replicas para manter a topologia de cluster equivalente entre o banco antigo e o novo
    • se REINDEX e a manutenção básica de VACUUM foram executados no novo banco
    • se as notas de release do Postgres foram revisadas novamente em busca de regressões na aplicação
    • se testes automáticos e manuais foram executados em um banco de staging com a nova versão
    • se as queries mais pesadas foram submetidas a teste de carga com pg_bench
    • se ainda resta algum risco que possa ser reduzido
    • se o procedimento de cutover foi treinado várias vezes em staging ou ambiente de teste
    • se foi criado um backup do banco imediatamente antes do cutover

Resultado da migração real

  • A Knock replicou as tabelas uma a uma ao longo de várias semanas, em geral fora do horário comercial e nos períodos de menor tráfego
  • Depois de praticar o cutover várias vezes em staging, o procedimento foi refinado para funcionar com pouca intervenção operacional
  • Quando a réplica em PG 15 e o código de troca da aplicação ficaram prontos, foi feita a checagem final e a flag foi alterada
  • O cutover real terminou em poucos segundos e, fora um pequeno blip intencional de latência para esperar a replicação, a aplicação continuou funcionando
  • Depois disso, as mudanças temporárias na aplicação foram revertidas, todas as conexões passaram permanentemente para o novo banco, e a subscription do novo banco e o banco antigo foram removidos
  • A Knock concluiu uma migração sem downtime do Postgres 11.9 para 15.3

Conclusão

  • Pular 4 versões major do Postgres de uma vez é trabalhoso, mas possível
  • A abordagem com replicação lógica pode ser mais segura do que um downtime planejado, porque permite praticar, testar e refazer várias vezes antes do cutover real
  • Se surgir algum problema durante o processo, é possível apagar a publication do banco antigo e recomeçar, o que permitiu reverter o procedimento sem degradação de serviço
  • Embora 100% de disponibilidade perfeita não seja tecnicamente possível, uma migração sem downtime ajuda a manter o sistema em operação sem grandes interrupções de serviço

1 comentários

 
GN⁺ 2023-12-14
Opiniões no Hacker News
  • A abordagem de copiar todo o conteúdo das tabelas uma por uma gera uma carga de I/O grande demais e não funciona para tabelas muito grandes.
    Um método melhor é criar um slot de replicação, tirar um snapshot, restaurá-lo em uma nova instância, avançar o LSN e então replicar a partir dali. Assim, você obtém uma réplica lógica com todos os dados e pode fazer o upgrade dessa réplica.
    O post da Instacart explica o método: https://archive.ph/K5ZuJ
    Se minha memória estiver correta, o post tinha alguns pequenos erros, mas o procedimento geral funcionava, e eu já fiz upgrade de instâncias na casa dos TB várias vezes dessa forma.

    • Esse método é uma boa receita, mas precisa de uma correção pequena e importante na ordem em que o pg_upgrade entra.
      Se você iniciar primeiro a replicação lógica e depois executar o pg_upgrade, há risco de corrupção. A discussão relacionada está na pgsql-hackers: https://www.postgresql.org/message-id/flat/20230217075433.u5...
      Para resolver, primeiro é preciso criar o slot lógico, avançar o novo cluster até a posição LSN do slot, mas ainda sem iniciar a replicação lógica; depois executar o pg_upgrade; e, após o cluster subir na nova versão do PostgreSQL, iniciar a replicação lógica.
      A Postgres.ai usou recentemente exatamente essa abordagem ao fazer upgrades sem downtime de vários clusters multi-TiB do GitLab sob alta carga, usando também PAUSE/RESUME do PgBouncer. Uma apresentação de Alexander Sosna está prevista para o fim desta semana: https://www.postgresql.eu/events/pgconfeu2023/schedule/sessi...
    • Como OP, eu também avaliei esse método, mas não tinha confiança em avançar o LSN manualmente como proposto, nem em detectar inconsistências caso a replicação deixasse passar algo.
      O progresso tabela por tabela era muito mais trabalhoso, mas parecia mais confiável.
    • O post foi atualizado: https://tech.instacart.com/zero-downtime-postgresql-cutovers...
    • Esse post cobre a base da abordagem de upgrade da Instacart, mas é bem antigo, e o artigo abaixo mostra melhor o procedimento atual.
      Usamos esse método para fazer upgrade com sucesso de muitos bancos de dados muito grandes e ativos.
      https://www.instacart.com/company/how-its-made/zero-downtime...
  • A abordagem é interessante e bem documentada, mas a frase “clientes modernos esperam 100% de disponibilidade” me incomoda.
    Não é minha preferência como cliente, nem minha experiência como fornecedor. Em muitas cargas de trabalho, consistência é muito mais importante do que disponibilidade.
    Quando um fornecedor anuncia uma janela de downtime, muitas vezes isso me deixa mais tranquilo, como um sinal de que ele está tratando meus dados com cuidado.

    • Como OP, ótimo feedback.
      Eu queria criar confiança tanto na confiabilidade do produto quanto na consistência da carga de trabalho. Claro que é muito melhor gerenciar as expectativas dos clientes e ter downtime deliberado em prol de um uptime melhor no longo prazo do que fingir consistência enquanto o sistema é instável.
      Fazer com que janelas periódicas de manutenção sejam esperadas com antecedência também pode levar a uma arquitetura mais robusta como um todo. Se os clientes criarem salvaguardas para tolerar downtime, a resiliência aumenta; e, quando a equipe pode confiar nos clientes dessa forma, ganha tempo para investir em um produto melhor.
      Talvez depois do próximo upgrade de versão major eu escreva um texto chamado “definir expectativas sobre downtime é o caminho para um uptime muito alto”.
    • Depende de quem é o cliente.
      Como cliente da AWS, espero 100% de disponibilidade, porque meus clientes estão no mundo todo e não há um horário em que eu possa colocar downtime.
  • A AWS agora oferece suporte a deployments blue/green: https://aws.amazon.com/about-aws/whats-new/2023/10/amazon-rd...

    • Eu testei pessoalmente algumas semanas atrás, e por enquanto é melhor não confiar nisso para PostgreSQL.
      Depois de algumas idas e vindas com a AWS, o experimento ficou travado por horas, e só depois a UI da AWS admitiu que a mudança não havia sido aplicada. Felizmente falhou de forma segura, mas não tenho confiança de que seja possível acertar o momento real da troca em datasets acima de GB.
    • É verdade. Como OP, na época estávamos no Aurora 11.9, que não tinha suporte a deployments blue/green.
      Talvez seja possível na próxima vez.
  • Isso é excelente.
    Criei uma ferramenta que automatiza a maior parte do que vocês passaram; se for útil ou se quiserem contribuir com feedback/ideias, serão muito bem-vindos: https://github.com/shayonj/pg_easy_replicate

    • Ferramenta muito legal.
      As descobertas feitas com tabelas grandes podem ser interessantes para uma ferramenta dessas. Se ela facilitar aplicar a estratégia adequada a cada tabela, pode se tornar uma ferramenta indispensável para equipes que farão esse tipo de migração no futuro.
  • É duvidoso dizer que “para um serviço como o Knock, nenhum downtime é aceitável, seja programado ou não”
    Se é um sistema complexo, há falhas e há downtime. Um downtime programado de 15 minutos é aceitável para praticamente qualquer negócio SaaS. Não é um hospital nem uma usina elétrica
    Muita tarefa falsa surge porque se acha que o serviço é mais importante do que realmente é. Se o tempo de engenharia gasto aqui tivesse sido usado para melhorar o produto ou a produtividade da equipe de desenvolvimento, é bem provável que os usuários ficassem mais felizes. Especialmente se as notificações puderem ser colocadas em uma fila e processadas depois do downtime
    Se houver um SLA enterprise com cláusulas de compensação por 15 minutos de downtime, isso pode justificar, mas na maioria dos casos não há. Na prática, é bem provável que já tenham ocorrido algumas indisponibilidades parecidas ou mais longas
    Em migrações de banco de dados, a diferença de esforço entre “downtime curto” e “zero downtime” costuma ser considerável, o que torna isso ainda mais importante. No caso de algo pontual como este, em que as versões mais recentes do PostgreSQL no RDS têm suporte nativo, acho especialmente difícil justificar

    • Como OP, concordo que todo serviço tem downtime por algum motivo
      Também discutimos definir uma janela de indisponibilidade, mas a questão em que continuamos pensando era como ensaiar o upgrade com dados de produção. Uma réplica PG 15 sincronizada com os dados de produção foi muito importante para validar se a carga de trabalho se comportava como esperado
      Com uma réplica em tempo real, é possível ensaiar no ambiente de produção com impacto mínimo
      A grande lição que tirei desta migração foi o quanto é útil rastrear e mitigar todos os riscos imagináveis em projetos desse tipo. No fim, o risco de um upgrade in-place pareceu maior que o risco do caminho escolhido, e isso foi uma decisão separada da existência ou não de uma janela de indisponibilidade
      De bônus, se precisarmos dessa abordagem no futuro, este post do blog servirá como ponto de partida e economizará semanas. Espero que também ajude outras equipes em situações parecidas
    • Do ponto de vista de um médico, acho engraçado que “não é um hospital” apareça como exemplo de sistema que não pode tolerar downtime
      Até a Epic, uma das maiores fornecedoras de prontuário eletrônico dos EUA, tem downtime programado para upgrades pelo menos uma vez por mês, geralmente de 30 a 60 minutos por vez
    • O problema é que no RDS não há como fazer upgrade de uma instância PostgreSQL com downtime programado de 15 minutos
      Você não consegue controlar o momento do reboot. Ao iniciar o processo, a transição pode começar uma hora, duas horas ou três horas depois, e você não sabe nem controla quando o reboot vai acontecer
      Se houver réplicas, elas são atualizadas em paralelo e reiniciadas em momentos arbitrários, o que complica ainda mais
      Portanto, se você não puder tolerar indisponibilidade arbitrária em uma janela que pode se estender por horas, dependendo do tamanho do banco de dados, a replicação lógica é, na prática, a única opção para upgrades no RDS
      Quanto maior a instância, mais difícil fica o problema
    • O verdadeiro problema do downtime é quando todos os sistemas caem ao mesmo tempo
      Se o Jira fica fora por 15 minutos em um dia, normalmente não há grande impacto. Há outras coisas na fila de trabalho e, no pior caso, mesmo com várias falhas combinadas, ainda existe alguma documentação prometida a alguém
      Mas, se toda a suíte Atlassian cai ao mesmo tempo, fica muito mais difícil manter trabalho de reserva para continuar. Se você fizer todos os apps da empresa usarem o mesmo array de armazenamento, a perda de produtividade pode saltar de 5% para 95%
    • Ao contrário da frase “um downtime programado de 15 minutos é aceitável para quase qualquer negócio SaaS”, pode haver concorrentes que não tenham downtime mensal
      Esse concorrente estaria colocando as minhas necessidades acima da própria conveniência
      A sua indisponibilidade também é a minha indisponibilidade
  • Estamos passando por esse processo agora na hava.io
    Estamos atualizando de AWS RDS PostgreSQL 11.13 para 15.5
    No fim, optamos por uma abordagem relativamente simples de replicação unidirecional usando pglogical. Como já tínhamos feito uma migração zero downtime do Google Cloud SQL para AWS RDS da mesma forma, tínhamos confiança de que funcionaria sem impacto visível para os clientes
    O pglogical torna esse tipo de migração bastante simples. Nem sempre é rápido, mas, se você puder esperar alguns dias enquanto o banco de dados inteiro é replicado gradualmente para a nova instância, funciona bem
    Esse método também nos deu mais liberdade para mudar o tipo e o tamanho do armazenamento. Como estávamos com armazenamento superdimensionado para obter IOPS, queríamos mudar o tipo de armazenamento e também reduzir o tamanho. Por isso, uma simples restauração de snapshot não servia

  • Fico me perguntando se é daquele recurso que a AWS prometeu na etapa de “engenharia de vendas”
    Na prática, quando fomos obrigados a fazer um upgrade de versão major, eles não conseguiram entregar

  • É surpreendente que não dê para inicializar uma réplica a partir de um backup
    Se fosse possível, isso teria reduzido o trabalho de fazer streaming do conteúdo estável do banco de dados existente para o novo servidor
    E isso não é “sem downtime”: há alguns segundos de indisponibilidade para trocar o serviço para o novo servidor
    O texto deixou de fora como a consistência foi preservada. Por exemplo, não dá para simplesmente conectar a aplicação aos dois servidores por um período. Leituras talvez possam ser atendidas pelos dois, embora isso também não seja perfeito, e escritas precisam ir necessariamente para apenas um servidor
    Por fim, também não há opção de rollback. Operações desse porte, movendo um volume grande de dados de uma vez, às vezes dão errado tarde da noite. Por isso sempre é preciso ter um plano para voltar à etapa anterior e poder dormir com a certeza de que o serviço ainda estará de pé de manhã
    Em especial, se você já enviou transações de escrita para o novo servidor e, por qualquer motivo, precisa voltar para o servidor antigo, fica difícil, e os dados já estarão inconsistentes

    • Como OP, é possível inicializar uma réplica a partir de um backup, mas você não obtém as escritas que continuam acontecendo durante o backup
      A menos que haja algum meio de replicação, ou que isso seja levado para a camada da aplicação, o sistema restaurado terá escritas ausentes
      Por exemplo, você pode modificar o app para aplicar escrita dupla. Pelo que sei, equipes que replatformaram uma aplicação inteira de um RDBMS para um banco totalmente diferente, como Apache Cassandra, também fizeram isso
      No nosso caso, escrita dupla parecia mais arriscado do que configurar replicação por streaming usando recursos nativos do PostgreSQL. Mas, para algumas equipes, pode ser uma escolha melhor
      Sobre os pontos “não é sem downtime” e “faltam detalhes sobre a preservação da consistência”, o texto tratou em detalhes de como mantivemos a consistência e evitamos downtime da API. O ponto principal é que o app estava conectado aos dois bancos de dados, mas ainda não usava o novo banco como principal
      Em seguida, enviamos um sinal de troca para todas as instâncias do app com o LaunchDarkly, que mantém conexões de baixa latência com todas as instâncias
      Durante o primeiro 1 segundo após o sinal, os servidores colocaram as requisições ao banco em fila para dar tempo de a replicação alcançar. Isso causou um breve pico de latência, mas dentro de uma margem aceitável calculada intencionalmente. Depois dessa pausa temporária, as requisições passaram a fluir normalmente, mas direcionadas ao novo banco de dados, e a troca foi concluída
      Para o tráfego que ainda permanecia no banco antigo, também adicionamos uma desconexão forçada com timeout de 500 ms. Esse valor era bem maior que o tempo de consulta p99, então nenhuma consulta em execução foi encerrada à força. Com isso, o tráfego para o banco antigo parou, dando tempo suficiente para a replicação alcançar
      A opção de rollback ficou de fora do post do blog, mas também avaliamos criar um banco de dados alternativo no PG 11.9 e replicar o banco 15.3 para esse terceiro banco. Se precisássemos abortar, poderíamos fazer rollforward para esse banco na mesma versão
      Depois de praticar o procedimento de upgrade várias vezes em staging e confirmar a chance de sucesso, decidimos não usar essa abordagem. Como fizemos vários ensaios, estávamos confiantes na troca real. Em produção, também validamos parte da carga de trabalho somente leitura contra a instância 15.3 com deploy canário, tratando-a como uma réplica de leitura
      Para evitar problemas tarde da noite, fizemos de propósito no começo da noite de um fim de semana. A troca foi cuidadosamente roteirizada e ensaiada para reduzir o risco de erro humano
      Se ocorresse uma falha crítica, o sistema também estava preparado para voltar ao banco antigo. Nesse caso, haveria alguma perda de dados inseridos no novo banco, e tínhamos preparado a conciliação das partes críticas. Para reduzir o risco de perda de dados, pausamos temporariamente alguns jobs em segundo plano durante a troca, diminuindo o número de escritas
      Não incluímos esses detalhes no blog porque queríamos focar nos detalhes relacionados ao PostgreSQL, mais do que em considerações específicas da Knock. Equipes que forem aplicar esse playbook devem sempre listar e mitigar os riscos no próprio contexto
  • A parte sobre sequências é realmente interessante
    Há algum tempo quase não uso sequências; uso principalmente UUIDs sequenciais, UUID v7 ou abordagens como HiLo
    https://en.wikipedia.org/wiki/Hi/Lo_algorithm

    • Para quem quer manter a responsabilidade de gerar UUID v7 dentro do banco de dados até que o PostgreSQL ofereça suporte nativo, uma função PL/pgSQL pode ajudar
      A ideia é criar uma sequência de 12 bits com base no rascunho da especificação da IETF e compor o UUID combinando os milissegundos atuais do UNIX epoch com 62 bits aleatórios
      O ponto central é ter uuidv7_seq e fazer a função generate_uuidv7() usar clock_timestamp(), NEXTVAL e RANDOM() para retornar um valor no formato UUID v7
    • Como OP, estamos evitando sequências, exceto em uma aplicação, por causa de dependências
      Usamos KSUID e UUID v4 em vários lugares. Essa “armadilha” se aplica a todas as sequências, então vale a pena mencioná-la como conselho geral ao fazer esse tipo de migração
      [1]: https://segment.com/blog/a-brief-history-of-the-uuid/
  • Não quero diminuir o trabalho enorme que foi feito com sucesso, mas fico curioso por que vocês não fizeram upgrades menores a cada nova versão
    É uma ótima leitura, mas parece a história de marinheiros que decidiram atravessar uma grande tempestade de frente, mesmo sabendo que poderia acabar em tragédia, em vez de contorná-la
    Nesse caso, upgrades menores estavam fora de cogitação? Fico imaginando se era algo como “mesmo um upgrade pequeno teria o mesmo custo de downtime de um upgrade grande, então adiamos ao máximo”. A introdução dá essa pista, mas talvez eu esteja lendo demais

    • Como OP, teríamos usado a mesma abordagem também para upgrades menores
      Não foi tanto “adiamos até ficar encurralados”, mas mais próximo de “se não está quebrado, não mexa”, embora soubéssemos que algum dia teríamos de dar o salto
    • Subir N versões, seja N igual a 1 ou 3, é praticamente igual em termos de ameaça à disponibilidade
    • Cada upgrade envolve downtime
      Mesmo que a resposta real seja menos de 60 segundos, teríamos passado por esse downtime várias vezes no caminho até a versão 15