Chiamate simultanee alla stessa funzione: come si verificano i deadlock?


15

La mia funzione new_customerviene chiamata più volte al secondo (ma solo una volta per sessione) da un'applicazione web. La prima cosa che fa è bloccare la customertabella (per fare un 'inserire se non esiste', una semplice variante di un upsert).

La mia comprensione dei documenti è che le altre chiamate a new_customerdovrebbero semplicemente fare la coda fino al termine di tutte le chiamate precedenti:

LOCK TABLE ottiene un blocco a livello di tabella, attendendo se necessario il rilascio di eventuali blocchi in conflitto.

Perché a volte è invece deadlock?

definizione:

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;
$$;

errore dal registro:

2015-07-28 08:02:58 DETTAGLIO BST: il processo 12380 attende l'esclusivo blocco sulla relazione 16438 del database 12141; bloccato dal processo 12379.
        Il processo 12379 attende ExclusiveLock sulla relazione 16438 del database 12141; bloccato dal processo 12380.
        Processo 12380: seleziona new_customer (decodifica ($ 1 :: testo, 'hex'))
        Processo 12379: seleziona new_customer (decodifica ($ 1 :: testo, 'hex'))
2015-07-28 08:02:58 BST SUGGERIMENTO: consultare il registro del server per i dettagli della query.
2015-07-28 08:02:58 BST CONTESTO: istruzione "new_customer" della funzione SQL 1
28/07/2015 08:02:58 DICHIARAZIONE BST: selezionare new_customer (decodifica ($ 1 :: testo, 'hex'))

relazione:

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

modificare:

Sono riuscito a ottenere un caso di test riproducibile semplice. Per me questo sembra un bug a causa di una sorta di condizione di razza.

schema:

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;
$$;

script bash eseguito contemporaneamente in due sessioni bash:

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

registro errori (di solito una manciata di deadlock oltre le 1000 chiamate):

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')

modifica 2:

@ypercube ha suggerito una variante con l' lock tableesterno della funzione:

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

interessante notare che questo elimina i deadlock.


2
Nella stessa transazione, prima di accedere a quella funzione, viene customerutilizzato in modo da afferrare un blocco più debole? Quindi potrebbe essere un problema di aggiornamento del blocco.
Daniel Vérité,

2
Non posso spiegarlo. Daniel potrebbe avere ragione. Potrebbe valere la pena sollevarlo su pgsql-general. Ad ogni modo, sei a conoscenza dell'implementazione di UPSERT nel prossimo Postgres 9.5? Depesz lo guarda.
Erwin Brandstetter,

2
Intendo nella stessa transazione, non solo nella stessa sessione (poiché i blocchi vengono rilasciati alla fine di TX). La risposta di @alexk è ciò a cui stavo pensando, ma se il tx inizia e termina con la funzione, questo non può spiegare il deadlock.
Daniel Vérité

1
@Erwin sarai senza dubbio interessato alla risposta che ho ricevuto dalla pubblicazione su pgsql-bugs :)
Jack dice di provare topanswers.xyz

2
Davvero molto interessante. Ha senso che funzioni anche in plpgsql, poiché ricordo casi simili di plpgsql che funzionano come previsto.
Erwin Brandstetter,

Risposte:


10

Ho pubblicato questo su pgsql-bugs e la risposta lì da Tom Lane indica che si tratta di un problema di escalation del blocco, mascherato dalla meccanica del modo in cui le funzioni del linguaggio SQL vengono elaborate. In sostanza, il blocco generato da insertè ottenuto prima del blocco esclusivo sulla tabella :

Credo che il problema sia che una funzione SQL eseguirà l'analisi (e forse anche la pianificazione; non mi va di controllare subito il codice) per l'intero corpo della funzione in una volta. Ciò significa che a causa del comando INSERT si acquisisce RowExclusiveLock sulla tabella "test" durante l'analisi del corpo della funzione, prima che il comando LOCK venga effettivamente eseguito. Quindi il BLOCCO rappresenta un tentativo di escalation dei blocchi e sono previsti deadlock.

Questa tecnica di codifica sarebbe sicura in plpgsql, ma non in una funzione in linguaggio SQL.

Ci sono state discussioni sulla reimplementazione delle funzioni del linguaggio SQL in modo che l'analisi avvenga un'istruzione alla volta, ma non trattenere il respiro per qualcosa che accade in quella direzione; non sembra essere una priorità per nessuno.

saluti, Tom Lane

Questo spiega anche perché il blocco della tabella all'esterno della funzione in un blocco wrapping plpgsql (come suggerito da @ypercube) impedisce i deadlock.


3
Punto positivo : ypercube ha effettivamente testato un blocco in SQL semplice in una transazione esplicita al di fuori di una funzione, che non è la stessa di un blocco plpgsql .
Erwin Brandstetter,

1
Giustissimo, mio ​​cattivo. Penso che mi stavo confondendo con un'altra cosa che abbiamo provato (che non ha impedito lo stallo).
Jack dice di provare topanswers.xyz il

4

Supponendo che eseguiate altre dichiarazioni prima di chiamare new_customer e che acquisiscano un blocco in conflitto con EXCLUSIVE(sostanzialmente, qualsiasi modifica dei dati nella tabella dei clienti), la spiegazione è molto semplice.

Si può riprodurre il problema con un semplice esempio (senza nemmeno includere una funzione):

CREATE TABLE test(id INTEGER);

1a sessione:

BEGIN;

INSERT INTO test VALUES(1);

2a sessione

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

1a sessione

LOCK TABLE test IN EXCLUSIVE MODE;

Quando la prima sessione esegue l'inserimento, acquisisce il ROW EXCLUSIVEblocco su una tabella. Nel frattempo, anche la sessione 2 tenta di ottenere il ROW EXCLUSIVEblocco e tenta di acquisire un EXCLUSIVEblocco. A quel punto deve attendere la prima sessione, poiché il EXCLUSIVEblocco è in conflitto con ROW EXCLUSIVE. Alla fine, la prima sessione salta gli squali e cerca di ottenere un EXCLUSIVEblocco, ma poiché i blocchi vengono acquisiti in ordine, si mette in coda dopo la seconda sessione. Questo, a sua volta, attende il primo, producendo un deadlock:

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

La soluzione a questo problema è acquisire i blocchi il più presto possibile, di solito come prima cosa in una transazione. D'altro canto, il carico di lavoro PostgreSQL necessita solo di blocchi in alcuni casi molto rari, quindi suggerirei di ripensare il modo in cui si fa l'upload (dai un'occhiata a questo articolo http://www.depesz.com/2012/06/10 / why-is-upsert-so-complicato / ).


2
Tutto questo è interessante, ma il messaggio nei registri db potrebbe leggere qualcosa del tipo: Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))Mentre Jack ha appena ricevuto: Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))- che indica che la chiamata di funzione è il primo comando in entrambe le transazioni (a meno che non mi manchi qualcosa).
Erwin Brandstetter,

Grazie, e sono d'accordo con quello che dici, ma questo non sembra essere la causa in questo caso. È più chiaro nel caso di test più minimale che ho aggiunto alla domanda (che potresti provare tu stesso).
Jack dice di provare topanswers.xyz il

2
In realtà risulta che avevi ragione sull'escalation dei blocchi, sebbene il meccanismo sia sottile .
Jack dice di provare topanswers.xyz il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.