- Di Linux 7.0, mode preemption PREEMPT_NONE yang sebelumnya menjadi nilai bawaan server lama dihapus, sehingga terjadi regresi performa serius: throughput PostgreSQL pada perangkat keras yang sama turun hingga setengah
- Hasil eksekusi
pgbench oleh engineer AWS pada mesin Graviton4 96-vCPU menunjukkan bahwa di Linux 7.0, dibanding Linux 6.x, transaksi per detik turun dari 98.565 menjadi 50.751, dengan 55% CPU habis di satu fungsi spinlock
- Spinlock yang melindungi akses ke shared buffer pool PostgreSQL, ketika berpadu dengan minor page fault dari halaman memori 4KB, dapat menyebabkan semua backend yang menunggu membuang CPU untuk berputar jika preemption oleh scheduler terjadi saat lock sedang dipegang
- Mengaktifkan Huge Pages (2MB atau 1GB) menurunkan jumlah page fault potensial dari 31 juta menjadi puluhan ribu hingga ratusan, sehingga menghilangkan regresi ini
- Dari sisi kernel, diusulkan adopsi Restartable Sequences (rseq), tetapi komunitas PostgreSQL berpendapat bahwa penurunan performa akibat upgrade kernel itu sendiri melanggar prinsip bahwa kernel "tidak merusak user space"
Gejala masalah
- Engineer AWS Salvatore Dipietro menjalankan
pgbench pada prosesor Graviton4 96-vCPU, melakukan uji beban paralel tinggi dengan scale factor 8.470 (tabel sekitar 847 juta baris), 1.024 klien, dan 96 thread
- Throughput turun hampir setengah, dari 98.565 TPS di Linux 6.x menjadi 50.751 TPS di Linux 7.0
- Hasil profiling
perf menunjukkan 55,60% waktu CPU habis di dalam fungsi s_lock
- Jalur pemanggilan:
StartReadBuffer → GetVictimBuffer → StrategyGetBuffer → s_lock
Apa itu preemption
- Preemption adalah keputusan scheduler OS untuk menghentikan thread yang sedang berjalan dan memberikan CPU ke thread lain
- Sebelum Linux 7.0, ada tiga opsi
- PREEMPT_NONE: thread hampir tidak dihentikan sampai ia secara sukarela melepas CPU (syscall, blok I/O, sleep). Ini adalah nilai bawaan server tradisional, dengan context switch lebih sedikit dan throughput lebih tinggi
- PREEMPT_FULL: thread yang sedang berjalan bisa dihentikan di hampir semua titik aman. Waktu respons menurun, tetapi overhead context switch meningkat. Ini adalah nilai bawaan desktop tradisional
- PREEMPT_LAZY: kompromi yang diperkenalkan di Linux 6.12; menunggu batas alami, tetapi tetap mengizinkan preemption bila perlu. Dirancang agar mendekati karakteristik throughput PREEMPT_NONE
- Di Linux 7.0, PREEMPT_NONE dihapus pada arsitektur CPU modern, sehingga hanya PREEMPT_FULL dan PREEMPT_LAZY yang tersisa
- Untuk sebagian besar software server, PREEMPT_LAZY berfungsi sebagai pengganti, tetapi pada PostgreSQL muncul perbedaan yang fatal
Manajemen memori PostgreSQL
- PostgreSQL menggunakan halaman data berukuran tetap (default 8KB) sebagai unit penyimpanan dasar; baris tabel, node indeks B-tree, metadata, dan lainnya disimpan di halaman ini
- Untuk mengurangi pembacaan disk, PostgreSQL menyimpan cache halaman data yang baru dibaca dalam area memori bersama besar bernama shared buffer pool
- Saat klien terhubung, dibuat proses backend khusus, dan jika halaman belum ada di buffer pool, PostgreSQL harus membacanya dari disk lalu mencari buffer kosong atau buffer yang bisa dikeluarkan
- Fungsi yang menangani pemilihan buffer ini adalah
StrategyGetBuffer
Spinlock di PostgreSQL
- Spinlock adalah mekanisme lock yang tidak tidur saat menunggu, tetapi terus berputar dalam loop sambil memeriksa apakah lock sudah tersedia
- Pada critical section yang sangat singkat, berputar lebih efisien daripada biaya menidurkan dan membangunkan thread
- Asumsi intinya: thread yang memegang lock akan segera melepaskannya
StrategyGetBuffer menggunakan satu spinlock global untuk melindungi pemilihan buffer
- Dalam lingkungan 96-vCPU dan 1.024 klien, semua backend bersaing memperebutkan lock yang sama
Memori virtual dan TLB
- Semua proses menggunakan alamat memori virtual, yang diterjemahkan perangkat keras menjadi alamat fisik melalui page table berbentuk pohon multi-level
- Karena menelusuri page table setiap saat lambat, CPU memiliki TLB (Translation Lookaside Buffer) yang menyimpan cache hasil translasi terbaru
- Saat TLB hit, akses cepat; saat TLB miss, diperlukan page table walk sehingga memakan waktu
- Linux memakai prinsip lazy allocation, sehingga saat memori virtual dialokasikan, halaman fisik yang sebenarnya baru dipetakan saat pertama kali diakses
- Pada akses pertama, terjadi minor page fault: kernel mengalokasikan halaman fisik dan menyimpan pemetaannya, yang lebih lambat beberapa mikrodetik dibanding baca/tulis biasa
Masalah halaman 4KB
- Dalam benchmark,
shared_buffers disetel ke 120GB, yang pada ukuran halaman memori 4KB berarti sekitar 31 juta halaman memori, yaitu 31 juta potensi page fault akses pertama
- Dalam benchmark berdurasi panjang yang memakai shared buffer pool 120GB, area memori baru terus masuk ke working set, sehingga page fault tidak hanya terjadi saat awal, tetapi terus berlangsung
- Jika
StrategyGetBuffer mengakses memori bersama saat masih memegang spinlock dan area itu belum dipetakan, maka terjadi minor page fault
- PREEMPT_NONE (sebelum Linux 7.0): walau backend A masuk ke page fault handler, ia cenderung menghindari titik rescheduling sukarela, sehingga kecil kemungkinan dijadwalkan keluar sebelum fault selesai. Waktu tunggu memang lebih panjang dari perkiraan, tetapi dampaknya terbatas
- PREEMPT_LAZY (setelah Linux 7.0): scheduler bisa mempreempt backend A di dalam page fault handler dan menjadwalkan proses lain. Meski fault sudah selesai diproses, akan ada tambahan waktu tunggu
t sampai scheduler mengembalikan kontrol
- Waktu tunggu tambahan ini bukan sekadar
t, tetapi membesar menjadi jumlah semua backend yang sedang berputar × t dalam bentuk pemborosan CPU
- Dalam lingkungan 96-vCPU dengan ratusan backend, efek pengali ini menjadi fatal, dan akhirnya 56% CPU habis di
s_lock
Solusi lewat Huge Pages
- Dengan
shared_buffers 120GB, perubahan ukuran halaman memori secara drastis mengurangi jumlah page fault potensial
- Halaman 4KB: ~31.000.000 potensi page fault
- Huge Pages 2MB: ~61.440
- Huge Pages 1GB: ~120
- Membesarkan ukuran halaman bukan hanya mengurangi page fault, tetapi juga mengurangi tekanan pada TLB: memori yang sama dapat dicakup oleh jauh lebih sedikit entri TLB, sehingga TLB miss dan page table walk ikut berkurang
- Karena
StrategyGetBuffer tidak lagi memicu fault saat lock dipegang, pemegang lock dapat selesai dengan cepat, dan backend lain hanya menunggu mikrodetik, bukan milidetik. Regresi pun hilang
- Di PostgreSQL, pengaturan huge pages dikendalikan oleh parameter
huge_pages
- Mendukung tiga nilai:
off, on, try (default)
try akan memakai huge pages bila memungkinkan, dan jika tidak maka diam-diam fallback ke 4KB, sehingga berisiko membuat konfigurasi salah tidak disadari
- Jika disetel ke
on, PostgreSQL akan gagal start bila huge pages tidak tersedia, sehingga masalah bisa langsung diketahui
- Trade-off: huge pages memakai model pra-alokasi dan reservasi, sehingga walau PostgreSQL tidak memakai semuanya, memori itu tetap tidak bisa dipakai bagian lain sistem. Jika hanya sebagian halaman yang terpakai, sisanya menjadi terbuang. Namun di lingkungan produksi dengan
shared_buffers besar, trade-off ini umumnya layak diambil
Perkembangan selanjutnya
- Peter Zijlstra, engineer kernel Intel yang merancang perubahan preemption ini, mengusulkan agar PostgreSQL mengadopsi Restartable Sequences (rseq)
rseq adalah fitur kernel Linux yang memungkinkan kode user space mendeteksi apakah preemption atau migrasi terjadi di dalam critical section dan mengulang section tersebut dari awal
- Jika
rseq diterapkan pada jalur spinlock PostgreSQL, skenario di mana pemegang lock yang ter-preempt menunda semua backend yang menunggu dapat dihindari
- Respons komunitas PostgreSQL cenderung negatif
- Sulit diterima bahwa untuk mendapatkan kembali performa yang sebelumnya tersedia gratis sebelum Linux 7.0, mereka harus mengadopsi fitur kernel tambahan
- Mereka menilai ini melanggar prinsip lama kernel, yaitu "tidak merusak user space" (software yang bekerja normal sebelum upgrade kernel harus tetap bekerja normal sesudahnya)
Belum ada komentar.