Cải Tiến Chia Sẻ Nguyên Mẫu Hàm Giữa Các Máy Ảo Lua
Trong hệ thống kiến trúc máy chủ của chúng tôi là skynet, thường cùng lúc khởi động hàng trăm máy ảo Lua trong cùng một tiến trình, đa phần chạy mã lệnh gần như trùng lặp (phục vụ cho người dùng khác nhau).
Do bản chất Lua xem mã nguồn cũng như dữ liệu, khác với Erlang vốn được thiết kế từ đầu để xử lý đa tiến trình, nền tảng Lua không tối ưu về quản lý bộ nhớ khi vận hành đa máy ảo song song. Vì vậy 5 năm trước, tôi đã xây dựng một bản vá (patch) cho Lua, cho phép trích xuất và chia sẻ dữ liệu nguyên mẫu hàm giữa các máy ảo, giúp tiết kiệm đáng kể bộ nhớ.
Tuy nhiên giải pháp này ban đầu chỉ có thể chia sẻ bytecode và thông tin gỡ lỗi của nguyên mẫu hàm, còn bảng hằng số (constant table) mà nguyên mẫu hàm dùng vẫn là vùng nhớ riêng. Lý do nằm ở bản chất chuỗi ký tự hằng - chúng là các đối tượng cụ thể, đặc biệt với chuỗi ngắn, Lua áp dụng kỹ thuật intern để tăng tốc độ so sánh, khiến việc chia sẻ giữa các máy ảo trở nên phức tạp.
Cốt lõi khác biệt giữa Lua và Erlang trong xử lý mã chia sẻ đa máy ảo nằm ở thiết kế ngôn ngữ. Erlang phân biệt rõ ràng giữa atom và string: các atom được chia sẻ toàn cục thông qua một bảng atom khổng lồ chỉ tăng không giảm. Mã nguồn Erlang tham chiếu hoàn toàn bằng atom nên dễ dàng chia sẻ. Trong khi đó Lua không phân biệt atom/string, việc áp dụng chính sách chỉ tăng không giảm với chuỗi thông thường sẽ gây lãng phí bộ nhớ nghiêm trọng.
Để giải quyết vấn đề hàng loạt chuỗi ngắn trong hằng số hàm Lua, tôi đã đề xuất cơ chế mô phỏng bảng atom của Erlang. Các chuỗi ngắn chia sẻ giữa máy ảo được quản lý tập trung trong một bảng băm duy nhất. Để ngăn ngừa tràn bộ nhớ, tôi thiết lập ngưỡng giới hạn, chỉ cho phép bảng này hoạt động trong giai đoạn khởi động tiến trình.
Gần đây, đồng nghiệp phát hiện dù đã áp dụng cơ chế chia sẻ mã, một máy ảo Lua vừa tải xong vẫn chiếm gần 6MB bộ nhớ, trong đó 1.6MB dùng cho bảng hằng số nguyên mẫu hàm. Nếu tối ưu được phần này, toàn hệ thống (với 2000-3000 máy ảo/trên một tiến trình) sẽ tiết kiệm khoảng 4GB bộ nhớ tổng.
Phân tích kỹ, tôi nhận thấy có thể cải tiến thêm bằng cách:
- Tích hợp bảng hằng số vào cấu trúc dữ liệu chia sẻ - trước đây bảng này nằm ngoài phạm vi chia sẻ.
- Với chuỗi dài trong hằng số, thay vì sao chép dữ liệu, chỉ cần trỏ đến địa chỉ bộ nhớ gốc.
- Với chuỗi ngắn, ưu tiên tạo bản sao (clone) để đảm bảo trùng khớp với đối tượng trong bảng chuỗi ngắn toàn cục, từ đó chia sẻ bằng con trỏ.
- Khi toàn bộ bảng hằng số có thể chia sẻ, trực tiếp tham chiếu dữ liệu chia sẻ mà không cần tạo bảng sao chép.
Tuy nhiên cải tiến này yêu cầu chỉnh sửa sâu quy trình đánh dấu và thu gom rác (GC). Nếu nguyên mẫu hàm không chia sẻ, cần đánh dấu và xóa bình thường. Ngược lại với phiên bản chia sẻ, phải bỏ qua thao tác này.
Sau khi hoàn thiện, kết quả vượt trội: bộ nhớ tiêu tốn cho bảng hằng số giảm hoàn toàn 1.6MB/trên mỗi máy ảo.
Câu hỏi được đặt ra: Tại sao không bắt buộc tất cả chuỗi ngắn trong hằng số đều đưa vào bảng toàn cục? Vấn đề nằm ở cơ chế intern của Lua - nếu trước khi tải mã, máy ảo đã tạo ra các chuỗi ngắn cục bộ chưa được đưa vào bảng toàn cục, thì giai đoạn clone nguyên mẫu sẽ không thể thay thế bằng chuỗi chia sẻ, làm hỏng cơ chế intern.
Một băn khoăn khác: Nếu mọi hàm con trong nguyên mẫu đều chia sẻ được, tại sao không tái sử dụng luôn đối tượng nguyên mẫu Lua? Lý do là phiên bản Lua 5.3.5 lưu trữ con trỏ closure trong mỗi nguyên mẫu để cache lần closure gần nhất tạo ra - đây là dữ liệu phụ thuộc máy ảo. Tuy nhiên trong bản phát triển Lua 5.4, cơ chế cache này đã bị loại bỏ, hứa hẹn cải thiện khả năng chia sẻ nguyên mẫu trong tương lai.
Các bạn đang sử dụng Skynet có thể cập nhật mã nguồn từ nhánh phát triển thử nghiệm, kiểm tra hiệu quả giảm bộ nhớ của bản cải tiến mới này trên dự án của mình.