Panggilan serentak ke fungsi yang sama: bagaimana deadlock terjadi?

15

Fungsi saya new_customerdipanggil beberapa kali per detik (tetapi hanya sekali per sesi) oleh aplikasi web. Hal pertama yang dilakukannya adalah mengunci customertabel (untuk melakukan 'masukkan jika tidak ada' — varian sederhana dari sebuah upsert).

Pemahaman saya tentang dokumen adalah bahwa panggilan lain new_customerharus cukup mengantri sampai semua panggilan sebelumnya selesai:

LOCK TABLE mendapatkan kunci tingkat meja, menunggu jika perlu untuk melepaskan kunci yang bertentangan.

Mengapa kadang-kadang menemui jalan buntu?

definisi:

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

kesalahan dari log:

2015-07-28 08:02:58 Detail BST: Memproses 12380 menunggu ExclusiveLock pada relasi 16438 dari basis data 12141; diblokir oleh proses 12379.
        Proses 12379 menunggu ExclusiveLock pada relasi 16438 dari basis data 12141; diblokir oleh proses 12380.
        Proses 12380: pilih new_customer (decode ($ 1 :: text, 'hex'))
        Proses 12379: pilih new_customer (decode ($ 1 :: text, 'hex'))
2015-07-28 08:02:58 BST HINT: Lihat log server untuk detail kueri.
2015-07-28 08:02:58 BST CONTEXT: SQL function "new_customer" pernyataan 1
2015-07-28 08:02:58 PERNYATAAN BST: pilih new_customer (decode ($ 1 :: text, 'hex'))

hubungan:

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

edit:

Saya sudah berhasil mendapatkan test case sederhana yang dapat direproduksi. Bagi saya ini terlihat seperti bug karena semacam kondisi balapan.

skema:

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

skrip bash dijalankan secara bersamaan dalam dua sesi bash:

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

log kesalahan (biasanya segelintir kebuntuan selama 1000 panggilan):

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

edit 2:

@ ypercube menyarankan varian dengan lock tablefungsi di luarnya:

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

Menariknya ini menghilangkan kebuntuan.

Jack mengatakan coba topanswers.xyz
sumber
2
Dalam transaksi yang sama, sebelum memasuki fungsi itu, customerdigunakan dengan cara yang akan mengambil kunci yang lebih lemah? Maka itu bisa menjadi masalah peningkatan kunci.
Daniel Vérité
2
Saya tidak bisa menjelaskan ini. Daniel mungkin ada benarnya. Mungkin layak mengangkat ini di pgsql-general. Either way, apakah Anda mengetahui implementasi UPSERT di Postgres 9.5 mendatang? Depesz melihatnya.
Erwin Brandstetter
2
Maksud saya dalam transaksi yang sama, bukan hanya sesi yang sama (seperti kunci dilepaskan pada akhir). Jawaban oleh @alexk adalah apa yang saya pikirkan, tetapi jika tx mulai dan berakhir dengan fungsi, itu tidak bisa menjelaskan kebuntuan.
Daniel Vérité
1
@ Erwin Anda pasti akan tertarik dengan jawaban yang saya dapatkan dari posting di pgsql-bugs :)
Jack mengatakan coba topanswers.xyz
2
Sangat menarik. Masuk akal bahwa ini bekerja di plpgsql, juga, karena saya ingat kasus serupa plpgsql bekerja seperti yang diharapkan.
Erwin Brandstetter

Jawaban:

10

Saya memposting ini ke pgsql-bug dan jawaban di sana dari Tom Lane menunjukkan ini adalah masalah eskalasi kunci, disamarkan oleh mekanisme cara fungsi bahasa SQL diproses. Pada dasarnya, kunci yang dihasilkan oleh insertdiperoleh sebelum kunci eksklusif di atas meja :

Saya percaya masalah dengan ini adalah bahwa fungsi SQL akan melakukan parsing (dan mungkin perencanaan juga; jangan merasa seperti memeriksa kode sekarang) untuk seluruh fungsi tubuh sekaligus. Ini berarti bahwa karena perintah INSERT Anda memperoleh RowExclusiveLock pada tabel "test" selama parsing fungsi tubuh, sebelum perintah LOCK benar-benar dijalankan. Jadi LOCK merupakan upaya eskalasi kunci, dan deadlock diharapkan.

Teknik pengkodean ini akan aman di plpgsql, tetapi tidak dalam fungsi SQL-language.

Ada diskusi tentang mengimplementasikan kembali fungsi-fungsi bahasa SQL sehingga parsing muncul satu per satu pernyataan, tetapi jangan menahan nafas tentang sesuatu yang terjadi ke arah itu; tampaknya tidak menjadi perhatian prioritas tinggi bagi siapa pun.

salam, tom lane

Ini juga menjelaskan mengapa mengunci tabel di luar fungsi dalam blok plpgsql pembungkus (seperti yang disarankan oleh @ypercube) mencegah kebuntuan.

Jack mengatakan coba topanswers.xyz
sumber
3
Titik halus: ypercube sebenarnya menguji kunci dalam SQL sederhana dalam transaksi eksplisit di luar fungsi, yang tidak sama dengan blok plpgsql .
Erwin Brandstetter
1
Benar sekali, salahku. Saya pikir saya menjadi bingung dengan hal lain yang kami coba (yang tidak mencegah kebuntuan).
Jack bilang coba topanswers.xyz
4

Dengan asumsi Anda menjalankan pernyataan lain sebelum memanggil new_customer, dan mereka mendapatkan kunci yang bertentangan dengan EXCLUSIVE (pada dasarnya, setiap modifikasi data dalam tabel pelanggan), penjelasannya sangat sederhana.

Seseorang dapat mereproduksi masalah dengan contoh sederhana (bahkan tidak termasuk fungsi):

CREATE TABLE test(id INTEGER);

Sesi 1:

BEGIN;

INSERT INTO test VALUES(1);

Sesi 2

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

Sesi 1

LOCK TABLE test IN EXCLUSIVE MODE;

Ketika sesi pertama melakukan penyisipan, ia memperoleh ROW EXCLUSIVEkunci di atas meja. Sementara itu, sesi 2 mencoba juga mendapatkan ROW EXCLUSIVEkunci, dan mencoba untuk mendapatkan EXCLUSIVEkunci. Pada titik mana ia harus menunggu sesi 1, karena EXCLUSIVEkunci konflik dengan ROW EXCLUSIVE. Akhirnya, sesi 1 melompat hiu dan mencoba untuk mendapatkan EXCLUSIVEkunci, tetapi karena kunci diperoleh secara berurutan, ia akan mengantri setelah sesi ke-2. Ini, pada gilirannya, menunggu yang pertama, menghasilkan jalan buntu:

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

Solusi untuk masalah ini adalah mendapatkan kunci sesegera mungkin, biasanya sebagai hal pertama dalam suatu transaksi. Di sisi lain, beban kerja PostgreSQL hanya perlu kunci dalam beberapa kasus yang sangat jarang, jadi saya sarankan memikirkan kembali cara Anda melakukan upert (lihat artikel ini http://www.depesz.com/2012/06/10 / why-is-upsert-so-ribet / ).

alexk
sumber
2
Ini semua menarik, tetapi pesan di log db akan membaca sesuatu seperti: Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))Sementara Jack baru saja: Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))- menunjukkan bahwa pemanggilan fungsi adalah perintah pertama dalam kedua transaksi (kecuali jika saya kehilangan sesuatu).
Erwin Brandstetter
Terima kasih, dan saya setuju dengan apa yang Anda katakan, tetapi ini tampaknya bukan penyebab dalam kasus ini. Itu lebih jelas dalam test case yang lebih minimal yang saya tambahkan ke pertanyaan (yang bisa Anda coba sendiri).
Jack bilang coba topanswers.xyz
2
Sebenarnya ternyata Anda benar tentang eskalasi kunci - meskipun mekanismenya halus .
Jack bilang coba topanswers.xyz