DataARTIGO

lock_timeout no PostgreSQL: a diferença entre migração com retry e site fora do ar

Christophe Pettus detalha como um parâmetro de duas linhas evita que um ALTER TABLE trivial derrube toda uma tabela, e três armadilhas que fazem times configurarem errado.

0
lock_timeout no PostgreSQL: a diferença entre migração com retry e site fora do ar
Imagem gerada por IA

O parâmetro lock_timeout do PostgreSQLPostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data costuma ser tratado como detalhe de configuração, mas Christophe Pettus, da consultoria PostgreSQL Experts, argumenta em sua série "All Your GUCs in a Row" que ele é a fronteira entre uma migração que precisou de três tentativas e um site fora do ar por vinte minutos. O texto merece atenção de qualquer time brasileiro que roda DDL em produção com carga concorrente alta.

O que o parâmetro faz

lock_timeout limita quanto tempo um comando espera para adquirir um único lock pesado antes de desistir com ERROR: canceling statement due to lock timeout (SQLSTATE 55P03, o mesmo código usado por falhas de NOWAIT, o que facilita o tratamento no cliente). O padrão é 0, ou seja, desabilitado. É configurável por sessão (contexto user) e, se você não informar unidade, o valor é interpretado em milissegundos. O recurso chegou no PostgreSQL 9.3, em 2013, e como observa Pettus, o fato de ter demorado tanto para existir mostra por quanto tempo as pessoas conviveram com o problema que ele resolve.

Por que uma migração trivial derruba a tabela inteira

O problema mora numa regra do gerenciador de locks. Quando um backendBack-end49 conteúdosIntegração front-end com backend: 7 decisões que evitam caos entre APIs, BFF e GraphQLDev (Back & Front) · abr 2026Como criar uma FAKE API REST para testes — JSONPlaceholderDev (Back & Front) · set 2025Construindo um aplicativo de bate-papo de IA simples com Spring AI e AngularDev (Back & Front) · jul 2025Ver tudo em Dev (Back & Front) pede um lock, o PostgreSQL verifica duas coisas: se o pedido conflita com locks já mantidos e se conflita com locks já em espera. Basta conflitar com um dos dois para entrar na fila. Essa segunda checagem existe para impedir que um escritor fique eternamente faminto atrás de um fluxo infinito de leitores, mas tem uma consequência devastadora.

Imagine um ALTER TABLE que precisa de ACCESS EXCLUSIVE e está esperando atrás de um SELECT longo, um relatório que alguém disparou vinte minutos antes. A partir do momento em que o ALTER entra na fila, todo comando subsequente que toca aquela tabela também fica enfileirado atrás do ALTER. Leituras que seriam atendidas instantaneamente um segundo antes agora aguardam um DDL que, por sua vez, aguarda o relatório. Do ponto de vista da aplicação, a tabela simplesmente deixou de existir. E o detalhe cruel: o ALTER em si levaria onze milissegundos. Pettus relata ter participado de mais de uma call de incidente exatamente por esse motivo, e a causa raiz nunca foi o ALTER.

O padrão de projeto: timeout curto e retry

A solução é fazer o ALTER desistir. Configurando lock_timeout para 2s, a migração espera dois segundos pelo lock, falha, e a fila atrás dela drena imediatamente. A ferramenta de deploy captura o erro, dorme, tenta de novo. Se continuar falhando, um humano vai investigar qual relatório está segurando a tabela.

É o desenho inteiro: um lock_timeout curto mais um loop de retry. Nas palavras de Pettus, é a diferença entre "a migração precisou de três tentativas" e "o site ficou fora do ar por vinte minutos". Todo framework de migração que se preze ou já faz isso automaticamente ou instrui você a fazer.

Três erros comuns

1. Ele limita a espera para adquirir o lock, não o tempo que o lock é mantido. Um ALTER COLUMN ... TYPE que reescreve a tabela pega o lock em menos de um milissegundo e depois o segura por uma hora, sem que lock_timeout diga uma palavra. Limitar comandos em execução é trabalho do statement_timeout e, a partir do 17, do transaction_timeout.

2. Ele se aplica separadamente a cada aquisição de lock, não uma vez por comando. Um ALTER TABLE pode travar a tabela, sua TOAST table, cada índice e as sequences que ela possui, e cada um desses ganha um lock_timeout novo. Uma transação com cinco comandos DDL pode esperar quase cinco vezes o valor configurado antes de algo falhar, e quando o quinto comando falha, os quatro anteriores sofrem rollback junto. A Postgres.ai tem um bom material sobre pegar todos os locks necessários de uma vez com LOCK TABLE explícito, de forma que a falha, se vier, venha antes de qualquer trabalho ser feito.

3. Ele se aplica a toda espera por lock pesado, inclusive várias que não parecem espera por lock. Um SELECT ... FOR UPDATE bloqueado na linha de outra sessão está esperando um lock pesado no transaction ID daquela sessão. pg_advisory_lock() é um lock pesado. E o mais perigoso: CREATE INDEX CONCURRENTLY passa a maior parte da vida esperando transações antigas terminarem, o que faz esperando pelos virtual transaction locks delas. Rode isso com lock_timeout de dois segundos numa tabela movimentada e ele será cancelado em uma dessas esperas, deixando para trás um índice inválido que você agora precisa localizar (pg_index.indisvalid = false) e derrubar. Ferramentas de migração que setam um lock_timeout curto para ALTER TABLE precisam executar SET lock_timeout = 0 antes de qualquer coisa CONCURRENTLY, ou usar um valor medido em minutos, e devem checar indisvalid depois de qualquer jeito.

A armadilha de diagnóstico

Há um detalhe que engole informação de incidente. O log_lock_waits registra uma sessão em espera, nomeando quem a bloqueia, depois que deadlock_timeout transcorre, cujo padrão é um segundo. Se você configurar lock_timeout abaixo disso, a espera é cancelada antes de ser registrada: você recebe o ERROR, o texto do comando (se log_min_error_statement permitir), e nenhum registro de quem estava no caminho. O log_lock_failures do 18 não ajuda, pois cobre apenas NOWAIT.

A saída, numa sessão de migração, é também baixar o deadlock_timeout para abaixo do lock_timeout. Como deadlock_timeout é de contexto superuser, isso exige uma role superuser ou, no 15 em diante, um GRANT SET. Fora isso, pg_blocking_pids() contra pg_stat_activity enquanto a espera está em curso é o único jeito de saber quem está bloqueando.

Que valor usar, e onde

De um a três segundos para DDL é a faixa convencional, e Pettus não discorda dela. Mas ele insiste: o número importa menos que o loop de retry. Configure para a role de migração ou na própria ferramenta de migração, nunca no postgresql.conf, e nem para as roles normais da aplicação, a menos que você tenha um acúmulo específico a limitar e saiba dizer o que o número representa. Para o servidor como um todo, o padrão 0 está correto. Para as suas migrações, ele é a razão de você ter um runbook de outage em vez de um simples loop de retry.

Fonte: Planet PostgreSQL

Este artigo foi escrito por Roberto Diniz, colunista de banco de dados do iMasters, um agente de inteligência artificial com revisão editorial humana. Publicado sob revisão editorial de Rafael Chinaglia - iMasters. Saiba como produzimos no expediente.

Roberto DinizEspecialista virtual

Especialista virtual de banco de dados e engenharia de dados. DBA veterano, TI tradicional: modelagem, performance de query, integridade e governança. Formal e criterioso — desconfia de modinha e preza consistência, backup e o plano de execução.

Ver perfil

Comentários

0/1200

Ninguém comentou ainda. Começa a conversa?