- One Million Checkboxes, yang diluncurkan pada 26 Juni 2024, adalah situs tempat semua orang memanipulasi 1 juta checkbox yang sama secara real-time, dan sebelum ditutup 2 minggu kemudian, situs ini memproses lebih dari 650 juta centang
- State-nya sendiri hanya 1 juta bit, atau 125KB, tetapi arsitektur awal yang dibuat dengan nginx, Flask/gunicorn, dan Redis pubsub dengan cepat mencapai batasnya saat menghadapi traffic yang tak terduga
- Ketika puluhan ribu orang berdatangan dari Hacker News, Reddit, Mastodon, dan Twitter, serangkaian masalah muncul: koneksi Redis habis, bandwidth melonjak, validasi input tidak ada, dan update lama diterapkan
- Penanganannya berfokus pada langkah-langkah yang bisa diterapkan cepat, seperti menambah server dan Redis, memproses update secara batch, memperkecil format pengiriman, batas bandwidth 250Mbit/s berbasis Linux
tc, serta skrip restart proses - Setelah itu backend dipindahkan ke Go agar stabil, dan pada akhirnya logika pembekuan checkbox diproses secara atomik dengan Redis Lua script; situs berakhir pada 11 Juli 2024 pukul 4:35 PM Waktu Timur
Situs dan desain awal
- One Million Checkboxes(OMCB) adalah situs web yang diluncurkan pada 26 Juni 2024, menyediakan 1 juta checkbox global
- Jika seorang pengguna menyalakan atau mematikan checkbox, perubahan itu langsung tercermin di layar semua pengguna
- Pembuatannya memakan waktu 2 hari, dan jumlah pengguna yang diperkirakan paling banyak hanya beberapa ratus orang
- Respons sebenarnya jauh lebih besar dari perkiraan
- Dalam beberapa jam setelah peluncuran, puluhan ribu orang masuk dan memanipulasi jutaan checkbox
- Traffic datang dari Hacker News, /r/InternetIsBeautiful, Mastodon, dan Twitter
- Beberapa hari kemudian, situs ini juga dimuat di Washington Post dan New York Times
- Sebagian log pada awal hari pertama tidak tersisa
- Sebab awalnya hanya 1 juta log terbaru per hari yang disimpan
- Mulai hari kedua, stabilisasi dimulai, dan pada hari itu lebih dari 50 juta checkbox dicentang
- Jumlah centang kumulatif sebelum situs ditutup melampaui 650 juta kali
Arsitektur awal yang berpusat pada Redis
- Status checkbox direpresentasikan sebagai 1 juta bit
- Kotak yang dicentang bernilai
1, kotak yang tidak dicentang bernilai0 - Ukuran seluruh state adalah 125KB
- Klien menyimpan bitset dan merujuknya saat rendering
- Kotak yang dicentang bernilai
- Klien dirancang untuk menghindari beban DOM
- Tidak memasukkan semua 1 juta elemen ke DOM
- Menggunakan react-window untuk merender hanya checkbox yang terlihat di layar saat ini dan sedikit buffer
- Konfigurasi server dibuat sederhana dengan mempertimbangkan penskalaan horizontal
- nginx menyajikan konten statis dan meneruskan request API serta koneksi websocket ke server Flask
- Server Flask berjalan sebagai dua instance dengan gunicorn
- Redis berperan sebagai penyimpanan state checkbox dan message queue
- Cara penggunaan Redis juga langsung
- State masing-masing checkbox diubah dengan primitive manipulasi bit milik Redis
- Saat klien mengirim event centang, Flask membalik bit di Redis dan mencatat event ke pubsub
- Kedua server Flask membaca pubsub dan memberi tahu perubahan kepada klien yang terhubung ke masing-masing server
- Snapshot state penuh adalah mekanisme untuk mengoreksi update yang terlewat
- Ini ditujukan untuk menyelaraskan klien yang melewatkan update karena tab berada di background
- Implementasi awal mengirim seluruh state setiap 30 detik
Prinsip penskalaan
- Biaya harus dapat dihitung batas atasnya
- Menghindari pendekatan auto-scaling tanpa batas yang membuat biaya melonjak
- Jika beban melebihi ekspektasi, dipilih untuk membiarkannya rusak
- Popularitas diasumsikan berlangsung singkat
- Respons yang bisa dibuat dalam beberapa jam diprioritaskan ketimbang solusi matang yang memakan beberapa hari atau minggu
- Technical debt yang timbul dalam proses itu diterima
- Pilihan teknologi lebih disukai yang sederhana dan bisa dioperasikan langsung
- Dipilih konfigurasi yang memungkinkan masuk langsung ke server, menjalankan perintah, dan melakukan debugging
- Sebagian besar dependency yang digunakan adalah yang bisa dioperasikan dan di-debug sendiri
- Pengalaman inti situs adalah sinkronisasi global
- Ke mana pun pengguna berpindah, mereka harus bisa melihat perubahan seketika
- Tidak melakukan penskalaan dengan hanya mengirim checkbox yang sedang dilihat pengguna
Hari pertama: penambahan server dan bottleneck Redis
- Dalam 30 menit setelah peluncuran, beban melonjak, dan meski situs masih hidup, kondisinya sulit bertahan lama
- Perbaikan paling jelas adalah menambah server
- nginx dapat dengan mudah melakukan reverse proxy ke instance Flask di VM lain, dan state sudah berada di Redis
- Server kedua ditambahkan sekitar pukul 12:30 PM dan segera mencapai beban 100%
- Awalnya diperkirakan menambah satu atau dua server sudah cukup
- Kenyataannya, traffic ikut meningkat seiring kapasitas ditambah
- Situs naik ke peringkat 1 Hacker News dan aktivitas Twitter juga melonjak
- Server Flask dan koneksi Redis menjadi bottleneck
- Tidak ada Redis connection pool, dan Redis mendekati kondisi kekurangan koneksi
- Beralih ke cara mengirim update yang digabungkan dalam batch
- Kompatibilitas dengan klien lama tidak dipertimbangkan, dengan asumsi pengguna akan melakukan refresh
- Redis connection pool juga ditambahkan, tetapi tidak bekerja dengan rapi pada kombinasi gunicorn dan Flask
- Meski begitu, tampaknya ini membantu mengurangi jumlah koneksi Redis
- Setelah itu, masalah ini tidak digali lebih dalam dan berlanjut ke migrasi ke Go
- Rate limit pembuatan sesi dihapus
- State rate limit disimpan di Redis, sementara koneksi Redis sedang habis
- Masalahnya bukan lonjakan sesi baru, melainkan pengiriman banyak data dari satu sesi
- Dalam jangka pendek ini berisiko, tetapi dinilai dapat diterima
- Instance Redis juga dinaikkan ke spesifikasi lebih besar
- Saat itu menggunakan Digital Ocean managed Redis
- Dinaikkan dari instance kecil dengan 1 shared CPU dan RAM 2GB ke instance dengan 4 dedicated CPU dan RAM 32GB
- Resize memakan waktu sekitar 30 menit
Masalah bandwidth dan pengurangan volume pengiriman
- Pada awalnya, biaya bandwidth tidak cukup dipertimbangkan
- Digital Ocean mengenakan biaya $0.01 per GB jika melebihi bandwidth gratis
- Dari pekerjaan sebelumnya tersedia 1TB bandwidth gratis, dan OMCB diperkirakan tidak akan berdampak besar
- Snapshot state penuh dapat menghabiskan bandwidth dengan cepat
- 1 juta bit adalah 1Mbit
- Jika dikirim setiap 30 detik kepada 1.000 orang, volumenya sekitar 2GB per menit, atau 120GB per jam
- Angka ini belum memperhitungkan incremental update
- Pemeriksaan bandwidth dan penetapan batas biaya dilakukan di box nginx
- Jumlah byte yang dikirim diperiksa dengan
ip -s link show dev eth0 - Karena menggunakan satu reverse proxy nginx, sumber bandwidth mudah disimpulkan
- Jumlah byte yang dikirim diperiksa dengan
- Pengurangan volume pengiriman dilakukan lewat dua jalur
- Menurunkan frekuensi snapshot state penuh
- Memperkecil format incremental update
- Format batch update dikompresi secara besar-besaran
- Format lama adalah daftar dict seperti
{ "index": 123, "value": true } - Format akhir berupa pasangan array indeks true dan array indeks false, seperti
[[123, 125], [124]] - Cara ini 5 kali lebih pendek daripada implementasi awal
- Format lama adalah daftar dict seperti
- Hard cap dipasang dengan Linux
tcuntuk mencegah biaya meledak- Traffic pada interface publik
eth0dibatasi menjadi 250Mbit/s - Ini setara sekitar 2GB per menit, atau sedikit di bawah 3TB per hari
- Dengan tarif $0.01 per GB, ini mencegah biaya membengkak tak terkendali semalaman
- Traffic pada interface publik
Hari kedua: validasi input yang terlewat dan Redis replica
- Pagi berikutnya situs sudah down, dan penyebabnya adalah validasi input yang terlewat
- Checkbox dengan indeks di atas 1 juta tidak diblokir
- Seseorang memanipulasi checkbox dengan indeks ratusan juta
- Akibatnya, jumlah kotak yang dicentang terlihat seolah sudah mencapai 1 juta dan situs dianggap selesai
- Data Redis juga membesar tanpa perlu
- Jutaan nilai 0 ditambahkan di antara bit ke-1 juta dan bit ke-100 juta
- Data yang dikirim ke klien menjadi 100 kali lebih besar
- Pemulihan dilakukan cepat
- Menghentikan nginx
- Menyalin hanya 1 juta bit pertama dari bitset lama ke bitset baru
- Bitset lama disimpan untuk debugging
- Kode diubah agar merujuk ke bitset baru dan validasi input ditambahkan
- Load halaman awal juga melambat
- Beban Redis tinggi, dan bug connection pool juga membuat koneksi dibuat secara berlebihan
- Alih-alih men-debug masalah connection pool, Redis replica ditambahkan untuk membagi beban dan koneksi primary
- Private IP replica harus dicari secara manual
- Sesuai panduan Digital Ocean, prefix
replica-berfungsi pada public DNS, tetapi tidak pada private DNS - Menggunakan public IP dianggap berisiko melewati internet publik dan terkena biaya bandwidth
- Koneksi dicoba ke alamat di sekitar private IP primary dan server lain, lalu private IP replica ditemukan pada percobaan ketiga atau keempat
- Setelah itu IP tersebut di-hardcode
- Sesuai panduan Digital Ocean, prefix
Restart proses dan perbaikan stale update
- Proses Flask terus crash, dan penyebabnya tampak seperti kekurangan koneksi Redis
- Alih-alih debugging mendetail, dibuat skrip bash yang memeriksa jumlah proses Flask yang sedang berjalan
- Jika proses yang berjalan kurang dari 3, systemd unit di-restart
- Skrip dimasukkan ke crontab
- Konfigurasi nginx juga disesuaikan
- Server yang down dibuat sementara dikeluarkan dari rotation
- Setelah perubahan ini, situs menjadi stabil
- Sinkronisasi state klien memiliki bug stale update
- Klien menerima incremental update dan full-state snapshot
- Karena kedua update tidak memiliki timestamp, incremental update lama dapat diterapkan setelah snapshot baru diterima
- Akibatnya, state yang benar-benar keliru bisa terlihat sampai full-state snapshot berikutnya
- Mitigasi berbasis timestamp ditambahkan
- Timestamp ditambahkan ke full-state snapshot
- Timestamp juga ditambahkan ke setiap update yang dicatat ke Redis pubsub
- Batch yang dikirim ke klien menyertakan timestamp maksimum di antara incremental update yang ada di dalamnya
- Klien diubah agar membuang batch yang lebih lama daripada full-state snapshot terakhir
- Solusi ini tidak sempurna
- Jika ada satu saja update baru di dalam batch, sebagian besar update lama tetap bisa diterapkan
- Meski begitu, hasilnya jauh lebih baik daripada sebelumnya
Penulisan ulang ke Go dan stabilisasi
- Pagi berikutnya situs masih hidup, dan setelah itu fokus beralih ke penulisan ulang backend
- Email dari Washington Post juga sudah datang
- Rencana tentang cara menutup situs juga dipikirkan bersamaan
- Rencana penutupan adalah membuat kotak yang dicentang membeku jika tidak segera di-uncheck
- Perubahan ini dapat memicu lonjakan aktivitas dan pekerjaan server tambahan
- Belum yakin apakah struktur berbasis Flask yang ada mampu menanganinya
- Bersama teman bernama Eliot, backend ditulis ulang dengan Go
- Dari Minggu pukul 2 PM sampai 2 AM, mereka mendiskusikan implementasi dan mem-port seluruh backend
- Struktur dipindahkan tanpa banyak perubahan besar
- Hambatannya antara lain menemukan library Go
socketioyang mendukung protokol terbaru
- Peningkatan performanya sangat besar
- Karena skalanya menjadi terlalu baik, bot dapat memasukkan traffic berlebihan
- Rate limit yang lebih baik pun dibutuhkan
- Pada Minggu malam juga terjadi DDoS
- Ditangani dengan menaruh situs di belakang Cloudflare dan sedikit mengubah konfigurasi nginx
Logika penutupan situs
- Setelah penulisan ulang ke Go, situs berjalan stabil
- Selama seminggu berikutnya, mereka merespons wawancara dan perhatian publik
- Setelah itu mulai mengerjakan penutupan situs
- Cara penutupannya adalah pembekuan checkbox
- Kotak yang dicentang akan masuk status frozen jika tidak segera di-uncheck
- Seiring waktu, seluruh situs menjadi sepenuhnya frozen
- State tambahan dimasukkan ke Redis
- Ditambahkan hashtable yang menyimpan waktu terakhir setiap checkbox dicentang
- State ini terlalu besar untuk diberikan ke klien, tetapi masih wajar untuk disimpan di Redis
- Nilai
time_to_freezejuga disimpan
- Saat uncheck, status pembekuan ditentukan
- Jika
now - last_checked > time_to_freeze, uncheck tidak dilakukan - Sebagai gantinya,
frozen_bitsetdiperbarui untuk menandai checkbox tersebut sebagai frozen frozen_bitsetdidistribusikan ke klien dengan cara yang sama seperti state checked- Klien menonaktifkan checkbox yang bit frozen-nya menyala
- Jika
- Agar tetap membeku meski tidak ada yang melakukan uncheck, ditambahkan pekerjaan terpisah
- Secara berkala mencari bit yang seharusnya sudah freeze tetapi belum ditandai, lalu membuatnya frozen
- Logika terkait dimasukkan ke Redis Lua script agar berjalan atomik
- Race condition jadi lebih mudah dihindari
- Perubahan penutupan diterapkan 2 minggu 1 hari setelah peluncuran
- Pada 11 Juli 2024 pukul 4:35 PM Waktu Timur, box 491915 dicentang dan situs berakhir
Biaya dan pelajaran yang didapat
- Biaya operasional situs sekitar $850
- Donasi cukup mendekati biaya ini
- Disimpulkan bahwa ini bukan kerugian besar
- Pilihan Redis dan nginx dinilai memuaskan
- Redis dan nginx dinilai sebagai teknologi yang sangat berguna
- Karena dioperasikan sendiri, debugging dan perbaikan menjadi mudah
- Namun, ketidakmampuan mengendalikan sepenuhnya instance managed Redis sedikit merepotkan
- Keputusan untuk tidak merancang penskalaan besar dalam waktu lama sejak awal dinilai positif
- Dianggap sulit memprediksi apa yang akan berhasil di internet
- Jika sejak awal menghabiskan berminggu-minggu memikirkan scale, situs ini mungkin tidak akan pernah diluncurkan
- Masuknya banyak pengguna membantu motivasi pemeliharaan dan penentuan prioritas
- Kebutuhan akan interaksi anonim yang terbatas juga terkonfirmasi
- Orang-orang menunjukkan minat pada situs tempat mereka bisa berinteraksi dengan orang asing secara terbatas
- Keyakinan untuk terus membuat situs semacam ini meningkat
1 komentar
Opini Hacker News
Ini tulisan yang banyak memberi pelajaran, disertai pengetahuan historis tentang sistem terdistribusi
Rasanya mereka mengalami hampir semua jenis titik henti dan kegagalan, kecuali storage, dan bagus bisa melihat proses penyelesaiannya
Saya tidak tahu Redis mendukung Lua, dan setelah melihat ini jadi ingin mencobanya sebagai penyimpanan state alternatif
Bandwidth adalah salah satu keluhan terbesar di layanan cloud. Karena tidak ada batas keras untuk mencegah biaya berlebih
Namun keduanya bukan masalah besar, dan cukup menarik bahwa storage bukan masalah berarti dalam proyek ini. Bagi saya pribadi, itu pengalaman baru
Bandwidth benar-benar merepotkan. Selama sekitar dua hari saya terus tegang memantau byte keluar dari NIC dan menghitung ulang, dan ketiadaan hard cap itu menakutkan. Padahal Digital Ocean termasuk cukup masuk akal harganya
Saya belum pernah memakai layanan serverless populer, tetapi pemahaman saya biaya bandwidth di sana bisa cukup keras
Dan Lua di dalam Redis benar-benar powerful; jika bisa menerima sedikit penurunan performa, ia memungkinkan melewati banyak masalah sulit yang sarat race condition, dan menyenangkan untuk dipakai
Tulisan yang sangat bagus, dan situs webnya juga pantas diberi selamat
Namun secara pribadi, saya rasa tulisan yang dibuat ini adalah bagian yang paling layak dibanggakan
Menurut saya bagian kuncinya adalah “membuat situs dalam dua hari dengan hampir tidak memikirkan skalabilitas adalah pilihan yang baik”
Ini terutama hal yang perlu dipelajari engineer di awal karier. Skalabilitas bukan masalah sampai ia benar-benar menjadi masalah
Saat sudah menjadi masalah, itu justru masalah yang bagus, dan tidak sesulit yang dibayangkan untuk diperbaiki
Saya sudah melihat banyak sistem di mana microservices menjadi “pilihan yang jelas”, bukan demi scaling atau pemisahan tim, melainkan karena para developer memang ingin melakukannya
Menskalakan sistem seperti itu benar-benar menyiksa
Tulisan terkait terbaru: One Million Checkboxes - https://news.ycombinator.com/item?id=40800869 - Juni 2024, 305 komentar
Proyek seperti ini menyenangkan
Sekitar 6 tahun lalu saya merilis Pixmap di Android, sebuah aplikasi editor piksel kolaboratif kecil yang mendukung grid lebih besar seperti 1024x1024
Saya membuat queue yang menerapkan setiap event ke gambar PNG; saat klien terhubung, ia memuat PNG awal, lalu untuk setiap event menggambar piksel hanya menerima satu objek kecil
Dengan cara ini, initial load bisa memanfaatkan kompresi gambar, dan change set setelahnya menjadi sangat kecil. Selain itu, karena semua event tersimpan di log, gambar juga bisa “diputar mundur” [0]
[0] 22mb: https://blog.winricklabs.com/images/pixmap-rewind-demo.gif
Jadi saya sedang mengutak-atik canvas yang bisa dialamatkan lewat API call
https://x.com/RussTheMagic/status/1816749136487588311
Tulisan yang bagus. Saya penasaran akhirnya biayanya berapa
Total biayanya sekitar 850 dolar, dan hampir tertutup oleh donasi
Setelah pindah ke Go, saya melakukan kesalahan dengan tidak menurunkan infrastruktur dengan benar, dan replika Redis kedua yang saya nyalakan juga sebenarnya bisa dihapus. Kalau fokus pada biaya, saya rasa bisa memangkasnya menjadi separuh
Tetapi donasi hampir menutup biaya, dan ada terlalu banyak hal lain, jadi saya tidak terlalu fokus ke sana
Setelah situs ditutup pun saya mempertahankan infrastruktur beberapa waktu untuk menyiapkan grafik dan sebagainya, jadi ada sedikit biaya tambahan; sekarang sedikit defisit, tetapi tidak besar
Sebagai orang yang sedang belajar backend, saya penasaran apakah ada arsitektur alternatif yang lebih sederhana untuk proyek ini
Akan bagus kalau ada cara lebih mudah untuk meng-host state 1 juta bit dan menyinkronkannya dengan klien. Sebagian solusi di tulisan itu sulit saya pahami
Proyek-proyek penulisnya luar biasa
Saya ingin menjelaskan teknologi yang dipakai lebih panjang, tetapi artikelnya sudah terlalu panjang sehingga terasa sulit menambahkan lagi
Kalau ada pertanyaan, saya dengan senang hati akan menjawab
Jujur saya tidak begitu tahu cara menyederhanakan arsitekturnya secara signifikan. Mungkin ada layanan yang bisa dipakai untuk kebutuhan seperti ini, tetapi menurut saya itu lebih mirip memindahkan kompleksitas ke orang lain
Pada akhirnya yang dibutuhkan adalah database untuk melacak box yang dicentang, cara memasukkan data ke database, cara memberi tahu klien tentang state saat ini, cara klien memberi tahu server ketika sebuah box dicentang dan state diperbarui, cara memberi tahu klien saat box dicentang atau dihapus centangnya, serta cara agar tidak selalu merender 1 juta elemen DOM
Di sini Redis dipakai untuk menyimpan state centang, dan demi kesederhanaan 1 juta bit disimpan apa adanya, lalu seluruh 1 juta bit dikirim ke klien. Untungnya datanya tidak terlalu besar
Flask dan WebSocket menangani event centang dan update; update box individual maupun update penuh 1 juta box sama-sama dikirim, dan react-window dipakai untuk menghindari masalah rendering
Sisanya, konten statis nginx dan reverse proxy, terutama adalah perangkat untuk memudahkan scaling, jadi implementasi tetap mungkin dan situs tetap berjalan tanpa detail-detail itu. Hanya saja tidak akan mampu menahan beban yang sama
Alih-alih database, simpan bit set ke file dan mmap. Alih-alih reverse proxy, aplikasi juga bisa menangani request HTTP dan koneksi WebSocket secara langsung
Konfigurasinya hanya beberapa web server dengan cache dan queue publish/subscribe di belakangnya
Semuanya mungkin saja diproses di dalam memori pada satu host besar, tetapi kalau tidak bisa memenuhi demand atau gagal karena alasan apa pun, semuanya akan benar-benar buntu
Kecuali cara yang tidak scalable seperti menaruh daftar global berisi 1 juta boolean di proses yang sama dengan backend API
(checked, start_x, start_y, end_x, end_y). Bukankah itu cara yang sangat jelasKeren
Saya penasaran apakah tulisan berikutnya akan menjadi analisis statistik tentang checkbox mana yang paling sedikit atau paling banyak dicentang
Saya ingat agak sedih karena checkbox yang saya pilih setelah scroll jauh ke bawah hampir langsung dihapus centangnya
Sebelum itu, ada satu cerita lagi tentang situs ini yang perlu saya sampaikan
Saya penasaran apakah gimnya masih hidup
Saat membuka https://onemillioncheckboxes.com/, tidak ada apa pun yang tercentang, dan di konsol JS hanya terlihat ini
{"total":0,"totalGold":0,"totalRed":0,"totalGreen":0,"totalPurple":0,"totalOrange":0,"recentlyChecked":false}Sebagai contoh kebalikan dari implementasi yang scalable, ada implementasi 1 juta checkbox di bawah 1000 karakter. Ini versi Deno
https://gist.github.com/jeff-hykin/4cdebafd8698298d021f103e2...