ATUALIZAÇÃO do Postgres… LIMITE 1

77

Eu tenho um banco de dados do Postgres que contém detalhes sobre grupos de servidores, como status do servidor ('ativo', 'em espera' etc.). Servidores ativos a qualquer momento podem precisar de failover para um modo de espera, e eu não me importo com qual modo de espera é usado em particular.

Desejo que uma consulta ao banco de dados altere o status de uma espera - APENAS UMA - e retorne o IP do servidor a ser usado. A escolha pode ser arbitrária: como o status do servidor muda com a consulta, não importa qual modo de espera está selecionado.

É possível limitar minha consulta a apenas uma atualização?

Aqui está o que eu tenho até agora:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

O Postgres não gosta disso. O que eu poderia fazer de diferente?

vastlysuperiorman
fonte
Basta escolher o servidor no código e adicioná-lo como um local restrito. Isso também permite que você verifique primeiro condições adicionais (mais antigas, mais recentes, mais recentes ativas, menos carregadas, mesma CC, rack diferente, menos erros). A maioria dos protocolos de failover exige alguma forma de determinismo.
eckes
@eckes Essa é uma ideia interessante. No meu caso, "escolher o servidor no código" significaria primeiro ler uma lista de servidores disponíveis no banco de dados e depois atualizar um registro. Como muitas instâncias do aplicativo podem executar esta ação, há uma condição de corrida e uma operação atômica é necessária (ou era há 5 anos). A escolha não precisava ser determinística.
vastlysuperiorman

Respostas:

125

Sem acesso de gravação simultâneo

Materialize uma seleção em uma CTE e junte-se a ela na FROMcláusula do UPDATE.

WITH cte AS (
   SELECT server_ip          -- pk column or any (set of) unique column(s)
   FROM   server_info
   WHERE  status = 'standby'
   LIMIT  1                  -- arbitrary pick (cheapest)
   )
UPDATE server_info s
SET    status = 'active' 
FROM   cte
WHERE  s.server_ip = cte.server_ip
RETURNING server_ip;

Originalmente, eu tinha uma subconsulta simples aqui, mas isso pode contornar os LIMITplanos de consulta certos, como Feike apontou:

O planejador pode optar por gerar um plano que execute um loop aninhado sobre a LIMITingsubconsulta, causando mais do UPDATEsque LIMIT, por exemplo:

 Update on buganalysis [...] rows=5
   ->  Nested Loop
         ->  Seq Scan on buganalysis
         ->  Subquery Scan on sub [...] loops=11
               ->  Limit [...] rows=2
                     ->  LockRows
                           ->  Sort
                                 ->  Seq Scan on buganalysis

Reprodução de caso de teste

A maneira de corrigir o exposto acima foi agrupar a LIMITsubconsulta em seu próprio CTE, pois, como o CTE é materializado, ele não retornará resultados diferentes em diferentes iterações do loop aninhado.

Ou use uma subconsulta pouco correlacionada para o caso simplesLIMIT 1. Mais simples, mais rápido:

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         )
RETURNING server_ip;

Com acesso de gravação simultâneo

Assumindo nível de isolamento padrãoREAD COMMITTED para tudo isso. Níveis de isolamento mais rigorosos ( REPEATABLE READe SERIALIZABLE) ainda podem resultar em erros de serialização. Vejo:

Em carga de gravação simultânea, adicione FOR UPDATE SKIP LOCKEDpara bloquear a linha para evitar condições de corrida. SKIP LOCKEDfoi adicionado no Postgres 9.5 , para versões mais antigas, veja abaixo. O manual:

Com SKIP LOCKED, todas as linhas selecionadas que não podem ser bloqueadas imediatamente são ignoradas. Ignorar linhas bloqueadas fornece uma visualização inconsistente dos dados; portanto, isso não é adequado para trabalhos de uso geral, mas pode ser usado para evitar contenção de bloqueios com vários consumidores acessando uma tabela semelhante a uma fila.

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE SKIP LOCKED
         )
RETURNING server_ip;

Se não houver nenhuma linha desbloqueada qualificada, nada acontece nesta consulta (nenhuma linha é atualizada) e você obtém um resultado vazio. Para operações não críticas, significa que você terminou.

No entanto, transações simultâneas podem ter linhas bloqueadas, mas não terminam a atualização ( ROLLBACKou outros motivos). Para ter certeza, execute uma verificação final:

SELECT NOT EXISTS (
   SELECT 1
   FROM   server_info
   WHERE  status = 'standby'
   );

SELECTtambém vê linhas bloqueadas. Enquanto isso não retorna true, uma ou mais linhas ainda estão sendo processadas e as transações ainda podem ser revertidas. (Ou novas linhas foram adicionadas enquanto isso.) Espere um pouco e, em seguida, execute os dois passos: ( UPDATEaté que você não recupere nenhuma linha; SELECT...) até obter true.

Palavras-chave:

Sem SKIP LOCKEDno PostgreSQL 9.4 ou mais antigo

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

As transações simultâneas que tentam bloquear a mesma linha são bloqueadas até a primeira liberar seu bloqueio.

Se a primeira foi revertida, a próxima transação pega o bloqueio e continua normalmente; outros na fila continuam esperando.

Se o primeiro confirmado, a WHEREcondição é reavaliada e, se não TRUEhouver mais ( statusmudou), o CTE (de maneira surpreendente) não retorna nenhuma linha. Nada acontece. Esse é o comportamento desejado quando todas as transações desejam atualizar a mesma linha .
Mas não quando cada transação deseja atualizar a próxima linha . E como queremos apenas atualizar uma linha arbitrária (ou aleatória ) , não há motivo para esperar.

Podemos desbloquear a situação com a ajuda de bloqueios consultivos :

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         AND    pg_try_advisory_xact_lock(id)
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

Dessa forma, a próxima linha ainda não bloqueada será atualizada. Cada transação recebe uma nova linha para trabalhar. Eu tive a ajuda do Czech Postgres Wiki para esse truque.

idsendo qualquer bigintcoluna exclusiva (ou qualquer tipo com uma conversão implícita como int4ou int2).

Se bloqueios consultivos estiverem em uso para várias tabelas no seu banco de dados simultaneamente, desambigue com pg_try_advisory_xact_lock(tableoid::int, id)- idsendo um exclusivo integeraqui.
Como tableoidé uma bigintquantidade, pode teoricamente transbordar integer. Se você é paranóico o suficiente, use (tableoid::bigint % 2147483648)::int- deixando uma "colisão de hash" teórica para o verdadeiro paranóico ...

Além disso, o Postgres é livre para testar WHEREcondições em qualquer ordem. Ele poderia testar pg_try_advisory_xact_lock()e adquirir um bloqueio antes status = 'standby' , o que poderia resultar em bloqueios consultivos adicionais em linhas não relacionadas, onde isso status = 'standby'não é verdade. Pergunta relacionada sobre SO:

Normalmente, você pode simplesmente ignorar isso. Para garantir que apenas as linhas qualificadas estejam bloqueadas, você pode aninhar o (s) predicado (s) em um CTE como acima ou em uma subconsulta com o OFFSET 0hack (impede o inlining) . Exemplo:

Ou (mais barato para verificações sequenciais) aninha as condições em uma CASEdeclaração como:

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

No entanto, o CASEtruque também impediria o Postgres de usar um índice status. Se esse índice estiver disponível, você não precisará de aninhamento extra: apenas as linhas qualificadas serão bloqueadas em uma verificação de índice.

Como você não pode ter certeza de que um índice é usado em todas as chamadas, você pode:

WHERE  status = 'standby'
AND    CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

O CASElogicamente é redundante, mas ele serve ao objetivo discutido.

Se o comando fizer parte de uma transação longa, considere bloqueios no nível da sessão que podem ser (e precisam ser) liberados manualmente. Assim, você pode desbloquear assim que terminar a linha bloqueada: pg_try_advisory_lock()epg_advisory_unlock() . O manual:

Uma vez adquirido no nível da sessão, um bloqueio consultivo é mantido até liberado explicitamente ou o término da sessão.

Palavras-chave:

Erwin Brandstetter
fonte