Manfaatkan semaksimal mungkin sistem yang Anda miliki
(blog.danslimmon.com)- Bottleneck Postgres pada satu aplikasi SaaS tunggal makin parah sehingga penggunaan CPU bertahan di kisaran 60~80%, dan sempat naik sampai 100% hingga menyebabkan gangguan singkat
- Skalabilitas vertikal dengan mengganti ke instance DB yang lebih besar sudah mencapai batas, sehingga sulit menyerap kenaikan beban lebih lanjut dengan cara yang sama
- Sharding tulis dan microservices memang dapat meningkatkan kapasitas serta fleksibilitas operasional, tetapi tetap meninggalkan biaya kompleksitas pada backup, monitoring, migration, ORM, hingga topologi jaringan
- Dalam praktiknya, selama 3 bulan dilakukan penghapusan kueri berat, optimasi kode Rails, tuning Postgres, dan pemindahan sebagian kueri read-only yang mahal ke DB replika
- Hasilnya, penggunaan CPU DB puncak mingguan turun dari 90% menjadi 30%, sehingga ada ruang untuk memanfaatkan sistem saat ini lebih lama sebelum beralih ke arsitektur generasi berikutnya
Bottleneck Postgres dan akhir dari skalabilitas vertikal
- Seiring beban pada satu aplikasi SaaS tunggal meningkat, performa Postgres menjadi bottleneck utama
- Penggunaan CPU bertahan di kisaran 60~80%
- Setidaknya satu kali sempat melonjak hingga 100% dan menyebabkan gangguan singkat
- Sebelumnya, setiap kali DB mulai sibuk, solusinya adalah mengganti ke instance yang lebih besar untuk membeli waktu
- Cara ini memungkinkan tim tetap fokus pada pekerjaan lain seperti pengembangan fitur
- Saat itu mereka sudah memakai instance terbesar, sehingga skalabilitas vertikal tidak lagi memungkinkan
Arsitektur generasi berikutnya yang menarik tetapi mahal
- Opsi utama yang dibahas adalah sharding tulis dan microservices
- Sharding tulis adalah pendekatan dengan cluster DB independen, lalu data ditulis ke DB tertentu berdasarkan strategi partisi
- Secara potensial dapat meningkatkan kapasitas hingga 2~3 digit lipat
- Microservices membagi monolit menjadi beberapa layanan, dan tiap layanan memiliki penyimpanan datanya sendiri
- Penyimpanan data bisa dipilih sesuai beban kerja tiap layanan
- Keduanya sama-sama memperluas pilihan dari sisi toleransi kegagalan dan ketahanan operasional, tetapi tujuan saat itu adalah mengembalikan performa DB ke kondisi yang bisa dikendalikan
Kompleksitas akan terus menimbulkan biaya setelah diperkenalkan
- Peningkatan kompleksitas tidak berhenti pada biaya implementasi struktur baru, tetapi berlanjut menjadi biaya perhatian setelahnya
- Jika memilih sharding DB, maka setiap keputusan teknis ke depan harus ikut menangani kompleksitas baru tersebut
- backup
- monitoring
- migration
- ORM
- topologi jaringan
- Microservices menimbulkan beban serupa, dan pemeliharaan arsitektur tambahan bisa memperlambat atau bahkan menggagalkan pengiriman fitur
Cari ruang dulu di sistem yang ada saat ini
- Bahkan ketika perubahan arsitektur besar terlihat perlu, sering kali masih ada ruang tambahan yang tersisa di sistem yang ada
- Penyesuaian workload, tuning performa, dan penambahan sistem pendukung saja bisa membeli waktu beberapa bulan atau beberapa tahun
- Jika opsi seperti ini masih layak dijalankan, ada baiknya dicoba lebih dulu sebelum membangun sistem generasi berikutnya dari nol
Optimasi yang benar-benar dilakukan
- Pada pekerjaan pertama, dua engineer selama sekitar 3 bulan terutama menangani masalah performa DB
- Tidak ada satu trik ajaib
- Mereka menemukan kueri berat lewat telemetri
- Mereka mengidentifikasi titik asal kueri di codebase Rails
- Mereka mengoptimalkan atau menghapus kueri
- Mereka melakukan tuning pada berbagai pengaturan Postgres
- Pada pekerjaan kedua, dua engineer lain memodifikasi codebase agar sebagian kueri read-only yang mahal dijalankan di DB replika
- Kueri
SELECTyang paling sering dipicu oleh polling dari web client dipisahkan dari DB utama
- Kueri
Hasil dan prinsip operasional
- Gabungan dua pekerjaan ini menurunkan penggunaan CPU puncak mingguan DB dari 90% menjadi 30%
- Penggunaan CPU puncak mingguan: {l:90,30}
- Ruang CPU jadi jauh lebih longgar, dan kemampuan untuk mengurangi beban dari DB utama juga meningkat
- Karena menyentuh banyak bagian codebase dan melibatkan beberapa developer, pengetahuan tersebar tentang sistem yang ada juga ikut terkumpul
- Kompleksitas tidak selalu buruk, dan pada akhirnya akan tiba saatnya beralih ke struktur yang lebih kompleks sebelum mencapai batas mendasar arsitektur DB
- Sebelum saat itu tiba, memeras sistem yang ada lebih dulu memungkinkan kita bekerja lebih lama dengan sistem yang membosankan dan sederhana sejauh mungkin, yang juga lebih menguntungkan dari sisi biaya dan kepraktisan
1 komentar
Komentar Hacker News
Melihat masalah performa basis data, pendapat saya yang paling panas untuk proyek baru adalah: rancang agar hot path aplikasi sama sekali tidak memakai join
Penyimpanan murah, jadi denormalisasi saja semuanya dan perbarui semuanya di dalam transaksi. Benar-benar mengejutkan seberapa cepat jadinya saat join dihilangkan. Query analitik ad hoc bisa direplikasi ke basis data lain untuk analitik
Saya punya perasaan campur aduk terhadap DynamoDB milik Amazon, tetapi gagasan bahwa untuk memakainya dengan benar kita harus merencanakan pola penggunaan lebih dulu dan menentukan skema belakangan layak dibawa juga ke basis data relasional. Sekarang saya bahkan berpendapat bahwa join tidak diperlukan kecuali untuk tujuan analitik. Basis data utama punya properti ACID dan penyimpanan luar biasa murah, jadi denormalisasi saja
Untuk mencegah hot partition, lebih baik memakai sesuatu yang mendekati UUID daripada integer. Ini bukan solusi serba guna dan ada kekurangannya, tetapi kita bisa terbiasa dengan performa “selalu cukup baik” yang bisa diskalakan horizontal, alih-alih performa luar biasa dari integer yang suatu hari akan runtuh
Pendapat yang lebih panas lagi adalah memberi indeks pada semua kolom, tetapi itu topik untuk lain hari
Dari sudut pandang utang teknis, kalau sejak awal kami mencoba membuat semuanya cepat, biayanya akan jauh lebih mahal. Kecepatan pengembangan akan melambat drastis dan kemungkinan besar kami gagal melewati beberapa krisis
Sebagai gantinya, kami membayar biaya mesin beberapa ribu dolar per bulan lebih banyak daripada yang sebenarnya diperlukan, dan menghemat tenaga kerja beberapa bulan pada masa ketika kami belum bisa merekrut cukup banyak insinyur dan opportunity cost pengembangan fitur sangat besar. Karena kami tidak bisa tahu sebelumnya di mana bottleneck akan muncul, membuat semuanya cepat sejak awal mungkin membutuhkan pekerjaan 10–20 kali lebih banyak. Beberapa bottleneck ternyata tidak terduga
Join bisa berbahaya pada skala besar, tetapi sebagian besar startup setidaknya pada tahap awal tidak punya masalah skala. Denormalisasi bisa menjadi optimisasi yang baik, tetapi setiap kali ada perubahan, kita membayar biaya kecepatan untuk menyinkronkan semua salinan. Seseorang akan membuat bug yang tidak memperbarui field non-otoritatif yang terdenormalisasi, sehingga pengguna melihat data usang. Biasanya memakai join lalu mengoptimalkan belakangan dengan read-aside cache dan semacamnya lebih murah secara total dibanding memelintir skema
Sebaliknya, menulis satu data ke 20 tempat alih-alih 1 tempat jauh lebih lambat dari sisi performa, dan query juga menjadi luar biasa rumit serta rawan bug. Akan terjadi kasus 18 tempat diperbarui tetapi 2 tempat terlewat
Keunggulan denormalisasi sering disebut karena penyimpanan murah, tetapi di sini penyimpanan adalah masalah paling kecil. Permukaan bug yang jauh lebih besar dan performa tulis yang buruk adalah masalah sebenarnya, dan performa tulis itu dengan mudah bisa ikut menggerus performa baca
Aplikasi tinggi pada dasarnya hanya melakukan satu hal, dan sisanya mendukung hal itu. Sebagian besar perusahaan big tech yang bisa Anda bayangkan termasuk di sini. Dalam model datanya, hanya ada beberapa konsep penggerak yang benar-benar penting
Facebook pada dasarnya hanya punya orang, postingan, dan iklan. Netflix kira-kira akun dan acara. Produk Amazon berpusat pada penjual, pembeli, dan barang, mungkin dengan beberapa hal logistik di belakangnya
Ada banyak aplikasi tinggi karena itu mudah. Jauh lebih mudah daripada aplikasi lebar yang sering disebut “enterprise”. Alasan perangkat lunak enterprise buruk adalah karena ia sulit, dan di sinilah ada wilayah yang paling kurang dieksplorasi serta peluang yang sangat besar. Pemain lama seperti Oracle berkinerja buruk sekali di sini, dan jika Anda masuk dengan pola pikir aplikasi tinggi, Anda juga akan begitu
Saran seperti “jangan pernah memakai join” atau “rancang berpusat pada satu tabel” masuk akal untuk aplikasi tinggi, tetapi merupakan saran buruk untuk aplikasi lebar. Kita kerap melihat perusahaan aplikasi yang sangat tinggi mencoba hal di luar kompetensi inti mereka lalu gagal total, karena mereka dipenuhi orang yang menganggap saran ini suci
Saran ini ditujukan untuk perusahaan yang sudah sukses, mengerjakan hal mudah, dan sudah memetik semua buah yang menggantung rendah. Bahkan aplikasi tinggi yang belum menjadi korban kesuksesannya sendiri pun tidak perlu mencincang model datanya demi performa. Hanya perusahaan yang sudah sukses raksasa dan sedang memeras tetes performa terakhir yang perlu memikirkan hal seperti itu, dan justru perusahaan seperti itulah yang paling tidak membutuhkan saran. Saran yang berpusat pada aplikasi tinggi seperti “FAANG melakukannya, jadi kamu juga harus” atau “bagaimana kalau pengguna kamu 1 miliar?” mencemari pikiran orang-orang yang ingin mengerjakan hal yang lebih menarik daripada menampilkan iklan kepada miliaran orang
Join besar bisa diperbaiki performanya nanti, misalnya dengan mendorongnya ke materialized view atau melakukan ETL ke column store. Tetapi jika seseorang menyalin kolom
subtotal_centske model Order, Invoice, Payment, NotificationEmail, UserProfileRecentOrders, lalu membuatnya dirujuk atau diperbarui di 296 tempat, jalan untuk kembali ke keadaan normal menjadi panjangBiasanya masalah muncul pada tabel yang bersifat catatan riwayat. Data yang diperlukan untuk operasi sehari-hari sebenarnya hanya sebagian sangat kecil dari tabel, tetapi operasi pada tabel raksasa pasti melambat, indeks apa pun yang ada. Tindakan menambahkan lebih banyak indeks itu sendiri juga menjadi masalah
Setidaknya pada basis data relasional tradisional, memberi indeks pada semua kolom saja tidak cukup; kita harus membuat indeks kombinasi kolom yang tepat dan bisa digunakan. DynamoDB mungkin berbeda
Peras semaksimal mungkin apa yang sudah Anda miliki, lalu setelah memerasnya untuk sementara, ubah cara memandang masalah; ketika Anda memeras di sini, di sini, dan sampai di sini, tiba-tiba Anda akan sadar masih banyak yang tersisa
Kami mengoptimalkan sebuah monolit besar selama sekitar dua bulan. Dari kondisi di bawah 2.000 RPS, yang menurut PM dan tim sudah tidak ada lagi yang bisa diperas, penggantian hardware menaikkannya menjadi di bawah 3.200 RPS; beberapa hari penyempurnaan membuatnya menjadi 4.000 RPS; sedikit usaha lagi menjadi 10.000 RPS; dan sekitar seminggu kemudian menjadi 40.000 RPS
Ada yang berkata, “Ini sudah cukup, tidak perlu lebih jauh,” tetapi setelah mengubah cukup banyak hal, performanya melonjak menjadi lebih dari 2 juta RPS pada satu mesin, dan sebulan kemudian sistem itu mampu menangani lebih dari 40 juta RPS secara stabil dengan latensi rendah. Masih ada sedikit ruang untuk mendorongnya lebih jauh
Sekarang kami bahkan tidak memakai 5% dari kapasitas yang bisa diekstraksi. Perubahan sebesar itu terjadi hanya karena kami mengubah cara memikirkan masalah. Sekadar memindahkan dari server lama ke server baru hanya menaikkan dari 1.800 RPS ke sedikit di atas 3.000 RPS. Menambah hardware tidak memperbaiki masalah mendasar, dan menambah kompleksitas hanya menunda masalah. Ketika cara memikirkan masalah diubah, masalah itu sendiri dan jawabannya ikut berubah
Menambahkan pada pernyataan “pecah monolit menjadi beberapa layanan yang saling terhubung, dan biarkan tiap layanan memiliki penyimpanan datanya sendiri yang bisa diskalakan sesuai kondisinya,” pada titik ini Anda tidak perlu memisahkan semua microservice yang mungkin. Cukup tanyakan, “Pemisahan mana yang akan memberi dampak terbesar?”
Dalam kasus saya, saya memisahkan sebagian data time-series dari Mongo ke Cassandra. Struktur tabel Cassandra jauh lebih cocok. Dataset itu memiliki skema yang terdefinisi dengan baik, dan Cassandra bisa mengemas datanya jauh lebih efisien. Di bagian itu, fleksibilitas dokumen JSON tidak diperlukan
Karena data tersebut merupakan sebagian besar dari seluruh data, setelah itu Mongo menjadi cukup memuaskan. Pemisahan yang dibutuhkan hanya satu. Secara teknis, sebelum dan sesudahnya tetap monolit; hanya saja layanan yang sama menulis ke dua database
Ironisnya, belakangan ada arsitek tipe “teori di atas meja” yang ingin menggabungkan semua data ke penyimpanan dokumen JSON, dan beberapa kali diskusi berlangsung dengan nada “kita sudah pernah menempuh jalan itu dan tahu ujungnya di mana”
Vertical scaling seharusnya menjadi solusi pertama yang jelas. Ini hal yang dilewatkan banyak orang, dan sebagian juga dibahas di artikel: vertical scaling mendekati horizontal scaling yang tidak merusak konsistensi database
Ada banyak ruang yang bisa diperas, dan jarang sekali Anda perlu dengan mudah melemparkan antipola seperti mengabaikan join atau validasi data hanya karena muncul masalah performa
Jika developer bisa membaca hasil EXPLAIN/ANALYZE serta melakukan indexing dan optimasi query yang benar, banyak keputusan over-engineering bisa dihindari
Catat query, saring yang sangat sering dijalankan atau yang waktu eksekusinya panjang, cache yang sering dijalankan, dan optimalkan yang berat. Jika ini dilakukan secara sistematis, sistem akan menjadi lebih sehat
Dari pengalaman saya, hal-hal yang sangat membantu adalah APM, slow query log, replika baca/tulis database, partitioning, dan sharding
Alat seperti https://explainmysql.com memperlihatkan dengan lebih jelas apa yang sebenarnya perlu dioptimalkan, sehingga menjadi kerangka yang lebih mudah bagi developer yang punya cukup pengetahuan untuk menyiapkan database tetapi belum sampai memahami cara kerja internalnya
Rasanya seseorang pasti sudah sedang membuat sistem AI yang, jika diberi skema dan log, secara ajaib menjalankan SQL untuk memperbaikinya. Entah bisa dipercaya atau tidak, tetapi banyak perusahaan pasti lebih ingin memakai hal seperti itu daripada merekrut DBA khusus
Saya tidak tahu apakah setiap query harus selalu diberi hint indeks. Kadang ada indeksnya, tetapi query tampak tidak memakai indeks tersebut. Menggunakan rencana eksekusi SQL sepertinya bisa membantu memahami masalah ini dengan lebih baik
Kode yang buruk tidak bisa diperbaiki dengan arsitektur baru. Itu hanya menunda masalah sebentar
Teringat salah satu kutipan yang saya suka. “Kita pergi berperang dengan pasukan yang kita punya sekarang, bukan pasukan yang kita inginkan atau yang ingin kita miliki nanti.”
Kita mungkin ingin mengabaikan fakta bahwa ucapan ini berasal dari Donald Rumsfeld. Meski begitu, ia memang punya beberapa kutipan bagus seperti “unknown unknowns.”
Saya sering teringat kutipan ini saat bekerja dalam tim. Tidak semua orang sepenuhnya setuju, atau memiliki pemahaman yang sama dan tujuan bersama. Ada orang yang mungkin bekerja dengan cara yang tidak efisien atau tidak saya sukai. Namun karena punya tim tetap lebih baik daripada tidak punya tim, kita harus mencari cara terbaik untuk mencapai tujuan dengan tim yang ada.
Ini juga sangat cocok diterapkan pada sistem.
https://www.youtube.com/watch?v=XH_34tqxAjA
https://www.google.com/search?q=no+battle+plan+survives
Ini lebih cocok diterapkan pada diri kita sendiri daripada pada sistem.
Dari pengalaman saya, pada web app yang dibuat di atas ORM, ada sangat banyak buah yang tergantung rendah yang bisa dipetik lewat optimasi query ketika beban database menjadi masalah.
Di luar hal mendasar seperti “apakah ada masalah N+1,” ORM kadang tidak menghasilkan query yang optimal. Saya tidak ingin membuat web app produksi yang kompleks tanpa ORM, tetapi sesekali kita harus bisa keluar dari ORM.
Kita perlu memprofilkan query yang benar-benar berjalan di produksi dan paling banyak memakai resource. Lihat query itu, lalu pahami bentuk tabel targetnya. Ada kasus ketika ORM memakai join padahal sebenarnya perlu subquery, dan sebaliknya. Kadang kita juga perlu melakukan agregasi awal atas sebagian hasil, atau menyesuaikan kondisi WHERE pada join yang kompleks.
Saya pernah melihat query yang dihasilkan ORM, yang dijalankan setengah-sering, membunuh database; dengan beberapa perubahan kecil, waktu eksekusinya turun dari lebih dari 20 detik menjadi kurang dari 1 detik.
expire_on_commit=Falseyang tidak intuitif.Opsi ini pun tidak menjamin penggunaan RETURNING; ia akan dipakai jika database driver dan database mendukungnya, serta ORM mendukung kombinasi driver/database tersebut. SQL yang dihasilkan muncul di log, tetapi tidak ada API untuk memeriksanya secara langsung, sehingga tidak ada cara untuk memaksa penggunaan RETURNING dalam test suite kecuali menangkap dan mengorek log sendiri. Untungnya, di dalam framework Pytest hal ini cukup mudah.
Saya menyukai ORM, tetapi hal seperti ini terasa menjengkelkan rumit di banyak lapisan. Saya juga paham SQLAlchemy adalah library raksasa sehingga tidak semuanya bisa mudah. Namun contoh ini menunjukkan dengan baik trade-off dalam penggunaan ORM.
Saya tahu bahwa jika memakai
insert()langsung di Core, hasilnya bisa sesuai keinginan. Di sini yang saya maksud adalah kasus ketika menambahkan objek ORM keAsyncSessiondengan.add().Sebagian besar business logic lebih baik diekspresikan dengan bahasa aljabar relasional dan beberapa ekstensi daripada dengan OOP.
Kalimat “biaya sebenarnya dari meningkatnya kompleksitas, dan sering kali biaya yang jauh lebih besar, adalah perhatian” juga berarti beban kognitif.
Saya lelah bekerja pada sistem microservices yang masih mengalami downtime, tetapi tidak ada yang tahu bagaimana keseluruhannya bekerja. Kebanyakan sebenarnya adalah distributed monolith: perubahan melintasi beberapa service dan harus dideploy secara berurutan. Data harus direplikasi, pekerjaan harus disinkronkan, dan state harus dibagi.
https://www.youtube.com/watch?v=y8OnoxKotPQ
Namun jika untuk memperbaiki atau memodifikasi sistem seseorang harus mengetahui keseluruhannya, itu sinyal kuat bahwa aturan seperti single responsibility atau abstraksi yang benar melalui API telah dilanggar. Namun dalam pengalaman saya, ini sangat umum. Untuk men-debug pipeline yang terdiri dari N microservices, sering kali kita harus menjalankan dan membangun semua N service secara lokal.
Secara ketat, ini adalah monolith yang ditambahi network partition serta variabilitas build/deploy yang tak terbatas. Pada akhirnya, menurut saya ini adalah lingkungan kerja yang luar biasa sulit, melampaui kemampuan programmer manusia mana pun.
Saya suka tulisan ini. Selama beberapa bulan terakhir saya mencoba menyampaikan pesan yang sama kepada manajer saya, tetapi tidak terlalu berhasil.
Cluster Redshift yang kelebihan beban sudah beberapa kali tumbang, dan karena sudah dinaikkan sampai batas maksimum dengan node RA3, kami sekarang hendak memulai migrasi besar-besaran ke “infrastruktur generasi berikutnya.” Yang dimaksud infrastruktur generasi berikutnya di sini adalah tiga cluster Redshift yang dikelola dengan CDK.
Infrastruktur baru ini jauh lebih kompleks daripada konfigurasi saat ini, dan saya tidak yakin apakah ia akan menjadi peluru perak yang diharapkan semua orang.
Ini solusi yang membosankan. Jika kita tidak sedang membangun sistem superkritis yang langsung berkaitan dengan nyawa dan tubuh manusia, ini seharusnya menjadi jawaban default. Total biaya kepemilikan jelas jauh lebih rendah.
Jika kita tidak punya resource untuk mengoperasikan sistem redundan yang besar, saya terlalu sering melihat kompleksitas tambahan dari sistem redundan itu sendiri menjadi masalah. Lebih baik fokus pada kesederhanaan.
Jika untuk mendukung kompleksitas kita harus menambah banyak orang, sementara dari sisi biaya dan penilaian risiko itu tidak perlu, maka opsi yang lebih sederhana jauh lebih baik. Saya tidak mengatakan belum pernah melihat kasus ketika proyek raksasa akhirnya diperlukan agar bisa maju, tetapi kadang bahkan proyek seperti itu pun terasa lebih kecil daripada total beban yang terus ditanggung untuk mempertahankan kompleksitas sampai titik tersebut. Ini sangat bergantung pada apa yang sedang dibangun.
Solusi yang mencari optimasi performa di dalam sistem lalu memeras sisa performa benar-benar menyenangkan
Saya teringat buku Richard L. Sites, Understanding_Software_Dynamics. Buku ini mengajarkan cara mengukur dan memperbaiki masalah latensi, serta seberapa besar penghematan yang bisa dihasilkan dari pengurangan latensi pada skala besar
Mengukur dan menalar masalah seperti ini memang sulit, tetapi solusinya sering kali sederhana. Misalnya, di halaman 9 tertulis, “[a] simple change paid for 10 years of my salary.”
Suatu hari saya ingin mencoba optimasi yang berdampak sebesar itu
https://research.google/pubs/pub36575/
Namun, ada banyak orang pintar di sekitar, jadi kalau menemukan peluang besar, biasanya ada alasan mengapa orang lain belum terjun ke sana. Alasannya bisa teknis, bisa juga organisasional
Sebagai contoh untuk yang terakhir, Google biasanya tidak memberi penghargaan besar untuk pekerjaan semacam ini kecuali saat ada tekanan sumber daya. Mungkin saja saya mendapat sekitar 100 dolar bonus rekan kerja untuk suatu optimasi, tetapi sama sekali bukan komisi 10%, promosi, atau hak untuk menerima gaji tanpa masuk kerja selama 10 tahun. Secara umum, perusahaan lebih ingin engineer mengerjakan pertumbuhan pendapatan daripada penghematan biaya. Apakah kebijakan ini benar atau tidak berada di luar tingkatan gaji saya
Sekarang mereka tidak lagi bisa menaikkan konfigurasi dengan beberapa klik di konsol manajemen, sehingga tampaknya mereka harus benar-benar memutar otak untuk menyelesaikan masalah kapasitas. Jika bagian tertentu dari kode dioptimalkan lebih awal, mungkin konfigurasi instans sebesar itu sejak awal tidak diperlukan