Kami mulai dengan masalah yang berkaitan dengan
isolasi , membuat penyimpangan tentang
pengorganisasian data pada tingkat rendah , dan berbicara secara rinci
tentang versi baris dan bagaimana
snapshots diperoleh dari versi.
Kemudian kami memeriksa berbagai jenis pembersihan:
intra-halaman (bersama dengan HOT-update),
reguler dan
otomatis .
Dan sampai pada topik terakhir dari siklus ini. Hari ini kita akan berbicara tentang masalah pembungkus dan pembekuan id transaksi.
Transaksi Counter Overflow
PostgreSQL memiliki 32 bit yang dialokasikan untuk nomor transaksi. Ini adalah jumlah yang cukup besar (sekitar 4 miliar), tetapi dengan operasi aktif dari server, itu mungkin habis. Misalnya, pada beban 1000 transaksi per detik, ini akan terjadi setelah hanya satu setengah bulan operasi yang berkelanjutan.
Tetapi kita berbicara tentang fakta bahwa mekanisme multi-versi bergantung pada urutan penomoran - kemudian dari dua transaksi, transaksi dengan angka yang lebih rendah dapat dianggap telah dimulai sebelumnya. Oleh karena itu, jelas bahwa Anda tidak dapat mengatur ulang penghitung dan melanjutkan penomoran lagi.

Mengapa 64 bit tidak dialokasikan untuk nomor transaksi - karena ini akan sepenuhnya menghilangkan masalah? Faktanya adalah bahwa (seperti yang dibahas
sebelumnya ) di header setiap versi baris disimpan dua nomor transaksi - xmin dan xmax. Header sudah cukup besar, setidaknya 23 byte, dan peningkatan kedalaman bit akan menyebabkan peningkatannya oleh 8 byte lainnya. Ini sama sekali tidak mungkin.
Nomor transaksi 64-bit diterapkan dalam produk perusahaan kami, Postgres Pro Enterprise, tetapi mereka juga tidak sepenuhnya jujur di sana: xmin dan xmax tetap 32-bit, dan tajuk halaman berisi halaman "permulaan era" yang umum.
Apa yang harus dilakukan Alih-alih diagram linear, semua nomor transaksi diulang. Untuk setiap transaksi, setengah dari angka "berlawanan arah jarum jam" dianggap milik masa lalu, dan setengah "searah jarum jam" ke masa depan.
Usia transaksi adalah jumlah transaksi yang telah berlalu sejak muncul dalam sistem (terlepas dari apakah penghitung melewati nol atau tidak). Ketika kami ingin memahami apakah satu transaksi lebih tua dari yang lain atau tidak, kami membandingkan usia mereka, bukan angka. (Oleh karena itu, omong-omong, operasi "lebih besar" dan "kurang" tidak ditentukan untuk tipe data xid.)

Tetapi dalam sirkuit yang berulang, situasi yang tidak menyenangkan muncul. Suatu transaksi yang berada di masa lalu yang jauh (transaksi 1 dalam gambar), setelah beberapa saat akan berada di setengah lingkaran yang berhubungan dengan masa depan. Ini, tentu saja, melanggar aturan visibilitas dan akan menimbulkan masalah - perubahan yang dilakukan oleh transaksi 1 akan hilang begitu saja dari pandangan.

Pembekuan Versi dan Aturan Visibilitas
Untuk mencegah "perjalanan" dari masa lalu ke masa depan, proses pembersihan (selain membebaskan ruang di halaman) melakukan tugas lain. Dia menemukan versi garis yang agak lama dan "dingin" (yang terlihat di semua gambar dan perubahan yang sudah tidak mungkin) dan dengan cara khusus menandai mereka - "membeku". Versi beku baris dianggap lebih tua dari data biasa dan selalu terlihat di semua snapshot data. Selain itu, tidak perlu lagi melihat nomor transaksi xmin, dan nomor ini dapat digunakan kembali dengan aman. Dengan demikian, versi string yang beku selalu tetap ada di masa lalu.

Untuk menandai nomor transaksi xmin sebagai beku, kedua bit petunjuk ditetapkan pada waktu yang sama - bit komit dan bit membatalkan.
Perhatikan bahwa transaksi xmax tidak perlu dibekukan. Kehadirannya berarti bahwa versi string ini tidak lagi relevan. Setelah tidak lagi terlihat dalam snapshot data, versi baris ini akan dihapus.
Untuk percobaan, buat tabel. Kami mengatur fillfactor minimum untuknya sehingga hanya dua baris yang sesuai pada setiap halaman - sehingga akan lebih mudah bagi kami untuk mengamati apa yang terjadi. Dan matikan otomatisasi untuk mengontrol waktu pembersihan sendiri.
=> CREATE TABLE tfreeze( id integer, s char(300) ) WITH (fillfactor = 10, autovacuum_enabled = off);
Kami telah membuat beberapa varian fungsi, yang, menggunakan ekstensi pageinspect, menunjukkan versi garis yang ada di halaman. Sekarang kita akan membuat varian lain dari fungsi yang sama: sekarang akan menampilkan beberapa halaman sekaligus dan menunjukkan usia transaksi xmin (usia fungsi sistem digunakan untuk ini):
=> CREATE FUNCTION heap_page(relname text, pageno_from integer, pageno_to integer) RETURNS TABLE(ctid tid, state text, xmin text, xmin_age integer, xmax text, t_ctid tid) AS $$ SELECT (pageno,lp)::text::tid AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin || CASE WHEN (t_infomask & 256+512) = 256+512 THEN ' (f)' WHEN (t_infomask & 256) > 0 THEN ' (c)' WHEN (t_infomask & 512) > 0 THEN ' (a)' ELSE '' END AS xmin, age(t_xmin) xmin_age, t_xmax || CASE WHEN (t_infomask & 1024) > 0 THEN ' (c)' WHEN (t_infomask & 2048) > 0 THEN ' (a)' ELSE '' END AS xmax, t_ctid FROM generate_series(pageno_from, pageno_to) p(pageno), heap_page_items(get_raw_page(relname, pageno)) ORDER BY pageno, lp; $$ LANGUAGE SQL;
Harap dicatat bahwa tanda pembekuan (yang kami tunjukkan dengan huruf f dalam tanda kurung) ditentukan oleh instalasi simultan dari prompt yang dilakukan dan dibatalkan. Banyak sumber (termasuk dokumentasi) menyebutkan nomor khusus FrozenTransactionId = 2, yang menandai transaksi beku. Sistem seperti itu dioperasikan hingga versi 9.4, tetapi sekarang telah digantikan oleh bit tooltip - ini memungkinkan Anda untuk menyimpan nomor transaksi asli dalam versi baris, yang nyaman untuk keperluan dukungan dan debugging. Namun, transaksi dengan nomor 2 masih dapat terjadi di sistem yang lebih lama, bahkan ditingkatkan ke versi terbaru.
Kami juga membutuhkan ekstensi pg_visibilitas, yang memungkinkan Anda melihat ke peta visibilitas:
=> CREATE EXTENSION pg_visibility;
Sebelum PostgreSQL 9.6, peta visibilitas berisi satu bit per halaman; itu menandai halaman yang hanya berisi versi string yang "cukup lama" yang sudah dijamin akan terlihat di semua gambar. Idenya di sini adalah bahwa jika halaman ditandai di peta visibilitas, maka untuk versi garisnya Anda tidak perlu memeriksa aturan visibilitas.
Dimulai dengan versi 9.6, peta beku ditambahkan ke lapisan yang sama - satu bit lebih per halaman. Peta beku menandai halaman di mana semua versi baris dibekukan.
Kami memasukkan beberapa baris ke dalam tabel dan segera melakukan pembersihan untuk membuat peta visibilitas:
=> INSERT INTO tfreeze(id, s) SELECT g.id, 'FOO' FROM generate_series(1,100) g(id); => VACUUM tfreeze;
Dan kita melihat bahwa kedua halaman sekarang ditandai di peta visibilitas (all_visible), tetapi belum dibekukan (all_frozen):
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno;
blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | f (2 rows)
Usia transaksi yang membuat baris (xmin_age) adalah 1 - ini adalah transaksi terakhir yang dilakukan pada sistem:
=> SELECT * FROM heap_page('tfreeze',0,1);
ctid | state | xmin | xmin_age | xmax | t_ctid -------+--------+---------+----------+-------+-------- (0,1) | normal | 697 (c) | 1 | 0 (a) | (0,1) (0,2) | normal | 697 (c) | 1 | 0 (a) | (0,2) (1,1) | normal | 697 (c) | 1 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 1 | 0 (a) | (1,2) (4 rows)
Usia minimum untuk pembekuan
Tiga parameter utama mengontrol pembekuan, dan kami akan mempertimbangkannya pada gilirannya.
Mari kita mulai dengan
vacuum_freeze_min_age , yang mendefinisikan usia transaksi minimum xmin di mana versi string dapat dibekukan. Semakin rendah nilai ini, biaya overhead yang lebih tidak perlu mungkin berubah menjadi: jika kita berurusan dengan "panas", secara aktif mengubah data, maka pembekuan lebih banyak dan lebih banyak versi baru akan sia-sia. Dalam hal ini, lebih baik menunggu.
Nilai default untuk parameter ini menetapkan bahwa transaksi mulai membeku setelah 50 juta transaksi lainnya berlalu sejak muncul:
=> SHOW vacuum_freeze_min_age;
vacuum_freeze_min_age ----------------------- 50000000 (1 row)
Untuk melihat bagaimana pembekuan terjadi, kami mengurangi nilai parameter ini menjadi satu.
=> ALTER SYSTEM SET vacuum_freeze_min_age = 1; => SELECT pg_reload_conf();
Dan kami akan memperbarui satu baris di halaman nol. Versi baru akan sampai ke halaman yang sama karena nilai fillfactor kecil.
=> UPDATE tfreeze SET s = 'BAR' WHERE id = 1;
Inilah yang kita lihat sekarang di halaman data:
=> SELECT * FROM heap_page('tfreeze',0,1);
ctid | state | xmin | xmin_age | xmax | t_ctid -------+--------+---------+----------+-------+-------- (0,1) | normal | 697 (c) | 2 | 698 | (0,3) (0,2) | normal | 697 (c) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (c) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 2 | 0 (a) | (1,2) (5 rows)
Sekarang baris yang lebih tua dari
vacuum_freeze_min_age = 1 harus dibekukan. Tetapi perhatikan bahwa garis nol tidak ditandai di peta visibilitas (bit direset oleh perintah UPDATE, yang mengubah halaman), dan yang pertama tetap diperiksa:
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno;
blkno | all_visible | all_frozen -------+-------------+------------ 0 | f | f 1 | t | f (2 rows)
Kami
telah mengatakan bahwa pembersihan hanya memindai halaman yang tidak ditandai di peta visibilitas. Dan ternyata:
=> VACUUM tfreeze; => SELECT * FROM heap_page('tfreeze',0,1);
ctid | state | xmin | xmin_age | xmax | t_ctid -------+---------------+---------+----------+-------+-------- (0,1) | redirect to 3 | | | | (0,2) | normal | 697 (f) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 (c) | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (c) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 2 | 0 (a) | (1,2) (5 rows)
Pada halaman nol, satu versi dibekukan, tetapi halaman pertama tidak mempertimbangkan pembersihan sama sekali. Jadi, jika hanya versi saat ini yang tersisa di halaman, maka pembersihan tidak akan sampai ke halaman seperti itu dan tidak akan membekukannya.
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno;
blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | f (2 rows)
Umur untuk membekukan seluruh tabel
Untuk tetap membekukan versi baris yang tersisa di halaman yang tidak dilihat oleh pembersihan, parameter kedua disediakan:
vacuum_freeze_table_age . Ini menentukan usia transaksi, di mana pembersihan mengabaikan peta visibilitas dan melewati semua halaman tabel untuk dibekukan.
Setiap tabel menyimpan nomor transaksi, yang diketahui bahwa semua transaksi lama dijamin akan dibekukan (pg_class.relfrozenxid). Dengan usia transaksi yang diingat ini, nilai parameter
vacuum_freeze_table_age dibandingkan .
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
relfrozenxid | age --------------+----- 694 | 5 (1 row)
Sebelum PostgreSQL 9.6, pembersihan melakukan pemindaian tabel penuh untuk memastikan bahwa semua halaman dirayapi. Untuk meja besar, operasi ini panjang dan menyedihkan. Masalah ini diperburuk oleh fakta bahwa jika pembersihan gagal mencapai akhir (misalnya, administrator yang tidak sabar mengganggu pelaksanaan perintah), itu perlu dimulai dari awal.
Dimulai dengan versi 9.6, berkat peta beku (yang kita lihat di kolom all_frozen di output pg_visibility_map), membersihkan bypass hanya halaman-halaman yang belum ditandai di peta. Ini bukan hanya jumlah pekerjaan yang jauh lebih kecil, tetapi juga ketahanan terhadap gangguan: jika proses pembersihan dihentikan dan dimulai lagi, dia tidak perlu lagi melihat halaman yang sudah dia tandai dalam peta pembekuan terakhir kali.
Dengan satu atau lain cara, semua halaman dalam tabel dibekukan sekali dalam (
vacuum_freeze_table_age -
vacuum_freeze_min_age ) transaksi. Dengan nilai default, ini terjadi sekali per juta transaksi:
=> SHOW vacuum_freeze_table_age;
vacuum_freeze_table_age ------------------------- 150000000 (1 row)
Dengan demikian, jelas bahwa terlalu banyak
vacuum_freeze_min_age tidak boleh diatur, karena alih-alih mengurangi overhead, ini akan mulai meningkatkannya.
Mari kita lihat bagaimana seluruh tabel dibekukan, dan untuk melakukan ini, kurangi
vacuum_freeze_table_age menjadi 5 sehingga kondisi untuk pembekuan terpenuhi.
=> ALTER SYSTEM SET vacuum_freeze_table_age = 5; => SELECT pg_reload_conf();
Mari kita bersihkan:
=> VACUUM tfreeze;
Sekarang, karena seluruh tabel telah dijamin untuk diverifikasi, jumlah transaksi yang dibekukan dapat ditingkatkan - kami yakin bahwa halaman tersebut tidak memiliki transaksi yang lebih lama, yang tidak dibekukan.
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
relfrozenxid | age --------------+----- 698 | 1 (1 row)
Sekarang semua versi baris pada halaman pertama dibekukan:
=> SELECT * FROM heap_page('tfreeze',0,1);
ctid | state | xmin | xmin_age | xmax | t_ctid -------+---------------+---------+----------+-------+-------- (0,1) | redirect to 3 | | | | (0,2) | normal | 697 (f) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 (c) | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (f) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (f) | 2 | 0 (a) | (1,2) (5 rows)
Selain itu, halaman pertama ditandai di peta beku:
=> SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno;
blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | t (2 rows)
Umur untuk respons "agresif"
Penting agar versi baris dibekukan tepat waktu. Jika muncul situasi di mana transaksi yang belum dibekukan berisiko memasuki masa depan, PostgreSQL akan macet untuk mencegah potensi masalah.
Apa yang bisa menjadi alasan untuk ini? Ada berbagai alasan.
- Pembersihan otomatis dapat dimatikan, dan pembersihan rutin tidak dimulai. Kami sudah mengatakan bahwa ini tidak perlu, tetapi secara teknis itu mungkin.
- Bahkan pembersihan otomatis yang disertakan tidak datang ke database yang tidak digunakan (ingat parameter track_counts dan database templat0 ).
- Seperti yang kita lihat terakhir kali , membersihkan melompati tabel di mana data hanya ditambahkan, tetapi tidak dihapus atau diubah.
Dalam kasus tersebut, operasi
pembersihan otomatis “agresif” disediakan, dan diatur oleh parameter
autovacuum_freeze_max_age . Jika dalam tabel mana pun dari basis data apa pun mungkin ada transaksi yang dibekukan lebih tua dari usia yang ditentukan dalam parameter, pembersihan otomatis dimulai secara paksa (bahkan jika dinonaktifkan) dan cepat atau lambat akan mencapai tabel masalah (terlepas dari kriteria yang biasa).
Nilai standarnya cukup konservatif:
=> SHOW autovacuum_freeze_max_age;
autovacuum_freeze_max_age --------------------------- 200000000 (1 row)
Batas untuk
autovacuum_freeze_max_age adalah 2 miliar transaksi, dan nilai 10 kali lebih kecil digunakan. Ini masuk akal: meningkatkan nilai kami meningkatkan risiko bahwa untuk waktu yang tersisa, pembersihan otomatis tidak punya waktu untuk membekukan semua versi garis yang diperlukan.
Selain itu, nilai parameter ini menentukan ukuran struktur XACT: karena seharusnya tidak ada transaksi lama dalam sistem yang Anda mungkin perlu mengetahui statusnya, pembersihan otomatis menghapus file segmen XACT yang tidak perlu, membebaskan ruang.
Mari kita lihat bagaimana pembersihan menangani tabel append-only, menggunakan tfreeze sebagai contoh. Untuk tabel ini, pembersihan otomatis umumnya dinonaktifkan, tetapi ini tidak akan menjadi hambatan.
Mengubah parameter
autovacuum_freeze_max_age membutuhkan server restart. Tetapi semua parameter yang dibahas di atas juga dapat diatur pada tingkat tabel individu menggunakan parameter penyimpanan. Biasanya masuk akal untuk melakukan ini dalam kasus-kasus khusus, ketika meja benar-benar membutuhkan perawatan khusus.
Jadi, kita akan mengatur
autovacuum_freeze_max_age di level tabel (dan pada saat yang sama mengembalikan fillfactor normal juga). Sayangnya, nilai minimum yang mungkin adalah 100.000:
=> ALTER TABLE tfreeze SET (autovacuum_freeze_max_age = 100000, fillfactor = 100);
Sayangnya, karena kita harus menyelesaikan 100.000 transaksi untuk mereproduksi situasi yang menarik minat kita. Tetapi, tentu saja, untuk tujuan praktis, ini adalah nilai yang sangat, sangat rendah.
Karena kami akan menambahkan data, kami akan memasukkan 100.000 baris ke tabel - masing-masing dalam transaksi kami. Dan lagi saya harus membuat reservasi bahwa dalam praktiknya ini tidak boleh dilakukan. Tapi sekarang kita hanya menjelajah, kita bisa.
=> CREATE PROCEDURE foo(id integer) AS $$ BEGIN INSERT INTO tfreeze VALUES (id, 'FOO'); COMMIT; END; $$ LANGUAGE plpgsql; => DO $$ BEGIN FOR i IN 101 .. 100100 LOOP CALL foo(i); END LOOP; END; $$;
Seperti yang bisa kita lihat, usia transaksi terakhir yang dibekukan dalam tabel telah melampaui nilai ambang:
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
relfrozenxid | age --------------+-------- 698 | 100006 (1 row)
Tetapi jika Anda menunggu sebentar sekarang, maka di log pesan server akan ada entri tentang kekosongan agresif tabel "test.public.tfreeze" otomatis, jumlah transaksi beku akan berubah, dan usianya akan kembali ke kesopanan:
=> SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
relfrozenxid | age --------------+----- 100703 | 3 (1 row)
Ada juga yang namanya membekukan multi-transaksi, tetapi kami belum akan membicarakannya - kami akan menundanya hingga kami berbicara tentang kunci agar tidak menjadi yang terdepan.
Pembekuan manual
Terkadang lebih mudah untuk mengontrol pembekuan secara manual daripada menunggu kedatangan pembersihan otomatis.
Anda dapat membekukan perintah secara manual menggunakan perintah VACUUM FREEZE - semua versi baris akan dibekukan, tanpa memandang usia transaksi (seolah-olah
autovacuum_freeze_min_age = 0 parameter). Ketika sebuah tabel dibangun kembali dengan perintah VACUUM FULL atau CLUSTER, semua baris juga dibekukan.
Untuk membekukan semua basis data, Anda dapat menggunakan utilitas:
vacuumdb --all --freeze
Data juga dapat dibekukan selama pemuatan awal menggunakan perintah COPY dengan menentukan parameter FREEZE. Untuk melakukan ini, tabel harus dibuat (atau dikosongkan dengan perintah TRUNCATE) dengan cara yang sama
transaksi sebagai SALINAN.
Karena ada aturan visibilitas terpisah untuk baris beku, baris tersebut akan terlihat dalam snapshot data dari transaksi lain yang melanggar aturan isolasi biasa (ini berlaku untuk transaksi dengan tingkat Repeatable Read atau Serializable).
Untuk memverifikasi ini, di sesi lain, mulailah transaksi dengan tingkat isolasi Baca Berulang:
| => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT txid_current();
Perhatikan bahwa transaksi ini membuat snapshot data, tetapi tidak mengakses tabel tfreeze. Sekarang kita akan mengosongkan tabel tfreeze dan memuat baris baru ke dalamnya dalam satu transaksi. Jika transaksi paralel membaca konten tfreeze, perintah TRUNCATE akan dikunci hingga akhir transaksi.
=> BEGIN; => TRUNCATE tfreeze; => COPY tfreeze FROM stdin WITH FREEZE;
1 FOO 2 BAR 3 BAZ \.
=> COMMIT;
Sekarang transaksi paralel melihat data baru, meskipun ini memecah isolasi:
| => SELECT count(*) FROM tfreeze;
| count | ------- | 3 | (1 row)
| => COMMIT;
Tapi, karena pemuatan data seperti itu tidak mungkin terjadi secara teratur, ini biasanya tidak menjadi masalah.
Lebih buruk lagi, COPY WITH FREEZE tidak berfungsi dengan peta visibilitas - halaman yang dimuat tidak ditandai sebagai hanya berisi versi garis yang dapat dilihat oleh semua orang. Karena itu, ketika Anda pertama kali mengakses tabel, pembersihan dipaksa untuk memproses ulang semuanya dan membuat peta visibilitas. Lebih buruk lagi, halaman data memiliki tanda visibilitas penuh di header mereka sendiri, jadi membersihkan tidak hanya membaca seluruh tabel, tetapi juga sepenuhnya menulis ulang, meletakkan bit yang diinginkan. Sayangnya, solusi untuk masalah ini tidak harus menunggu lebih awal dari versi 13 (
diskusi ).
Kesimpulan
Ini menyimpulkan seri artikel saya tentang isolasi dan multiversion PostgreSQL. Terima kasih atas perhatian Anda dan terutama atas komentarnya - mereka meningkatkan materi dan sering menunjukkan area yang membutuhkan perhatian lebih pada bagian saya.
Tetap bersama kami, untuk dilanjutkan!