Re-arsitektur: Beralih dari Redis ke SQLite
(wafris.org)- Wafris mengganti penyimpanan Redis milik pengguna pada klien middleware Rails v1 menjadi penyimpanan berbasis SQLite di v2 untuk mengurangi kesulitan deployment sekaligus latensi evaluasi permintaan
- Pemilihan Redis sebelumnya dipengaruhi oleh kebiasaan di ekosistem Rails seperti Heroku dan Sidekiq, tetapi dalam operasional nyata masalahnya membesar karena pengguna juga harus memikul peran sebagai admin Redis
- Jalur baca yang membandingkan semua permintaan HTTP masuk dengan aturan adalah bottleneck utama, sedangkan penulisan untuk pelaporan bisa diproses lebih lambat atau dipisahkan secara batch dan asinkron
- Dalam pengujian kueri rentang IP terburuk dengan dataset 1,2 juta rentang pada MacBook Air M2 lokal, SQLite menunjukkan performa sekitar 3 kali lebih cepat daripada Redis lokal, dan ini belum memasukkan latensi jaringan
- v2 menggunakan struktur sinkronisasi yang memeriksa aturan baru berdasarkan interval waktu atau jumlah permintaan lalu mengunduh seluruh DB SQLite baru, dan jumlah instalasi yang berhasil meningkat sekitar 3 kali lipat
Friksi deployment yang terlihat pada v1 berbasis Redis
- Wafris adalah perusahaan firewall aplikasi web open source yang menyediakan klien middleware untuk aplikasi Rails
- Klien v1 memerlukan penyimpanan Redis milik pengguna yang dideploy bersama aplikasi
- Pemilihan Redis di awal dipengaruhi oleh lingkungan tempat Redis mudah dipasang di Heroku, kemudahan akses jarak jauh, dan contoh sukses seperti Sidekiq
- Lingkungan pengguna nyata jauh lebih beragam, dan banyak pengguna kesulitan men-debug masalah deployment dan konfigurasi Redis
- Di RailsWorld 2023 juga ada sentimen negatif terhadap anggapan bahwa server Redis pasti dibutuhkan di samping aplikasi Rails
Inti masalah kecepatan adalah latensi jaringan
- Redis memang cepat dibandingkan RDBMS tradisional, tetapi sebagai database terpisah tetap memerlukan pengelolaan koneksi, memori, dan proses
- Di lingkungan cloud, latensi jaringan secara langsung menggoyang performa pemrosesan permintaan
- Wafris harus membandingkan semua permintaan HTTP yang masuk ke aplikasi dengan aturan yang tersimpan
- Secepat apa pun klien v1 dioptimalkan, jika jaringan tempat aplikasi berada lambat maka keseluruhan respons bisa ikut melambat
- Aplikasi Rails juga tidak selalu dideploy sebagai satu “majestic monolith”
- aplikasi yang dideploy di beberapa zona
- aplikasi yang dipecah ke beberapa server dengan tanggung jawab yang saling tumpang tindih
- aplikasi yang hanya sebagian menggunakan Rails dan dideploy bersama bahasa atau framework lain
- Dalam lingkungan operasional seperti ini, friksi penggunaan Redis menjadi lebih besar
Pemisahan baca dan tulis dalam pemrosesan permintaan Wafris
- Wafris dipasang sebagai middleware Rails, lalu setelah aturan seperti “blokir IP 1.2.3.4” diatur, permintaan masuk dibandingkan dengan aturan tersebut
- Alur pemrosesan yang disederhanakan terdiri dari dua tahap
- Membandingkan permintaan HTTP dengan aturan, jika cocok maka hasilnya 403, jika tidak 200
- Melaporkan hasil pemrosesan seperti diblokir, diizinkan, atau lolos
- Tahap pertama, yaitu pembacaan aturan, jauh lebih penting daripada penulisan laporan
- permintaan harus diproses secara berurutan
- jika filtering tidak bekerja, permintaan buruk bisa lolos
- evaluasi permintaan memengaruhi performa situs yang dirasakan pengguna
- penulisan laporan bisa diproses lebih lambat, secara batch, atau asinkron
Alasan memilih SQLite
- Bottleneck utama Wafris adalah I/O jaringan, dan kalimat dalam dokumentasi SQLite, “SQLite does not compete with client/server databases. SQLite competes with fopen()”, memengaruhi keputusan ini
- Berdasarkan asumsi bahwa menghilangkan network round-trip saja sudah bisa membuatnya lebih cepat daripada arsitektur berbasis Redis, mereka membandingkan SQLite dan Redis melalui benchmark
- Referensi yang digunakan adalah sebagai berikut
- Aaron Francis, “High Performance SQLite”: https://highperformancesqlite.com/
- Stephen Margheim, SQLite on Rails - the how and why of optimal performance
- Oldmoe: https://oldmoe.blog/
Cakupan benchmark dan keterbatasan yang disengaja
- Benchmark ini bukan perbandingan performa database secara umum, melainkan pengujian yang sengaja bias untuk hot path Wafris dan kueri terburuknya
- Kueri terburuk adalah pencarian pada struktur data “lexical decimal” yang memetakan rentang IP ke kategori
- Sebagai contoh sederhana, ada pemetaan IP → negara yang memeriksa apakah suatu alamat IP berada di antara dua alamat lalu mengembalikan negaranya
- Struktur ini berukuran ratusan ribu hingga jutaan baris, dan pada IPv6 setiap entri berukuran besar
- Pencarian rentang dihitung terlebih dahulu lalu ditulis ke kedua penyimpanan
- tabel SQLite
- sorted set di Redis
- Dalam kasus yang patologis, setiap permintaan HTTP masuk harus membandingkan IP permintaan dengan rentang berikut
- rentang izin kustom
- rentang blokir kustom
- rentang GeoIP
- rentang reputasi IP
- Jenis kueri ini dianggap cukup penting sehingga kueri atau fitur lain tidak dipindahkan; hanya satu jenis ini yang diuji
Metode pengujian dan hasilnya
- Pengujian dilakukan di MacBook Air M2 lokal menggunakan Redis yang dipasang via Homebrew dan DB SQLite lokal
- Protokolnya adalah sebagai berikut
- menggunakan dataset rentang yang ada dengan 1,2 juta entri
- menjalankan beberapa kumpulan IP ke SQLite dan Redis dalam urutan yang sama
- untuk setiap kelipatan, pengujian dijalankan 5 kali lalu diambil rata-ratanya
- Dalam kasus penggunaan spesifik Wafris, SQLite menunjukkan performa sekitar 3 kali lebih cepat daripada Redis lokal
- Hasil ini adalah angka sebelum mempertimbangkan latensi jaringan
- Pengujian ini sengaja mencerminkan konfigurasi sederhana dan kekurangan penggunaan nyata, sehingga sulit digeneralisasi sebagai perbandingan database yang universal
Perbedaan operasional yang tidak terlihat di grafik
- Bahkan jika performa SQLite dalam benchmark jauh lebih buruk daripada Redis, mereka menilai bahwa di lingkungan nyata SQLite bisa tetap lebih cepat karena latensi jaringan menuju Redis di data center atau region yang sama
- Walau server Redis disusun kuat dengan cluster atau sharding, batasan seperti bandwidth jaringan, jumlah koneksi, dan latensi antar-region tetap ada
- Karena SQLite berada secara lokal di setiap instance compute, dalam kasus penggunaan Wafris ini biaya horizontal scaling nyaris hilang
- Onboarding juga lebih sederhana di sisi SQLite
- pengguna bisa menambahkan gem ke aplikasi web mereka dan menjalankannya tanpa perlu tahu bahwa SQLite sedang digunakan
- Redis masih punya banyak ruang optimasi tambahan, tetapi Wafris bahkan kesulitan meyakinkan pengguna untuk secara konsisten mengubah pengaturan dasar seperti kebijakan eviction cache
Perubahan struktur yang diperlukan setelah beralih ke SQLite
- Alur pembaruan pada v1 berbasis Redis itu sederhana
- pengguna memperbarui aturan di Wafris Hub
- Wafris Hub memperbarui aturan di penyimpanan Redis milik pengguna
- Pada SQLite, Wafris Hub tidak bisa langsung “mendorong” database SQLite ke server web
- Beberapa penyedia SQLite as a service memungkinkan pendekatan serupa, tetapi karena pertimbangan biaya, performa, dan keamanan, itu tidak cocok untuk Wafris
- tiap pengguna harus mendeploy-nya sendiri
- port harus dibuka
- koneksi masuk harus diizinkan
- Alur pembaruan pada v2 berbasis SQLite adalah sebagai berikut
- pengguna memperbarui aturan di Wafris Hub
- klien memeriksa aturan yang diperbarui pada interval tertentu berdasarkan waktu atau jumlah permintaan
- jika aturan berubah, klien mengunduh database SQLite yang sepenuhnya baru
- Struktur ini sangat mengurangi tanggung jawab instalasi dan konfigurasi di sisi pengguna, dan jumlah instalasi klien v2 yang berhasil meningkat sekitar 3 kali lipat
Struktur SQLite dalam deployment terdistribusi
- Di lingkungan cloud seperti AWS, Heroku, dan Fly, ketika aplikasi Rails melakukan autoscaling, instance compute sering bertambah tetapi database tidak ikut bertambah
- Ketika permintaan meningkat dari 100 req/s menjadi 10.000 req/s, dyno, machine, atau instance EC2 akan naik, tetapi bottleneck database bisa tetap ada
- Wafris mengamati bahwa penyebab utama gangguan aplikasi Rails sering kali bukan DDoS sungguhan, melainkan credential stuffing atau traffic bot jahat
- traffic seperti ini memicu autoscaling
- setelah itu koneksi database bisa habis dan aplikasi dapat tumbang
- Dengan menyinkronkan DB SQLite ke setiap instance compute, semua panggilan pada instance baru bisa tetap diproses secara lokal
Jalur tulis dihapus dari klien
- Pengujian sebelumnya tidak mempertimbangkan operasi tulis, dan mereka juga tidak menganggap SQLite cocok untuk semua peran
- Wafris menggunakan SQLite untuk peran yang berfokus pada baca, lalu merancang ulang jalur tulis secara terpisah
- Jalur pelaporan pada v2 berubah menjadi seperti berikut
- melapor melalui koneksi asinkron ke Wafris Hub
- mengirim data laporan secara batch
- sepenuhnya menghapus penulisan database dari klien
- Struktur ini mungkin tidak cocok untuk sebagian besar layanan lain, tetapi sesuai dengan tujuan Wafris untuk memberi pengguna deployment yang mudah dan klien yang cepat
- Arsitektur v2 berbasis SQLite ini sudah membantu banyak situs bertahan dari serangan dan tetap online, sekaligus mengurangi beban dukungan Wafris dan ketidaknyamanan pengguna
1 komentar
Komentar Hacker News
Model di mana setiap server aplikasi memiliki salinan file database SQLite dan menggantinya secara utuh secara berkala benar-benar menarik
Di sini digunakan untuk aturan web application firewall, tetapi sepertinya juga cocok untuk konfigurasi feature flag. Feature flag bisa diperiksa puluhan kali per request, dan sering kali perlu query seperti “pengguna termasuk grup A dan IP berada di negara B”, yang dapat ditangani dengan baik oleh SQLite lokal. Untuk feature flag, keterlambatan propagasi update beberapa detik, atau bahkan lebih, umumnya masih bisa diterima
Namun, saya belum melihat SQLite digunakan untuk tujuan ini; biasanya yang dipakai adalah database bergaya DBM seperti LMDB atau Kyoto Cabinet
Awalnya, job Cron di tiap server mengambil data untuk layanan terkait, dan setelah pindah ke Kubernetes, mereka menggunakan DaemonSet serta pendekatan mirip host tagging/taints untuk mengunduh berbagai data ke tiap host, lalu memastikan layanan yang menggunakan data tersebut hanya berjalan di host itu. Di Ruby, ini disebut “hammerspace”: https://github.com/airbnb/hammerspace
Dengan begitu, satu request tidak perlu membayar biaya lookup mahal lebih dari sekali, dan tidak ada risiko nilai menjadi tidak konsisten karena flag diperbarui di tengah pemrosesan request
Di atasnya bisa dipasang berbagai mekanisme distribusi, seperti jaringan gossip atau jaringan cache/distribusi berbentuk pohon yang eksplisit. Siapa pun yang memiliki versi lebih baru bisa memberikan update
Versi DB saat ini bisa disertakan dalam artefak aplikasi, tetapi setelah aplikasi berjalan bisa diperbarui secara efisien. Untuk feature flag atau live configuration, fallback bisa memakai status terbaru alih-alih default di level kode
Klien mana pun bisa mengirim counter global seperti ETAG dan hanya menerima delta, sehingga reconnect setelah jaringan terputus sementara juga menjadi sederhana dan murah
Jika bisa menyimpan riwayat selama beberapa menit, counter dapat disertakan dalam panggilan microservice dengan cara seperti header W3C Baggage, sehingga beberapa layanan dapat mengevaluasi flag/konfigurasi/data berdasarkan nomor versi yang sama. Bahkan tanpa fitur evaluasi berbasis waktu, mencatat generation number di log sangat membantu debugging setelah kejadian
Selain latensi jaringan, di Redis saya melihat latensi baca/tulis cenderung hampir linear terhadap jumlah key yang di-query, dan grafik di artikel juga tampak serupa
Saat menggunakan Postgres dan Redis berdampingan sesuai kebutuhan di aplikasi monolitik, hasilnya cukup baik, tetapi terlalu mudah memasukkan fitur baru ke cluster Redis bersama. Redis bersifat single-threaded, sehingga satu fitur yang tidak berhati-hati dan melakukan bulk lookup atas 100 ribu key lebih dapat membuat fitur lain ikut lambat
Karena itu, saya mengusulkan guideline bahwa Redis paling baik ketika membaca dan menulis satu key atau sejumlah kecil key yang tetap dalam sekali operasi, misalnya untuk lock atau rate limiting pada endpoint populer, terutama saat banyak fitur acak memakai Redis
Dalam kasus ini, Redis akan unggul untuk lookup satu key sederhana (alamat IP), tetapi tampaknya kurang cocok untuk pembacaan yang lebih kompleks seperti ekspresi query rentang. Saya tidak terlalu memahami secara mendalam mengapa SQLite bekerja sebaik ini dibanding Redis lokal, jadi hasilnya tak terduga dan menarik
Redis paling tepat dilihat sebagai cache dengan operasi dasar yang lebih kaya. Jika digunakan dengan tepat, ia cepat dan andal
Namun, ketika mulai memasukkan hal-hal yang tidak cocok dengan RDBMS utama ke Redis, tak lama kemudian muncul job queue, berbagai jenis lock, dan sebagainya; pada akhirnya tinggal menunggu waktu sebelum performanya melewati jurang atau runtuh karena alasan lain. Proses pemulihannya biasanya berantakan, dan sering berakhir dengan menerima sebagian kehilangan data
Pola seperti ini mudah terjadi sedikit demi sedikit, sehingga perlu disiplin untuk menghindarinya. Performa SQLite bukan hanya karena menghindari overhead jaringan; faktor besar lainnya adalah banyak orang meremehkan biaya serialisasi/deserialisasi. Meski protokol Redis cukup minimalis, biayanya tetap menumpuk, sementara di SQLite banyak pekerjaan berujung pada penyalinan memori di dalam proses
Beberapa minggu lalu, dalam hackathon internal Neon, kami membuat server Node.js kecil yang mengubah protokol wire Redis, RESP, menjadi query Postgres
Itu proyek hacking yang sangat menyenangkan: https://github.com/btholt/redis-to-postgres
Ini terdengar seperti use case khusus yang cukup cocok untuk SQLite di sisi server yang read-only, sehingga tidak memerlukan replikasi
Alternatif lain mungkin memuat file statis ke memori, tetapi dalam kasus ini datanya tampaknya terlalu banyak untuk terus disimpan di memori, sehingga SQLite menjadi alternatif yang baik
Bedanya, SQLite sangat kokoh, berperforma baik, dan diuji dengan sangat baik secara luar biasa
Saya penasaran dengan bagian di RailsWorld 2023 yang menyebut ada suasana “mencium bau darah” terhadap Redis
Dalam karier saya, saya hanya pernah menangani satu aplikasi Rails produksi, dan aplikasi itu memakai Redis, jadi saya tidak terlalu tahu tren saat ini. Saya penasaran apakah ekosistem sedang berpaling dari Redis dari sudut pandang bisnis, apakah karena perubahan lisensi, atau apakah ini lebih dekat ke situasi YAGNI
Saat itu, bersama Rescue, Redis terutama dipakai untuk menjadwalkan pekerjaan asinkron seperti pembuatan indeks dan transcoding, dan waktu itu terlihat seperti alat yang cukup bagus
Dalam survei komunitas Rails 2024 pun, Redis masih muncul sebagai penyimpanan data yang paling banyak digunakan di aplikasi
Namun meski banyak aplikasi memakai Redis, dalam praktiknya sering kali hanya dipakai untuk hal seperti Sidekiq, bukan untuk leaderboard real-time atau fitur vector DB, sehingga pola penggunaan sebenarnya agak kabur
Saat ini, untuk konfigurasi Rails dengan trafik tertentu, biasanya ada server frontend, SQL DB, penyimpanan key-value (Redis atau Memcached), lalu penyimpanan cache yang menunjuk ke penyimpanan key-value itu. Namun penggunaan key-value umum untuk mempertahankan kuota API atau rate limiting dan pola penggunaan cache cukup berbeda, sehingga merepotkan
Karena performa disk sudah cukup cepat dan performa SQL juga membaik, ada gerakan untuk menghapus penyimpanan key-value, lalu membagi penggunaan key-value tradisional ke SQL DB dan backend cache ke disk. Disk NVMe baru hampir mendekati RAM dalam hal kecepatan tetapi jauh lebih murah, sehingga bisa melakukan caching lebih banyak
Namun di era Kubernetes dan Redis-as-a-service, “memori bersama” itu berada di VM lain di rak lain. Pada titik itu, efisiensi sumber daya sudah bisa dibilang dikorbankan, jadi menurut saya lebih baik sekalian membaca dan menulis file dari S3
Mengenai bagian “Redis lebih cepat daripada RDBMS tradisional, tetapi tetap merupakan database yang harus mengelola koneksi, memori, proses, dan sebagainya, sehingga membuat stack lebih rapuh”, menurut saya begitu mulai menangani transaksi di atas level mainan, semua database, baik relasional maupun nonrelasional, kira-kira memerlukan tingkat pengelolaan dan pemeliharaan yang serupa
Bagian “cepat” juga agak menarik. Kalau tidak perlu memikirkan join, penyisipan dan pembacaan baris pun cukup cepat
Kalau memahami bahwa ia membaca langsung dari disk lokal, pembicaraan tentang join atau transaksi menjadi tidak terlalu berarti. Jalur itu sendiri sudah beberapa orde magnitudo lebih cepat
Setiap kali membaca dokumentasi tentang kapan harus menjalankan VACUUM, saya selalu bingung. Terakhir kali saya men-deploy aplikasi berbasis SQLite, saya menanganinya dengan menaruh counter dan menjalankan VACUUM setelah banyak operasi tulis
Jika database harus menjalankan proses server independen, pasti ada perbedaan dalam upaya pengelolaan dan pemeliharaan. Secara ekstrem, sulit dipercaya bahwa memakai SQLite dan Oracle benar-benar tidak ada bedanya
Anda mungkin tertarik dengan Redka, implementasi ulang Redis di Go dengan SQLite: https://github.com/nalgeon/redka
Memang proyek yang keren, tetapi dengan tingkat seperti ini hampir tidak ada insentif untuk beralih
Saya ingin memastikan apakah cara penggunaan Redis ini benar
Di v1, apakah strukturnya seperti ini: WAF dan Redis berada di server yang sama; ketika klien menetapkan aturan baru di panel admin, aturan itu masuk ke Redis di server yang sama dengan panel admin; lalu berkat mekanisme sinkronisasi internal Redis, aturan diperbarui ke Redis lokal di sebelah WAF di seluruh dunia? Setelah itu, ketika permintaan baru masuk ke suatu WAF, apakah alurnya WAF memvalidasi permintaan/IP dengan aturan Redis yang sudah diperbarui?
Di v2, apakah strukturnya seperti ini: cluster Redis dihapus; semua server WAF memiliki DB SQLite; dibuat mekanisme sinkronisasi terpisah yang menyebarkan aturan baru dari panel admin ke tiap server WAF+SQLite; lalu ketika permintaan baru datang, WAF melakukan validasi cepat dengan aturan SQLite yang sudah diperbarui?
Kalimat terbaiknya adalah “SQLite tidak bersaing dengan database klien/server. SQLite bersaing dengan fopen()”