Appels simultanés à la même fonction: comment se produisent les interblocages?

15

Ma fonction new_customerest appelée plusieurs fois par seconde (mais seulement une fois par session) par une application web. La toute première chose qu'il fait est de verrouiller la customertable (pour faire un «insert s'il n'existe pas» - une variante simple d'un upsert).

Ma compréhension des documents est que les autres appels vers new_customerdoivent simplement faire la queue jusqu'à ce que tous les appels précédents soient terminés:

LOCK TABLE obtient un verrou au niveau de la table, attendant si nécessaire que les verrous en conflit soient libérés.

Pourquoi est-il parfois dans l'impasse à la place?

définition:

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

erreur du journal:

2015-07-28 08:02:58 BST DETAIL: Le processus 12380 attend ExclusiveLock sur la relation 16438 de la base de données 12141; bloqué par le processus 12379.
        Le processus 12379 attend ExclusiveLock sur la relation 16438 de la base de données 12141; bloqué par le processus 12380.
        Processus 12380: sélectionnez new_customer (décode ($ 1 :: text, 'hex'))
        Processus 12379: sélectionnez new_customer (décode ($ 1 :: text, 'hex'))
2015-07-28 08:02:58 CONSEIL BST: Voir le journal du serveur pour les détails de la requête.
2015-07-28 08:02:58 BST CONTEXT: instruction "new_customer" de la fonction SQL 1
2015-07-28 08:02:58 BST STATEMENT: select new_customer (decode ($ 1 :: text, 'hex'))

relation:

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

Éditer:

J'ai réussi à obtenir un cas de test reproductible simple. Pour moi, cela ressemble à un bug en raison d'une sorte de condition de concurrence.

schéma:

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 exécuté simultanément dans deux sessions bash:

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

journal des erreurs (généralement une poignée de blocages sur les 1000 appels):

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

modifier 2:

@ypercube a proposé une variante avec l' lock tableextérieur de la fonction:

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

fait intéressant, cela élimine les blocages.

Jack dit d'essayer topanswers.xyz
la source
2
Dans la même transaction, avant d'entrer dans cette fonction, est customerutilisé de manière à saisir un verrou plus faible? Cela pourrait alors être un problème de mise à niveau du verrou.
Daniel Vérité
2
Je ne peux pas l'expliquer. Daniel a peut-être raison. Cela pourrait valoir la peine de le soulever sur pgsql-general. Dans tous les cas, êtes-vous au courant de l'implémentation d'UPSERT dans le prochain Postgres 9.5? Depesz y jette un œil.
Erwin Brandstetter
2
Je veux dire au sein de la même transaction, pas seulement la même session (car les verrous sont libérés à la fin du tx). La réponse de @alexk est ce à quoi je pensais, mais si le tx commence et se termine par la fonction, cela ne peut pas expliquer l'impasse.
Daniel Vérité
1
@Erwin, vous serez sans aucun doute intéressé par la réponse que j'ai obtenue en postant sur pgsql-bugs :)
Jack dit essayez topanswers.xyz
2
Très intéressant en effet. Il est logique que cela fonctionne également dans plpgsql, car je me souviens de cas plpgsql similaires fonctionnant comme prévu.
Erwin Brandstetter

Réponses:

10

J'ai posté ceci sur pgsql-bugs et la réponse de Tom Lane indique qu'il s'agit d'un problème d'escalade de verrous, déguisé par la mécanique de la façon dont les fonctions du langage SQL sont traitées. Essentiellement, le verrou généré par le insertest obtenu avant le verrou exclusif sur la table :

Je crois que le problème avec cela est qu'une fonction SQL effectuera l'analyse (et peut-être aussi la planification; ne pas avoir envie de vérifier le code en ce moment) pour le corps entier de la fonction à la fois. Cela signifie qu'en raison de la commande INSERT, vous acquérez RowExclusiveLock sur la table "test" pendant l'analyse du corps de la fonction, avant que la commande LOCK ne s'exécute réellement. Le LOCK représente donc une tentative d'escalade de verrouillage, et des blocages sont à prévoir.

Cette technique de codage serait sûre dans plpgsql, mais pas dans une fonction en langage SQL.

Il y a eu des discussions sur la réimplémentation des fonctions du langage SQL afin que l'analyse se produise une instruction à la fois, mais ne retenez pas votre souffle à propos de quelque chose qui se passe dans cette direction; cela ne semble être une préoccupation prioritaire pour personne.

salutations, tom lane

Cela explique également pourquoi le verrouillage de la table en dehors de la fonction dans un bloc plpgsql d'encapsulation (comme suggéré par @ypercube) empêche les blocages.

Jack dit d'essayer topanswers.xyz
la source
3
Point fin: ypercube a effectivement testé un verrou en SQL brut dans une transaction explicite en dehors d' une fonction, ce qui n'est pas la même chose qu'un bloc plpgsql .
Erwin Brandstetter
1
C'est vrai, ma mauvaise. Je pense que je me confondais avec une autre chose que nous avons essayée (ce qui n'a pas empêché l'impasse).
Jack dit d'essayer topanswers.xyz le
4

En supposant que vous exécutiez une autre instruction avant d'appeler new_customer et que celles-ci acquièrent un verrou qui entre en conflit EXCLUSIVE(essentiellement, toute modification de données dans la table client), l'explication est très simple.

On peut reproduire le problème avec un exemple simple (ne comprenant même pas de fonction):

CREATE TABLE test(id INTEGER);

1ère session:

BEGIN;

INSERT INTO test VALUES(1);

2e session

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

1ère session

LOCK TABLE test IN EXCLUSIVE MODE;

Lorsque la première session effectue l'insertion, elle acquiert le ROW EXCLUSIVEverrou sur une table. Pendant ce temps, la session 2 tente également d'obtenir le ROW EXCLUSIVEverrou et essaie d'acquérir un EXCLUSIVEverrou. À quel moment il doit attendre la 1ère session, car le EXCLUSIVEverrou entre en conflit avec ROW EXCLUSIVE. Enfin, la 1ère session saute les requins et essaie d'obtenir un EXCLUSIVEverrou, mais puisque les verrous sont acquis dans l'ordre, il fait la queue après la 2ème session. Ceci, à son tour, attend le premier, produisant un blocage:

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 solution à ce problème consiste à acquérir des verrous le plus tôt possible, généralement comme première chose dans une transaction. D'un autre côté, la charge de travail PostgreSQL n'a besoin de verrous que dans de très rares cas, donc je vous suggère de repenser la façon dont vous effectuez l'upsert (jetez un œil à cet article http://www.depesz.com/2012/06/10 / pourquoi-est-upsert-si-compliqué / ).

alexk
la source
2
Tout cela est intéressant, mais le message dans les journaux de la base de données se lirait comme suit: Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))Alors que Jack vient de recevoir: Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))- indiquant que l'appel de fonction est la première commande dans les deux transactions (sauf s'il me manque quelque chose).
Erwin Brandstetter
Merci, et je suis d'accord avec ce que vous dites, mais cela ne semble pas être la cause dans ce cas. C'est plus clair dans le cas de test le plus minimal que j'ai ajouté à la question (que vous pouvez essayer vous-même).
Jack dit d'essayer topanswers.xyz le
2
En fait, il s'avère que vous aviez raison sur l'escalade des verrous - bien que le mécanisme soit subtil .
Jack dit d'essayer topanswers.xyz le