1 poin oleh GN⁺ 2024-09-26 | 1 komentar | Bagikan ke WhatsApp
  • 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
    1. Membandingkan permintaan HTTP dengan aturan, jika cocok maka hasilnya 403, jika tidak 200
    2. 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

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
  • Dalam kasus yang patologis, setiap permintaan HTTP masuk harus membandingkan IP permintaan dengan rentang berikut
    1. rentang izin kustom
    2. rentang blokir kustom
    3. rentang GeoIP
    4. 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
    1. menggunakan dataset rentang yang ada dengan 1,2 juta entri
    2. menjalankan beberapa kumpulan IP ke SQLite dan Redis dalam urutan yang sama
    3. 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
    1. pengguna memperbarui aturan di Wafris Hub
    2. 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
    1. pengguna memperbarui aturan di Wafris Hub
    2. klien memeriksa aturan yang diperbarui pada interval tertentu berdasarkan waktu atau jumlah permintaan
    3. 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

 
GN⁺ 2024-09-26
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

    • Pendekatan ini juga merupakan model yang digunakan di CDN. File konfigurasi global yang memuat sertifikat semua pelanggan, aturan routing HTTP, dan sebagainya diperbarui sebagai struktur B-tree satu file, lalu “bundle” itu sering didistribusikan ke semua lokasi edge
      Namun, saya belum melihat SQLite digunakan untuk tujuan ini; biasanya yang dipakai adalah database bergaya DBM seperti LMDB atau Kyoto Cabinet
    • Airbnb juga mendistribusikan terjemahan, feature flag, konfigurasi, indeks pencarian, dan lainnya dengan model ini. Alih-alih SQLite, mereka menggunakan Sparkey, format file key-value buatan Spotify
      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
    • Masalah membaca nilai feature flag berkali-kali per request diselesaikan dengan mengambil nilai flag sekali saja lalu menyimpannya di objek request
      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
    • Setelah lama menangani feature flag selama migrasi panjang, saya makin yakin bahwa primitif dasar yang benar-benar berguna adalah “seperti SQLite, tetapi setiap perubahan menaikkan counter versi global, dan perubahan di antara nomor versi bisa diambil atau diterapkan secara terpisah”
      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
    • SQLite untuk distribusi itu keren. Ide ini tampaknya sedikit banyak terinspirasi oleh proyek Datasette, dan mungkin nantinya masih bisa dimanfaatkan untuk reporting dan eksplorasi data
  • 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 sering menjadi jebakan karena orang mudah salah memahami kelebihan dan kekurangannya
      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

    • Semoga tersampaikan dalam artikelnya, tetapi ini adalah solusi hebat yang cocok untuk use case tertentu, bukan pengganti 1:1 untuk Redis atau Postgres
    • Jika memulai dari pendekatan memuat file statis ke memori lalu menambahkan cukup banyak fitur yang diperlukan agar bekerja untuk use case umum, pada akhirnya itu menjadi database relasional seperti SQLite
      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

    • Sedikit lebih dekat ke YAGNI. Alasan terbesar Redis banyak dipakai di komunitas Rails adalah pekerjaan asinkron, dan alat yang paling banyak diambil orang adalah Sidekiq
      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
    • Menurut saya ini murni soal kesederhanaan
      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
    • Dulu Redis berguna seperti memori bersama yang sangat pintar dengan fitur bawaan seperti kedaluwarsa. Dengan clustering, berbagi antar-mesin juga memungkinkan
      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
    • Karena SSD sekarang cepat, lebih banyak orang seharusnya memakai RocksDB. Penyimpanan key-value yang jauh lebih besar bisa dijalankan di atas SSD
  • 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

    • Alasan SQLite sangat cepat di lingkungan server adalah karena tidak memerlukan panggilan jaringan saat melakukan kueri atau mengambil data
      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
    • SQLite memiliki operasi VACUUM yang terlihat seperti semacam garbage collection
      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
    • Saya tidak setuju dengan pernyataan bahwa “semua database memerlukan tingkat pengelolaan dan pemeliharaan yang kira-kira sama”
      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

    • Cukup menarik untuk dipertimbangkan sebagai opsi migrasi, tetapi saya jadi ragu setelah melihat bagian “menurut benchmark, Redka beberapa kali lebih lambat daripada Redis”
      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()