A migration que travou a tabela

Prévia grátis 4 min de leitura

Um deploy sai às 14:02. Ele leva uma migration:

class AddArchivedToOrders < ActiveRecord::Migration[8.1]
  def change
    add_index :orders, :archived_at
  end
end

No banco de staging, ela terminou em 80 milissegundos. Em produção, orders tem 240 milhões de linhas. A criação do índice roda por onze minutos, e durante onze minutos todo checkout do site trava.

Nada caiu. Nenhuma exceção foi levantada. A migration acabou dando certo, e o deploy foi marcado como verde. A análise do incidente precisou reconstruir a indisponibilidade a partir de gráficos de tempo de resposta, porque a ferramenta que a causou relatou sucesso.

Esse é o assunto deste curso: em pequena escala, uma mudança de schema é um comando que você roda, e em grande escala é uma operação que você planeja.

Por que o mesmo comando se comporta diferente

Uma mudança de schema numa tabela em uso disputa a própria tabela com o tráfego. O banco organiza essa disputa com locks, e o custo de um lock é proporcional ao tempo em que você o segura.

Numa tabela de 2.000 linhas, "quanto tempo" são milissegundos, pouco o bastante para nenhuma requisição perceber que esperou. Numa tabela de 240 milhões de linhas, o mesmo comando reescreve ou percorre cada uma dessas linhas segurando o mesmo lock. A semântica do lock não mudou. A duração mudou, e é a duração que transforma um lock numa indisponibilidade.

Este é o formato:

Momento Tabela pequena Tabela grande
t0 ALTER obtém o lock ALTER obtém o lock
t0 + 40ms Trabalho concluído Ainda faltam 11 minutos de trabalho
t0 + 41ms Lock liberado Escritas entram na fila atrás dele
… Ninguém esperou O pool de conexões enche, requisições estouram o tempo
t0 + 11m (liberado há muito tempo) Lock liberado

Repare no que não está nessa tabela: um aviso, um pedido de confirmação ou qualquer diferença no comando que você digitou.

A lacuna que este curso fecha

A maioria das pessoas que trabalham com Rails aprende migrations como um fluxo local: escrever o arquivo, rodar bin/rails db:migrate, ver verde. Esse fluxo está certo e escala até um tamanho surpreendente. O problema é que ele não dá nenhum sinal quando você cruza a linha em que ele deixa de estar certo, e aí a tabela já está grande demais para ser corrigida sem cuidado.

Há três famílias de respostas, e uma pessoa sênior precisa das três:

  1. Saber quais comandos são baratos. Uma grande parte das mudanças de schema pode ficar segura em qualquer tamanho de tabela se for escrita de outro jeito. Isso é de graça e cobre a maior parte do que você precisa.
  2. Saber copiar uma tabela online. Quando a mudança precisa mesmo reescrever a tabela, ferramentas como gh-ost e pg-osc constroem uma cópia-sombra enquanto o tráfego continua, e depois fazem a troca.
  3. Saber desacoplar schema de código. Expandir e contrair (entregar a mudança aditiva, preencher os dados em lotes, migrar as leituras e depois remover o formato antigo) transforma um passo perigoso em quatro passos sem graça.

O que você vai estudar

  • Lição 2: os modos de lock do Postgres, e a fila de locks, que é onde o dano de verdade acontece.
  • Lição 3: o subconjunto seguro: CREATE INDEX CONCURRENTLY, constraints NOT VALID, padrões baratos, lock_timeout, e strong_migrations como a proteção que faz o seu CI falhar em vez da sua página de checkout.
  • Lição 4: cópia com tabela-sombra, e como o gh-ost lê o log binário em vez de instalar triggers.
  • Lição 5: rodar o gh-ost de verdade: throttling, tamanho dos lotes, cut-over adiado e as limitações que o desqualificam.
  • Lição 6: o lado do Postgres: o pg-osc, e o roteiro de expandir e contrair que funciona em qualquer banco.
  • Lição 7: o runbook, o ensaio, e a árvore de decisão que diz a qual das três famílias uma mudança pertence.

Uma coisa para levar para a lição 2

Uma migration que esta aplicação rodou em setembro cria quatro índices GIN:

add_index :courses, "(title->>'en') gin_trgm_ops",
  using: :gin, name: "index_courses_on_title_en_trgm"

O catálogo tem mais de 350 cursos e 2.500 lições, então isso roda na hora e não bloqueia nada. Escrito do mesmo jeito contra uma tabela de cem milhões de linhas, é o deploy das 14:02. A próxima lição mede o que esse comando trava, neste banco, ao vivo.

Aula 1 de 7 · grátis

Continue com a aula 2

Modos de lock, e a fila atrás deles

ShareLock e AccessExclusiveLock, a diferença entre rigidez e duração do lock, a fila de locks e o lock_timeout.

Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.

Digite para buscar...