Gleichzeitige Aufrufe derselben Funktion: Wie kommt es zu Deadlocks?

15

Meine Funktion new_customerwird mehrmals pro Sekunde (jedoch nur einmal pro Sitzung) von einer Webanwendung aufgerufen. Als erstes wird die customerTabelle gesperrt (Einfügen, wenn nicht vorhanden upsert).

Nach meinem Verständnis der Dokumentationnew_customer sollten andere Anrufe einfach anstehen, bis alle vorherigen Anrufe beendet sind:

LOCK TABLE ruft eine Sperre auf Tabellenebene ab und wartet bei Bedarf darauf, dass widersprüchliche Sperren freigegeben werden.

Warum ist es stattdessen manchmal Deadlocking?

Definition:

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

Fehler aus dem Protokoll:

2015-07-28 08:02:58 BST DETAIL: Prozess 12380 wartet auf ExclusiveLock für Relation 16438 der Datenbank 12141; gesperrt durch Prozess 12379.
        Der Prozess 12379 wartet auf ExclusiveLock für die Beziehung 16438 der Datenbank 12141. durch den Prozess 12380 blockiert.
        Prozess 12380: new_customer auswählen (decodieren ($ 1 :: text, 'hex'))
        Prozess 12379: new_customer auswählen (decodieren ($ 1 :: text, 'hex'))
2015-07-28 08:02:58 BST TIPP: Weitere Informationen zu Abfragen finden Sie im Serverprotokoll.
2015-07-28 08:02:58 BST CONTEXT: SQL-Funktionsanweisung "new_customer" 1
2015-07-28 08:02:58 BST STATEMENT: new_customer auswählen (decodieren ($ 1 :: text, 'hex'))

Beziehung:

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

bearbeiten:

Ich habe es geschafft, einen einfach reproduzierbaren Testfall zu bekommen. Für mich sieht das nach einem Fehler aus, der auf eine Art Rennbedingung zurückzuführen ist.

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

Bash-Skript wird gleichzeitig in zwei Bash-Sitzungen ausgeführt:

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

Fehlerprotokoll (normalerweise eine Handvoll Deadlocks über die 1000 Aufrufe):

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

2 bearbeiten:

@ypercube schlug eine Variante mit lock tableder Funktion außerhalb vor:

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

Interessanterweise beseitigt dies die Deadlocks.

Jack Douglas
quelle
2
Wird in derselben Transaktion vor dem Aufrufen dieser Funktion customereine Methode verwendet, die eine schwächere Sperre aufhebt? Dann könnte es ein Problem mit dem Upgrade der Sperre sein.
Daniel Vérité
2
Ich kann das nicht erklären. Daniel hat vielleicht einen Punkt. Könnte es wert sein, dies auf pgsql-general zu erwähnen. Wie auch immer, kennen Sie die UPSERT-Implementierung in Postgres 9.5? Depesz schaut es sich an.
Erwin Brandstetter
2
Ich meine innerhalb der gleichen Transaktion, nicht nur in der gleichen Sitzung (da die Sperren am Ende der Übertragung freigegeben werden). Die Antwort von @alexk ist das, worüber ich nachgedacht habe, aber wenn das Senden mit der Funktion beginnt und endet, kann das den Deadlock nicht erklären.
Daniel Vérité
1
@ Erwin Sie werden zweifellos an der Antwort interessiert sein, die ich vom Posten bei pgsql-bugs bekam :)
Jack Douglas
2
Sehr interessant. Es macht Sinn, dass dies auch in plpgsql funktioniert, da ich mich an ähnliche plpgsql-Fälle erinnere, die erwartungsgemäß funktionieren.
Erwin Brandstetter

Antworten:

10

Ich habe dies auf pgsql-bugs gepostet und die Antwort von Tom Lane zeigt an, dass es sich um ein Problem mit der Sperreneskalation handelt, das durch die Mechanik der Verarbeitung von SQL-Sprachfunktionen verschleiert ist. Im Wesentlichen wird die von generierte Sperre vor der exklusiven Sperre für die Tabelle insertabgerufen :

Ich glaube, das Problem dabei ist, dass eine SQL-Funktion das Parsen (und vielleicht auch das Planen; ich habe jetzt keine Lust, den Code zu überprüfen) für den gesamten Funktionskörper auf einmal ausführt. Dies bedeutet, dass Sie aufgrund des INSERT-Befehls RowExclusiveLock in der "test" -Tabelle während der Analyse des Funktionskörpers abrufen, bevor der LOCK-Befehl tatsächlich ausgeführt wird. Das LOCK stellt also einen Sperreneskalationsversuch dar, und Deadlocks sind zu erwarten.

Diese Codierungstechnik wäre in plpgsql sicher, jedoch nicht in einer SQL-Sprachfunktion.

Es gab Diskussionen über die Neuimplementierung von SQL-Sprachfunktionen, so dass das Parsen jeweils für eine Anweisung ausgeführt wird, aber halten Sie nicht den Atem an, wenn etwas in diese Richtung geschieht. es scheint für niemanden von hoher Priorität zu sein.

Grüße, Tom Lane

Dies erklärt auch, warum das Sperren der Tabelle außerhalb der Funktion in einem umhüllenden plpgsql-Block (wie von @ypercube vorgeschlagen) die Deadlocks verhindert.

Jack Douglas
quelle
3
Feiner Punkt: ypercube hat die Sperre in einfachem SQL in einer expliziten Transaktion außerhalb einer Funktion getestet , die nicht mit einem plpgsql- Block identisch ist .
Erwin Brandstetter
1
Ganz richtig, mein Schlechtes. Ich glaube, ich wurde mit einer anderen Sache verwirrt , die wir ausprobiert haben (was den Deadlock nicht verhinderte).
Jack Douglas
4

Angenommen, Sie führen vor dem Aufruf von new_customer eine andere Anweisung aus, und diese Anweisungen erhalten eine Sperre, die im Widerspruch zu EXCLUSIVE(im Grunde genommen jede Datenänderung in der Kundentabelle ) steht, ist die Erklärung sehr einfach.

Man kann das Problem mit einem einfachen Beispiel reproduzieren (das nicht einmal eine Funktion enthält):

CREATE TABLE test(id INTEGER);

1. Sitzung:

BEGIN;

INSERT INTO test VALUES(1);

2. Sitzung

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

1. Sitzung

LOCK TABLE test IN EXCLUSIVE MODE;

Wenn die erste Sitzung das Einfügen ausführt, erhält sie die ROW EXCLUSIVESperre für eine Tabelle. Währenddessen ruft Sitzung 2 auch die ROW EXCLUSIVESperre ab und versucht, eine EXCLUSIVESperre zu erwerben . An diesem Punkt muss auf die erste Sitzung gewartet werden, da die EXCLUSIVESperre mit in Konflikt steht ROW EXCLUSIVE. Endlich springt die erste Sitzung über die Haie und versucht, eine EXCLUSIVESperre zu erhalten. Da die Sperren jedoch in der richtigen Reihenfolge erworben wurden, wird sie nach der zweiten Sitzung in die Warteschlange gestellt. Dies wiederum wartet auf den ersten, was zu einem Deadlock führt:

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

Die Lösung für dieses Problem besteht darin, Sperren so früh wie möglich zu erwerben, normalerweise als erstes in einer Transaktion. Auf der anderen Seite benötigt die PostgreSQL-Workload nur in sehr seltenen Fällen Sperren. Ich empfehle daher, die Vorgehensweise beim Upsert zu überdenken (siehe Artikel unter http://www.depesz.com/2012/06/10) / warum-ist-so-kompliziert / ).

alexk
quelle
2
Das ist alles interessant, aber die Meldung in den Datenbankprotokollen würde ungefähr so ​​lauten: Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))Während Jack gerade Folgendes bekam: Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))- Zeigt an, dass der Funktionsaufruf der erste Befehl in beiden Transaktionen ist (es sei denn, mir fehlt etwas).
Erwin Brandstetter
Vielen Dank und ich stimme dem zu, was Sie sagen, aber dies scheint in diesem Fall nicht der Grund zu sein. Das ist klarer in dem minimaleren Testfall, den ich der Frage hinzugefügt habe (den Sie selbst ausprobieren könnten).
Jack Douglas
2
Tatsächlich stellte sich heraus, dass Sie mit der Sperreneskalation Recht hatten - obwohl der Mechanismus subtil ist .
Jack Douglas