Dalam artikel sebelumnya, di mana saya berbicara tentang pemantauan database PostgreSQL , ada ungkapan seperti itu:Grow wait
- aplikasi telah "beristirahat" di kunci seseorang . Jika ini adalah anomali satu kali yang lalu - suatu kesempatan untuk memahami alasan aslinya.
Situasi ini adalah salah satu yang paling tidak menyenangkan untuk DBA:- pada pandangan pertama, pangkalan berfungsi
- tidak ada sumber daya server yang habis
- ... tetapi beberapa permintaan "melambat"
Kemungkinan menangkap kunci "saat ini" sangat kecil, dan mereka hanya dapat bertahan beberapa detik, tetapi memperburuk waktu eksekusi yang direncanakan dari permintaan tersebut puluhan kali. Tetapi Anda hanya tidak ingin duduk dan menangkap apa yang sedang terjadi secara online, tetapi dalam lingkungan yang tenang untuk mencari tahu fakta dari pengembang mana yang menghukum apa sebenarnya masalahnya - siapa, dengan siapa dan karena sumber daya mana dari basis data itu mengalami konflik.Tapi bagaimana caranya? Memang, tidak seperti permintaan dengan rencananya, yang memungkinkan Anda untuk memahami secara terperinci apa yang terjadi dengan sumber daya dan berapa lama, kuncinya tidak meninggalkan jejak yang jelas ...Kecuali entri log pendek: Tapi mari kita coba menangkapnya!process ... still waiting for ...
Kami menangkap kunci dalam log
Tetapi bahkan untuk setidaknya baris ini muncul di log, server harus dikonfigurasi dengan benar:log_lock_waits = 'on'
... dan menetapkan batas minimum untuk analisis kami:deadlock_timeout = '100ms'
Ketika parameter diaktifkan log_lock_waits
, parameter ini juga menentukan setelah waktu berapa pesan tentang menunggu pemblokiran akan ditulis ke log server. Jika Anda mencoba menyelidiki penundaan yang disebabkan oleh kunci, masuk akal untuk menguranginya dibandingkan dengan nilai biasanya deadlock_timeout
.
Semua kebuntuan yang tidak punya waktu untuk "melepaskan" selama waktu ini akan robek. Dan di sini adalah kunci "biasa" - dibuang ke log oleh beberapa entri:2019-03-27 00:06:46.026 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest LOG: process 38162 still waiting for ExclusiveLock on advisory lock [225382138,225386226,141586103,2] after 100.047 ms
2019-03-27 00:06:46.026 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest DETAIL: Process holding the lock: 38154. Wait queue: 38162.
2019-03-27 00:06:46.026 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest CONTEXT: SQL statement "SELECT pg_advisory_xact_lock( '""'::regclass::oid::integer, 141586103::integer )"
PL/pgSQL function inline_code_block line 2 at PERFORM
2019-03-27 00:06:46.026 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest STATEMENT: DO $$ BEGIN
PERFORM pg_advisory_xact_lock( '""'::regclass::oid::integer, 141586103::integer );
EXCEPTION
WHEN query_canceled THEN RETURN;
END; $$;
…
2019-03-27 00:06:46.077 MSK [38162:84/166010018] [inside.tensor.ru] [local] csr-inside-bl10:10081: MergeRequest LOG: process 38162 acquired ExclusiveLock on advisory lock [225382138,225386226,141586103,2] after 150.741 ms
Contoh ini menunjukkan bahwa kita hanya memiliki 50 ms dari saat kunci muncul di log hingga saat resolusi. Hanya dalam interval ini kita dapat memperoleh setidaknya beberapa informasi tentangnya, dan semakin cepat kita dapat melakukan ini, semakin banyak kunci yang dapat kita analisis.Di sini informasi paling penting bagi kami adalah PID dari proses yang menunggu kunci. Di atasnya kita dapat membandingkan informasi antara log dan keadaan database. Dan pada saat yang sama untuk mengetahui seberapa bermasalah kunci tertentu - yaitu, berapa lama "tergantung".Untuk melakukan ini, kita hanya perlu beberapa RegExp pada LOG
catatan konten :const RE_lock_detect = /^process (\d+) still waiting for (.*) on (.*) after (\d+\.\d{3}) ms(?: at character \d+)?$/;
const RE_lock_acquire = /^process (\d+) acquired (.*) on (.*) after (\d+\.\d{3}) ms(?: at character \d+)?$/;
Kita sampai ke log
Sebenarnya, ini tetap sedikit - untuk mengambil log, belajar bagaimana memonitor dan meresponsnya.Kami sudah memiliki segalanya untuk ini - parser log online dan koneksi basis data yang sudah diaktifkan, yang saya bicarakan dalam sebuah artikel tentang sistem pemantauan PostgreSQL kami :
Tetapi jika Anda tidak memiliki sistem serupa yang digunakan, maka tidak apa-apa, kami akan melakukan semuanya “Langsung dari konsol”!Jika versi PostgreSQL 10 yang diinstal dan yang lebih tinggi beruntung, karena muncul fungsi pg_current_logfile () , yang secara eksplisit memberikan nama file log saat ini. Mendapatkannya dari konsol akan cukup mudah:psql -U postgres postgres -t -A -c "SELECT CASE WHEN substr(current_setting('log_directory'), 1, 1) <> '/' THEN current_setting('data_directory') ELSE '' END || '/' || pg_current_logfile()"
Jika versi ini tiba-tiba lebih muda - semuanya akan berubah, tetapi sedikit lebih rumit:ps uw -U postgres \
| grep [l]ogger \
| awk '{print "/proc/"$2"/fd"}' \
| xargs ls -l \
| grep `cd; psql -U postgres postgres -t -A -c "SELECT CASE WHEN substr(current_setting('log_directory'), 1, 1) = '/' THEN current_setting('log_directory') ELSE current_setting('data_directory') || '/' || current_setting('log_directory') END"` \
| awk '{print $NF}'
Kami tail -f
mendapatkan nama file lengkap, yang kami berikan dan akan menangkap penampilan di sana 'still waiting for'
.Nah, begitu sesuatu muncul di utas ini, kami menyebutnya ...Permintaan Parsing
Sayangnya, objek pemblokiran dalam log bukan berarti selalu sumber daya yang menyebabkan konflik. Misalnya, jika Anda mencoba membuat indeks dengan nama yang sama secara paralel, hanya akan ada sesuatu seperti itu di log still waiting for ExclusiveLock on transaction 12345
.Karena itu, kami harus menghapus status semua kunci dari basis data untuk proses yang menarik bagi kami. Dan informasi hanya tentang sepasang proses (yang memasang kunci / siapa yang mengharapkannya) tidak cukup, karena sering kali ada "rantai" harapan dari sumber daya yang berbeda antara transaksi:tx1 [resA] -> tx2 [resA]
tx2 [resB] -> tx3 [resB]
...
Klasik: "Kakek untuk lobak, nenek untuk kakek, cucu untuk nenek, ..."Oleh karena itu, kami akan menulis permintaan yang akan memberi tahu kami dengan tepat siapa yang menjadi akar penyebab masalah di sepanjang seluruh rantai kunci untuk PID tertentu:WITH RECURSIVE lm AS (
SELECT
rn
, CASE
WHEN lm <> 'Share' THEN row_number() OVER(PARTITION BY lm <> 'Share' ORDER BY rn)
END rx
, lm || 'Lock' lm
FROM (
SELECT
row_number() OVER() rn
, lm
FROM
unnest(
ARRAY[
'AccessShare'
, 'RowShare'
, 'RowExclusive'
, 'ShareUpdateExclusive'
, 'Share'
, 'ShareRowExclusive'
, 'Exclusive'
, 'AccessExclusive'
]
) T(lm)
) T
)
, lt AS (
SELECT
row_number() OVER() rn
, lt
FROM
unnest(
ARRAY[
'relation'
, 'extend'
, 'page'
, 'tuple'
, 'transactionid'
, 'virtualxid'
, 'object'
, 'userlock'
, 'advisory'
, ''
]
) T(lt)
)
, lmx AS (
SELECT
lr.rn lrn
, lr.rx lrx
, lr.lm lr
, ld.rn ldn
, ld.rx ldx
, ld.lm ld
FROM
lm lr
JOIN
lm ld ON
ld.rx > (SELECT max(rx) FROM lm) - lr.rx OR
(
(lr.rx = ld.rx) IS NULL AND
(lr.rx, ld.rx) IS DISTINCT FROM (NULL, NULL) AND
ld.rn >= (SELECT max(rn) FROM lm) - lr.rn
)
)
, lcx AS (
SELECT DISTINCT
(lr.locktype, lr.database, lr.relation, lr.page, lr.tuple, lr.virtualxid, lr.transactionid::text::bigint, lr.classid, lr.objid, lr.objsubid) target
, ld.pid ldp
, ld.mode ldm
, lr.pid lrp
, lr.mode lrm
FROM
pg_locks ld
JOIN
pg_locks lr ON
lr.pid <> ld.pid AND
(lr.locktype, lr.database, lr.relation, lr.page, lr.tuple, lr.virtualxid, lr.transactionid::text::bigint, lr.classid, lr.objid, lr.objsubid) IS NOT DISTINCT FROM (ld.locktype, ld.database, ld.relation, ld.page, ld.tuple, ld.virtualxid, ld.transactionid::text::bigint, ld.classid, ld.objid, ld.objsubid) AND
(lr.granted, ld.granted) = (TRUE, FALSE)
JOIN
lmx ON
(lmx.lr, lmx.ld) = (lr.mode, ld.mode)
WHERE
(ld.pid, ld.granted) = ($1::integer, FALSE)
)
SELECT
lc.locktype "type"
, CASE lc.locktype
WHEN 'relation' THEN ARRAY[relation::text]
WHEN 'extend' THEN ARRAY[relation::text]
WHEN 'page' THEN ARRAY[relation, page]::text[]
WHEN 'tuple' THEN ARRAY[relation, page, tuple]::text[]
WHEN 'transactionid' THEN ARRAY[transactionid::text]
WHEN 'virtualxid' THEN regexp_split_to_array(virtualxid::text, '/')
WHEN 'object' THEN ARRAY[classid, objid, objsubid]::text[]
WHEN 'userlock' THEN ARRAY[classid::text]
WHEN 'advisory' THEN ARRAY[classid, objid, objsubid]::text[]
END target
, lc.pid = lcx.ldp "locked"
, lc.pid
, regexp_replace(lc.mode, 'Lock$', '') "mode"
, lc.granted
, (lc.locktype, lc.database, lc.relation, lc.page, lc.tuple, lc.virtualxid, lc.transactionid::text::bigint, lc.classid, lc.objid, lc.objsubid) IS NOT DISTINCT FROM lcx.target "conflict"
FROM
lcx
JOIN
pg_locks lc ON
lc.pid IN (lcx.ldp, lcx.lrp);
Masalah # 1 - Mulai Lambat
Seperti yang dapat Anda lihat pada contoh dari log, di sebelah baris yang memberi tanda terjadinya kunci ( LOG
), ada 3 baris lagi ( DETAIL, CONTEXT, STATEMENT
) yang melaporkan informasi tambahan.Pertama, kami menunggu pembentukan "paket" lengkap dari semua 4 entri dan hanya setelah itu kami beralih ke database. Tetapi kita dapat menyelesaikan pembentukan paket hanya ketika Rekaman berikutnya (sudah ke-5!) Tiba, dengan rincian proses lain, atau dengan batas waktu. Jelas bahwa kedua opsi memancing menunggu yang tidak perlu.Untuk menghilangkan penundaan ini, kami beralih ke aliran memproses catatan kunci. Yaitu, sekarang kita beralih ke database target segera setelah kami mengurutkan catatan pertama, dan menyadari bahwa itu adalah kunci yang dijelaskan di sana.Masalah # 2 - permintaan lambat
Tapi tetap saja, beberapa kunci tidak punya waktu untuk dianalisis. Mereka mulai menggali lebih dalam - dan menemukan bahwa di beberapa server permintaan analisis kami dijalankan selama 15-20 ms !Alasannya ternyata biasa - jumlah kunci aktif di pangkalan, mencapai beberapa ribu:
pg_locks
Di sini harus dicatat bahwa kunci di PostgreSQL tidak "disimpan" di mana saja, tetapi disajikan hanya dalam tampilan sistem pg_locks , yang merupakan refleksi langsung dari fungsi tersebut pg_lock_status
. Dan dalam permintaan kami, kami membacanya tiga kali ! Sementara itu, kami membaca ribuan contoh kunci ini, dan membuang yang tidak terkait dengan PID kami, kunci itu sendiri berhasil "menyelesaikan" ...Solusinya adalah "mematerialisasikan" keadaan pg_locks di CTE - setelah itu Anda dapat menjalankannya sebanyak yang Anda inginkan, itu sudah tidak berubah. Selain itu, setelah duduk di algoritma, adalah mungkin untuk mengurangi semuanya menjadi hanya dua pemindaiannya.Dinamika Berlebihan
Dan bahkan jika sedikit melihat lebih dekat pada query di atas, kita dapat melihat bahwa CTE lm
dan lmx
tidak dengan cara apa pun terkait dengan data, tetapi hanya menghitung selalu definisi "statis" matriks yang sama dari konflik antara mode pemblokiran . Jadi mari kita atur sebagai kualitas VALUES
.Masalah # 3 - tetangga
Tetapi sebagian dari kunci masih dilewati. Biasanya ini terjadi sesuai dengan skema berikut - seseorang memblokir sumber daya populer, dan beberapa proses dengan cepat dan cepat berlari ke dalamnya atau sepanjang rantai lebih lanjut .Akibatnya, kami menangkap PID pertama, pergi ke basis target (dan agar tidak membebani itu, kami hanya menyimpan satu koneksi), apakah analisis, kembali dengan hasilnya, mengambil PID berikutnya ... Tapi hidup tidak sepadan di pangkalan, dan PID mengunci # 2 sudah hilang!Sebenarnya, mengapa kita perlu membuka database untuk setiap PID secara terpisah, jika kita masih memfilter kunci dari daftar umum dalam permintaan? Mari kita segera mengambil seluruh daftar proses yang saling bertentangan langsung dari database dan mendistribusikan kunci mereka di semua PID yang jatuh selama pelaksanaan permintaan:WITH lm(ld, lr) AS (
VALUES
('AccessShareLock', '{AccessExclusiveLock}'::text[])
, ('RowShareLock', '{ExclusiveLock,AccessExclusiveLock}'::text[])
, ('RowExclusiveLock', '{ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
, ('ShareUpdateExclusiveLock', '{ShareUpdateExclusiveLock,ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
, ('ShareLock', '{RowExclusiveLock,ShareUpdateExclusiveLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
, ('ShareRowExclusiveLock', '{RowExclusiveLock,ShareUpdateExclusiveLock,ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
, ('ExclusiveLock', '{RowShareLock,RowExclusiveLock,ShareUpdateExclusiveLock,ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
, ('AccessExclusiveLock', '{AccessShareLock,RowShareLock,RowExclusiveLock,ShareUpdateExclusiveLock,ShareLock,ShareRowExclusiveLock,ExclusiveLock,AccessExclusiveLock}'::text[])
)
, locks AS (
SELECT
(
locktype
, database
, relation
, page
, tuple
, virtualxid
, transactionid::text::bigint
, classid
, objid
, objsubid
) target
, *
FROM
pg_locks
)
, ld AS (
SELECT
*
FROM
locks
WHERE
NOT granted
)
, lr AS (
SELECT
*
FROM
locks
WHERE
target::text = ANY(ARRAY(
SELECT DISTINCT
target::text
FROM
ld
)) AND
granted
)
, lcx AS (
SELECT DISTINCT
lr.target
, ld.pid ldp
, ld.mode ldm
, lr.pid lrp
, lr.mode lrm
FROM
ld
JOIN
lr
ON lr.pid <> ld.pid AND
lr.target IS NOT DISTINCT FROM ld.target
)
SELECT
lc.locktype "type"
, CASE lc.locktype
WHEN 'relation' THEN
ARRAY[relation::text]
WHEN 'extend' THEN
ARRAY[relation::text]
WHEN 'page' THEN
ARRAY[relation, page]::text[]
WHEN 'tuple' THEN
ARRAY[relation, page, tuple]::text[]
WHEN 'transactionid' THEN
ARRAY[transactionid::text]
WHEN 'virtualxid' THEN
regexp_split_to_array(virtualxid::text, '/')
WHEN 'object' THEN
ARRAY[classid, objid, objsubid]::text[]
WHEN 'userlock' THEN
ARRAY[classid::text]
WHEN 'advisory' THEN
ARRAY[classid, objid, objsubid]::text[]
END target
, lc.pid = lcx.ldp as locked
, lc.pid
, regexp_replace(lc.mode, 'Lock$', '') "mode"
, lc.granted
, lc.target IS NOT DISTINCT FROM lcx.target as conflict
FROM
lcx
JOIN
locks lc
ON lc.pid IN (lcx.ldp, lcx.lrp);
Sekarang kita punya waktu untuk menganalisis bahkan kunci yang ada hanya 1-2 ms setelah masuk ke log.Analisa ini
Sebenarnya, mengapa kami mencoba begitu lama? Apa yang ini memberi kita sebagai hasilnya? ..type | target | locked | pid | mode | granted | conflict
---------------------------------------------------------------------------------------
relation | {225388639} | f | 216547 | AccessShare | t | f
relation | {423576026} | f | 216547 | AccessShare | t | f
advisory | {225386226,194303167,2} | t | 24964 | Exclusive | f | t
relation | {341894815} | t | 24964 | AccessShare | t | f
relation | {416441672} | f | 216547 | AccessShare | t | f
relation | {225964322} | f | 216547 | AccessShare | t | f
...
Dalam lempeng ini, nilai target
dengan tipe berguna bagi kami relation
, karena setiap nilai tersebut adalah OID dari tabel atau indeks . Sekarang hanya ada sedikit yang tersisa - untuk belajar menginterogasi database OID yang masih belum diketahui oleh kami, untuk menerjemahkannya ke dalam bentuk yang dapat dibaca manusia:SELECT $1::oid::regclass;
Sekarang, mengetahui "matriks" penuh dari kunci dan semua objek di mana mereka ditumpangkan oleh masing-masing transaksi, kita dapat menyimpulkan "apa yang terjadi sama sekali" dan sumber daya apa yang menyebabkan konflik, bahkan jika permintaan diblokir, kita tidak tahu. Ini dapat dengan mudah terjadi, misalnya, ketika waktu eksekusi permintaan pemblokiran tidak cukup lama untuk sampai ke log, tetapi transaksi aktif setelah selesai dan menciptakan masalah untuk waktu yang lama.
Tetapi tentang ini - di artikel lain."Semua dengan sendirinya"
Sementara itu, kami akan mengumpulkan versi lengkap "pemantauan", yang secara otomatis akan menghapus status kunci untuk arsip kami segera setelah sesuatu yang mencurigakan muncul di log:ps uw -U postgres \
| grep [l]ogger \
| awk '{print "/proc/"$2"/fd"}' \
| xargs ls -l \
| grep `cd; psql -U postgres postgres -t -A -c "SELECT CASE WHEN substr(current_setting('log_directory'), 1, 1) = '/' THEN current_setting('log_directory') ELSE current_setting('data_directory') || '/' || current_setting('log_directory') END"` \
| awk '{print $NF}' \
| xargs -l -I{} tail -f {} \
| stdbuf -o0 grep 'still waiting for' \
| xargs -l -I{} date '+%Y%m%d-%H%M%S.%N' \
| xargs -l -I{} psql -U postgres postgres -f detect-lock.sql -o {}-lock.log
Dan selanjutnya untuk detect-lock.sql
menempatkan permintaan terakhir. Jalankan - dan dapatkan banyak file dengan konten yang diinginkan di dalamnya:# cat 20200526-181433.331839375-lock.log
type | target | locked | pid | mode | granted | conflict
---------------+----------------+--------+--------+--------------+---------+----------
relation | {279588430} | t | 136325 | RowExclusive | t | f
relation | {279588422} | t | 136325 | RowExclusive | t | f
relation | {17157} | t | 136325 | RowExclusive | t | f
virtualxid | {110,12171420} | t | 136325 | Exclusive | t | f
relation | {279588430} | f | 39339 | RowExclusive | t | f
relation | {279588422} | f | 39339 | RowExclusive | t | f
relation | {17157} | f | 39339 | RowExclusive | t | f
virtualxid | {360,11744113} | f | 39339 | Exclusive | t | f
virtualxid | {638,4806358} | t | 80553 | Exclusive | t | f
advisory | {1,1,2} | t | 80553 | Exclusive | f | t
advisory | {1,1,2} | f | 80258 | Exclusive | t | t
transactionid | {3852607686} | t | 136325 | Exclusive | t | f
extend | {279588422} | t | 136325 | Exclusive | f | t
extend | {279588422} | f | 39339 | Exclusive | t | t
transactionid | {3852607712} | f | 39339 | Exclusive | t | f
(15 rows)
Nah, lalu - duduk dan menganalisis ...