Xây Dựng Máy Chủ Cache Unity Bằng Skynet - nói dối e blog

Xây Dựng Máy Chủ Cache Unity Bằng Skynet

Trong một số dự án Unity của công ty, khi dữ liệu trên máy chủ cache tăng lên hàng trăm GB, chúng tôi thường xuyên gặp sự cố. Lần gần đây nhất, tiến trình bị sập do nodejs tiêu thụ quá nhiều bộ nhớ. Điều này khiến tôi băn khoăn: tại sao một dịch vụ gần như không cần lưu trữ trạng thái trong RAM lại ngốn nhiều tài nguyên đến vậy? Sau khi xem qua mã nguồn chính thức của cache server, tôi nhận thấy cách triển khai có nhiều điểm chưa tối ưu. Dù logic nghiệp vụ đơn giản, nhưng việc tự xây dựng một giải pháp dựa trên giao thức có thể hiệu quả hơn nhiều.

Máy chủ cache giải quyết bài toán đóng gói tài nguyên trong Unity. Khi một máy tính chuyển đổi dữ liệu từ định dạng A sang B, kết quả B sẽ được tải lên cache server. Những người sau có thể bỏ qua quá trình chuyển đổi phức tạp bằng cách tải trực tiếp B từ máy chủ.
Cách xác định mối liên hệ giữa B và A như thế nào? Mỗi tài nguyên Unity đều có một GUID duy nhất. Quá trình chuyển đổi sẽ tạo ra một hash dựa trên các yếu tố: giá trị băm của dữ liệu gốc, phiên bản, nền tảng, chương trình chuyển đổi và tham số sử dụng. Bất kỳ thay đổi nào trong các yếu tố này cũng làm thay đổi hash. Do đó, sự kết hợp giữa GUID và hash sẽ đại diện cho phiên bản tài nguyên duy nhất. Khi cần chuyển đổi, máy khách sẽ tính hash và truy vấn máy chủ. Nếu không tìm thấy, nó sẽ thực hiện chuyển đổi cục bộ rồi tải lên; nếu có người làm trước đó, chỉ cần tải về mà không cần xử lý lại.

Quan trọng là cache server không cần hiểu logic chuyển đổi. Nó hoạt động như một bảng băm khổng lồ, nơi máy khách đọc/ghi dữ liệu dựa trên khóa GUID+hash.
Phiên bản chính thức được viết bằng nodejs. Vài năm trước, tôi từng xem qua mã nguồn chỉ vài trăm dòng, nhưng giờ đây nó đã phình to hơn dù chức năng cốt lõi không thay đổi. Cá nhân tôi cho rằng cách triển khai này luôn tồn tại nhiều điểm yếu, thậm chí ngày càng tệ hơn.

Dưới đây là những vấn đề nổi bật tôi nhận thấy:

  1. Quy trình dọn dẹp cache phức tạp: Việc dùng JavaScript để xử lý logic loại bỏ file cache không còn cần thiết là một thiết kế sai lầm. Vì nodejs chạy trên luồng đơn, quy trình cleanup có thể làm chậm hoạt động chính của dịch vụ. Thay vào đó, chỉ cần dùng crontab kết hợp script là đủ, không cần tăng độ phức tạp cho máy chủ.
  2. Hỗ trợ cả cache file và cache RAM: Đây có thể là nguyên nhân gây tràn bộ nhớ. Việc dùng mem cache là thừa thãi vì nếu tiến trình tiêu thụ RAM ít, hệ điều hành sẽ tự động tận dụng bộ nhớ rảnh cho cache I/O. Dịch vụ này về bản chất là một máy chủ file tĩnh - chỉ cần mở file từ ổ cứng và gửi cho client. Việc tự xây thêm lớp mem cache không mang lại hiệu suất đáng kể, nếu có cải thiện thì cũng do lỗi triển khai ở phần cũ.
  3. Phân bố file cache không hợp lý: Phiên bản chính thức chỉ chia thư mục cấp 1, dẫn đến tình trạng hàng chục nghìn file cùng nằm trong một thư mục. Dù ext4 trên Linux xử lý tốt, nhưng với hệ thống chạy Windows (NTFS), hiệu suất sẽ tụt giảm nghiêm trọng. Chưa kể, việc quản lý từ xa trở nên khó khăn do thao tác liệt kê thư mục bị treo (vì hệ thống mặc định phải sắp xếp tên file).
  4. Thiết kế giao thức cẩu thả: Giao thức truyền tải được xây dựng một cách qua loa. Ví dụ, đoạn mô tả về quá trình bắt tay trong tài liệu chính thức sau đây cho thấy rõ điều này:

“Máy chủ đọc 8 byte dữ liệu từ gói tin đầu tiên nhận được. Nếu gói tin nhỏ hơn 8 byte, chỉ sử dụng số byte có sẵn. Ngoại lệ duy nhất là khi nhận được gói 1 byte, lúc này cần chờ gói tiếp theo để đảm bảo ít nhất 2 byte dữ liệu.”

Thiết kế giao thức TCP mà không có cơ chế phân biệt rõ ràng giữa các gói tin khiến quá trình bắt tay có thể nhận 8 byte hoặc thậm chí 1 byte.

Dựa trên những phân tích này, tôi tin rằng việc dùng Skynet để xây dựng một máy chủ cache tương đương sẽ không tốn nhiều công sức. Tuần trước, tôi đã thử nghiệm trong vài giờ và kết quả chỉ cần khoảng 200 dòng mã, trong đó 100 dòng là lõi chức năng chính. Phần còn lại tập trung vào tối ưu hóa để phù hợp với các dự án quy mô lớn (hỗ trợ hàng ngàn người dùng đồng thời, quản lý hàng triệu file).

Hiện tại, phiên bản do tôi viết đang được thử nghiệm nội bộ tại công ty. Mục tiêu dài hạn là thay thế hoàn toàn phiên bản nodejs chính thức, từ đó linh hoạt cải tiến dựa trên nhu cầu thực tế.

0%