- Giải thích cách xây dựng kiến trúc sử dụng cơ sở dữ liệu riêng cho từng tenant trong Rails và những thách thức trong quá trình triển khai
- ActiveRecord về cơ bản được thiết kế với giả định chỉ có một kết nối DB, nên việc chuyển đổi kết nối theo tenant khá phức tạp và khó xử lý
- Đề xuất cách dùng tính năng connected_to từ Rails 6 trở lên để chuyển đổi kết nối động khi runtime
- SQLite3 phù hợp để xử lý nhiều DB độc lập, quy mô nhỏ, nên việc sao lưu, gỡ lỗi, xóa đều thuận tiện
- Nhấn mạnh rằng khác với hạ tầng Rails phát triển theo hướng tối ưu cho các hệ thống quy mô lớn, vẫn có thể xây dựng kiến trúc xoay quanh các cơ sở dữ liệu nhỏ và độc lập
Vì sao dùng cơ sở dữ liệu riêng cho từng tenant
- Nếu tách theo đơn vị tenant (
Site) vận hành độc lập trong mô hình dữ liệu, việc cô lập và quản lý dữ liệu sẽ dễ hơn
- Lưu dữ liệu của từng tenant vào DB riêng cũng có lợi cho việc mở rộng các site lớn hoặc xử lý vấn đề bảo mật
- Dùng SQLite cho phép vận hành cơ sở dữ liệu chỉ với một tệp, không cần cấu hình máy chủ, nên đơn giản và linh hoạt
Những điểm khó trong Rails
- Thao tác
open/close cơ bản của SQLite rất đơn giản, nhưng ActiveRecord lại có cấu trúc quản lý kết nối nội bộ khá phức tạp
- ActiveRecord được thiết kế theo kiểu gắn kết nối cố định với model, nên việc chuyển tenant khi runtime gặp nhiều khó khăn
- Connection pool, query cache, schema cache... đều phụ thuộc vào kết nối, nên việc đổi kết nối liên tục tạo thêm gánh nặng
Lịch sử quản lý nhiều cơ sở dữ liệu trong Rails
- Rails 1: có thể chỉ định DB ở cấp
ActiveRecord::Base
- Rails 3: giới thiệu connection pool
- Rails 4: thêm
connection_handling
- Rails 6: giới thiệu
connected_to
- Rails 7: mở rộng
connected_to và hỗ trợ sharding
- Nhưng các kịch bản như "thêm/xóa tenant động khi runtime" vẫn chưa được hỗ trợ mặc định
Ưu điểm của cơ sở dữ liệu theo từng tenant
- Có thể chỉ sao lưu hoặc khôi phục tệp của từng tenant, nên vận hành và gỡ lỗi trở nên đơn giản
- Việc xóa tenant chỉ cần xóa tệp (
unlink)
- Máy chủ cơ sở dữ liệu cỡ lớn tối ưu cho DB hàng chục terabyte, còn SQLite lại tối ưu cho hàng nghìn DB nhỏ
- Trên thực tế, iCloud cũng áp dụng cấu trúc lưu trữ hàng triệu DB SQLite nhỏ trên Cassandra
Quá trình giải quyết vấn đề
- Cách cũ (dùng
establish_connection thủ công) gây ra lỗi ConnectionNotEstablished trong môi trường nhiều kết nối đồng thời
- Theo cách làm từ Rails 6 trở đi, thay vì tự quản lý connection pool, cấu trúc được chuyển sang để Rails tự xử lý
- Tạo connection pool động cho từng tenant, rồi bọc tác vụ trong khối
connected_to
- Cải tiến bằng cách dùng middleware để chuẩn bị và giải phóng kết nối DB cần thiết một cách động tại thời điểm request
Mẫu mã nguồn cốt lõi
- Kiểm tra connection pool, nếu chưa có thì tạo mới
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
- Sau khi kết nối, thực thi truy vấn an toàn bên trong khối
connected_to
ActiveRecord::Base.connected_to(role: role_name) do
pages = Page.order(created_at: :desc).limit(10)
end
Xử lý Rack streaming
- Khi phản hồi Rack ở dạng streaming, để quản lý kết nối, bài viết dùng
Rack::BodyProxy và Fiber để đóng kết nối an toàn
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]
Cấu trúc middleware cuối cùng
- Viết middleware
Shardine::Middleware để với mỗi request, tìm kết nối DB phù hợp, chuyển bằng connected_to, rồi dọn dẹp sau khi phản hồi kết thúc
- Có thể áp dụng trong tệp
config.ru của dự án Rails như sau
use Shardine::Middleware do |env|
site_name = env["SERVER_NAME"]
{adapter: "sqlite3", database: "sites/#{site_name}.sqlite3"}
end
Những việc còn lại
- Trong ActiveRecord 6, bài viết chưa dùng tính năng
shard, nhưng ở các phiên bản sau thì cũng có thể tách riêng đọc/ghi
- Chức năng dọn dẹp connection pool khi xóa tenant hiện chưa cần thiết nên chưa được triển khai
- Trong tương lai, kiến trúc xử lý "nhiều cơ sở dữ liệu nhỏ" có thể sẽ nhận được nhiều sự chú ý hơn
1 bình luận
Ý kiến trên Hacker News
Đang sử dụng mô hình "database-per-tenant" với khoảng 1 triệu người dùng
Thích SQLite, nhưng tự hỏi liệu các cơ sở dữ liệu OLTP truyền thống có cần phải unload một phần index khỏi bộ nhớ hay không
Hầu hết mọi người không cần cơ sở dữ liệu theo tenant, và đây không phải cách làm phổ biến
Có thể cân nhắc một hướng tiếp cận trung gian như sau
Tình cờ là đang làm FeebDB cho Elixir
Forward Email làm điều tương tự bằng cách dùng sqlite db được mã hóa cho từng mailbox/người dùng
Cái tên này rất tuyệt. Gợi nhớ đến Sean Connery
Workflow "database per tenant" giờ mới chỉ bắt đầu
Đã từng dùng một thứ tương tự trước đây và rất hài lòng
rm username.sqlKhi dữ liệu được cô lập với nhau và không có vấn đề mở rộng trong phạm vi một tenant đơn lẻ, thì rất khó để thiết kế sai