Llamadas simultáneas a la misma función: ¿cómo se producen los puntos muertos?


15

new_customerUna aplicación web llama a mi función varias veces por segundo (pero solo una vez por sesión). Lo primero que hace es bloquear la customertabla (hacer un 'insertar si no existe', una variante simple de un upsert).

Entiendo que los documentos son que otras llamadas new_customersimplemente deberían hacer cola hasta que todas las llamadas anteriores hayan terminado:

LOCK TABLE obtiene un bloqueo a nivel de tabla, esperando si es necesario para liberar cualquier bloqueo en conflicto.

¿Por qué a veces es un punto muerto?

definición:

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

error del registro:

2015-07-28 08:02:58 BST DETALLE: El proceso 12380 espera a ExclusiveLock en la relación 16438 de la base de datos 12141; bloqueado por el proceso 12379.
        El proceso 12379 espera a ExclusiveLock en la relación 16438 de la base de datos 12141; bloqueado por el proceso 12380.
        Proceso 12380: seleccione new_customer (decode ($ 1 :: text, 'hex'))
        Proceso 12379: seleccione new_customer (decode ($ 1 :: text, 'hex'))
2015-07-28 08:02:58 BST SUGERENCIA: Consulte el registro del servidor para obtener detalles de la consulta.
2015-07-28 08:02:58 BST CONTEXTO: función SQL "nuevo_cliente" declaración 1
2015-07-28 08:02:58 BST STATEMENT: select new_customer (decode ($ 1 :: text, 'hex'))

relación:

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

editar:

Me las arreglé para obtener un caso de prueba reproducible simple. Para mí, esto parece un error debido a algún tipo de condición de carrera.

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

El script bash se ejecuta simultáneamente en dos sesiones bash:

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

registro de errores (generalmente un puñado de puntos muertos en las 1000 llamadas):

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:

@ypercube sugirió una variante con el lock tableexterior de la función:

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

Curiosamente, esto elimina los puntos muertos.


2
En la misma transacción, antes de ingresar a esa función, ¿se customerusa de una manera que agarre un bloqueo más débil? Entonces podría ser un problema de actualización de bloqueo.
Daniel Vérité

2
No puedo explicar esto. Daniel puede tener un punto. Podría valer la pena plantear esto en pgsql-general. De cualquier manera, ¿conoce la implementación de UPSERT en el próximo Postgres 9.5? Depesz mirándolo.
Erwin Brandstetter

2
Quiero decir dentro de la misma transacción, no solo la misma sesión (ya que los bloqueos se liberan al final del tx). La respuesta de @alexk es en lo que estaba pensando, pero si el tx comienza y termina con la función, eso no puede explicar el punto muerto.
Daniel Vérité

1
@Erwin, sin duda, te interesará la respuesta que recibí al publicar en pgsql-bugs :)
Jack dice que intente topanswers.xyz

2
Muy interesante de hecho. Tiene sentido que esto también funcione en plpgsql, ya que recuerdo casos similares de plpgsql que funcionan como se esperaba.
Erwin Brandstetter

Respuestas:


10

Publiqué esto en pgsql-bugs y la respuesta de Tom Lane indica que se trata de un problema de escalada de bloqueo, disfrazado por la mecánica de la forma en que se procesan las funciones del lenguaje SQL. Esencialmente, el bloqueo generado por el insertse obtiene antes del bloqueo exclusivo en la tabla :

Creo que el problema con esto es que una función SQL hará un análisis (y tal vez también una planificación; no tengo ganas de verificar el código en este momento) para todo el cuerpo de la función a la vez. Esto significa que, debido al comando INSERT, adquiere RowExclusiveLock en la tabla "prueba" durante el análisis del cuerpo de la función, antes de que el comando LOCK se ejecute realmente. Por lo tanto, el BLOQUEO representa un intento de escalado de bloqueo, y se deben esperar puntos muertos.

Esta técnica de codificación sería segura en plpgsql, pero no en una función de lenguaje SQL.

Ha habido discusiones sobre la reimplementación de las funciones del lenguaje SQL para que el análisis ocurra una declaración a la vez, pero no contenga la respiración sobre algo que sucede en esa dirección; no parece ser una preocupación de alta prioridad para nadie.

saludos, tom lane

Esto también explica por qué bloquear la tabla fuera de la función en un bloque envolvente plpgsql (como lo sugiere @ypercube) evita los puntos muertos.


3
Punto fino: ypercube realmente probó un bloqueo en SQL simple en una transacción explícita fuera de una función, que no es lo mismo que un bloque plpgsql .
Erwin Brandstetter

1
Muy bien, mi mal. Creo que me estaba confundiendo con otra cosa que probamos (que no evitó el punto muerto).
Jack dice que intente topanswers.xyz

4

Suponiendo que ejecuta otras instrucciones antes de llamar a new_customer, y adquieren un bloqueo que entra en conflicto EXCLUSIVE(básicamente, cualquier modificación de datos en la tabla del cliente), la explicación es muy simple.

Uno puede reproducir el problema con un ejemplo simple (ni siquiera incluye una función):

CREATE TABLE test(id INTEGER);

1ra sesión:

BEGIN;

INSERT INTO test VALUES(1);

2da sesión

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

1ra sesión

LOCK TABLE test IN EXCLUSIVE MODE;

Cuando la primera sesión realiza la inserción, adquiere el ROW EXCLUSIVEbloqueo en una tabla. Mientras tanto, la sesión 2 intenta también obtener el ROW EXCLUSIVEbloqueo e intenta adquirir un EXCLUSIVEbloqueo. En ese momento tiene que esperar a la primera sesión, ya que el EXCLUSIVEbloqueo entra en conflicto con ROW EXCLUSIVE. Por fin, la primera sesión salta a los tiburones e intenta obtener un EXCLUSIVEbloqueo, pero dado que los bloqueos se adquieren en orden, se pone en cola después de la segunda sesión. Esto, a su vez, espera al primero, produciendo un punto muerto:

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 solución a este problema es adquirir bloqueos lo antes posible, generalmente como una primera cosa en una transacción. Por otro lado, la carga de trabajo de PostgreSQL solo necesita bloqueos en algunos casos muy raros, por lo que te sugiero que reconsideres la forma en que lo haces (mira este artículo http://www.depesz.com/2012/06/10 / why-is-upsert-so-complicado / ).


2
Todo esto es interesante, pero el mensaje en los registros de DB leería 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'))Mientras Jack acaba de recibir: Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))- indicando que la llamada a la función es el primer comando en ambas transacciones (a menos que me falte algo).
Erwin Brandstetter

Gracias, y estoy de acuerdo con lo que dices, pero esta no parece ser la causa en este caso. Eso está más claro en el caso de prueba más mínimo que he agregado a la pregunta (que puedes probar tú mismo).
Jack dice que intente topanswers.xyz

2
En realidad resulta que tenías razón sobre la escalada de bloqueo, aunque el mecanismo es sutil .
Jack dice que intente topanswers.xyz
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.