Chia Sẻ Bảng Dữ Liệu Bất Biến Giữa Các Máy Ảo Khác Nhau - nói dối e blog

Chia Sẻ Bảng Dữ Liệu Bất Biến Giữa Các Máy Ảo Khác Nhau

Trong vài năm trở lại đây, tôi đã dành nhiều năm nghiên cứu cách chia sẻ lượng lớn dữ liệu cấu trúc bất biến giữa các máy ảo Lua khác nhau. Đây là nhu cầu thiết yếu đối với các phần mềm dựa trên dữ liệu như game. Trong tựa game “Đại Lục Gió” (tựa game đang vận hành) do chúng tôi phát triển, các bảng dữ liệu do策划 (nhóm thiết kế) tạo ra trên server, sau khi chuyển đổi thành file nguồn Lua đã lên tới 300MB. Khi tải toàn bộ vào máy ảo Lua, lượng dữ liệu này tiêu tốn tới 700MB bộ nhớ.

Tôi đã triển khai nhiều giải pháp khác nhau cho vấn đề này: từ module Sharedata ban đầu, đến phiên bản thay thế DataSheet sau này.

Các giải pháp truyền thống đều áp dụng nguyên tắc lưu trữ bảng dữ liệu trong đối tượng C, sau đó tạo proxy object trong Lua để truy cập. Đối tượng C có thể chia sẻ giữa các máy ảo, còn proxy object sẽ được tạo riêng cho từng máy ảo. Cách tiếp cận này khá phù hợp với chuẩn mực Lua nhưng tồn tại hai nhược điểm chính:

  1. Tốn kém bộ nhớ do proxy object: Khi cấu trúc bảng dữ liệu phức tạp, mỗi cấp con bảng đều cần tạo proxy object riêng. Nếu truy cập nhiều dữ liệu, tổng lượng proxy object sẽ rất lớn, gây tiêu hao đáng kể bộ nhớ.

  2. Hiệu năng truy cập kém: Việc truy cập đối tượng C thông qua hàm C chậm hơn nhiều so với truy cập trực tiếp bảng Lua. Đặc biệt, việc truyền tải chuỗi giữa máy ảo Lua và đối tượng C cũng gây ra chi phí không nhỏ (đây cũng là lý do chính chúng tôi thay thế Sharedata bằng DataSheet).

Tôi chưa bao giờ từ bỏ ý định cải tiến máy ảo Lua để giải quyết triệt để vấn đề này. Sau nhiều lần thử nghiệm không thành công, gần đây tôi mới tìm ra phương án khả thi.

Vài ngày trước, tôi đã cải tiến cơ chế chia sẻ prototype hàm giữa các máy ảo. Cốt lõi là chia sẻ bảng hằng số trong prototype. Từ đó nảy sinh ý tưởng: Nếu một bảng chỉ chứa các khóa/giá trị là số, boolean, hàm C nhẹ và chuỗi, chúng ta hoàn toàn có thể chia sẻ bảng đó theo cách tương tự như prototype hàm.

Chúng ta chỉ cần giải quyết hai vấn đề nhỏ:

  1. Bảng chia sẻ phải bất biến để đảm bảo an toàn đa luồng.
  2. Tách bảng khỏi chu trình GC của máy ảo cục bộ.

Vấn đề đầu tiên có thể giải quyết bằng metatable. Chúng ta tạo một bảng proxy trống, gắn metatable với:

  • __index trỏ đến bảng dữ liệu thật
  • __newindex trỏ đến hàm ném lỗi

Cách này ngăn người dùng sửa đổi dữ liệu, đồng thời chỉ làm chậm truy cập một chút. Việc trỏ __index đến bảng (thay vì hàm) giúp hiệu năng truy cập cao hơn đáng kể.

Vấn đề thứ hai yêu cầu sửa đổi mã nguồn máy ảo. Tôi tận dụng bit trong trường flags của kiểu Table (dùng để tăng tốc truy cập metafield) để đánh dấu bảng được chia sẻ từ bên ngoài. Trong chu trình đánh dấu GC, khi gặp bit này, máy ảo sẽ dừng duyệt sâu hơn.

Phần còn lại khá đơn giản:

  • Tạo một máy ảo độc lập để quản lý dữ liệu chia sẻ
  • Chuyển đổi toàn bộ bảng con thành bảng chỉ đọc theo phương án trên
  • Kiểm tra toàn bộ bảng đảm bảo không chứa Thread/Function/Userdata
  • Sau đó có thể trực tiếp chia sẻ con trỏ Table cho các máy ảo khác

Tuy nhiên, vẫn tồn tại ngoại lệ liên quan đến chuỗi ngắn (short string). Vấn đề này tương tự với việc chia sẻ bảng hằng số trong prototype hàm. Năm 2015 tôi đã trình bày giải pháp này, và gần đây cũng đề cập đến cách tiếp cận của Erlang khi phân biệt atom và string. Lua không phân biệt hai loại này, nên tôi đề xuất giải pháp thay thế:

Giải pháp SSM (Shared Short String Manager):

  • Trong khoảng thời gian kiểm soát, mở bảng chuỗi ngắn toàn cục (SSM)
  • Các chuỗi ngắn tạo ra trong giai đoạn này được xử lý như atom trong Erlang: tồn tại vĩnh viễn trong SSM và chia sẻ giữa các máy ảo
  • Khi SSM đóng, các chuỗi ngắn mới sẽ được tạo cục bộ (LSS - Local Short String)

Nếu một chuỗi đã tồn tại dưới dạng LSS, chuỗi trùng lặp sẽ không thể vào máy ảo. Đây là trường hợp hiếm gặp, nhưng nếu xảy ra trong bảng hằng số prototype hàm, chúng ta sẽ sao chép toàn bộ bảng theo cách truyền thống. Trong thực tế, ngoại lệ này gần như không xuất hiện, chỉ được tạo ra nhân tạo trong test.

Với bảng dữ liệu chia sẻ, nếu gặp LSS:

  • Tạo proxy table cục bộ thay thế proxy ngăn sửa dữ liệu
  • Sao chép cặp khóa-giá trị chứa LSS vào proxy cục bộ

Vấn đề phức tạp hơn là duyệt bảng:

  • Nếu LSS ở value: viết hàm pairs đặc biệt, mỗi lần lấy value theo key
  • Nếu LSS ở key: không thể duyệt trạng thái vô hại, phải sao chép toàn bộ bảng con

Tôi đã thử nghiệm với bảng dữ liệu 300MB từ sản phẩm thực tế, không gặp ngoại lệ nào. Các giải pháp trên chỉ là phương án dự phòng, hiện tại chỉ kích hoạt trong test case nhân tạo.

Tôi cho rằng đây sẽ là nỗ lực cuối cùng để giải quyết bài toán chia sẻ bảng dữ liệu trong skynet. Giải pháp mới tối ưu hiệu năng, đơn giản hóa việc sử dụng. Trong tương lai nên loại bỏ module sharedata và datasheet trong skynet.

Hiện tại, tính năng mới sharetable đang được thử nghiệm trong nhánh riêng. Lưu ý vấn đề cập nhật nóng dữ liệu (nếu cần thiết).

Khi triển khai tính năng mới, tôi nhận ra việc cập nhật nóng dữ liệu nên được tách khỏi module chia sẻ. Sharetable mới chỉ cung cấp phương thức query - mỗi lần remote query sẽ lấy con trỏ mới nhất từ bảng mẹ, tạo bản sao cục bộ. Khi có bảng mẹ mới (dữ liệu cập nhật), cần cơ chế thông báo các dịch vụ đang dùng phiên bản cũ, sau đó từng dịch vụ tự query bản sao mới.

Về thu hồi dữ liệu cũ, tôi cho rằng chỉ nên thực hiện khi dịch vụ thoát. Vì không có cách hiệu quả để thu hồi chuỗi trong bảng dữ liệu. Chuỗi trong bảng mẹ phải do bảng mẹ quản lý vòng đời. Khi dịch vụ thoát, mọi tham

0%