Bluesky chuyển sang SQLite đơn tenant
(github.com/bluesky-social)- PR refactor PDS #1705 của Bluesky atproto thay đổi PDS sang sử dụng kho dữ liệu SQLite đơn tenant, đồng thời lưu repo của từng người dùng và trạng thái tài khoản riêng tư trong các tệp SQLite riêng của họ
- DB người dùng được lưu theo cấu trúc đường dẫn
/${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}, và khóa ký của mỗi repo được lưu ngay cạnh tệp SQLite tương ứng - Lớp trừu tượng truy cập dữ liệu người dùng hiện tại được thay bằng ActorStore; do SQLite không hỗ trợ transaction đồng thời, các thao tác ghi phải liên kết tường minh với store và transaction
- Các file handle DB đang mở và khóa ký được quản lý bằng LRUCache; tối đa giữ 30k file handle đang mở và 30k khóa trong bộ nhớ, và khi DB bị đẩy khỏi cache thì file handle sẽ được đóng
- Ba DB SQLite riêng được đưa vào để quản lý trạng thái dịch vụ và chạy ở chế độ WAL nhằm cho phép đọc đồng thời và sao chép streaming; bản phân phối PDS dự kiến sẽ kèm Litestream hoặc công cụ tương tự
Các thay đổi chính của PR
- PR #1705 refactor PDS dựa trên kho dữ liệu SQLite đơn tenant
- Mỗi người dùng có một tệp SQLite chuyên dụng của riêng mình, trong đó lưu repo và trạng thái tài khoản riêng tư của người dùng đó
- DB người dùng được lưu theo đường dẫn phân cấp sử dụng hash của DID
- Định dạng đường dẫn:
/${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}
- Định dạng đường dẫn:
- repo signing key của mỗi repo được lưu cùng vị trí với tệp SQLite
ActorStore và mô hình transaction
- Lớp trừu tượng truy cập dữ liệu người dùng được đổi từ “services” hiện có sang ActorStore
- Điểm khác biệt chính của ActorStore là tách riêng các lớp cho đọc và ghi
- Vì SQLite không hỗ trợ transaction đồng thời, để thực hiện thao tác ghi cần phải liên kết rõ ràng với store và transaction
- Log commit bao gồm việc làm lại reader và transactor, xử lý race transaction của actor store, dọn dẹp interface của store, v.v.
Quản lý cache và file handle
- LRUCache được duy trì cho khóa ký và cơ sở dữ liệu
- Các giới hạn được cấu hình như sau
- Tối đa 30k file handle đang mở
- Tối đa 30k khóa được giữ trong bộ nhớ
- Khi cơ sở dữ liệu bị đẩy khỏi cache thì file handle sẽ được đóng
- Các commit liên quan bao gồm
actor store in lru cache,fix open handles
Ba DB SQLite cho trạng thái dịch vụ
- Ngoài DB theo từng người dùng, còn có thêm 3 cơ sở dữ liệu SQLite riêng để quản lý trạng thái dịch vụ
- service DB: quản lý thông tin tài khoản, mã mời, refresh token, v.v.
- did cache DB: chỉ gồm một bảng duy nhất để cache DID resolution
- sequencer DB: chỉ gồm một bảng duy nhất để quản lý thứ tự cập nhật repo của toàn bộ một dịch vụ
- Mỗi tệp SQLite chạy ở WAL mode
- Mục đích của WAL mode là cho phép đọc đồng thời và sao chép streaming
- Bản phân phối PDS dự kiến sẽ bao gồm Litestream hoặc công cụ tương tự
Trạng thái review và hợp nhất
- PR này gồm tổng cộng 143 commits và được hợp nhất từ nhánh
pds-sqlite-refactorvào nhánhpds-v2 - Ngày hợp nhất là 1 tháng 11, 2023, và merge commit là
8449ceb - Reviewer devinivy đã để lại nhiều ghi chú và bình luận trước khi phê duyệt thay đổi
- devinivy đánh giá rằng bản refactor có “nhiều sự đơn giản hóa tuyệt vời” và nhìn chung cho cảm giác gọn gàng hơn
- Sau khi hợp nhất, nhánh
pds-sqlite-refactorđã bị xóa
Câu hỏi sau đó
- Vào ngày 28 tháng 2, 2025, npetrangelo xem xét quy mô thay đổi của PR này và yêu cầu tóm tắt các trade-off giữa kiến trúc Postgres trước đây và kiến trúc SQLite được đưa vào bởi PR này
- Nội dung được cung cấp không bao gồm câu trả lời từ phía Bluesky cho câu hỏi đó
1 bình luận
Ý kiến trên Hacker News
Tôi thích SQLite, nhưng cách tách riêng schema hoặc database cho từng tenant nhìn chung có rất nhiều khó khăn
Nếu dùng bảo mật cấp hàng (RLS) trên một instance dùng chung, dù migration thất bại vẫn có thể rollback toàn bộ; nhưng với schema theo từng tenant, nếu migration dữ liệu thất bại vì dữ liệu không lường trước, người dùng sẽ bị kẹt ở các phiên bản schema khác nhau cho đến khi tìm ra nguyên nhân
Khi đạt đến quy mô sharding thì dù sao chuyện tương tự cũng có thể xảy ra, nhưng trước thời điểm đó thì một database duy nhất là dễ nhất, và sau này cũng có thể cần hợp nhất dữ liệu hoặc chuyển quyền sở hữu tài nguyên một cách nguyên tử
Tôi không phản đối cấu hình này, nó vẫn có chỗ dùng, nhưng ở công ty chúng tôi đang cố hết sức rời khỏi schema theo từng tenant. Nếu không đầu tư đúng mức thì có quá nhiều vấn đề, và tôi nghĩ hiếm khi người ta đã sẵn sàng cho điều đó ngay lúc nảy ra ý tưởng ban đầu
Điều thú vị là khoảng 10 năm trước ứng dụng bắt đầu với SQLite theo từng tenant, rồi chuyển sang schema theo từng tenant trên PostgreSQL, và giờ đang chuyển sang một schema duy nhất có RLS, tức là đi theo hướng hoàn toàn ngược lại
Với tư cách người từng xử lý database khổng lồ trong production, tôi không muốn làm lại lần nữa
Khi tải đủ lớn, mọi thay đổi đều trở nên rủi ro, vì không thể kiểm thử trọn vẹn mọi trường hợp cực đoan về hiệu năng
Người dùng gói miễn phí tìm ra một đường code không có index rồi làm hỏng production cũng là một mẫu rất thường gặp
Việc một số người dùng bị kẹt ở phiên bản schema khác do migration dữ liệu thất bại có thể không phải vấn đề lớn
Nếu dịch vụ lớn và phức tạp đến mức đó, thường schema upgrade sẽ được làm theo giai đoạn: 1. làm cho code tương thích với schema tương lai, 2. migrate dữ liệu, 3. gỡ hỗ trợ schema cũ
Vì vậy thông thường hệ thống phải an toàn khi chạy lâu trong trạng thái giữa bước 1 và bước 2. Tất nhiên bug mới là ngoại lệ, nhưng từ góc độ vận hành, miễn là dùng quy trình này thì tôi vẫn thấy ổn với một hệ thống có thể quay về trạng thái trung gian của migration
Nếu sản phẩm có dưới 100 khách hàng, việc mỗi người dùng ở một phiên bản schema khác nhau có khi lại tốt
Mỗi khách hàng có thể có lịch và yêu cầu nâng cấp khác nhau, và tôi cũng biết những doanh nghiệp làm tuỳ chỉnh cho một số khách hàng đến mức về cơ bản không còn chạy cùng một code nữa
Rốt cuộc còn tuỳ vào cấu trúc kinh doanh
Công bằng mà nói, 10 năm trước RLS vẫn chưa có. Nó xuất hiện trong PostgreSQL 9.5 vào năm 2016
https://blog.turso.tech/introducing-embedded-replicas-deploy...
https://electric-sql.com/
Tôi không hiểu câu “SQLite không hỗ trợ transaction đồng thời” nghĩa là gì
Theo tôi biết thì nó có hỗ trợ, miễn là không truy cập file
.dbqua chia sẻ file như UNC hay NFS: https://www.sqlite.org/wal.htmlTôi đã dùng nó để nhiều thread/process trên cùng một máy đọc và cập nhật database; nếu cần một view nhất quán hoặc không muốn giữ transaction quá lâu thì cũng có thể tạo snapshot bằng sqlite backup API
Có thể tôi đã bỏ sót điều gì đó, và cũng đã vài năm tôi chưa đụng đến SQLite nên không chắc
Không phải vậy. Tôi đã nhầm. Thực tế gần với nhiều reader, một writer hơn
Có lẽ lâu nay tôi cứ giả định như vậy và không kiểm tra đủ kỹ. Dù vậy, phần lớn database tôi tạo bằng SQLite thiên về đọc hơn là ghi
Xin đính chính
Nếu chờ thêm, hctree [1] sẽ ổn định, và ta sẽ có thể chọn giữa cơ chế backend truyền thống và backend mới được triển khai có hỗ trợ concurrency
[1] https://sqlite.org/hctree/doc/hctree/doc/hctree/index.html
Theo tài liệu, writer chỉ nối thêm nội dung mới vào cuối file WAL nên đọc và ghi có thể diễn ra đồng thời, nhưng vì chỉ có một file WAL nên chỉ có một writer có thể ghi cùng lúc
Có vẻ ý bài gốc là các thao tác cập nhật phải được thực thi tuần tự
Nếu lưu lượng thấp thì vẫn hoạt động, nhưng khi transaction lớn hơn hoặc số lượt ghi đồng thời tăng lên, dù bật WAL thì đến một lúc nào đó cũng sẽ gặp lỗi database locked
Có thể né phần nào ở tầng ứng dụng, nhưng nhìn chung nếu đã chạm đến điểm đó thì nên nghiêm túc cân nhắc một backend database khác
Ít nhất theo lần cuối tôi kiểm tra, có khả năng ý là không có khóa cấp hàng, và khóa cấp bảng cũng rất hạn chế
Theo tài liệu, writer vẫn giữ khóa trên toàn bộ database
Thú vị, tôi thích chiến lược 1:1 giữa 1 người dùng và 1 database
Tuy nhiên tôi tò mò họ xử lý dữ liệu cần tổng hợp giữa các người dùng như thế nào. Nếu tôi đang subscribe một người khác và người đó đăng bài, database của tôi được cập nhật bài mới ra sao, hay đây là kiến trúc chỉ áp dụng cho dữ liệu bền vững như dữ liệu profile hay quan hệ follow, còn dữ liệu tương tác như feed thì được xử lý riêng?
Tôi cũng thích việc “connection pooling” chỉ đơn giản là giới hạn số handle đang mở bằng LRU cache. Việc mỗi kết nối DB là single-threaded nên xử lý concurrency ở cấp tenancy thay vì cấp connection cũng rất thú vị
Có vẻ cũng dễ đặt rate limit theo từng database lên trên kiến trúc này để ngăn lạm dụng từ một người dùng cụ thể
Tôi cũng tò mò liệu có cách đơn giản nào để cấu hình Litestream cho số lượng database tuỳ ý hay không
Luôn vui khi thấy việc dùng SQLite/Litestream trên server ngày càng tăng. Chúng tôi cũng đang dùng nó khi xây ứng dụng mới
SQLite + Litestream là lựa chọn tốt hơn cho database theo tenant, và chi phí replicate/backup lên S3/R2 rẻ hơn rất nhiều so với database quản lý trên cloud đắt đỏ [1]
Rẻ hơn tới 3900% so với SQLServer trên Azure
[1] https://docs.servicestack.net/ormlite/litestream
Không hiểu rẻ hơn 3900% nghĩa là gì
Ở công ty fintech trước đây, công ty lưu tài khoản khách hàng dưới dạng tệp sqlite3 được mã hóa trong blob storage, và nó khá phù hợp với mẫu truy cập
Nhìn bề ngoài thì trông như một sự kết hợp giữa tệ nhất và kinh khủng
Mong ai đó viết một bài hay giải thích ưu điểm bằng số liệu thực tế và phân tích các khiếm khuyết dự kiến. Nếu học cho đến nơi đến chốn thì đây có thể là một chủ đề thật sự thú vị
Nhìn bề ngoài, đặc biệt nếu giả định đang xây dựng một hệ thống phân tán để nhiều người dùng không phải quản trị viên hệ thống chuyên nghiệp chạy và triển khai, thì đây có vẻ là một lựa chọn khá hợp lý
Mục tiêu ở đây có lẽ cũng nên như vậy, và tôi kỳ vọng mục tiêu thiết kế là tránh nhu cầu phải thiết lập, cấu hình và quản lý thêm cơ sở dữ liệu hay máy chủ khác
Mong ai đó hiểu rõ Bluesky hơn giải thích dữ liệu nào được lưu trong SQLite và dữ liệu nào thì không
Tôi đang giả định những thứ như tin nhắn giữa người dùng với nhau thì không
Hãy hình dung như email. Nếu bạn gửi một email và CC cho năm người, bảy người sẽ mỗi người lưu một bản sao của cùng email đó trên máy chủ email riêng của mình
Tức là không có cấu trúc kiểu một cơ sở dữ liệu trung tâm chứa một email để những người khác tham chiếu đến
Sharding cơ sở dữ liệu quan hệ về cơ bản cũng hoạt động theo kiểu này
Việc phi chuẩn hóa dữ liệu như vậy gần như là bắt buộc khi ứng dụng mở rộng, đặc biệt trong các ứng dụng nhiều-nhiều có tỷ lệ đọc cao so với ghi
Nếu tỷ lệ đọc so với ghi thấp, cấu trúc cơ sở dữ liệu quan hệ một master và nhiều slave cũng có thể xử lý lượng yêu cầu và dữ liệu lớn đến đáng ngạc nhiên
Hiện tại Bluesky thực tế đang tự host PDS gần như duy nhất, nhưng mục tiêu cuối cùng là mọi người dùng cuối đều có PDS của riêng mình
Inrupt/SOLID gọi khái niệm này là “pod”
Trên thực tế, hôm qua họ đã onboarding PDS production thứ hai, nên cũng có tiến triển
Chỉ có các tin nhắn công khai được phát sóng ra toàn thế giới
Tôi chưa tìm hiểu riêng xem có kế hoạch cho tin nhắn trực tiếp hay không
Vì sao họ băm người dùng bằng sha256 rồi chia vào thư mục đích hai ký tự?
md5 nhanh hơn nhiều và chẳng phải giải quyết cùng một vấn đề sao?
Giá trị lớn hơn là không phải trả lời câu hỏi “vì sao lại dùng hàm băm không an toàn”, đồng thời loại bỏ hoặc ít nhất giảm thiểu khả năng xảy ra một lớp vấn đề bảo mật
Hoặc cũng có thể giống tôi, họ bị các công cụ bảo mật của công ty bủa vây nên không muốn phải tạo ngoại lệ riêng cho mỗi lần dùng md5
Nếu không cần hàm băm bảo mật thì có rất nhiều hàm băm phi mật mã nhanh
Bluesky vẫn còn theo chế độ mời à?
Đó là cách hạn chế tăng trưởng trong khi họ mở rộng hệ thống ở phía backend và chống lạm dụng
Có một hàng đợi riêng cho nhà phát triển, và bạn có thể nhận quyền truy cập khá nhanh: https://atproto.com/blog/call-for-developers