Chamadas simultâneas para a mesma função: como estão ocorrendo os deadlocks?

15

Minha função new_customeré chamada várias vezes por segundo (mas apenas uma vez por sessão) por um aplicativo da web. A primeira coisa que faz é bloquear a customertabela (fazer uma "inserção se não existir" - uma simples variante de uma upsert).

Meu entendimento dos documentos é que outras chamadas para new_customerdevem simplesmente enfileirar até que todas as chamadas anteriores sejam concluídas:

LOCK TABLE obtém um bloqueio no nível da tabela, aguardando, se necessário, que quaisquer bloqueios conflitantes sejam liberados.

Por que às vezes é um impasse?

definição:

create function new_customer(secret bytea) returns integer language sql 
                security definer set search_path = postgres,pg_temp as $$
  lock customer in exclusive mode;
  --
  with w as ( insert into customer(customer_secret,customer_read_secret)
              select secret,decode(md5(encode(secret, 'hex')),'hex') 
              where not exists(select * from customer where customer_secret=secret)
              returning customer_id )
  insert into collection(customer_id) select customer_id from w;
  --
  select customer_id from customer where customer_secret=secret;
$$;

erro do log:

2015-07-28 08:02:58 DETALHE DO BST: O processo 12380 aguarda ExclusiveLock na relação 16438 do banco de dados 12141; bloqueado pelo processo 12379.
        O processo 12379 aguarda ExclusiveLock na relação 16438 do banco de dados 12141; bloqueado pelo processo 12380.
        Processo 12380: selecione new_customer (decodifique ($ 1 :: text, 'hex'))
        Processo 12379: selecione new_customer (decodifique ($ 1 :: text, 'hex'))
2015-07-28 08:02:58 BST DICA: Consulte o log do servidor para obter detalhes da consulta.
2015-07-28 08:02:58 CONTEXTO BST: instrução "new_customer" da função SQL 1
2015-07-28 08:02:58 DECLARAÇÃO BST: selecione new_customer (decodificar ($ 1 :: texto, 'hex'))

relação:

postgres=# select relname from pg_class where oid=16438;
┌──────────┐
 relname  
├──────────┤
 customer 
└──────────┘

editar:

Consegui obter um caso de teste reproduzível simples. Para mim, isso parece um bug devido a algum tipo de condição de corrida.

esquema:

create table test( id serial primary key, val text );

create function f_test(v text) returns integer language sql security definer set search_path = postgres,pg_temp as $$
  lock test in exclusive mode;
  insert into test(val) select v where not exists(select * from test where val=v);
  select id from test where val=v;
$$;

O script bash é executado simultaneamente em duas sessões do bash:

for i in {1..1000}; do psql postgres postgres -c "select f_test('blah')"; done

log de erros (geralmente um punhado de deadlocks nas 1000 chamadas):

2015-07-28 16:46:19 BST ERROR:  deadlock detected
2015-07-28 16:46:19 BST DETAIL:  Process 9394 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9393.
        Process 9393 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9394.
        Process 9394: select f_test('blah')
        Process 9393: select f_test('blah')
2015-07-28 16:46:19 BST HINT:  See server log for query details.
2015-07-28 16:46:19 BST CONTEXT:  SQL function "f_test" statement 1
2015-07-28 16:46:19 BST STATEMENT:  select f_test('blah')

editar 2:

O @ypercube sugeriu uma variante com lock tablea função fora:

for i in {1..1000}; do psql postgres postgres -c "begin; lock test in exclusive mode; select f_test('blah'); end"; done

Curiosamente, isso elimina os impasses.

Jack Douglas
fonte
2
Na mesma transação, antes de entrar nessa função, é customerusado de uma maneira que agarra um bloqueio mais fraco? Então pode ser um problema de atualização do bloqueio.
Daniel Vérité
2
Eu não posso explicar isso. Daniel pode ter razão. Pode valer a pena aumentar isso no pgsql-general. De qualquer forma, você está ciente da implementação do UPSERT no próximo Postgres 9.5? Depesz dando uma olhada.
Erwin Brandstetter
2
Quero dizer, dentro da mesma transação, não apenas da mesma sessão (como os bloqueios são liberados no final do TX). A resposta de @alexk é o que eu estava pensando, mas se o tx começa e termina com a função, isso não pode explicar o impasse.
Daniel Vérité
1
@Erwin você vai, sem dúvida, estar interessado na resposta que eu tenho de publicar em pgsql-bugs :)
Jack Douglas
2
Muito interessante mesmo. Faz sentido que isso funcione no plpgsql também, pois me lembro de casos semelhantes do plpgsql funcionando conforme o esperado.
Erwin Brandstetter

Respostas:

10

Eu postei isso para pgsql-bugs e a resposta há de Tom Lane, indica esta é uma questão de escalonamento de bloqueio, disfarçado pela mecânica das funções da linguagem SQL forma como são processados. Essencialmente, o bloqueio gerado pelo inserté obtido antes do bloqueio exclusivo na tabela :

Acredito que o problema disso é que uma função SQL fará análise (e talvez também planeje; não tenha vontade de verificar o código agora) para todo o corpo da função de uma só vez. Isso significa que, devido ao comando INSERT, você adquire RowExclusiveLock na tabela "test" durante a análise do corpo da função, antes que o comando LOCK seja executado. Portanto, o LOCK representa uma tentativa de escalação de bloqueios, e são esperados impasses.

Essa técnica de codificação seria segura no plpgsql, mas não na função da linguagem SQL.

Houve discussões sobre a reimplementação de funções da linguagem SQL para que a análise ocorra uma instrução de cada vez, mas não prenda a respiração sobre algo acontecendo nessa direção; não parece ser uma preocupação de alta prioridade para ninguém.

Atenciosamente, Tom Lane

Isso também explica por que o bloqueio da tabela fora da função em um bloco plpgsql de quebra automática (como sugerido por @ypercube) impede os bloqueios.

Jack Douglas
fonte
3
Ponto fino: o ypercube realmente testou o bloqueio no SQL simples em uma transação explícita fora de uma função, que não é a mesma que um bloco plpgsql .
Erwin Brandstetter
1
Muito bem, meu mal. Acho que estava me confundindo com outra coisa que tentamos (que não impediu o impasse).
31415 Jack Douglas
4

Supondo que você execute outras instruções antes de chamar new_customer e que adquiram um bloqueio que conflite com EXCLUSIVE(basicamente, qualquer modificação de dados na tabela do cliente), a explicação é muito simples.

Pode-se reproduzir o problema com um exemplo simples (nem mesmo incluindo uma função):

CREATE TABLE test(id INTEGER);

1ª sessão:

BEGIN;

INSERT INTO test VALUES(1);

2ª sessão

BEGIN;
INSERT INTO test VALUES(1);
LOCK TABLE test IN EXCLUSIVE MODE;

1ª sessão

LOCK TABLE test IN EXCLUSIVE MODE;

Quando a primeira sessão faz a inserção, ela adquire o ROW EXCLUSIVEbloqueio em uma tabela. Enquanto isso, a sessão 2 tenta também obtém o ROW EXCLUSIVEbloqueio e tenta adquiri-lo EXCLUSIVE. Nesse momento, ele deve aguardar a 1ª sessão, pois o EXCLUSIVEbloqueio entra em conflito com ROW EXCLUSIVE. Por fim, a 1ª sessão pula os tubarões e tenta obter um EXCLUSIVEbloqueio, mas, como os bloqueios são adquiridos em ordem, eles ficam na fila após a 2ª sessão. Isso, por sua vez, aguarda o primeiro, produzindo um impasse:

DETAIL:  Process 28514 waits for ExclusiveLock on relation 58331454 of database 44697822; blocked by process 28084.
Process 28084 waits for ExclusiveLock on relation 58331454 of database 44697822; blocked by process 28514

A solução para esse problema é adquirir bloqueios o mais cedo possível, geralmente como primeira coisa em uma transação. Por outro lado, a carga de trabalho do PostgreSQL só precisa de bloqueios em alguns casos muito raros, então sugiro repensar a maneira como você faz o upsert (dê uma olhada neste artigo http://www.depesz.com/2012/06/10 / por que é tão complicado demais / ).

alexk
fonte
2
Isso tudo é interessante, mas a mensagem nos logs do banco de dados seria algo como: Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))Enquanto Jack apenas obtinha: Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))- indicando que a chamada de função é o primeiro comando em ambas as transações (a menos que esteja faltando alguma coisa).
Erwin Brandstetter
Obrigado, e eu concordo com o que você diz, mas isso não parece ser a causa neste caso. Isso fica mais claro no caso de teste mais mínimo que eu adicionei à pergunta (que você pode tentar).
Jack Douglas
2
Na verdade, você estava certo sobre a escalação de bloqueios - embora o mecanismo seja sutil .
Jack Douglas