Hãy tưởng tượng bạn có một thư viện khổng lồ, chứa hàng nghìn tỷ cuốn sách, và bạn cần di chuyển toàn bộ sang một thư viện mới mà không để mất một cuốn nào, cũng không làm gián đoạn việc phục vụ đọc giả. Đó chính là thách thức mà Discord đã phải đối mặt khi quyết định di chuyển toàn bộ dữ liệu từ Cassandra sang ScyllaDB.
Ban đầu, với công cụ có sẵn, họ ước tính sẽ mất ba tháng để hoàn thành công việc khổng lồ này. Nhưng ba tháng là khoảng thời gian không thể chấp nhận được khi hệ thống cũ đang chịu áp lực ngày càng lớn.
Thế là đội ngũ Developer của Discord đã ngồi lại thảo luận, và chỉ trong một buổi chiều, họ quyết định viết lại công cụ Migration bằng Rust — một bước đi đầy táo bạo nhưng cực kỳ hiệu quả. Kết quả? Thời gian di chuyển giảm từ ba tháng xuống còn chín ngày! Hãy cùng mình khám phá hành trình đó trong bài viết này nhé.
1. Chúng tôi gặp vấn đề với Cassandra
1.1 Hot partitions
Trong Cassandra, dữ liệu được phân phối và lưu trữ trên các node, chia nhỏ thành các phân vùng (partitions). Mỗi phân vùng được xác định bởi một partition key, dữ liệu thuộc cùng phân vùng sẽ được lưu trữ trên cùng một nhóm node.
Hot partitions xảy ra khi một phân vùng nhận quá nhiều truy vấn (đọc hoặc ghi) so với các phân vùng khác. Gây ra tình trạng mất cân bằng tải, làm một hoặc một nhóm node phải xử lý khối lượng công việc vượt quá khả năng, trong khi các node khác lại nhàn rỗi.
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);
Discord sử dụng Cassandra để lưu trữ tin nhắn, với hàng nghìn tỷ tin nhắn được phân vùng dựa trên channel_id
(ID kênh) và bucket
(khoảng thời gian cố định). Dữ liệu trong mỗi phân vùng được lưu trữ cùng nhau và truy vấn dựa trên khóa chính (primary key). Trong đó Partition key: (channel_id, bucket)
, Clustering key: message_id
.
Vấn đề cụ thể của Discord là:
- Sự mất cân bằng giữa các phân vùng: Các server lớn với hàng trăm nghìn người dùng sẽ tạo ra một lượng lớn tin nhắn. Điều này dẫn đến các phân vùng tương ứng với các server này phải xử lý khối lượng truy vấn khổng lồ, trong khi các server nhỏ hơn thì gần như không phải chịu áp lực nào cả.
Ví dụ: Server A (nhỏ) có 50 người dùng, chỉ tạo ra 1.000 tin nhắn/ngày. Server B (lớn) có 1 triệu người dùng, tạo ra 100 triệu tin nhắn/ngày. Phân vùng của Server B sẽ bị quá tải trong khi Server A gần như nhàn rỗi. - Không kiểm soát được truy vấn: Khi hàng nghìn người dùng cùng lúc đọc dữ liệu từ một phân vùng "nóng" (ví dụ: một kênh lớn vừa có thông báo @everyone), các node chứa phân vùng này sẽ bị quá tải. Cassandra không có cơ chế giới hạn số lượng truy vấn đồng thời, dẫn đến "unbounded concurrency".
- Hiệu ứng dây chuyền: Khi một node bị quá tải, nó không thể xử lý các truy vấn kịp thời được, gây ra tình trạng độ trễ tăng dần (cascading latency). Do Cassandra sử dụng cơ chế quorum consistency (cần sự đồng ý của nhiều node), bất kỳ node nào bị chậm trễ cũng làm các truy vấn khác bị ảnh hưởng, lan rộng ra toàn bộ cụm cơ sở dữ liệu.
1.2 Compaction trong Cassandra
Compaction là quá trình gom các tệp SSTable lại, hợp nhất và sắp xếp chúng với mục đích: loại bỏ các bản ghi trùng lặp hoặc đã bị xóa, gom dữ liệu liên quan vào một file mới, sắp xếp lại dữ liệu để tăng tốc độ đọc.
Trong cụm Cassandra của Discord (đặc biệt là cụm lưu trữ tin nhắn), compaction gây ra nhiều vấn đề do quy mô dữ liệu khổng lồ và lưu lượng truy cập cao, cụ thể là:
- Compaction diễn ra chậm: Với hàng nghìn tỷ tin nhắn và gần 200 node, việc compaction các tệp SSTable trở thành một nhiệm vụ nặng nề. Khi compaction không theo kịp tốc độ ghi dữ liệu, số lượng SSTable trên đĩa tăng lên, làm giảm hiệu suất đọc.
- Ảnh hưởng đến thời gian đọc: Khi các file SSTable không được nén kịp thời, Cassandra phải tìm kiếm qua nhiều file để trả lời truy vấn, dẫn đến độ trễ cao.
- Hiệu ứng dây chuyền: Compaction tiêu tốn nhiều tài nguyên CPU và I/O. Khi một node bận compaction, hiệu suất của nó giảm, ảnh hưởng đến toàn bộ cụm do Cassandra sử dụng cơ chế quorum consistency.
- "Gossip Dance": Discord phải phát triển một giải pháp tạm thời để xử lý backlog compaction. Họ tạm thời loại node bị quá tải ra khỏi cụm, cho phép nó thực hiện compaction xong trước đã (không bị truy vấn). Sau đó, node này được đưa trở lại cụm và nhận dữ liệu chưa kịp ghi nhận (do bận compact) qua cơ chế hinted handoff. Quá trình này phải lặp lại nhiều lần, rất tốn thời gian và công sức.
2. Chiến lược cải tiến hệ thống
Để cải thiện hiệu suất và đảm bảo tính ổn định cho hệ thống, Discord đã xây dựng một chiến lược cải tiến gồm các bước chính sau:
- Chuyển đổi cơ sở dữ liệu từ Cassandra sang ScyllaDB: ScyllaDB khắc phục được các vấn đề lớn mà Cassandra gặp phải, đặc biệt là không sử dụng cơ chế garbage collector (GC), giúp giảm độ trễ và cải thiện sự ổn định. ScyllaDB cũng cung cấp hiệu suất cao hơn và khả năng sửa chữa nhanh hơn nhờ kiến trúc shard-per-core.
- Tối ưu hóa ScyllaDB trước khi triển khai trên cụm lớn nhất (cassandra-messages): Với quy mô khổng lồ của cụm cassandra-messages (hàng nghìn tỷ tin nhắn, gần 200 node), việc di chuyển trực tiếp sẽ rất rủi ro. Discord cần đảm bảo ScyllaDB hoạt động tối ưu bằng cách tinh chỉnh hiệu suất và giải quyết các hạn chế, như cải thiện hiệu suất của truy vấn ngược (reverse queries).
- Cải thiện các hệ thống upstream để giảm tải cho cơ sở dữ liệu: Các vấn đề như hot partitions có thể vẫn xảy ra trên ScyllaDB. Discord đầu tư cải tiến hệ thống phía trên cơ sở dữ liệu, xây dựng các lớp trung gian xử lý truy vấn, nhằm giảm áp lực trực tiếp lên cụm cơ sở dữ liệu.
3. Xây dựng Data Services
Để giảm tải cho cơ sở dữ liệu và cải thiện hiệu suất tổng thể, Discord đã triển khai một lớp trung gian gọi là Data Services— một giải pháp chiến lược nằm giữa hệ thống API và các cụm cơ sở dữ liệu bằng ngôn ngữ Rust với mục tiêu:
1. Gộp yêu cầu (Request Coalescing): Khi nhiều người dùng yêu cầu cùng một dòng dữ liệu, thay vì gửi hàng nghìn truy vấn riêng lẻ, Data Services chỉ gửi một truy vấn duy nhất đến cơ sở dữ liệu.
- Cách hoạt động:
- Người dùng đầu tiên khởi tạo một tác vụ (worker task) trong Data Services.
- Các yêu cầu tiếp theo kiểm tra và đăng ký vào tác vụ này.
- Tác vụ sẽ thực hiện truy vấn cơ sở dữ liệu và trả về kết quả cho tất cả các yêu cầu đã đăng ký.
- Ví dụ cụ thể: Một kênh Discord với 1 triệu thành viên nhận được thông báo @everyone. Khi 100.000 user mở ứng dụng và đọc tin nhắn cùng lúc:
- Trước đây: Cassandra phải xử lý 100.000 truy vấn đọc, dẫn đến vấn đề hot partitions và tăng độ trễ.
- Với Data Services: Chỉ một truy vấn được gửi đến Cassandra. Data Services sẽ phân phối kết quả truy vấn này cho tất cả người dùng.
2. Định tuyến thông minh: Data Services sử dụng định tuyến dựa trên consistent hashing để phân phối lưu lượng truy cập hiệu quả hơn:
- Mỗi yêu cầu được định tuyến đến một instance của Data Services dựa trên channel_id.
- Tất cả yêu cầu từ cùng một channel được xử lý bởi cùng một instance, giúp tối ưu hóa khả năng gộp yêu cầu và giảm tải cho cơ sở dữ liệu.
4. Bắt đầu quá trình Migrate
Quá trình migrate hệ thống cơ sở dữ liệu hàng nghìn tỷ tin nhắn của Discord là một nhiệm vụ phức tạp. Yêu cầu đặt ra là phải hoàn tất mà không có downtime.
Discord xây dựng một cụm ScyllaDB mới với kiến trúc super-disk, cụm mới này được chuẩn bị để tiếp nhận khối lượng dữ liệu khổng lồ từ Cassandra. Kế hoạch được chia thành hai giai đoạn:
- Ghi dữ liệu mới: Sử dụng mốc thời gian chuyển đổi (cutover time), ghi dữ liệu mới đồng thời vào cả Cassandra và ScyllaDB.
- Di chuyển dữ liệu lịch sử: Lần lượt chuyển dữ liệu cũ từ Cassandra sang ScyllaDB.
Sau khi thử nghiệm ScyllaDB Spark Migrator, Discord nhận thấy thời gian hoàn thành dự kiến là ba tháng — quá lâu để giải quyết tình trạng khẩn cấp. Do đó, đội ngũ quyết định viết lại công cụ migration này bằng Rust, thời gian di chuyển giảm từ ba tháng xuống còn chín ngày và chạy với tốc độ lên đến 3,2 triệu tin nhắn mỗi giây.
Sau khi hoàn tất quá trình Migrate ScyllaDB vận hành ổn định dưới mức tải lớn, trong khi Cassandra tiếp vẫn tục gặp vấn đề độ trễ (lúc này họ vẫn đang vận hành song song hai hệ thống để thu thập các chỉ số hiệu suất).
Các bài viết liên quan:
Bài viết liên quan
Keycloak là gì? Hướng dẫn tích hợp Keycloak với Spring Boot
Dec 09, 2024 • 7 min read
Idempotent Consumer: Xử lý thông điệp trùng lặp trong Microservices
Dec 04, 2024 • 7 min read
Hướng dẫn tích hợp Redux và React Query trong dự án React Vite
Nov 22, 2024 • 8 min read
Giới thiệu Kiến trúc Backend for Frontend (BFF)
Nov 16, 2024 • 10 min read
Flask là gì? Hướng dẫn tạo Ứng dụng Web với Flask
Nov 15, 2024 • 7 min read
Webhook là gì? So sánh Webhook và API
Nov 15, 2024 • 8 min read