Menskalakan PostgreSQL untuk mendukung 800 juta pengguna ChatGPT
Oleh Bohan Zhang, Anggota Staf Teknis
Selama bertahun-tahun, PostgreSQL telah menjadi salah satu sistem data paling penting di balik produk inti kami, seperti ChatGPT dan API OpenAI. Seiring basis pengguna kami berkembang pesat, tuntutan terhadap basis data juga meningkat secara eksponensial. Dalam setahun terakhir, beban PostgreSQL kami meningkat lebih dari 10 kali lipat dan terus tumbuh dengan cepat.
Upaya kami untuk memajukan infrastruktur produksi guna mempertahankan pertumbuhan ini menghasilkan insight baru: PostgreSQL dapat diskalakan secara andal untuk mendukung beban kerja yang jauh lebih besar dan berfokus pada pembacaan dibandingkan dengan yang sebelumnya dianggap mungkin oleh banyak pihak. Sistem ini (awalnya dikembangkan oleh tim ilmuwan di University of California, Berkeley) memungkinkan kami mendukung lalu lintas global yang sangat besar dengan satu instance server fleksibel Azure PostgreSQL(terbuka di jendela baru) utama serta hampir 50 replika baca yang tersebar di berbagai wilayah di seluruh dunia. Inilah kisah tentang bagaimana kami meningkatkan skala PostgreSQL di OpenAI untuk mendukung jutaan kueri per detik bagi 800 juta pengguna melalui pengoptimalan yang ketat dan rekayasa yang solid, sekaligus membagikan poin-poin penting yang kami pelajari sepanjang perjalanan.
Setelah peluncuran ChatGPT, lalu lintas meningkat pada tingkat yang belum pernah terjadi sebelumnya. Untuk mendukung lonjakan tersebut, kami dengan cepat menerapkan berbagai pengoptimalan ekstensif pada lapisan aplikasi dan basis data PostgreSQL, meningkatkan skala secara vertikal dengan memperbesar ukuran instance, serta memperluas skala secara horizontal dengan menambahkan lebih banyak replika baca. Arsitektur ini telah melayani kami dengan sangat baik dalam jangka waktu yang lama. Dengan peningkatan yang berkelanjutan, arsitektur tersebut terus menyediakan fondasi yang kuat dan memadai untuk mendukung pertumbuhan di masa depan.
Mungkin terdengar mengejutkan bahwa arsitektur single-primary mampu memenuhi tuntutan skala OpenAI; namun, mewujudkannya dalam praktik sama sekali tidak mudah. Kami telah mengalami beberapa SEV yang disebabkan oleh kelebihan beban Postgres, dan insiden tersebut sering kali mengikuti pola yang serupa: masalah di lapisan hulu memicu lonjakan mendadak pada beban basis data, seperti cache miss berskala besar akibat kegagalan lapisan cache, lonjakan join multi-arah yang mahal hingga membuat CPU jenuh, atau badai penulisan yang muncul akibat peluncuran fitur baru. Seiring meningkatnya pemanfaatan sumber daya, latensi kueri ikut naik dan permintaan mulai mengalami time-out. Upaya percobaan ulang kemudian semakin memperbesar beban, memicu siklus yang merugikan dan berpotensi menurunkan kualitas seluruh layanan ChatGPT dan API.
Meskipun PostgreSQL dapat diskalakan dengan baik untuk beban kerja kami yang didominasi pembacaan, kami tetap menghadapi tantangan selama periode lalu lintas penulisan yang tinggi. Hal ini sebagian besar disebabkan oleh implementasi multiversion concurrency control (MVCC) pada PostgreSQL, yang membuatnya kurang efisien untuk beban kerja dengan intensitas penulisan tinggi. Sebagai contoh, ketika sebuah kueri memperbarui sebuah tuple atau bahkan hanya satu bidang, seluruh baris akan disalin untuk membentuk versi baru. Di bawah beban tulis yang berat, kondisi ini menyebabkan amplifikasi tulis yang signifikan. Hal tersebut juga meningkatkan amplifikasi pembacaan, karena kueri harus memindai beberapa versi tuple (termasuk tuple yang sudah tidak aktif) untuk memperoleh versi terbaru. Selain itu, MVCC memperkenalkan tantangan tambahan seperti pembengkakan tabel dan indeks, peningkatan beban pemeliharaan indeks, serta kebutuhan penyetelan autovacuum yang kompleks. (Pembahasan mendalam mengenai isu-isu ini dapat ditemukan dalam blog yang saya tulis bersama Prof. Andy Pavlo dari Carnegie Mellon University berjudul The Part of PostgreSQL We Hate the Most(terbuka di jendela baru), yang dikutip(terbuka di jendela baru) di halaman Wikipedia PostgreSQL.)
Untuk mengatasi keterbatasan ini dan mengurangi tekanan penulisan, kami telah memigrasikan dan terus memigrasikan elemen yang dapat dipecah (yaitu, beban kerja yang dapat dipartisi secara horizontal) serta beban kerja yang intensif penulisan ke sistem yang di-shard seperti Azure Cosmos DB, sambil mengoptimalkan logika aplikasi untuk meminimalkan penulisan yang tidak perlu. Selain itu, kami tidak lagi mengizinkan penambahan tabel baru pada penerapan PostgreSQL saat ini. Beban kerja baru kini secara default menggunakan sistem yang terpecah.
Bahkan seiring berkembangnya infrastruktur kami, PostgreSQL tetap tidak di-shard, dengan satu instance utama yang melayani seluruh operasi penulisan. Alasan utamanya adalah bahwa menerapkan sharding pada beban kerja aplikasi yang sudah ada akan sangat kompleks dan memakan waktu, karena memerlukan perubahan pada ratusan endpoint aplikasi serta berpotensi menghabiskan waktu berbulan-bulan atau bahkan bertahun-tahun. Karena beban kerja kami terutama didominasi pembacaan dan kami telah menerapkan berbagai optimasi secara ekstensif, arsitektur saat ini masih menyediakan ruang yang memadai untuk mendukung pertumbuhan lalu lintas yang berkelanjutan. Meskipun kami tidak menutup kemungkinan untuk melakukan sharding PostgreSQL di masa depan, langkah tersebut belum menjadi prioritas dalam waktu dekat, mengingat ruang gerak yang masih cukup besar untuk mendukung pertumbuhan saat ini maupun ke depan.
Pada bagian-bagian berikut, kami akan membahas tantangan yang kami hadapi serta berbagai optimalisasi ekstensif yang kami terapkan untuk mengatasinya dan mencegah gangguan di masa mendatang, dengan mendorong PostgreSQL hingga ke batas kemampuannya dan meningkatkan skalanya hingga mencapai jutaan kueri per detik (QPS).
Tantangan: Dengan hanya satu penulis, arsitektur satu-primer tidak dapat meningkatkan skala penulisan. Lonjakan penulisan yang berat dapat dengan cepat membebani sistem utama dan berdampak pada layanan seperti ChatGPT dan API kami.
Solusi: Kami meminimalkan beban pada node utama semaksimal mungkin—baik pembacaan maupun penulisan—untuk memastikan node tersebut memiliki kapasitas yang cukup dalam menangani lonjakan penulisan. Lalu lintas baca dialihkan ke replika jika memungkinkan. Namun, sebagian kueri baca harus tetap dijalankan di server utama karena merupakan bagian dari transaksi penulisan. Untuk kasus-kasus tersebut, kami berfokus memastikan seluruh kueri berjalan secara efisien dan menghindari kueri yang lambat. Untuk lalu lintas penulisan, kami telah memigrasikan beban kerja yang dapat dipecah dan bersifat intensif penulisan ke sistem yang di-shard seperti Azure CosmosDB. Beban kerja yang lebih sulit untuk dipecah tetapi tetap menghasilkan volume penulisan yang tinggi membutuhkan waktu lebih lama untuk dimigrasikan, dan proses tersebut masih terus berlangsung. Kami juga secara agresif mengoptimalkan aplikasi guna mengurangi beban penulisan; misalnya, dengan memperbaiki bug aplikasi yang menyebabkan penulisan berlebihan serta memperkenalkan mekanisme penulisan tertunda, jika sesuai, untuk meratakan lonjakan lalu lintas. Selain itu, saat melakukan pengisian ulang kolom tabel, kami menerapkan pembatasan laju yang ketat guna mencegah tekanan penulisan yang berlebihan.
Tantangan: Kami mengidentifikasi beberapa kueri PostgreSQL yang mahal. Di masa lalu, lonjakan volume yang tiba-tiba pada kueri-kueri ini akan menghabiskan sumber daya CPU dalam jumlah besar, sehingga memperlambat permintaan ChatGPT dan API.
Solusi: Beberapa kueri yang mahal, seperti kueri yang melibatkan penggabungan banyak tabel, dapat secara signifikan menurunkan kinerja atau bahkan melumpuhkan seluruh layanan. Oleh karena itu, kami perlu terus mengoptimalkan kueri PostgreSQL untuk memastikan kueri tersebut berjalan efisien dan menghindari anti-pola umum dalam Online Transaction Processing (OLTP). Sebagai contoh, kami pernah mengidentifikasi kueri yang sangat mahal yang menggabungkan 12 tabel, di mana lonjakan eksekusi kueri ini bertanggung jawab atas SEV dengan tingkat keparahan tinggi di masa lalu. Join multi-tabel yang kompleks sebaiknya dihindari kapan pun memungkinkan. Jika join memang diperlukan, kami belajar untuk mempertimbangkan pemecahan kueri serta memindahkan logika join yang kompleks ke lapisan aplikasi sebagai alternatif. Banyak kueri bermasalah ini dihasilkan oleh framework Object-Relational Mapping (ORM), sehingga penting untuk meninjau secara cermat SQL yang dihasilkan dan memastikan perilakunya sesuai dengan yang diharapkan. Selain itu, kueri idle yang berjalan lama juga sering ditemukan di PostgreSQL. Mengonfigurasi batas waktu seperti idle_in_transaction_session_timeout menjadi langkah yang sangat penting untuk mencegah kondisi tersebut menghambat proses autovacuum.
Tantangan: Jika replika baca mengalami gangguan, lalu lintas masih dapat dialihkan ke replika lainnya. Namun, ketergantungan pada satu penulis menciptakan satu titik kegagalan—jika terjadi masalah, seluruh layanan akan terpengaruh.
Solusi: Permintaan yang paling kritis sebagian besar hanya melibatkan kueri baca. Untuk mengurangi titik kegagalan tunggal pada sistem utama, kami memindahkan operasi pembacaan tersebut dari penulis ke replika, sehingga permintaan tetap dapat dilayani meskipun sistem utama mengalami kegagalan. Meskipun operasi penulisan tetap akan gagal, dampaknya menjadi lebih terbatas; kondisi ini tidak lagi dikategorikan sebagai SEV0 karena operasi pembacaan masih tersedia.
Untuk mengurangi risiko kegagalan pada sistem utama, kami menjalankan sistem utama dalam mode High Availability (HA) dengan hot standby, yaitu replika yang terus disinkronkan dan selalu siap untuk mengambil alih layanan lalu lintas. Jika sistem utama mengalami gangguan atau perlu dinonaktifkan untuk keperluan pemeliharaan, kami dapat dengan cepat mempromosikan sistem siaga guna meminimalkan waktu henti. Tim Azure PostgreSQL telah melakukan pekerjaan yang signifikan untuk memastikan proses failover ini tetap aman dan andal, bahkan di bawah beban yang sangat tinggi. Untuk menangani kegagalan pada replika baca, kami menerapkan beberapa replika di setiap wilayah dengan ruang kapasitas yang memadai, sehingga kegagalan satu replika tidak menyebabkan gangguan pada tingkat regional.
Tantangan: Kami sering menghadapi situasi di mana permintaan tertentu mengonsumsi sumber daya yang tidak sebanding pada instance PostgreSQL. Kondisi ini dapat menyebabkan penurunan kinerja pada beban kerja lain yang berjalan di instance yang sama. Sebagai contoh, peluncuran fitur baru dapat memperkenalkan kueri yang tidak efisien dan sangat menguras CPU PostgreSQL, sehingga memperlambat permintaan untuk fitur-fitur penting lainnya.
Solution: To mitigate the “noisy neighbor” problem, we isolate workloads onto dedicated instances to ensure that sudden spikes in resource-intensive requests don’t impact other traffic. Specifically, we split requests into low-priority and high-priority tiers and route them to separate instances. This way, even if a low-priority workload becomes resource-intensive, it won’t degrade the performance of high-priority requests. We apply the same strategy across different products and services as well, so that activity from one product does not affect the performance or reliability of another.
Challenge: Each instance has a maximum connection limit (5,000 in Azure PostgreSQL). It’s easy to run out of connections or accumulate too many idle ones. We’ve previously had incidents caused by connection storms that exhausted all available connections.
Solution: We deployed PgBouncer as a proxy layer to pool database connections. Running it in statement or transaction pooling mode allows us to efficiently reuse connections, greatly reducing the number of active client connections. This also cuts connection setup latency: in our benchmarks, the average connection time dropped from 50 milliseconds (ms) to 5 ms. Inter-region connections and requests can be expensive, so we co-locate the proxy, clients, and replicas in the same region to minimize network overhead and connection use time. Moreover, PgBouncer must be configured carefully. Settings like idle timeouts are critical to prevent connection exhaustion.
Setiap replika baca memiliki deployment Kubernetes tersendiri yang menjalankan beberapa pod PgBouncer. Kami menjalankan beberapa deployment Kubernetes di belakang Kubernetes Service yang sama, yang berfungsi menyeimbangkan beban lalu lintas ke seluruh pod.
Challenge: A sudden spike in cache misses can trigger a surge of reads on the PostgreSQL database, saturating CPU and slowing user requests.
Solution: To reduce read pressure on PostgreSQL, we use a caching layer to serve most of the read traffic. However, when cache hit rates drop unexpectedly, the burst of cache misses can push a large volume of requests directly to PostgreSQL. This sudden increase in database reads consumes significant resources, slowing down the service. To prevent overload during cache-miss storms, we implement a cache locking (and leasing) mechanism so that only a single reader that misses on a particular key fetches the data from PostgreSQL. When multiple requests miss on the same cache key, only one request acquires the lock and proceeds to retrieve the data and repopulate the cache. All other requests wait for the cache to be updated rather than all hitting PostgreSQL at once. This significantly reduces redundant database reads and protects the system from cascading load spikes.
Challenge: The primary streams Write Ahead Log (WAL) data to every read replica. As the number of replicas increases, the primary must ship WAL to more instances, increasing pressure on both network bandwidth and CPU. This causes higher and more unstable replica lag, which makes the system harder to scale reliably.
Solution: We operate nearly 50 read replicas across multiple geographic regions to minimize latency. However, with the current architecture, the primary must stream WAL to every replica. Although it currently scales well with very large instance types and high-network bandwidth, we can’t keep adding replicas indefinitely without eventually overloading the primary. To address this, we’re collaborating with the Azure PostgreSQL team on cascading replication(terbuka di jendela baru), where intermediate replicas relay WAL to downstream replicas. This approach allows us to scale to potentially over a hundred replicas without overwhelming the primary. However, it also introduces additional operational complexity, particularly around failover management. The feature is still in testing; we’ll ensure it’s robust and can fail over safely before rolling it out to production.
Challenge: A sudden traffic spike on specific endpoints, a surge of expensive queries, or a retry storm can quickly exhaust critical resources such as CPU, I/O, and connections, which causes widespread service degradation.
Solution: We implemented rate-limiting across multiple layers—application, connection pooler, proxy, and query—to prevent sudden traffic spikes from overwhelming database instances and triggering cascading failures. It’s also crucial to avoid overly short retry intervals, which can trigger retry storms. We also enhanced the ORM layer to support rate limiting and when necessary, fully block specific query digests. This targeted form of load shedding enables rapid recovery from sudden surges of expensive queries.
Challenge: Even a small schema change, such as altering a column type, can trigger a full table rewrite(terbuka di jendela baru). We therefore apply schema changes cautiously—limiting them to lightweight operations and avoiding any that rewrite entire tables.
Solution: Only lightweight schema changes are permitted, such as adding or removing certain columns that do not trigger a full table rewrite. We enforce a strict 5-second timeout on schema changes. Creating and dropping indexes concurrently is allowed. Schema changes are restricted to existing tables. If a new feature requires additional tables, they must be in alternative sharded systems such as Azure CosmosDB rather than PostgreSQL. When backfilling a table field, we apply strict rate limits to prevent write spikes. Although this process can sometimes take over a week, it ensures stability and avoids any production impact.
This effort demonstrates that with the right design and optimizations, Azure PostgreSQL can be scaled to handle the largest production workloads. PostgreSQL handles millions of QPS for read-heavy workloads, powering OpenAI’s most critical products like ChatGPT and the API platform. We added nearly 50 read replicas, while keeping replication lag near zero, maintained low-latency reads across geo-distributed regions, and built sufficient capacity headroom to support future growth.
This scaling works while still minimizing latency and improving reliability. We consistently deliver low double-digit millisecond p99 client-side latency and five-nines availability in production. And over the past 12 months, we’ve had only one SEV-0 PostgreSQL incident (it occurred during the viral launch(terbuka di jendela baru) of ChatGPT ImageGen, when write traffic suddenly surged by more than 10x as over 100 million new users signed up within a week.)
While we’re happy with how far PostgreSQL has taken us, we continue to push its limits to ensure we have sufficient runway for future growth. We’ve already migrated the shardable write-heavy workloads to our sharded systems like CosmosDB. The remaining write-heavy workloads are more challenging to shard—we’re actively migrating those as well to further offload writes from the PostgreSQL primary. We’re also working with Azure to enable cascading replication so we can safely scale to significantly more read replicas.
Looking ahead, we’ll continue to explore additional approaches to further scale, including sharded PostgreSQL or alternative distributed systems, as our infrastructure demands continue to grow.
Penulis
Ucapan Terima Kasih
Terima kasih khusus kepada Jon Lee, Sicheng Liu, Chaomin Yu, dan Chenglong Hao atas kontribusi mereka dalam penulisan ini, serta kepada seluruh tim yang telah membantu meningkatkan skala PostgreSQL. Kami juga ingin mengucapkan terima kasih kepada tim Azure PostgreSQL atas kemitraan mereka yang solid.


