Hành Trình Vấp Ngã Với Redis Trong Dự Án Momo Tranh Bá
Lưu ý: Mặc dù mình không trực tiếp tham gia vào thiết kế hệ thống Redis cho dự án, nhưng thông qua các buổi thảo luận và đề xuất ý kiến cùng các đồng nghiệp như Fei Long, Xiao Jing và Aply, mình đã ghi nhận được hàng loạt vấn đề đáng suy ngẫm. Các phân tích dưới đây chủ yếu dựa trên những cuộc trao đổi nội bộ và suy đoán cá nhân, chưa kiểm chứng trực tiếp qua tài liệu chính thức hoặc mã nguồn Redis. Hy vọng các bạn đọc sẽ tiếp nhận thông tin với tinh thần phê bình và sáng suốt.
Bối cảnh triển khai Redis
Trước dự án Momo Tranh Bá, chúng tôi hầu như chưa từng sử dụng Redis ở quy mô lớn. Tuy nhiên, dựa trên trực giác kỹ thuật và đặc thù của game, Redis dường như là lựa chọn lý tưởng nhờ ưu điểm:
- Dữ liệu hoàn toàn được quản lý bởi game engine, Redis chỉ đóng vai trò kho lưu trữ dạng key-value
- Tổng lượng dữ liệu lớn nhưng tốc độ tăng trưởng ổn định
- Yêu cầu thống nhất thế giới game (không phân cụm server) đòi hỏi hệ thống lưu trữ tập trung độc lập với các node game server
Chúng tôi chia hệ thống thành 32 database con dựa trên Player ID, đảm bảo sự độc lập hoàn toàn giữa dữ liệu các người chơi. Một điểm quan trọng trong thiết kế là từ chối kiến trúc truy cập đơn điểm - mỗi game server đều kết nối trực tiếp tới toàn bộ 32 redis instance. Đây là quyết định gây tranh cãi nhưng sau này đã chứng minh tính đúng đắn khi tránh được bottleneck tiềm ẩn.
Cấu hình ban đầu và những rủi ro tiềm ẩn
Với dự báo tải ban đầu, 4 máy chủ vật lý (mỗi máy 8 instance Redis) là đủ sức gánh. Ban đầu dùng máy 64GB RAM, sau nâng lên 96GB khi lưu lượng tăng. Mỗi Redis instance tiêu tốn trung bình 4-5GB, tưởng chừng dư thừa thoải mái.
Để phòng ngừa rủi ro, chúng tôi thiết lập 4 máy slave đồng bộ dữ liệu từ master. Tuy nhiên, sự hiểu biết về cơ chế persistence của Redis lúc này còn khá mơ hồ, chỉ dựa trên tài liệu hướng dẫn và một số nguyên tắc cơ bản.
Sự cố ngày 3/2: Bài học từ đồng bộ hóa
Trong kỳ nghỉ Tết, khi mọi việc tưởng chừng yên ổn, một tai họa bất ngờ ập đến:
- Một máy chủ master không thể truy cập, khiến hàng nghìn người chơi không thể login
- Sau 2 giờ khắc phục, nguyên nhân được xác định là do cú sốc đồng bộ từ 8 instance Redis trên máy slave
Phân tích kỹ cho thấy hai vấn đề then chốt:
-
Tại sao slave lại thiếu bộ nhớ trước master?
Dù cấu hình phần cứng giống nhau, máy slave bất ngờ chạm ngưỡng RAM. Nguyên nhân ban đầu cho rằng do dự báo tăng trưởng người chơi thiếu chính xác. Tuy nhiên khi kiểm tra lại, script backup định kỳ mới là thủ phạm chính. Việc chạy BGSAVE luân phiên trên các instance Redis cùng lúc gây ra hiện tượng fork tiến trình ồ ạt, tiêu tốn lượng lớn bộ nhớ tạm thời. -
Cơ chế đồng bộ hóa khiến hệ thống sập hoàn toàn
Redis sử dụng fork() để tạo snapshot đồng bộ. Khi 8 slave cùng lúc yêu cầu SYNC từ master, hệ thống phải fork 8 tiến trình Redis con - đây là nguyên nhân trực tiếp khiến master rơi vào trạng thái swap memory, hiệu năng giảm sút nghiêm trọng.
Giải pháp khắc phục sau sự cố
-
Loại bỏ kiến trúc master-slave vì thấy rằng:
- Tăng độ phức tạp hệ thống
- Gây áp lực bộ nhớ không cần thiết
- Hiệu quả backup chưa chắc đã cao
-
Cải tiến cơ chế BGSAVE:
- Thay vì dùng timer cố định, viết script điều phối luân phiên giữa các instance trên cùng máy
- Chuyển toàn bộ quy trình backup lạnh sang chạy trên master để kiểm soát IO hiệu quả
Sự cố ngày 27/2: Khi memory cache phản đòn
Dù đã tăng cấp phần cứng và tối ưu script, sự cố nghiêm trọng khác tiếp tục xảy ra. Nguyên nhân gốc nằm ở:
- Cơ chế backup file Redis kích hoạt cache bộ nhớ quá mức của hệ điều hành
- Khi BGSAVE chạy đồng loạt (do script “an toàn” yêu cầu sau 30 phút chờ), hệ thống RAM bị “đốt sạch”
Giải pháp tạm thời:
- Điều chỉnh tham số kernel để giới hạn cache memory
- Xem xét lại toàn bộ chiến lược persistence
Hướng đi mới: Thiết kế lại hệ thống lưu trữ bền bỉ
Sau hai sự cố lớn, chúng tôi nhận ra:
- BGSAVE định kỳ không còn phù hợp với đặc thù dữ liệu ít thay đổi
- Cơ chế AOF không tối ưu vì gây bloat dữ liệu trên disk
- Không thể dùng middle layer vì ảnh hưởng đến hiệu năng đọc
Giải pháp sáng tạo:
Triển khai “dịch vụ giám hộ” (Guardian Service) đồng bộ trên từng máy Redis:
graph TD A[Game Server] -->|Gửi dữ liệu| B[Redis] B -->|Xác nhận thành công| C[Guardian Service] C --> D[unQLite Storage] E[Backup Server] <-- F[Cron Job]graph TD A[Game Server] -->|Gửi dữ liệu| B[Redis] B -->|Xác nhận thành công| C[Guardian Service] C --> D[unQLite Storage] E[Backup Server] <-- F[Cron Job]
Ưu điểm vượt trội:
- Chỉ lưu trữ dữ liệu thực sự thay đổi
- Tách biệt hoàn toàn nhiệm vụ memory cache và persistence
- Khôi phục dữ liệu siêu tốc bằng cách replay sẵn Redis command
- Dễ dàng mở rộng với kiến trúc microservice
Thách thức triển khai:
- Khó thuyết phục các thành viên chuyển từ Python sang Go (theo đề xuất của Xiao Jing)
- Cần đảm bảo tính atomic khi đồng thời cập nhật Redis + Guardian Service
- Vấn đề version control giữa các nguồn dữ liệu
Hiện tại, giải pháp mới đang trong giai đoạn thử nghiệm. Tuy nhiên, đây được kỳ vọng sẽ là bước đột phá trong cách quản lý dữ liệu realtime cho các thế hệ game di động tiếp theo.
Bài học xương máu
- Đừng bao giờ coi thường memory footprint của cơ chế fork()
- Backup không chỉ là copy file đơn thuần
- Một script “an toàn” có thể trở thành quả bom hẹn giờ
- Sự đơn giản đôi khi lại là giải pháp bền bỉ nhất
Hy vọng những chia sẻ này sẽ giúp các bạn tránh được những vết xe đổ trong hành trình xây dựng hệ thống phân tán của chính mình.