
Menskalakan DBMS adalah masa depan yang terus maju. DBMS meningkat dan skala lebih baik pada platform perangkat keras, sementara platform perangkat keras itu sendiri meningkatkan produktivitas, jumlah core, dan memori - Achilles mengejar ketinggalan dengan kura-kura, tetapi masih belum. Masalah penskalaan DBMS sedang dalam ayunan penuh.
Postgres Professional memiliki masalah dengan penskalaan tidak hanya secara teoritis, tetapi juga secara praktis: dengan pelanggannya. Dan lebih dari sekali. Salah satu kasus seperti itu akan dibahas dalam artikel ini.
PostgreSQL memiliki skala yang baik pada sistem NUMA jika merupakan motherboard tunggal dengan banyak prosesor dan beberapa bus data. Beberapa optimasi dapat dibaca di
sini dan di
sini . Namun, ada kelas lain dari sistem, mereka memiliki beberapa motherboard, pertukaran data di antaranya dilakukan dengan menggunakan interkoneksi, sementara satu instance dari OS bekerja pada mereka dan untuk pengguna desain ini terlihat seperti mesin tunggal. Dan meskipun secara formal sistem tersebut juga dapat dikaitkan dengan NUMA, tetapi pada dasarnya mereka lebih dekat ke superkomputer, seperti akses ke memori lokal dari node dan akses ke memori dari node tetangga berbeda secara radikal. Komunitas PostgreSQL percaya bahwa satu-satunya instance Postgres yang berjalan pada arsitektur seperti itu adalah sumber masalah, dan belum ada pendekatan sistematis untuk menyelesaikannya.
Ini karena arsitektur perangkat lunak yang menggunakan memori bersama pada dasarnya dirancang untuk fakta bahwa waktu akses berbagai proses ke memori mereka sendiri dan jarak jauh lebih atau kurang sebanding. Dalam kasus ketika kita bekerja dengan banyak node, taruhan pada memori bersama sebagai saluran komunikasi cepat berhenti untuk membenarkan dirinya sendiri, karena karena latensi itu jauh "lebih murah" untuk mengirim permintaan untuk tindakan tertentu ke node (node) di mana ia berada data menarik daripada mengirim data ini di bus. Oleh karena itu, untuk superkomputer dan, secara umum, sistem dengan banyak node, solusi cluster relevan.
Ini tidak berarti bahwa kombinasi dari sistem multi-simpul dan arsitektur shared-memory khas Postgres harus diakhiri. Lagi pula, jika proses postgres menghabiskan sebagian besar waktu mereka melakukan perhitungan kompleks secara lokal, maka arsitektur ini bahkan akan sangat efisien. Dalam situasi kami, klien telah membeli server multi-node yang kuat, dan kami harus menyelesaikan masalah PostgreSQL di dalamnya.
Tetapi masalahnya serius: permintaan penulisan paling sederhana (ubah beberapa nilai bidang dalam satu catatan) dieksekusi dalam periode beberapa menit hingga satu jam. Seperti yang kemudian dikonfirmasikan, masalah-masalah ini memanifestasikan diri mereka dalam semua kemuliaan mereka justru karena banyaknya inti dan, karenanya, paralelisme radikal dalam pelaksanaan permintaan dengan pertukaran yang relatif lambat antara node.
Karenanya, artikel tersebut akan berubah, seolah-olah, untuk tujuan ganda:
- Berbagi pengalaman: apa yang harus dilakukan jika dalam sistem multi-simpul database melambat dengan sungguh-sungguh. Mulai dari mana, cara mendiagnosis ke mana harus pindah.
- Jelaskan bagaimana masalah DBMS PostgreSQL itu sendiri dapat diselesaikan dengan konkurensi tingkat tinggi. Termasuk bagaimana perubahan dalam algoritma untuk mengambil kunci mempengaruhi kinerja PostgreSQL.
Server dan DB
Sistem ini terdiri dari 8 bilah dengan masing-masing 2 soket. Secara total, lebih dari 300 core (tidak termasuk hypertreading). Ban cepat (teknologi pabrikan berpemilik) menghubungkan bilah. Bukan berarti itu superkomputer, tetapi untuk satu contoh DBMS, konfigurasi sangat mengesankan.
Bebannya juga agak besar. Data lebih dari 1 terabyte. Sekitar 3000 transaksi per detik. Lebih dari 1000 koneksi ke postgres.
Setelah mulai berurusan dengan harapan perekaman per jam, hal pertama yang kami lakukan adalah menulis ke disk sebagai penyebab penundaan. Segera setelah penundaan yang tidak dapat dipahami dimulai, tes mulai dilakukan secara eksklusif pada
tmpfs
. Gambar tidak berubah. Disk tidak ada hubungannya dengan itu.
Memulai dengan Diagnosis: Tampilan
Karena masalah muncul kemungkinan besar karena tingginya persaingan proses yang "mengetuk" objek yang sama, hal pertama yang perlu diperiksa adalah kunci. Di PostgreSQL, ada tampilan
pg.catalog.pg_locks
dan
pg_stat_activity
untuk
pg_stat_activity
semacam itu. Yang kedua, sudah dalam versi 9.6, menambahkan informasi tentang apa proses menunggu (
Amit Kapila, Ildus Kurbangaliev ) -
wait_event_type
. Nilai yang mungkin untuk bidang ini dijelaskan di
sini .
Tapi pertama-tama, hitung saja:
postgres=
Ini adalah bilangan real. Mencapai hingga 200.000 kunci.
Pada saat yang sama, kunci seperti itu tergantung pada permintaan naas:
SELECT COUNT(mode), mode FROM pg_locks WHERE pid =580707 GROUP BY mode; count | mode β
Saat membaca buffer, DBMS menggunakan kunci
share
, sambil menulis -
exclusive
. Artinya, menulis kunci menyumbang kurang dari 1% dari semua permintaan.
Dalam tampilan
pg_locks
, tipe kunci tidak selalu terlihat seperti yang dijelaskan
dalam dokumentasi pengguna.
Ini plat pertandingannya:
AccessShareLock = LockTupleKeyShare RowShareLock = LockTupleShare ExclusiveLock = LockTupleNoKeyExclusive AccessExclusiveLock = LockTupleExclusive
Mode SELECT FROM permintaan pg_locks menunjukkan bahwa CREATE INDEX (tanpa CONCURRENTLY) akan menunggu 234 INSERT dan 390 INSERT untuk
buffer content lock
. Solusi yang mungkin adalah "mengajar" INSERT dari sesi yang berbeda untuk memotong lebih sedikit buffer.
Saatnya menggunakan perf
Utilitas
perf
mengumpulkan banyak informasi diagnostik. Dalam mode
record
... ini menulis statistik peristiwa sistem ke file (secara default mereka berada di
./perf_data
), dan dalam mode
report
ini menganalisis data yang dikumpulkan, misalnya, Anda dapat memfilter peristiwa yang hanya menyangkut
postgres
atau
pid
diberikan:
$ perf record -u postgres $ perf record -p 76876 , $ perf report > ./my_results
Akibatnya, kita akan melihat sesuatu seperti

Cara menggunakan
perf
untuk mendiagnosis PostgreSQL dijelaskan, misalnya, di
sini , serta di
wiki pg .
Dalam kasus kami, bahkan mode paling sederhana pun memberikan informasi penting -
perf top
, yang berfungsi, tentu saja, dalam semangat sistem operasi
top
. Dengan
perf top
kami melihat bahwa sebagian besar waktu yang dihabiskan prosesor dalam
PinBuffer()
inti, serta dalam fungsi
PinBuffer()
dan
LWLockAttemptLock().
.
PinBuffer()
adalah fungsi yang meningkatkan counter referensi ke buffer (memetakan halaman data ke RAM), berkat proses postgres yang tahu buffer mana yang dapat dipaksa keluar dan mana yang tidak bisa.
LWLockAttemptLock()
- fungsi penangkapan
LWLock
.
LWLock
adalah sejenis kunci dengan dua tingkat yang
shared
dan
exclusive
, tanpa menentukan
deadlock
, kunci telah dialokasikan sebelumnya ke
shared memory
, proses menunggu menunggu dalam antrian.
Fungsi-fungsi ini telah dioptimalkan secara serius di PostgreSQL 9.5 dan 9.6. Spinlocks di dalamnya digantikan oleh penggunaan langsung operasi atom.
Grafik nyala
Tidak mungkin tanpa mereka: bahkan jika mereka tidak berguna, masih layak diceritakan tentang mereka - mereka luar biasa cantik. Tetapi mereka bermanfaat. Ini adalah ilustrasi dari
github
, bukan dari kasus kami (kami maupun klien belum siap untuk mengungkapkan detailnya).

Gambar-gambar indah ini dengan sangat jelas menunjukkan apa yang dilakukan siklus prosesor.
perf
sama dapat mengumpulkan data, tetapi
flame graph
cerdas memvisualisasikan data, dan membangun pohon berdasarkan tumpukan panggilan yang dikumpulkan. Anda dapat membaca lebih lanjut tentang pembuatan profil dengan grafik nyala, misalnya, di
sini , dan unduh semua yang Anda butuhkan di
sini .
Dalam kasus kami, sejumlah besar
nestloop
terlihat pada grafik nyala. Rupanya, GABUNGAN dari sejumlah besar tabel dalam berbagai permintaan baca bersamaan menyebabkan sejumlah besar kunci
access share
.
Statistik yang dikumpulkan oleh
perf
menunjukkan kemana siklus prosesor pergi. Dan meskipun kami melihat bahwa sebagian besar waktu prosesor melewati kunci, kami tidak melihat apa yang sebenarnya mengarah pada harapan kunci yang lama, karena kami tidak melihat dengan tepat di mana harapan kunci terjadi, karena Waktu CPU tidak sia-sia menunggu.
Untuk melihat harapan itu sendiri, Anda dapat membuat permintaan ke tampilan sistem
pg_stat_activity
.
SELECT wait_event_type, wait_event, COUNT(*) FROM pg_stat_activity GROUP BY wait_event_type, wait_event;
mengungkapkan bahwa:
LWLockTranche | buffer_content | UPDATE ************* LWLockTranche | buffer_content | INSERT INTO ******** LWLockTranche | buffer_content | \r | | insert into B4_MUTEX | | values (nextval('hib | | returning ID Lock | relation | INSERT INTO B4_***** LWLockTranche | buffer_content | UPDATE ************* Lock | relation | INSERT INTO ******** LWLockTranche | buffer_mapping | INSERT INTO ******** LWLockTranche | buffer_content | \r
(tanda bintang di sini hanya mengganti detail permintaan yang tidak kami ungkapkan).
Anda dapat melihat nilai
buffer_content
(memblokir konten buffer) dan
buffer_mapping
(memblokir komponen dari plat hash
shared_buffers
).
Untuk bantuan ke gdb
Tetapi mengapa ada begitu banyak harapan untuk jenis kunci ini? Untuk informasi lebih rinci tentang harapan, saya harus menggunakan debugger
GDB
. Dengan
GDB
kita bisa mendapatkan setumpuk panggilan proses tertentu. Dengan menerapkan sampling, mis. Setelah mengumpulkan sejumlah tumpukan panggilan acak, Anda bisa mendapatkan gagasan tumpukan mana yang memiliki harapan terlama.
Pertimbangkan proses penyusunan statistik. Kami akan mempertimbangkan kumpulan statistik "manual", meskipun dalam kehidupan nyata skrip khusus digunakan yang melakukan ini secara otomatis.
Pertama,
gdb
harus dilampirkan ke proses PostgreSQL. Untuk melakukan ini, cari
pid
proses server, katakan dari
$ ps aux | grep postgres
Katakanlah kita menemukan:
postgres 2025 0.0 0.1 172428 1240 pts/17 S 23 0:00 /usr/local/pgsql/bin/postgres -D /usr/local/pgsql/data
dan sekarang masukkan
pid
ke debugger:
igor_le:~$gdb -p 2025
Setelah di dalam debugger, kami menulis
bt
[yaitu,
backtrace
] atau di
where
. Dan kami mendapatkan banyak informasi tentang jenis ini:
(gdb) bt #0 0x00007fbb65d01cd0 in __write_nocancel () from /lib64/libc.so.6 #1 0x00000000007c92f4 in write_pipe_chunks ( data=0x110e6e8 "2018β06β01 15:35:38 MSK [524647]: [392β1] db=bp,user=bp,app=[unknown],client=192.168.70.163 (http://192.168.70.163) LOG: relation 23554 new block 493: 248.389503\n2018β06β01 15:35:38 MSK [524647]: [393β1] db=bp,user=bp,app=["..., len=409, dest=dest@entry=1) at elog.c:3123 #2 0x00000000007cc07b in send_message_to_server_log (edata=0xc6ee60 <errordata>) at elog.c:3024 #3 EmitErrorReport () at elog.c:1479
Setelah mengumpulkan statistik, termasuk tumpukan panggilan dari semua proses postgres, dikumpulkan berulang kali pada titik waktu yang berbeda, kami melihat bahwa
buffer partition lock
di dalam
relation extension lock
berlangsung 3706 detik (sekitar satu jam), yaitu, mengunci sepotong tabel hash buffer manajer, yang diperlukan untuk mengganti buffer lama, untuk kemudian menggantinya dengan yang baru yang sesuai dengan bagian tabel yang diperpanjang. Sejumlah kunci
buffer content lock
juga terlihat, yang sesuai dengan harapan mengunci halaman indeks
B-tree
untuk dimasukkan.

Pada awalnya, dua penjelasan datang untuk waktu tunggu yang mengerikan:
- Orang lain mengambil
LWLock
ini dan terjebak. Tapi ini tidak mungkin. Karena tidak ada yang rumit terjadi di dalam kunci partisi penyangga. - Kami menemukan beberapa perilaku patologis
LWLock
. Artinya, terlepas dari kenyataan bahwa tidak ada yang mengambil kunci terlalu lama, harapannya bertahan lama tidak masuk akal.
Tambalan diagnostik dan perawatan pohon
Dengan mengurangi jumlah koneksi simultan, kami mungkin akan mengalirkan aliran permintaan ke kunci. Tapi itu akan seperti menyerah. Alih-alih,
Alexander Korotkov , kepala arsitek Postgres Professional (tentu saja, ia membantu menyiapkan artikel ini), mengusulkan serangkaian tambalan.
Pertama-tama, perlu untuk mendapatkan gambaran yang lebih rinci tentang bencana. Tidak peduli sebagus apa pun alat yang digunakan, tambalan diagnostik buatan mereka sendiri juga akan bermanfaat.
Patch ditulis yang menambahkan pencatatan terperinci waktu yang dihabiskan dalam
relation extension
, apa yang terjadi di dalam fungsi
RelationAddExtraBlocks()
. Jadi kami mencari tahu berapa waktu yang dihabiskan di dalam
RelationAddExtraBlocks().
Dan untuk mendukungnya, tambalan lain ditulis melaporkan dalam
pg_stat_activity
tentang apa yang kita lakukan sekarang dalam
relation extension
. Itu dilakukan dengan cara ini: ketika
relation
berkembang,
application_name
menjadi
RelationAddExtraBlocks
. Proses ini sekarang mudah dianalisis dengan detail maksimum menggunakan
gdb bt
dan
perf
.
Sebenarnya tambalan medis (dan bukan diagnostik) ditulis dua. Tambalan pertama mengubah perilaku kunci daun
Bβtree
: sebelumnya, ketika diminta untuk memasukkan, daun diblokir sebagai
share
, dan setelah itu menjadi
exclusive
. Sekarang dia langsung menjadi
exclusive
. Sekarang tambalan
ini telah dikomit untuk
PostgreSQL 12 . Untungnya, tahun ini
Alexander Korotkov menerima
status committer - committer PostgreSQL kedua di Rusia dan yang kedua di perusahaan.
Nilai
NUM_BUFFER_PARTITIONS
juga ditingkatkan dari 128 menjadi 512 untuk mengurangi beban pada kunci pemetaan: tabel hash manajer buffer dibagi menjadi potongan-potongan yang lebih kecil, dengan harapan bahwa beban pada setiap bagian tertentu akan berkurang.
Setelah menerapkan tambalan ini, kunci pada konten buffer hilang, tetapi meskipun ada peningkatan
NUM_BUFFER_PARTITIONS
,
buffer_mapping
tetap ada, yaitu, kami mengingatkan Anda tentang memblokir bagian dari tabel hash manajer buffer:
locks_count | active_session | buffer_content | buffer_mapping ----βββ--βββ+β------βββββββββ+βββ------βββββββ+ββ------βββ 12549 | 1218 | 0 | 15
Dan itu pun tidak banyak. B - tree tidak lagi menjadi hambatan. Perpanjangan
heap-
datang ke depan.
Perawatan hati nurani
Selanjutnya, Alexander mengajukan hipotesis dan solusi berikut:
Kami menunggu banyak waktu pada
buffer parittion lock
ketika
buffer parittion lock
buffer. Mungkin pada
buffer parittion lock
sama ada beberapa halaman yang sangat dituntut, misalnya, root dari beberapa
Bβtree
. Pada titik ini ada aliran permintaan terus menerus untuk
shared lock
dari permintaan membaca.
LWLock
di
LWLock
βtidak adil.β Karena
shared lock
dapat diambil sebanyak yang dibutuhkan sekaligus, maka jika
shared lock
sudah diambil, maka
shared lock
selanjutnya lewat tanpa antrian. Dengan demikian, jika aliran kunci bersama adalah intensitas yang cukup sehingga tidak ada "jendela" di antara mereka, maka menunggu
exclusive lock
berjalan hampir hingga tak terbatas.
Untuk memperbaikinya, Anda dapat mencoba menawarkan - sepetak kunci "sopan" dari kunci. Ini membangkitkan hati nurani dari
shared locker
dan mereka jujur ββmengantri ketika sudah ada
exclusive lock
(yang menarik, kunci berat -
hwlock
- tidak memiliki masalah dengan hati nurani: mereka selalu jujur ββmengantri)
locks_count | active_session | buffer_content | buffer_mapping | reladdextra | inserts>30sec ββββββ-βββββ+ββββββββββββββββ+ββββββββββββββββ+βββββββββββ--β-β+ββββββ-ββββββ+ββββ------ 173985 | 1802 | 0 | 569 | 0 | 0
Semuanya baik-baik saja! Tidak ada
insert
panjang. Meskipun kunci pada potongan pelat hash tetap ada. Tapi apa yang harus dilakukan, ini adalah sifat-sifat ban superkomputer kecil kami.
Tambalan ini juga
ditawarkan kepada komunitas . Tetapi tidak peduli bagaimana nasib patch ini dalam komunitas berkembang, tidak ada yang menghalangi mereka untuk masuk ke versi
Postgres Pro Enterprise berikutnya , yang dirancang khusus untuk pelanggan dengan sistem yang sarat muatan.
Akhlak
Kunci
share
ringan bermoral tinggi - blok
exclusive
melewatkan antrian - telah memecahkan masalah keterlambatan per jam dalam sistem multi-simpul. Tag hash
buffer manager
tidak berfungsi karena terlalu banyak aliran
share lock
, yang tidak memberikan peluang bagi kunci yang diperlukan untuk mengganti buffer lama dan memuat yang baru. Masalah dengan ekstensi buffer untuk tabel database hanya konsekuensi dari ini. Sebelum ini, adalah mungkin untuk memperluas bottleneck dengan akses ke root
B-tree
.
PostgreSQL tidak dirancang untuk arsitektur dan superkomputer NUMA. Beradaptasi dengan arsitektur Postgres seperti itu adalah pekerjaan besar yang akan membutuhkan (dan mungkin memerlukan) upaya terkoordinasi dari banyak orang dan bahkan perusahaan. Tetapi konsekuensi yang tidak menyenangkan dari masalah arsitektur ini dapat dikurangi. Dan kita harus: jenis-jenis beban yang menyebabkan penundaan yang serupa dengan yang dijelaskan cukup tipikal, sinyal marabahaya yang serupa dari tempat lain terus mendatangi kita. Masalah serupa muncul sebelumnya - pada sistem dengan core lebih sedikit, hanya konsekuensinya tidak begitu mengerikan, dan gejalanya diobati dengan metode lain dan patch lainnya. Sekarang obat lain telah muncul - tidak universal, tetapi jelas bermanfaat.
Jadi, ketika PostgreSQL bekerja dengan memori seluruh sistem sebagai lokal, tidak ada bus berkecepatan tinggi antara node yang dapat dibandingkan dengan waktu akses ke memori lokal. Tugas muncul karena ini sulit, seringkali mendesak, tetapi menarik. Dan pengalaman memecahkannya bermanfaat tidak hanya untuk yang menentukan, tetapi juga untuk seluruh komunitas.
