Ghi Chú Phát Triển (10): Cơ Sở Dữ Liệu Trong Bộ Nhớ - nói dối e blog

Ghi Chú Phát Triển (10): Cơ Sở Dữ Liệu Trong Bộ Nhớ

Đã gần một tháng trôi qua kể từ lần cập nhật Ghi chú phát triển gần nhất. Trong khoảng thời gian này, chúng tôi có kỳ nghỉ dài 10 ngày, tiến độ dự án cũng vì thế mà chậm lại. Phần lớn thời gian được dùng để giải quyết những vấn đề kỹ thuật nhỏ nhặt, đặc biệt là ở phía client. Về phía server, chủ yếu là sửa lỗi và hoàn thiện chức năng trước kỳ nghỉ. Đáng tiếc đa số việc này đều do các thành viên khác trong nhóm đảm nhiệm, bản thân tôi không trực tiếp tham gia nhiều.

Vì cảm thấy thiếu nhân lực nên tôi đã âm thầm thực hiện một vài cuộc phỏng vấn nhỏ. May mắn tìm được một hai bạn sinh viên nhiệt huyết muốn cùng tham gia. Lâu rồi không làm công tác tuyển dụng, nhiều khi không biết phải bắt đầu từ đâu. Thêm nữa điều kiện công ty cũng không quá hấp dẫn, yêu cầu lại nhiều, đôi khi thấy ngại mở lời với ứng viên.

Thực ra vẫn còn nhiều thứ đáng ghi lại. Hôm nay tôi sẽ chia sẻ về một chủ đề cụ thể.

Trở lại với Ghi chú số 6, tôi từng đề cập đến việc chia sẻ dữ liệu cấu trúc. Việc triển khai chi tiết cho module này thực ra còn nhiều việc phải làm. Phần lõi tôi đã tự mình hoàn thành mà không tốn nhiều thời gian. Cách đây không lâu có một sinh viên trường Đại học Công nghệ Nam Hoa muốn đến thực tập. Vì trường cách văn phòng chúng tôi chỉ 10 phút đi bộ, tôi đã mời bạn logicouter thử sức với module này để tiếp tục phát triển. Dĩ nhiên bạn ấy không thể toàn tâm toàn ý với dự án, lại cần thời gian làm quen với mã nguồn cũ nên tiến độ cũng không nhanh.

Kế hoạch của tôi như sau:

Phần lõi mới chỉ hoàn thành việc biểu diễn dữ liệu cấu trúc trong bộ nhớ. Tuy nhiên dữ liệu lưu trong RAM chưa thể sử dụng trực tiếp. Dù API cho C đã được triển khai nhưng hiệu năng còn hạn chế. Tôi sử dụng cấu trúc dữ liệu không khóa (lock-free) dựa trên danh sách liên kết đơn để lưu trữ, độ phức tạp khi truy vấn một thuộc tính là O(n). Rõ ràng đây là mức hiệu năng chưa thể chấp nhận được trong dự án thực tế. Để cải thiện, chúng tôi cần thêm một lớp cache. Giải pháp tối ưu là sử dụng bảng băm (hash table) trong máy ảo Lua để ánh xạ dữ liệu, giúp giảm độ phức tạp truy cập xuống O(1). Vì quen thuộc với Lua nên tôi tự đảm nhiệm phần đóng gói mỏng này. Kết quả thử nghiệm cho thấy hiệu năng chỉ kém hơn 3-4 lần so với truy cập bảng Lua thông thường - mức chênh lệch hoàn toàn chấp nhận được, đặc biệt so với hiệu suất của phương pháp IPC truyền thống. Quan trọng hơn là không phải lo lắng về gánh nặng đồng bộ hóa bất đồng bộ. Về sau khi viết logic chỉ cần lưu ý một chút là được.

Về phía thiết kế dữ liệu, tôi cần phát triển một ngôn ngữ mô tả nhỏ, tương tự như việc định nghĩa struct trong C. Trong thiết kế lưu trữ, tôi không cho phép kiểu từ điển (dictionary) không có định dạng thông tin. Các khóa của map đều phải được định nghĩa trước trong cấu trúc dữ liệu, và được lưu dưới dạng số hiệu trong bộ nhớ. Cách làm này mang lại ba lợi thế: dễ triển khai, hỗ trợ cấu trúc dữ liệu không khóa, và đảm bảo tính nghiêm ngặt trong định nghĩa cấu trúc để phát hiện sớm lỗi chính tả (typo).

Ban đầu tôi định dùng trực tiếp ngôn ngữ định nghĩa giao thức của Google Protobuf. Nhưng khi bắt tay vào thì thấy không phù hợp lắm. Cuối cùng quyết định tự thiết kế một ngôn ngữ đơn giản hơn, phù hợp hơn với nhu cầu cụ thể. Ví dụ mẫu như sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Etc {
 int[] object_id = 1
}
enum Gender {
 male = 1
 female = 2
}
type main {
 string name = 1
 int id = 2
 Gender gender = 3
 Etc etc = 4 [deprecated]
}

Đây chỉ là bản phác thảo ban đầu. Tôi muốn logicouter tự do thiết kế và quyết định chi tiết. Về vấn đề tương thích phiên bản, tôi đã dự liệu trước sẽ gặp phải. Chiến lược tạm thời là chỉ bổ sung trường mới, không sửa đổi trường cũ, nhưng cho phép đánh dấu các trường lỗi thời bằng thuộc tính [deprecated]. Những trường này sẽ được xử lý đặc biệt khi cần lưu trữ bền vững (persist), giúp từ từ loại bỏ các phiên bản giao thức cũ.

Lớp Lua sẽ không trực tiếp phân tích định dạng văn bản này. Thay vào đó, chúng tôi sẽ sinh ra các thông tin cấu trúc dữ liệu cần thiết dưới dạng table Lua. Những table này có thể được sinh động và lưu cache dưới dạng tuần tự hóa. Dĩ nhiên dùng lpeg để phân tích văn bản không phải là vấn đề lớn, nhưng lpeg tiêu tốn khá nhiều bộ nhớ. Trong khi đó, chúng tôi muốn giữ cho mỗi trạng thái Lua (Lua state) thật gọn nhẹ để có thể tạo hàng chục nghìn trạng thái Lua trên cùng một máy chủ.

Nhân tiện nói thêm, tôi luôn cố gắng phân công mỗi trạng thái Lua làm một nhiệm vụ duy nhất. Mỗi trạng thái xử lý một lượng công việc nhỏ, thậm chí có thể đóng ngay sau khi hoàn thành mà không cần gánh nặng của garbage collector. Lua khởi động rất nhanh, và với những điều chỉnh tối ưu, kích thước bộ nhớ của một trạng thái Lua trống có thể giảm xuống dưới 10KB. Tuy nhiên việc tải mã Lua vẫn là một gánh nặng đáng kể, bởi bộ phân tích cú pháp (parser) tiêu tốn bộ nhớ, ít nhất là cần thêm không gian chứa mã nguồn văn bản. Dù có thể tiền biên dịch mã Lua nhưng cách này bất tiện cho phát triển vì làm tăng số bước xử lý. Một số mã động sinh ra cũng khó áp dụng tiền biên dịch (như việc dùng lpeg phân tích DSL phía trên để sinh table Lua rồi tuần tự hóa). Nếu hệ thống giao tiếp giữa các trạng thái Lua đủ tốt, bạn có thể tạo một trạng thái Lua độc lập chuyên làm công việc sinh mã động hay biên dịch tải mã, dump ra bytecode (có cache thích hợp), rồi truyền cho các trạng thái Lua đích sử dụng. Tất nhiên đây chỉ là hướng tối ưu phụ, không phải trọng tâm công việc lần này.

Lợi thế của việc tự thiết kế DSL là cho phép phân tích cú pháp với nhiều cách hiểu khác nhau từ cùng một mô tả. Chẳng hạn với thuộc tính [deprecated], module chuyên về lưu trữ bền vững có thể phát hiện và xử lý riêng, trong khi các module logic khác hoàn toàn có thể bỏ qua các trường này.

Trong môi trường sản phẩm, chúng tôi sẽ nghiêm ngặt áp dụng mô hình sản

0%