Chia Sẻ Chuỗi Ngắn Giữa Các Máy Ảo Lua - nói dối e blog

Chia Sẻ Chuỗi Ngắn Giữa Các Máy Ảo Lua

Trong Lua, các chuỗi dưới 40 byte sẽ được nội hóa (interned) vào một bảng đặc biệt nằm trong cấu trúc global state. Điều này có nghĩa là các chuỗi ngắn giống nhau trong cùng một máy ảo Lua sẽ chỉ tồn tại duy nhất một bản sao.

Trong hệ thống Skynet, việc sử dụng hàng trăm máy ảo Lua là điều phổ biến. Các máy ảo này thường tải cùng một bộ mã nguồn Lua, dẫn đến việc tạo ra nhiều bản sao của các đối tượng Proto không cần thiết. Trước đây, mình đã tiến hành cải tiến máy ảo Lua để chia sẻ Proto giữa các máy ảo, điều này giúp tăng tốc độ khởi tạo máy ảo và giảm đáng kể việc sử dụng bộ nhớ.

Tuy nhiên, việc chia sẻ Proto mới chỉ giải quyết một phần vấn đề. Trong mã nguồn Lua, một phần lớn dung lượng thực tế đến từ các hằng chuỗi (string constants). Những hằng số này không thể chia sẻ thông qua cơ chế Proto. Giải pháp trước đây là sao chép các chuỗi hằng khi clone function, nhưng rõ ràng điều này chưa tối ưu.

Hướng tiếp cận mới: Bảng chuỗi ngắn toàn cục

Mình đã nghiên cứu một phương án mới: tạo một bảng chuỗi ngắn chung cho tất cả các máy ảo Lua. Điều này đòi hỏi giải quyết hai vấn đề chính:

  1. Xung đột đa luồng
  2. Quản lý thu gom rác (garbage collection)
Giải quyết tranh chấp đồng thời

Chọn cấu trúc bảng băm mở (open hash table) với 131072 (128K) bucket được cấp phát trước. Mỗi bucket sẽ được bảo vệ bằng một khóa đọc/ghi riêng biệt. Trong thực tế, phần lớn chuỗi ngắn được tạo ra khi tải mã nguồn (một lần duy nhất), nên tần suất ghi vào bảng là cực thấp. Kết quả thử nghiệm cho thấy hiệu năng không bị ảnh hưởng đáng kể.

Cơ chế thu gom rác thông minh

Vì các chuỗi được chia sẻ toàn cục nên không thể dùng cơ chế đếm tham chiếu thông thường. Mình đã thiết kế một hệ thống đánh dấu dựa trên phiên bản (versioning):

  • Thêm một biến toàn cục global_version để theo dõi trạng thái chia sẻ
  • Khi máy ảo thực hiện GC phase mark, các chuỗi được sử dụng sẽ được gắn nhãn phiên bản hiện tại
  • Các chuỗi từ Proto chia sẻ sẽ được đánh dấu “không bao giờ thu gom”

Quy trình dọn dẹp:

  1. Tăng global_version
  2. Yêu cầu service launcher khởi động full GC trên tất cả máy ảo
  3. Xóa tất cả chuỗi có phiên bản nhỏ hơn global_version - 1

Bạn có thể xem chi tiết implement tại nhánh sstring trên GitHub. Các lợi ích chính bao gồm:

  • Giảm 10-15% bộ nhớ tiêu thụ trên mỗi agent
  • Tăng 20-30% tốc độ khởi tạo máy ảo

⚠️ Lưu ý: Patch này thay đổi cốt lõi của Lua, nên cần chạy make cleanall trước khi rebuild.

Cải tiến mới: Giới hạn thông minh (8/21 cập nhật)

Sau khi thảo luận nội bộ, mình đã phát triển cơ chế mới đơn giản hơn nhiều. Thay vì quản lý phức tạp, ta có thể:

  1. Cho phép chia sẻ chuỗi trong giai đoạn khởi động
  2. Đặt ngưỡng giới hạn bảng chia sẻ (ví dụ 500K chuỗi)
  3. Tự động tắt cơ chế chia sẻ khi đạt tới ngưỡng

Quy trình chi tiết tại nhánh shrtbl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function create_short_string(str)
    -- Bước 1: Kiểm tra local pool
    if current_vm.string_pool:exists(str) then
        return string_pool:get(str)
    end
    
    -- Bước 2: Kiểm tra global pool
    if global_string_pool:exists(str) then
        return global_string_pool:get(str)
    end
    
    -- Bước 3: Nếu chưa đạt giới hạn global
    if not global_reach_limit() then
        return global_string_pool:insert(str)
    end
    
    -- Bước 4: Lưu vào local pool
    return current_vm.string_pool:insert(str)
end

Kết quả thử nghiệm thực tế:

  • Tiết kiệm 200-700KB RAM mỗi máy ảo
  • Giảm 30% thời gian khởi tạo 1000 máy ảo đồng thời
  • Giảm 40% chu kỳ GC trên mỗi máy ảo

Cơ chế này đặc biệt hiệu quả với các hệ thống có chu kỳ sống dài (trên 1 tuần) vì ngăn chặn hiện tượng “bloat” do các chuỗi tạm thời tạo ra trong runtime.

0%