Locks in PostgreSQL: 4. Locks in memory

Biarkan saya mengingatkan Anda bahwa kita sudah berbicara tentang kunci hubungan , kunci tingkat baris , tentang kunci objek lain (termasuk yang predikat), dan tentang hubungan antara berbagai jenis kunci.

Hari ini saya mengakhiri seri ini dengan artikel tentang kunci memori . Kami akan berbicara tentang spinlocks, kunci ringan dan kunci buffer, serta alat pemantauan dan pengambilan sampel harapan.



Putar kunci


Tidak seperti kunci biasa, "berat", kunci lebih ringan dan lebih murah (dalam hal overhead) digunakan untuk melindungi struktur dalam RAM bersama.

Yang paling sederhana adalah spin-lock atau spinlocks . Mereka dirancang untuk menangkap untuk waktu yang sangat singkat (beberapa instruksi prosesor) dan melindungi masing-masing bagian memori dari perubahan simultan.

Spin lock diimplementasikan berdasarkan instruksi atom dari prosesor, seperti perbandingan dan pertukaran. Mereka mendukung mode eksklusif tunggal. Jika kunci sibuk, proses menunggu melakukan menunggu aktif - perintah diulangi ("berputar" dalam loop, maka nama itu) sampai dieksekusi dengan sukses. Ini masuk akal, karena kunci pintal digunakan ketika probabilitas konflik diperkirakan sangat rendah.

Spin lock tidak menyediakan deteksi kebuntuan (pengembang PostgreSQL memantau ini) dan tidak menyediakan alat pemantauan. Pada umumnya, satu-satunya hal yang dapat kita lakukan dengan kunci pintal adalah mengetahui keberadaannya.

Kunci ringan


Berikutnya datang apa yang disebut kunci cahaya (kunci ringan, lwlocks).

Mereka ditangkap untuk waktu yang singkat untuk bekerja dengan struktur data (misalnya, tabel hash atau daftar pointer). Sebagai aturan, kunci cahaya tidak dipegang dalam waktu lama, tetapi dalam beberapa kasus, kunci ringan melindungi operasi I / O, sehingga pada prinsipnya, waktu dapat berubah menjadi signifikan.

Dua mode didukung: eksklusif (untuk mengubah data) dan dibagikan (hanya baca). Dengan demikian, tidak ada antrian tunggu: jika beberapa proses menunggu kunci dilepaskan, salah satu dari mereka akan mendapatkan akses lebih atau kurang secara acak. Dalam sistem dengan tingkat paralelisme dan beban berat yang tinggi, ini dapat menyebabkan efek yang tidak menyenangkan (lihat, misalnya, diskusi ).

Mekanisme untuk memeriksa kebuntuan tidak disediakan, ini tetap pada hati nurani pengembang kernel. Namun, kunci cahaya memiliki alat pemantauan, oleh karena itu, tidak seperti kunci kunci, mereka dapat "dilihat" (beberapa saat kemudian saya akan menunjukkan caranya).

Buffer klip


Jenis kunci lain yang telah kita bahas dalam artikel tentang cache buffer adalah pinning buffer .

Dengan penyangga tetap, Anda dapat melakukan berbagai tindakan, termasuk mengubah data, tetapi dengan ketentuan bahwa perubahan ini tidak akan terlihat oleh proses lain karena multiversion. Artinya, Anda dapat menambahkan baris baru ke halaman, tetapi Anda tidak dapat mengganti halaman di buffer dengan yang lain.

Jika proses terhambat oleh pengikatan, biasanya hanya melewatkan buffer seperti itu dan memilih yang lain. Tetapi dalam beberapa kasus, ketika penyangga khusus ini diperlukan, proses mengantri dan tertidur - sistem akan membangunkannya ketika pengancing dihapus.

Harapan konsolidasi tersedia untuk pemantauan.

Contoh: cache buffer




Sekarang, untuk mendapatkan wawasan (tidak lengkap!) Tentang bagaimana dan di mana kunci digunakan, pertimbangkan contoh cache penyangga.

Untuk mengakses tabel hash yang berisi referensi ke buffer, proses tersebut harus menangkap kunci pemetaan buffer cahaya dalam mode bersama, dan jika tabel perlu diubah, maka dalam mode luar biasa. Untuk mengurangi rincian, kunci ini disusun sebagai tahap , yang terdiri dari 128 kunci terpisah, yang masing-masing melindungi bagiannya sendiri dari tabel hash.

Proses mendapatkan akses ke header buffer menggunakan spin-lock. Operasi individual (seperti menambah penghitung) juga dapat dilakukan tanpa kunci eksplisit menggunakan instruksi atom dari prosesor.

Untuk membaca konten buffer, diperlukan kunci konten buffer. Biasanya ditangkap hanya untuk waktu yang diperlukan untuk membaca pointer ke versi garis, dan kemudian perlindungan yang disediakan oleh klip buffer sudah cukup. Untuk memodifikasi konten buffer, kunci ini harus ditangkap dalam mode luar biasa.

Saat membaca buffer dari disk (atau menulis ke disk), IO dalam kunci progres juga ditangkap, yang menandakan proses lain bahwa halaman sedang dibaca (atau ditulis) - mereka dapat mengantri jika mereka juga perlu melakukan sesuatu dengan halaman ini.

Pointer ke buffer gratis dan untuk korban berikutnya dilindungi oleh strategi kunci spin kunci penyangga tunggal.

Contoh: buffer log



Contoh lain: log buffer.

Untuk cache jurnal, tabel hash juga digunakan yang berisi pemetaan halaman ke buffer. Tidak seperti cache buffer, tabel hash ini dilindungi oleh satu-satunya kunci ringan WALBufMappingLock, karena ukuran cache jurnal lebih kecil (biasanya 1/32 dari cache buffer) dan akses ke buffer lebih efisien.

Menulis halaman ke disk dilindungi oleh kunci WALWriteLock yang ringan sehingga hanya satu proses yang dapat melakukan operasi ini pada satu waktu.

Untuk membuat entri jurnal, proses tersebut harus terlebih dahulu menyediakan ruang di halaman WAL. Untuk melakukan ini, ia menangkap kunci posisi memasukkan kunci pengunci. Setelah tempat dicadangkan, proses menyalin isi catatannya ke tempat yang ditunjuk. Menyalin dapat dilakukan oleh beberapa proses pada saat yang sama, di mana catatan dilindungi oleh tranche dari 8 kunci penguncian kunci mudah (proses harus menangkap salah satu dari mereka).

Gambar ini tidak menunjukkan semua kunci yang terkait dengan log prarekam, tetapi ini dan contoh sebelumnya harus memberikan beberapa gagasan tentang penggunaan kunci dalam RAM.

Pemantauan harapan


Dimulai dengan PostgreSQL 9.6, tunggu alat pemantauan dibangun ke tampilan pg_stat_activity. Ketika suatu proses (sistem atau pemeliharaan) tidak dapat melakukan pekerjaannya dan sedang menunggu sesuatu, harapan ini dapat dilihat dalam tampilan: kolom wait_event_type menunjukkan jenis harapan, dan kolom wait_event menunjukkan nama harapan tertentu.

Perlu diingat bahwa pandangan hanya menunjukkan harapan-harapan yang ditangani dengan tepat dalam kode sumber. Jika pandangan tidak menunjukkan harapan, ini umumnya tidak berarti dengan probabilitas 100 persen bahwa prosesnya benar-benar tidak mengharapkan apa pun.

Sayangnya, satu-satunya informasi yang tersedia tentang harapan adalah informasi saat ini . Tidak ada statistik yang disimpan. Satu-satunya cara untuk mendapatkan gambaran harapan dari waktu ke waktu adalah dengan mengambil sampel kondisi tampilan pada interval tertentu. Tidak ada cara bawaan untuk ini, tetapi Anda dapat menggunakan ekstensi, misalnya, pg_wait_sampling .

Penting untuk memperhitungkan sifat probabilistik dari pengambilan sampel. Untuk mendapatkan gambar yang kurang lebih andal, jumlah pengukuran harus cukup besar. Pengambilan sampel pada frekuensi rendah mungkin tidak memberikan gambaran yang andal, dan meningkatkan frekuensi akan menyebabkan peningkatan overhead. Untuk alasan yang sama, pengambilan sampel tidak berguna untuk menganalisis sesi yang berumur pendek.

Semua harapan dapat dibagi menjadi beberapa tipe.

Harapan dari kunci yang dianggap membentuk kategori besar:

  • menunggu kunci objek (Nilai kunci di kolom wait_event_type);
  • menunggu kunci ringan (LWLock);
  • menunggu buffer yang disematkan (BufferPin).

Tetapi proses dapat mengharapkan peristiwa lain:

  • I / O harapan (IO) terjadi ketika suatu proses perlu menulis atau membaca data;
  • proses dapat menunggu data yang diperlukan untuk bekerja dari klien (Klien) atau dari proses lain (IPC);
  • ekstensi dapat mendaftarkan harapan spesifik mereka (Ekstensi).

Ada situasi ketika suatu proses sama sekali tidak melakukan pekerjaan yang bermanfaat. Kategori ini termasuk:

  • menunggu proses latar belakang di loop utama (Aktivitas);
  • menunggu penghitung waktu (Timeout).

Sebagai aturan, harapan seperti itu β€œnormal” dan tidak membicarakan masalah apa pun.

Jenis ekspektasi diikuti oleh nama ekspektasi tertentu. Tabel lengkap dapat ditemukan dalam dokumentasi .

Jika tidak ada nama tunggu yang ditentukan, prosesnya tidak dalam kondisi menunggu. Waktu seperti itu harus dianggap tidak terhitung , karena pada kenyataannya tidak diketahui apa yang sebenarnya terjadi pada saat ini.

Namun, sudah saatnya untuk melihat.

=> SELECT pid, backend_type, wait_event_type, wait_event FROM pg_stat_activity; 
  pid | backend_type | wait_event_type | wait_event -------+------------------------------+-----------------+--------------------- 28739 | logical replication launcher | Activity | LogicalLauncherMain 28736 | autovacuum launcher | Activity | AutoVacuumMain 28963 | client backend | | 28734 | background writer | Activity | BgWriterMain 28733 | checkpointer | Activity | CheckpointerMain 28735 | walwriter | Activity | WalWriterMain (6 rows) 

Dapat dilihat bahwa semua proses layanan latar belakang "main-main." Nilai kosong di wait_event_type dan wait_event menunjukkan bahwa proses tidak mengharapkan apa pun - dalam kasus kami, proses penyajian sibuk mengeksekusi permintaan.

Sampling


Untuk mendapatkan gambaran ekspektasi yang kurang lebih lengkap menggunakan sampling, kami menggunakan pg_wait_sampling ekstensi. Itu harus dikompilasi dari kode sumber; Saya akan menghilangkan bagian ini. Kemudian kami mendaftarkan pustaka di parameter shared_preload_libraries dan me-restart server.

 => ALTER SYSTEM SET shared_preload_libraries = 'pg_wait_sampling'; 

 student$ sudo pg_ctlcluster 11 main restart 

Sekarang instal ekstensi di database.

 => CREATE EXTENSION pg_wait_sampling; 

Ekstensi memungkinkan Anda untuk melihat riwayat ekspektasi, yang disimpan dalam buffer melingkar. Tetapi hal yang paling menarik adalah melihat profil harapan - statistik yang terakumulasi untuk seluruh waktu kerja.

Inilah yang akan kita lihat dalam beberapa detik:

 => SELECT * FROM pg_wait_sampling_profile; 
  pid | event_type | event | queryid | count -------+------------+---------------------+---------+------- 29074 | Activity | LogicalLauncherMain | 0 | 220 29070 | Activity | WalWriterMain | 0 | 220 29071 | Activity | AutoVacuumMain | 0 | 219 29069 | Activity | BgWriterMain | 0 | 220 29111 | Client | ClientRead | 0 | 3 29068 | Activity | CheckpointerMain | 0 | 220 (6 rows) 

Karena tidak ada yang terjadi sejak server dimulai, harapan utama adalah dari tipe Aktivitas (proses layanan menunggu pekerjaan muncul) dan Klien (psql sedang menunggu pengguna untuk mengirim permintaan).

Dengan pengaturan default ( pg_wait_sampling.profile_ Periode parameter), periode sampling adalah 10 milidetik, yaitu, nilai-nilai disimpan 100 kali per detik. Oleh karena itu, untuk memperkirakan durasi menunggu dalam detik, nilai hitungan harus dibagi 100.

Untuk memahami apa yang menjadi harapan proses, kami menambahkan tampilan pg_stat_activity ke permintaan:

 => SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid ORDER BY p.pid, p.count DESC; 
  pid | backend_type | app | event_type | event | count -------+------------------------------+------+------------+----------------------+------- 29068 | checkpointer | | Activity | CheckpointerMain | 222 29069 | background writer | | Activity | BgWriterMain | 222 29070 | walwriter | | Activity | WalWriterMain | 222 29071 | autovacuum launcher | | Activity | AutoVacuumMain | 221 29074 | logical replication launcher | | Activity | LogicalLauncherMain | 222 29111 | client backend | psql | Client | ClientRead | 4 29111 | client backend | psql | IPC | MessageQueueInternal | 1 (7 rows) 

Mari memuat dengan pgbench dan melihat bagaimana gambar berubah.

 student$ pgbench -i test 

Kami mereset profil yang dikumpulkan menjadi nol dan menjalankan tes selama 30 detik dalam proses terpisah.

 => SELECT pg_wait_sampling_reset_profile(); 

 student$ pgbench -T 30 test 

Permintaan harus diselesaikan sebelum proses pgbench selesai:

 => SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC; 
  pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+------------+------- 29148 | client backend | pgbench | IO | WALWrite | 8 29148 | client backend | pgbench | Client | ClientRead | 1 (2 rows) 

Tentu saja, harapan dari proses pgbench akan berubah menjadi sedikit berbeda tergantung pada sistem spesifik. Dalam kasus kami, sangat mungkin bahwa menunggu entri log (IO / WALWrite) akan disajikan, tetapi sebagian besar waktu prosesnya tidak berhenti, tetapi melakukan sesuatu yang mungkin berguna.

Kunci ringan


Anda harus selalu ingat bahwa tidak adanya harapan ketika pengambilan sampel tidak berarti bahwa tidak ada harapan. Jika lebih pendek dari periode sampling (seperseratus detik dalam contoh kami), maka itu tidak bisa jatuh ke dalam sampel.

Oleh karena itu, kunci cahaya tidak muncul di profil - tetapi mereka akan muncul jika Anda mengumpulkan data untuk waktu yang lama. Untuk menjamin melihatnya, Anda dapat memperlambat sistem file secara artifisial, misalnya, gunakan proyek slowfs yang dibangun pada sistem file FUSE .

Inilah yang dapat kita lihat dalam pengujian yang sama jika operasi I / O membutuhkan waktu 1/10 detik.

 => SELECT pg_wait_sampling_reset_profile(); 

 student$ pgbench -T 30 test 

 => SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE a.application_name = 'pgbench' ORDER BY p.pid, p.count DESC; 
  pid | backend_type | app | event_type | event | count -------+----------------+---------+------------+----------------+------- 29240 | client backend | pgbench | IO | WALWrite | 1445 29240 | client backend | pgbench | LWLock | WALWriteLock | 803 29240 | client backend | pgbench | IO | DataFileExtend | 20 (3 rows) 

Sekarang, harapan utama dari proses pgbench terkait dengan I / O, atau lebih tepatnya, entri log yang dieksekusi dalam mode sinkron dengan setiap komit. Karena (seperti yang ditunjukkan pada contoh di atas), menulis log ke disk dilindungi oleh kunci WALWriteLock yang ringan, kunci ini juga ada di profil - ini persis seperti yang ingin kami lihat.

Buffer klip


Untuk melihat pin dari buffer, kami mengambil keuntungan dari fakta bahwa kursor terbuka menahan pin sehingga membaca baris berikutnya lebih cepat.

Kami memulai transaksi, buka kursor dan pilih satu baris.

 => BEGIN; => DECLARE c CURSOR FOR SELECT * FROM pgbench_history; => FETCH c; 
  tid | bid | aid | delta | mtime | filler -----+-----+-------+-------+----------------------------+-------- 9 | 1 | 35092 | 477 | 2019-09-04 16:16:18.596564 | (1 row) 

Periksa apakah buffer disematkan (pinning_backends):

 => SELECT * FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('pgbench_history') AND relforknumber = 0 \gx 
 -[ RECORD 1 ]----+------ bufferid | 190 relfilenode | 47050 reltablespace | 1663 reldatabase | 16386 relforknumber | 0 relblocknumber | 0 isdirty | t usagecount | 1 pinning_backends | 1 <--   1  

Sekarang kita akan menghapus tabel:

 | => SELECT pg_backend_pid(); 
 | pg_backend_pid | ---------------- | 29367 | (1 row) 

 | => VACUUM VERBOSE pgbench_history; 
 | INFO: vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 0 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers. 
 | Skipped 1 page due to buffer pins, 0 frozen pages. 
 | 0 pages are entirely empty. | CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. | VACUUM 

Seperti yang dapat kita lihat, halaman dilewati (Melewati 1 halaman karena pin penyangga). Memang, pembersihan tidak dapat mengatasinya, karena dilarang untuk menghapus versi baris secara fisik dari halaman dalam buffer yang disematkan. Tetapi pembersihan tidak akan menunggu - halaman akan diproses lain kali.

Dan sekarang kita akan melakukan pembersihan dengan pembekuan :

 | => VACUUM FREEZE VERBOSE pgbench_history; 

Dengan pembekuan yang diminta dengan jelas, Anda tidak dapat melewati satu halaman yang tidak ditandai di peta pembekuan - jika tidak, tidak mungkin untuk mengurangi usia maksimum transaksi yang tidak dibekukan di pg_class.relfrozenxid. Oleh karena itu, pembersihan terhenti hingga kursor ditutup.

 => SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass; 
  age ----- 27 (1 row) 
 => COMMIT; --    

 | INFO: aggressively vacuuming "public.pgbench_history" | INFO: "pgbench_history": found 0 removable, 26 nonremovable row versions in 1 out of 1 pages | DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 732651 | There were 0 unused item pointers. 
 | Skipped 0 pages due to buffer pins, 0 frozen pages. 
 | 0 pages are entirely empty. | CPU: user: 0.00 s, system: 0.00 s, elapsed: 3.01 s. | VACUUM 

 => SELECT age(relfrozenxid) FROM pg_class WHERE oid = 'pgbench_history'::regclass; 
  age ----- 0 (1 row) 

Baiklah, mari kita lihat profil ekspektasi dari sesi psql kedua di mana perintah VACUUM dijalankan:

 => SELECT p.pid, a.backend_type, a.application_name AS app, p.event_type, p.event, p.count FROM pg_wait_sampling_profile p LEFT JOIN pg_stat_activity a ON p.pid = a.pid WHERE p.pid = 29367 ORDER BY p.pid, p.count DESC; 
  pid | backend_type | app | event_type | event | count -------+----------------+------+------------+------------+------- 29367 | client backend | psql | BufferPin | BufferPin | 294 29367 | client backend | psql | Client | ClientRead | 10 (2 rows) 

Tipe menunggu BufferPin menunjukkan bahwa flush sedang menunggu buffer dibebaskan.

Dalam hal ini kami akan menganggap bahwa kami telah menyelesaikan kunci. Terima kasih atas perhatian dan komentar Anda!

Source: https://habr.com/ru/post/id466199/


All Articles