1 poin oleh GN⁺ 1 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • Kunci eksklusif global pada Postgres LISTEN/NOTIFY membatasi throughput implementasi sederhana, tetapi dengan membuffer notifikasi dan mengirimnya secara batch, satu server dapat menangani hingga 60 ribu stream write per detik
  • Transaksi yang memanggil NOTIFY menahan kunci global hingga commit dan fsync() selesai untuk menjamin urutan commit notifikasi, sehingga commit terserialisasi dan group commit juga tidak bisa dimanfaatkan
  • Implementasi awal yang memanggil NOTIFY lewat trigger pada setiap penulisan ke tabel stream memberikan latensi rendah, tetapi mengalami bottleneck di 2.900 operasi per detik tanpa benar-benar memanfaatkan CPU, memori, dan IOPS
  • Dengan menjadikan tabel database sebagai source of truth, bukan notifikasi, lalu mengirim notifikasi yang dikumpulkan di memori secara berkala dalam satu transaksi, jumlah pengambilan kunci bisa dikurangi drastis
  • Kemungkinan notifikasi di buffer hilang akibat kegagalan proses ditutupi dengan polling berfrekuensi rendah, dan bahkan dalam lingkungan baca serentak tetap tercapai latensi 15~100ms serta throughput 20 kali lebih tinggi dibanding sebelumnya

Stream berlatensi rendah yang dibangun dengan Postgres

  • Stream berbasis Postgres menyimpan setiap potongan stream sebagai baris baru di tabel streams, dan token respons LLM pun bisa menjadi satu potongan
  • Di sisi pembaca, sulit menunggu secara efisien hanya dengan query biasa karena tidak diketahui kapan potongan berikutnya akan tiba
  • Polling periodik menambah latensi untuk penggunaan interaktif seperti chat online jika intervalnya panjang, dan jika terlalu pendek maka poller serentak akan membanjiri database
  • Dengan LISTEN/NOTIFY, proses pembaca bisa menunggu dalam keadaan blokir lalu langsung bangun ketika ada notifikasi bahwa potongan baru telah ditulis, sehingga polling yang tidak perlu bisa dihindari

Bottleneck dari NOTIFY yang dikirim pada setiap write

  • Dalam implementasi awal, setiap kali potongan baru ditulis ke tabel streams, trigger menjalankan fungsi untuk mengirim NOTIFY satu kali, lalu proses pembaca menunggu notifikasi sebelum membaca potongan baru
  • Akurasi dan latensi rendah memang tercapai, tetapi bahkan pada database Postgres yang besar pun tidak dapat mempertahankan lebih dari 2.900 stream write per detik
  • Saat bottleneck terjadi, penggunaan CPU, memori, dan IOPS tidak terlihat tinggi, dan penyebabnya adalah kunci global pada jalur commit NOTIFY

Kunci global yang menjamin urutan commit

  • Transaksi yang memanggil NOTIFY memperoleh kunci eksklusif global saat commit dimulai, dan tidak melepaskannya sampai commit selesai sepenuhnya dan isi transaksi ditulis ke disk melalui fsync()
  • Postgres menjamin notifikasi dikirim sesuai urutan commit transaksi, dan menyimpan semua notifikasi keluar dalam antrean internal global yang harus persis sama dengan urutan commit
  • Penambahan notifikasi ke antrean juga harus diproses secara transaksional sebagai bagian dari commit, tetapi karena waktu commit tiap transaksi berbeda, urutannya tidak bisa ditentukan sebelum commit selesai
  • Kunci global membuat commit transaksi yang memuat notifikasi menjadi terserialisasi agar urutannya bisa dipastikan lebih dulu, dan ditambahkan ke antrean notifikasi internal dalam urutan yang sama

Bagaimana serialisasi commit membatasi throughput

  • Karena semua stream write memanggil NOTIFY melalui trigger, setiap transaksi write menahan kunci global selama seluruh commit dan flush ke disk
  • Karena transaksi di-commit satu per satu, group commit Postgres yang memproses beberapa transaksi dengan satu fsync() tidak bisa dimanfaatkan
  • Throughput tidak bisa melampaui kecepatan Postgres meng-commit transaksi individual, dan pekerjaan menunggu pada kunci sehingga CPU serta disk juga tidak terpakai penuh
  • Patch yang akan masuk ke Postgres 19 tidak menghapus kunci global, jadi tidak menyelesaikan bottleneck ini
    • Sebagai gantinya, patch itu mengoptimalkan kasus terbatas dengan banyak channel notifikasi dan tiap listener hanya menunggu satu channel tertentu

Buffering notifikasi dan pengiriman batch

  • Dalam berbagai penggunaan LISTEN/NOTIFY termasuk stream, notifikasi bukan source of truth, melainkan hanya sinyal untuk memeriksa tabel tempat data sebenarnya disimpan
  • Dalam struktur seperti ini, notifikasi itu sendiri tidak perlu memiliki urutan global yang sempurna atau durabilitas penuh, sehingga bisa dibuffer di memori lalu dikirim berkala dalam satu transaksi batch
  • Kunci global diambil bukan pada setiap stream write individual, melainkan hanya saat mengosongkan buffer
  • Write individual dapat berjalan cepat karena terpisah dari pengiriman notifikasi latar belakang, dan throughput bisa ditingkatkan dengan memanfaatkan optimasi Postgres seperti group commit

Polling frekuensi rendah untuk menutupi notifikasi yang hilang

  • Jika proses berhenti saat notifikasi masih berada di memori, notifikasi tersebut mungkin tidak terkirim
  • Proses pembaca, sambil menunggu notifikasi, juga melakukan query ke database secara berkala untuk memeriksa apakah ada data stream yang ditulis tanpa notifikasi
  • Karena polling ini hanyalah sarana pendukung untuk memulihkan notifikasi yang hilang, ia bisa dijalankan dengan frekuensi rendah dan dampaknya pada performa juga kecil

Throughput dan latensi

  • Implementasi yang dioptimalkan dapat menangani hingga 60 ribu stream write per detik dalam lingkungan dengan proses pembaca serentak, mencatat throughput 20 kali lebih tinggi daripada implementasi awal
  • Bahkan dengan throughput yang ditingkatkan, latensi tetap berada di kisaran 15~100ms
  • Pada throughput maksimum, CPU Postgres terpakai penuh, menunjukkan bahwa sistem telah mencapai saturasi nyata database itu sendiri, bukan lagi contention pada kunci
  • Seluruh kode benchmark dapat dilihat di dbos-postgres-benchmark

1 komentar

 
GN⁺ 1 jam lalu
Komentar Hacker News
  • Skalabilitas adalah spektrum yang kontinu, bukan dikotomi. 60 ribu per detik bisa 100 ribu kali lebih banyak daripada kebutuhan untuk satu sistem, dan 100 ribu kali kurang untuk sistem lain. Jika harus menyebut kesalahan umum developer, saya akan memilih memilih teknologi dengan karakteristik skalabilitas yang tidak cocok, lebih daripada “optimisasi prematur”
    Teknologi yang terlalu kecil memang jelas gagal ketika melewati batasnya, tetapi teknologi yang terlalu skalabel juga membawa beban operasional dan batasan. Mengadopsi teknologi seperti itu pada sistem kecil, yang sebenarnya bisa sangat mengurangi upaya pengembangan dengan model yang lebih kaya, juga merupakan pilihan buruk
    Batas LISTEN/NOTIFY cukup rendah sehingga perlu diperhatikan; jadi setelah menghitung beban puncak secara pesimistis pun sebaiknya sisakan margin minimal 10x, tetapi untuk banyak proyek itu sudah memadai. Karena kelebihannya berupa integrasi dengan database, ketersediaan, dan tidak perlu mengoperasikan layanan terpisah, ini bukan opsi yang harus langsung disingkirkan; bahkan angka 2 ribu per detik yang sebelumnya disebutkan pun besar untuk sistem yang memproses satu pesan dalam hitungan detik

    • Ini hanya sanggahan kecil terhadap tulisan yang bagus, tetapi hampir tidak ada sistem yang menangani 6 miliar request per detik, dan kalaupun ada, kemungkinan besar memakai alat buatan sendiri yang disesuaikan dengan tujuannya
    • Mengira beban akan di bawah 60 ribu per detik lalu melonjak dari 20 ribu ke 200 ribu adalah masalah yang lebih baik daripada membangun untuk target 1 juta per detik tetapi beban aktualnya 20 ribu. Kesuksesan tak terduga pada kasus pertama bisa menutup biaya tindakan sementara dan ekspansi, sedangkan kasus kedua membuat Anda terikat pada struktur biaya tinggi dan investasi di muka
      Lebih baik merancang berdasarkan skala yang benar-benar diperkirakan dengan tambahan kelonggaran yang wajar, dan memilih lebih dari itu hanya ketika skalabilitas tambahan pada dasarnya gratis. Jika bisa membeli hardware yang lebih besar dengan beberapa ribu dolar, atau pilihannya setara selain skalabilitas, pilih yang lebih besar
    • Dalam kasus ini, bisa dibilang memang skalabel karena mencapai batas maksimum hardware. Bottleneck-nya ada pada hardware, bukan database atau I/O; pada throughput maksimum, CPU Postgres terpakai penuh, menunjukkan kejenuhan database itu sendiri, bukan kontensi
  • Saya pernah sangat sukses menggabungkan LISTEN/NOTIFY dengan broker langganan GraphQL berbasis Rust. Langganannya berjumlah puluhan ribu, tetapi koneksi LISTEN hanya satu per host, total 3–4 saja
    Semua perubahan dikirim ke tiap host, lalu host mengelola langganan pengguna yang sebenarnya dan memutuskan apa yang akan dipublikasikan. Mengganti ratusan host Ruby atau Node dengan beberapa host Rust dapat sangat menyederhanakan arsitektur, dan pendekatan yang dianggap tidak skalabel pun bisa bekerja cukup baik

    • Saya penasaran library GraphQL apa yang direkomendasikan untuk Rust
  • Saat bekerja sebagai CTO, semua layanan kami memproses sekitar 100 ribu kejadian per hari, lalu tumbuh menjadi jutaan, dan akhirnya puluhan juta. Dalam proses itu, seorang engineer membangun queue di atas semantik LISTEN/NOTIFY untuk memanfaatkan konsistensi kuat dengan model data. Itu tidak sulit dipahami, dan karena bisa menghilangkan lapisan penyimpanan serta pengiriman terpisah, saat itu tampak masuk akal
    Namun ketika fitur buatan sendiri itu diskalakan, kami harus mengakali cara kerja internal PostgreSQL, yang menjadi sangat merepotkan, dan seharusnya kami pindah ke sistem lain lebih awal. Skalabilitasnya juga buruk; di RDS terjadi kontensi disk parah yang sulit dilacak penyebabnya, dan VACUUM pada tabel tersebut adalah mimpi buruk. Karena queue yang familiar diimplementasikan secara asing memakai fitur internal PostgreSQL, engineer lain merasa takut serta enggan melakukan debugging dan mengambil kepemilikan
    Pelajaran inti, di luar detail seperti skema dan indeks, adalah selalu mulai dari teknologi yang sederhana dan dapat diprediksi. Jika tidak benar-benar membutuhkan konsistensi data yang sangat kuat, lebih baik memakai queue yang kontrak API-nya sederhana seperti SQS atau Redis queue, meskipun berarti menambah satu komponen infrastruktur, lalu menyesuaikan sisanya ke sana. Semakin sedikit tanggung jawab mekanis yang dipikul satu penyimpanan data inti, semakin baik

    • Pada akhirnya ini bisa diringkas menjadi “kalau digunakan tanpa memahami cara kerjanya, ia tidak akan skalabel”. LISTEN/NOTIFY dasar memang tidak skalabel, tetapi tulisan asli benar-benar menemukan cara untuk menskalakannya, sehingga tiap tim tidak perlu memecahkan masalah itu lagi
      Seiring ekosistem sistem terdistribusi berkembang, kita makin memahami apa yang bisa dilakukan tiap komponen. Memulai dengan komponen yang sedikit lalu menambahkannya saat benar-benar dibutuhkan menghasilkan sistem yang lebih sehat
    • Jarang ada komponen baru yang mengalahkan faktor lain, tetapi ada manfaat besar jika queue dioperasikan secara independen dari komponen arsitektur lain. Queue memang dibuat untuk menyimpan lalu meneruskan, jadi memisahkan siklus hidupnya juga dapat memisahkan sistem-sistem lain saat update, analisis kegagalan, dan waktu patching
    • Ini lebih dekat ke masalah manajemen, dan saya tidak setuju dengan kesimpulan bahwa kita harus menambahkan komponen baru ke infrastruktur. Menambahkan node jaringan baru hanya karena developer tidak mau menangani sebagian stack tidaklah tepat; tugaskan saja mereka untuk menangani bagian itu
  • Saya terus menyukai DBOS yang memanfaatkan Postgres, dan kini juga SQLite, dengan benar. Ini bisa diperkenalkan ke stack CRUD yang sudah ada dengan hampir tanpa usaha
    Begitu mulai memakai workflow yang durable, Anda akan terus melihat tempat untuk menerapkannya. Belakangan ini saya bereksperimen dengan memandang tiap email sebagai workflow yang durable, lalu membiarkan pengguna, pihak lawan, agent, serta alat seperti GitHub atau Attio ikut masuk ke alur secara bergiliran
    https://housecat.com/blog/gmail-durable-workflows-sandbox-vm

  • Tulisan seperti ini umumnya merupakan hasil evaluasi independen atas masalah, pemahaman, dan solusi masing-masing. Agak sulit menyebutnya kurang ahli hanya karena mereka mengharapkan performa tertentu dari pengaturan default sebuah alat; semua orang terus belajar melalui kegagalan
    Fakta bahwa eksperimen menggunakan server database 96 core·384GB RAM (https://github.com/dbos-inc/dbos-postgres-benchmark/blob/mai...) sangat penting dan seharusnya dinyatakan dengan jelas. Database memang bisa diskalakan secara vertikal, tetapi itu juga ada batasnya. Siapa yang terhubung dari mana juga memengaruhi performa dan latensi keseluruhan
    60 ribu per detik mungkin tampak besar, tetapi yang merobohkan sistem nyata adalah lonjakan trafik sesaat, bukan trafik normal. Jika bukan perusahaan besar, Anda tidak akan memulai dengan server sebesar ini. Jika termasuk read replica dan redundansi lintas region, satu cluster database produksi bisa menelan biaya lebih dari 100 ribu dolar

  • Tampaknya artikel terkait: Postgres LISTEN/NOTIFY does not scale - https://news.ycombinator.com/item?id=44490510 - Juli 2025, 321 komentar

  • Sepertinya bagian terpenting dalam tulisan itu terlewat, yaitu cara menetapkan offset atau nomor urut yang memungkinkan pelacakan sampai sejauh mana konsumen sudah membaca dan mengambil pesan baru lewat jalur alternatif. Ada beberapa cara, tetapi tidak mudah diselesaikan tanpa kompleksitas atau kontensi lock, dan jika implementasinya salah bisa menimbulkan race condition dengan konsumen. Biasanya penulis akan mengunci satu baris di tabel status atau semacamnya untuk menetapkan nomor berikutnya bagi topik event
    Saya penasaran apa cara terbaiknya. Tool yang membaca change data capture (CDC) lalu menulis nomor event yang ditetapkan ke tabel lain mungkin cukup baik, tetapi latensinya bisa menjadi tinggi, dan pemroses CDC itu juga harus melakukan NOTIFY
    Di sisi konsumen, ada juga penggunaan yang bisa sangat meningkatkan throughput lewat batch processing. Dalam kasus ini, tanpa LISTEN/NOTIFY pun konsumen bisa dijalankan berulang untuk memproses semua pesan baru yang belum diproses setiap kali, lalu menyimpan nomor urut terakhir di antara iterasi

  • Sepingat saya, rilis yang pertama kali mendukung LISTEN/NOTIFY punya implementasi lock yang kurang baik sehingga ada masalah performa. Artikel lama yang dikritik di sini juga memperbaiki hal itu dalam koreksi tepat setelah paragraf pertama
    Jika tanggal koreksinya 8 Mei, maka artikel 24 Juli perlu mengakui bahwa tulisan terkenal yang mengatakan fitur ini tidak dapat diskalakan bukan ditulis dengan niat buruk dan mungkin juga tidak salah untuk konteks saat itu

    • Jika yang dimaksud adalah optimasi yang akan masuk ke Postgres 19, itu juga dibahas di tulisan aslinya. Patch tersebut (https://github.com/postgres/postgres/commit/282b1cde9dedf456...) tidak menghapus lock global atau menyelesaikan bottleneck yang diamati
      Sebaliknya, patch itu mengoptimalkan kasus yang lebih terbatas, ketika ada banyak channel notifikasi dan masing-masing penerima hanya menunggu satu channel tertentu
  • Terakhir kali saya cek, LISTEN/NOTIFY memiliki batas 8.000 byte pada data notifikasi, sehingga ada sisi yang jelas tidak skalabel. Jika datanya tidak bisa disimpan sebagai baris notifikasi dan hanya ID-nya yang dikirim, fitur ini sulit dipakai
    Event pada game web adalah data sementara yang menjelaskan perubahan status, sehingga tidak ada alasan untuk menyimpannya di database, dan bisa saja melebihi 8.000 byte, jadi tidak cocok untuk penggunaan ini

    • Jika saya mengimplementasikan sistem notifikasi yang skalabel, saya akan menetapkan batas atas ukuran notifikasi. Dengan menjaga ukuran pesan tetap O(1), kita bisa fokus pada penskalaan jumlah notifikasi, sementara pesan yang ukurannya arbitrer besar dapat menghentikan performa dan juga menunjukkan kemungkinan sistem notifikasi digunakan secara keliru
    • Saya penasaran apakah mungkin mengirim pesan yang merujuk pada status yang berubah, meskipun tidak menunjuk ke baris tertentu
  • Tulisan itu membahas kontensi lock pada antrean global, tetapi sepertinya tidak menyebut masalah lain dari antrean global berukuran tetap. Satu penerima lambat pada satu channel bisa memblokir penulisan ke semua channel. Setidaknya beberapa tahun lalu bentuk kegagalan seperti itu mungkin terjadi, meski sekarang mungkin sudah berubah