- Menjelaskan cara membangun arsitektur yang menggunakan database terpisah untuk setiap tenant di Rails serta tantangan yang dihadapi
- ActiveRecord pada dasarnya dirancang dengan asumsi satu koneksi DB, sehingga perpindahan koneksi per tenant menjadi rumit dan sulit
- Mengusulkan cara untuk mengganti koneksi secara dinamis saat runtime dengan memanfaatkan fitur connected_to di Rails 6 ke atas
- SQLite3 cocok untuk menangani banyak DB kecil yang saling independen, sehingga memudahkan backup, debugging, dan penghapusan
- Menekankan bahwa, berbeda dari infrastruktur Rails yang berkembang dengan fokus pada optimasi sistem besar, arsitektur berbasis database kecil dan independen juga memungkinkan
Alasan menggunakan database terpisah untuk setiap tenant
- Jika dipisahkan berdasarkan unit tenant (
Site) yang bekerja secara independen di dalam model data, isolasi dan pengelolaan data menjadi lebih mudah
- Jika data disimpan di DB terpisah per tenant, ini lebih menguntungkan untuk skalabilitas situs besar maupun isu keamanan
- Dengan memanfaatkan SQLite, database dapat dijalankan hanya dengan satu file tanpa konfigurasi server, sehingga sederhana dan fleksibel
Hal yang sulit di Rails
- Operasi dasar
open/close pada SQLite sangat sederhana, tetapi ActiveRecord secara internal memiliki struktur pengelolaan koneksi yang kompleks
- ActiveRecord dirancang dengan struktur yang mengikat koneksi ke model, sehingga sulit melakukan pergantian tenant saat runtime
- Connection pool, query cache, dan schema cache semuanya bergantung pada koneksi, sehingga mengganti koneksi setiap saat terasa membebani
Sejarah pengelolaan multi-database di Rails
- Rails 1: dapat menentukan DB per
ActiveRecord::Base
- Rails 3: memperkenalkan connection pool
- Rails 4: menambahkan
connection_handling
- Rails 6: memperkenalkan
connected_to
- Rails 7: memperluas fungsi
connected_to dan menambahkan dukungan sharding
- Namun, skenario seperti "menambah/menghapus tenant secara dinamis saat runtime" masih belum didukung secara bawaan
Kelebihan database per tenant
- Hanya file per tenant yang perlu di-backup atau dipulihkan, sehingga operasional dan debugging menjadi sederhana
- Menghapus tenant dapat dilakukan hanya dengan menghapus file (
unlink)
- Server database besar dioptimalkan untuk DB berskala puluhan terabyte, sementara SQLite dioptimalkan untuk ribuan DB kecil
- Bahkan iCloud juga mengadopsi arsitektur yang menyimpan jutaan DB SQLite kecil di atas Cassandra
Proses pemecahan masalah
- Cara lama (
establish_connection manual) memicu error ConnectionNotEstablished di lingkungan dengan banyak koneksi
- Disesuaikan dengan pendekatan setelah Rails 6, struktur diubah agar Rails yang menangani alih-alih mengelola connection pool secara manual
- Membuat connection pool secara dinamis untuk setiap tenant, lalu membungkus pekerjaan dengan blok
connected_to
- Ditingkatkan dengan pendekatan yang menggunakan middleware untuk menyiapkan dan melepas koneksi DB yang diperlukan secara dinamis saat request diproses
Pola kode inti
- Periksa connection pool, lalu buat jika belum ada
MUX.synchronize do
if ActiveRecord::Base.connection_handler.connection_pool_list(role_name).none?
ActiveRecord::Base.connection_handler.establish_connection(database_config_hash, role: role_name)
end
end
- Setelah terhubung, jalankan query dengan aman di dalam blok
connected_to
ActiveRecord::Base.connected_to(role: role_name) do
pages = Page.order(created_at: :desc).limit(10)
end
Penanganan streaming Rack
- Jika respons Rack berupa streaming, koneksi ditutup dengan aman menggunakan
Rack::BodyProxy dan Fiber untuk mengelola koneksi
connected_to_context_fiber = Fiber.new do
ActiveRecord::Base.connected_to(role: role_name) do
Fiber.yield
end
end
connected_to_context_fiber.resume
status, headers, body = @app.call(env)
body_with_close = Rack::BodyProxy.new(body) { connected_to_context_fiber.resume }
[status, headers, body_with_close]
Struktur middleware akhir
- Menulis middleware
Shardine::Middleware yang pada setiap request mencari koneksi DB yang sesuai, beralih dengan connected_to, lalu membersihkan setelah respons selesai
- Dapat diterapkan di file
config.ru proyek Rails seperti berikut
use Shardine::Middleware do |env|
site_name = env["SERVER_NAME"]
{adapter: "sqlite3", database: "sites/#{site_name}.sqlite3"}
end
Tugas yang masih tersisa
- Di ActiveRecord 6, fitur
shard belum dimanfaatkan, tetapi pada versi berikutnya pemisahan baca/tulis juga dimungkinkan
- Fungsi pembersihan connection pool saat tenant dihapus belum diimplementasikan karena belum diperlukan
- Ke depan, arsitektur yang menangani "banyak database kecil" kemungkinan akan semakin mendapat perhatian
1 komentar
Komentar Hacker News
Menggunakan pendekatan "database-per-tenant" dengan sekitar 1 juta pengguna
Menyukai SQLite, tetapi bertanya-tanya apakah database OLTP tradisional perlu meng-unload sebagian indeks dari memori
Kebanyakan orang tidak memerlukan database per tenant, dan ini bukan pendekatan umum
Sebagai pendekatan menengah, hal berikut bisa dipertimbangkan
Kebetulan sedang mengerjakan FeebDB untuk Elixir
Forward Email melakukan hal serupa dengan menggunakan sqlite db terenkripsi untuk setiap mailbox/pengguna
Namanya sangat bagus. Mengingatkan pada Sean Connery
Workflow "database per tenant" sekarang baru mulai
Pernah menggunakan sesuatu yang mirip di masa lalu, dan sangat puas
rm username.sqlSulit membuat desain yang salah ketika data saling terisolasi dan tidak ada masalah penskalaan dalam satu tenant