Discord: từ Cassandra sang ScyllaDB
Tung Bui Thanh (TungDaDev)10 mins read

Dữ liệu từ vài triệu lên vài nghìn tỷ bản ghi thì chọn database đúng hay sai không còn là chuyện kỹ thuật thuần tuý nữa. Chọn đúng thì dịch vụ chạy êm, số máy phải nuôi cũng ít đi trông thấy.
# Discord đã đổi chỗ lưu dữ liệu mấy lần
Discord ra mắt đầu năm 2015 với đúng một replica set MongoDB. Tới tháng 11/2015, khi chạm mốc 100 triệu tin nhắn, dữ liệu và index không còn nằm gọn trong RAM, latency bắt đầu khó đoán.
Discord chuyển sang Apache Cassandra, một database NoSQL kiểu wide-column chạy phân tán. Năm 2017 cụm Cassandra mới có 12 node, chứa vài tỷ tin nhắn. Đầu năm 2022 nó đã phình lên 177 node với hàng nghìn tỷ tin nhắn, và lúc này giới hạn của JVM cùng chuyện vận hành compaction bắt đầu lòi ra.
# mô hình dữ liệu tin nhắn và cách chia partition
Với Cassandra và ScyllaDB, chọn partition key thế nào quyết định luôn dữ liệu có rải đều trên cluster hay không. Bảng tin nhắn của Discord trông như sau:
CREATE TABLE messages (
channel_id bigint,
bucket int,
message_id bigint,
author_id bigint,
content text,
PRIMARY KEY ((channel_id, bucket), message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);- Partition Key
((channel_id, bucket)): quyết định dữ liệu nằm ở node nào trong cluster. - Clustering Key
(message_id): quyết định thứ tự lưu trên đĩa của các tin nhắn trong từng partition.message_idcủa Discord là Snowflake ID (có nhúng timestamp), nên tin mới nhất luôn nằm trên đầu, đọc tin gần đây rất nhanh.
# sao phải chia bucket?
Nếu chỉ lấy channel_id làm partition key, một kênh lớn có vài chục triệu tin nhắn sẽ phình thành một partition khổng lồ, cỡ vài GB. Cassandra khuyên giữ partition dưới khoảng 100MB, vì partition lớn gây áp lực GC lúc compaction, lúc mở rộng cluster, và không chia tải ra được nhiều node. Vì thế Discord cắt mỗi kênh thành nhiều bucket theo thời gian, mỗi bucket chứa khoảng 10 ngày tin nhắn, để partition nào cũng nằm dưới ngưỡng an toàn.
Cái giá phải trả: muốn đọc lịch sử dài thì ứng dụng phải tự tính các bucket liên quan rồi query lần lượt từng bucket. Kênh ít tin nhắn thì phải lội qua nhiều bucket rỗng mới đủ một trang.
# Cassandra gặp khó ở đâu khi lên tới nghìn tỷ bản ghi
Cassandra sinh ra để scale ngang, nhưng tới cỡ dữ liệu này thì nó lộ ra mấy vấn đề khó né. Theo bài viết của Discord, cái đau đầu nhất là hot partition: một kênh lớn có đông người đọc cùng lúc dồn tải vào đúng vài node giữ partition đó, latency node ấy tăng, rồi kéo theo cả các query khác đi qua node. Ngoài ra còn ba chuyện dưới đây.
# Stop-The-World (STW) của JVM Garbage Collection
Cassandra chạy trên JVM. Tải đọc ghi lớn thì lượng object sống ngắn sinh ra trên heap cũng lớn. Discord kể họ tốn rất nhiều thời gian tune garbage collector và cấu hình heap, nhưng GC pause vẫn gây những cú latency spike đáng kể, có lúc kéo dài tới mức phải tự tay khởi động lại node. Mỗi lần như vậy là P99/P99.9 nhảy vọt.
# tombstone quá nhiều
Khi người dùng xoá tin nhắn hoặc tin nhắn hết hạn, Cassandra không xoá ngay trên đĩa mà ghi một dấu đánh dấu gọi là tombstone. Tombstone chỉ được dọn sau khi qua gc_grace_seconds (mặc định 10 ngày) và có compaction chạy qua. Câu SELECT nào quét qua khoảng thời gian có nhiều tin đã xoá thì node phải lội qua hàng chục nghìn tombstone mới tới được dữ liệu còn sống. Vượt ngưỡng tombstone_failure_threshold (mặc định 100.000) thì query bị huỷ luôn, log báo đại ý như sau:
-- Nếu khoảng quét chứa quá 100,000 tombstones:
TombstoneOverwhelmingException: Nodes scanned over 100001 tombstones...Chuyện này đổ một đống rác lên JVM heap và làm thread đọc của Cassandra gần như đứng hình.
# compaction storm và giành nhau I/O
LSM-tree (Log-Structured Merge-tree) phải liên tục gộp các SSTable nhỏ thành SSTable lớn. Giờ cao điểm, lượng ghi dồn dập, compaction giành băng thông đĩa với chính các request đọc ghi của người dùng. Compaction mà chạy không kịp thì mỗi lần đọc phải mở nhiều SSTable hơn, đọc lại càng chậm. Discord kể họ thường phải làm "gossip dance": rút một node khỏi vòng phục vụ cho nó compact xong rồi mới đưa vào lại, lặp đi lặp lại.
# ScyllaDB: thread-per-core và Seastar
Để dứt điểm mấy vấn đề trên, Discord chuyển sang ScyllaDB. ScyllaDB nói chuyện được bằng giao thức CQL y như Cassandra, nhưng được viết lại hoàn toàn bằng C++, chạy trên framework bất đồng bộ Seastar.
Mấy thay đổi kiến trúc làm nên khác biệt của ScyllaDB:
Shared-nothing (thread-per-core):
- Mỗi core CPU được gắn cứng một thread (pinned thread), có phần RAM và hàng đợi I/O của riêng nó.
- Không lock, không mutex, các core không phải giành bộ nhớ với nhau.
- Core nói chuyện với nhau bằng message passing qua các hàng đợi lock-free, rẻ hơn nhiều so với tranh lock.
- Mỗi shard (core) giữ một phần token range của node, nên request được chuyển thẳng tới core sở hữu dữ liệu.
Tự quản lý bộ nhớ, không qua JVM:
- Không có garbage collector nên cũng không có Stop-The-World pause kiểu JVM.
- Cache của ScyllaDB được quản lý ở userspace, tối ưu riêng cho kiểu dòng dữ liệu NoSQL thay vì dựa vào OS page cache.
Direct I/O và tự lo I/O scheduler:
- Đọc ghi đĩa bằng direct I/O (
O_DIRECT) qua Linux AIO (Seastar cũng hỗ trợio_uring), bỏ qua page cache của kernel. - Tự điều tiết băng thông: I/O scheduler chia đĩa thành nhiều lớp ưu tiên, khi request của người dùng tăng thì compaction được nhường bớt băng thông.
- Đọc ghi đĩa bằng direct I/O (
Đổi database không phải toàn bộ câu chuyện. Trước khi migrate, Discord dựng thêm một lớp data service viết bằng Rust nằm giữa API và database. Lớp này làm request coalescing: nhiều người cùng đọc một hàng trong cùng lúc thì chỉ một query xuống database, các request còn lại dùng chung kết quả. Request còn được route theo consistent hashing theo channel_id, nên các request vào cùng một kênh đi về cùng một instance và gộp được với nhau. Nhờ vậy hot partition bớt đè lên database, dù database là gì.
# chuyển dữ liệu lúc hệ thống vẫn chạy (zero-downtime migration)
Chuyển hàng nghìn tỷ bản ghi trong khi hệ thống vẫn đang phục vụ thì phải tính rủi ro rất kỹ. Ban đầu Discord định dùng Spark migrator của ScyllaDB, ước tính mất khoảng ba tháng. Sau đó họ tự viết một migrator bằng Rust (dựa trên chính thư viện của lớp data service) và chép xong dữ liệu trong khoảng chín ngày, tốc độ đỉnh khoảng 3,2 triệu tin nhắn mỗi giây. Quá trình có thể tóm thành 4 giai đoạn:
- Dual Writing: chọn một mốc thời gian, từ đó tin nhắn mới được ghi cùng lúc vào cả Cassandra và ScyllaDB. Như vậy chỉ còn phần dữ liệu trước mốc là phải chép.
- Historical Data Migration: migrator Rust chia không gian token của cluster thành nhiều token range nhỏ, đọc từng range từ Cassandra, lưu checkpoint cục bộ bằng SQLite để chạy lại được, rồi ghi dồn sang ScyllaDB. Đoạn cuối bị kẹt ở vài token range chứa tombstone khổng lồ, phải chạy compaction cho range đó rồi mới đi tiếp.
- Shadow Reading: gửi một phần nhỏ request đọc sang cả hai database rồi so kết quả, để chắc dữ liệu hai bên khớp.
- Cutover: kiểm chứng xong thì chuyển toàn bộ traffic sang ScyllaDB. Discord hoàn tất việc này vào tháng 5/2022.
# kết quả thực tế
Chuyển xong, cụm database của Discord cải thiện thấy rõ. Số liệu dưới đây lấy từ bài viết của Discord (p99 của Cassandra là khoảng dao động, ScyllaDB là mức ổn định):
| Chỉ số | Apache Cassandra cũ | ScyllaDB mới | Cải thiện |
|---|---|---|---|
| Quy mô cluster | 177 nodes (~4TB/node) | 72 nodes (9TB/node) | 📉 Giảm 59% số node |
| Độ trễ đọc P99 | 40ms – 125ms (dao động) | 15ms (ổn định) | ⚡ Thấp và đều hơn |
| Độ trễ ghi P99 | 5ms – 70ms | 5ms (ổn định) | ⚡ Hết dao động |
| Thời gian dừng do GC | Gây latency spike thường gặp | Không có GC | 🎯 Hết hẳn GC pause |
TIP
Bài học ở đây không phải "Cassandra dở". Khi mỗi node phải gánh nhiều TB dữ liệu và tải cao liên tục, kiến trúc thread-per-core viết bằng C++ của ScyllaDB tận dụng phần cứng hiện đại tốt hơn kiểu đa luồng chia sẻ bộ nhớ trên JVM. Nhưng phần cải thiện cũng đến từ lớp data service (request coalescing) và từ việc thiết kế lại cách vận hành, chứ không chỉ nhờ đổi database.
Nguồn: How Discord Stores Trillions of Messages (2023) và How Discord Stores Billions of Messages (2017).
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.