Cải Tiến Hiệu Năng Máy Chủ Trò Chơi "Đại Mộng Tây Du"
Công việc sửa chữa những hệ thống lâu đời luôn là chuyện vô cùng nan giải, như việc tu bổ một công trình cổ xưa vậy. Cách đây hai ngày, tôi bắt đầu bàn bạc về việc tối ưu hóa máy chủ game “Đại Mộng Tây Du”. Nhân dịp này, tôi đã đến Quảng Châu sinh sống tạm thời, dự định dành trọn một tuần để xử lý dứt điểm vấn đề. Vì trước giờ chỉ trao đổi qua mạng, việc cùng ngồi chung một phòng mới có thể thấu hiểu tận gốc rễ những khó khăn.
Hiện tại, hệ thống của “Đại Mộng Tây Du” vẫn vận hành chỉ trên một máy chủ đơn, cấu hình cao nhất gồm 8 CPU và 8GB RAM. Dù là máy chủ đông đúc nhất cũng chưa từng tiêu hao hết tài nguyên (chỉ sử dụng khoảng 3 CPU và một nửa RAM). Chương trình cốt lõi gần như được viết cách đây 10 năm, kế thừa từ thời “Đại Thoại Tây Du”. Hai năm gần đây, nhờ tận dụng hiệu ứng “bữa trưa miễn phí” từ việc nâng cấp phần cứng, khả năng phục vụ đồng thời của máy chủ đã đạt mức 12.000 người online. Theo dõi biểu đồ phản hồi máy chủ, chúng tôi nhận ra vấn đề tồn đọng là hiện tượng giật lag định kỳ. Chu kỳ cố định, thời gian phản hồi của máy chủ có lúc vượt quá 1.000ms. Nguyên nhân chính là do tắc nghẽn I/O đĩa. Hai yếu tố gây áp lực lớn nhất gồm: dịch vụ tự động lưu dữ liệu người chơi định kỳ và tiến trình sao lưu dữ liệu do quản trị viên thiết lập, đều tiêu tốn lượng lớn băng thông I/O.
Việc quá tải I/O ảnh hưởng như thế nào đến hiệu suất game? Đây là vấn đề phức tạp cần nghiên cứu sâu hơn. Hai ngày qua, tôi tập trung phân tích kiến trúc hệ thống hiện tại và suy nghĩ phương án cải tiến.
Khác với tưởng tượng, hệ thống cũ không quá phức tạp, lượng mã nguồn cũng khá khiêm tốn. Các dịch vụ liên quan chỉ gồm vài nghìn dòng mã C sạch sẽ. Dù thiết kế có thể chưa tối ưu, hiệu năng chưa hoàn hảo, nhưng điều quan trọng nhất chính là sự ổn định. Vì hệ thống ảnh hưởng trực tiếp đến dữ liệu của hàng triệu người dùng và quy trình thanh toán tài chính. Những hạn chế do “lịch sử để lại” thường chỉ được nhắc đến như chủ đề càu nhàu trong những cuộc trò chuyện phiếm, kiểu như “Nếu thiết kế lại nhất định sẽ không làm như vậy nữa”. Hai năm gần đây, tôi ngày càng ít quan tâm đến việc tái cấu trúc hệ thống. Câu hỏi “Tại sao không làm cách này? Tại sao không chọn phương pháp kia?” dần trở thành đề tài bàn luận trên bàn ăn của các lập trình viên. Mỗi hệ thống khi hoàn thành đều mang theo những tiếc nuối. Nhưng nếu nó vẫn chạy tốt, khả năng cao sẽ tiếp tục vận hành mãi như vậy. Những ý tưởng mới, xin để dành cho cơ hội khác.
Với những hệ thống đã vận hành ổn định hàng chục năm, việc cải tạo toàn diện không phải lựa chọn khôn ngoan. Trọng tâm nằm ở việc bổ sung những thành phần mới với tối thiểu ảnh hưởng đến hệ thống cũ. Việc phân chia rõ ràng các mô-đun trở nên đặc biệt quan trọng, tính độc lập của dịch vụ cũng cần được đảm bảo. Một sai lầm nghiêm trọng hiện tại là gộp chung dịch vụ xử lý dữ liệu, hệ thống thanh toán và xác thực người dùng vào cùng một tiến trình. Điều này khiến việc tách biệt thao tác đọc/ghi dữ liệu trở nên cực kỳ khó khăn.
Dịch vụ dữ liệu hiện tại sử dụng kiến trúc C/S (Client/Server), nhưng không sử dụng cơ sở dữ liệu mà trực tiếp thao tác trên hệ thống tệp tin cục bộ. Thiết kế tổng thể có thể xem là ổn, nhưng cơ chế vận hành bên trong lại có nhiều bất cập. C và S giao tiếp qua bộ nhớ chung nhằm tối ưu hiệu năng IPC. Trong đó, C chỉ có một duy nhất chính là tiến trình chính của game, còn S có thể có nhiều instance làm việc song song. Nhiều tiến trình S và C dùng ống dẫn để truyền lệnh, dùng bộ nhớ chung để trao đổi dữ liệu. Ý tưởng này đáng lẽ rất tốt, nhưng giao thức thiết kế lại có vấn đề. Vì C trực tiếp kiểm soát vùng dữ liệu, dẫn đến việc thiết kế phân bổ khối dữ liệu lại do C đảm nhiệm thay vì để S xử lý.
Chẳng hạn, khi tiến trình game (C) cần tải dữ liệu người dùng, trước tiên nó tự tìm vị trí trống trong vùng dữ liệu, sau đó yêu cầu S tải dữ liệu vào vị trí chỉ định. Thao tác dọn dẹp vùng dữ liệu cũng do C thực hiện. Việc này khiến S không thể chủ động xây dựng cơ chế cache trên vùng dữ liệu chung. Nếu muốn lưu trữ tạm thời dữ liệu không dùng đến (như người chơi offline), C phải tự làm hoặc phải xây dựng thêm một dịch vụ cache riêng (sẽ tốn gấp đôi bộ nhớ và phát sinh thao tác sao chép). Có lẽ thiết kế này xuất phát từ yêu cầu hỗ trợ nhiều S phục vụ cho một C, nhưng theo tôi đánh giá, đây là một lựa chọn thiếu tối ưu.
Kết quả, toàn bộ dịch vụ dữ liệu hiện tại đều không có cơ chế cache. Việc cache hoàn toàn phụ thuộc vào hệ điều hành. Điều này không thành vấn đề khi số lượng người dùng chỉ ở mức hàng nghìn. Nhưng khi quy mô tăng lên hàng vạn người, hạn chế này lập tức bộc lộ. Rõ ràng, càng tùy biến sâu vào nhu cầu cụ thể, ta càng khai thác triệt để hiệu năng phần cứng.
Dưới đây là ghi chép về thiết kế cơ sở dữ liệu key/value trong bộ nhớ mà tôi đã triển khai thành công.
Để thực hiện chiến lược chỉ lưu trữ thông tin thay đổi như đã hoạch định (kết quả thực tế giúp giảm 90% thao tác ghi I/O), trước tiên cần thống nhất vị trí phục vụ đọc/ghi dữ liệu. Không thể tiếp tục dựa vào hệ thống tệp tin cục bộ. Tôi đã khảo sát nhiều cơ sở dữ liệu trong bộ nhớ như Redis, nhưng cuối cùng quyết định tự xây dựng một hệ thống riêng. Vì đã hiểu rõ yêu cầu, tôi có thể tùy biến thuật toán tối đa hóa hiệu năng phần cứng, đồng thời giữ mã nguồn gọn nhẹ (dự trù dưới 500 dòng C, thực tế chỉ 300 dòng).
Yêu cầu cụ thể như sau: dịch vụ sẽ dừng bảo trì định kỳ mỗi tuần. Tổng cộng mỗi tuần xử lý khoảng 100.000 bản ghi người chơi. Mỗi bản ghi có dung lượng từ 4KB đến 32KB, toàn bộ là dữ liệu văn bản. Có thể xem đây là dịch vụ lưu trữ key/value dạng id → chuỗi dữ liệu. Qua tính toán, toàn bộ dữ liệu có thể chứa vừa vặn trong RAM. Dữ liệu sẽ thường xuyên được cập nhật, độ dài thay đổi sau mỗi lần chỉnh sửa.
Tôi