Triển Khai Driver MongoDB Bằng Lua
Gần đây, theo lời đề nghị của đồng nghiệp, tôi bắt đầu tìm hiểu về MongoDB. Hai năm trước, một người bạn từ Turing Press đã tặng tôi cuốn “Cẩm nang MongoDB toàn diện”. Tôi đã đọc xong trong hai đêm và kiến thức của tôi về MongoDB đều bắt nguồn từ quyển sách này. Dự án hợp tác gần đây nhất mà chúng tôi thực hiện có tên “Kiếm Bá” cũng sử dụng MongoDB, trong giai đoạn đóng cửa thử nghiệm vừa qua hệ thống cơ sở dữ liệu đã gặp nhiều sự cố. Bạn Tốc Hành đã hỗ trợ các đồng nghiệp ở Thành Đô trong việc tinh chỉnh hiệu năng, thực hiện rất nhiều công việc quan trọng. Giờ đây trong văn phòng, tôi thường xuyên nghe thấy các cuộc thảo luận về MongoDB.
Tôi quyết định phát triển một driver MongoDB dành riêng cho nền tảng Skynet. Skynet sử dụng Lua làm ngôn ngữ lập trình mặc định, vậy tại sao lại không tận dụng trực tiếp thư viện luamongo đã có sẵn? Vấn đề nằm ở chỗ Skynet yêu cầu một thư viện bất đồng bộ, không mong muốn tình trạng bị chặn luồng khi thực hiện thao tác cơ sở dữ liệu. Điều này khiến việc sử dụng luamongo dưới dạng thư viện thông thường trở nên không khả thi.
Giải pháp tối ưu là xây dựng một kiến trúc tương tự như cách Skynet hiện tại đóng gói Redis (tất nhiên phiên bản Redis trong Skynet cũng là phi chặn). Cụ thể, tạo một dịch vụ độc lập chịu trách nhiệm truy cập cơ sở dữ liệu, các dịch vụ khác sẽ gửi yêu cầu bất đồng bộ đến nó. Nếu tiếp tục dùng luamongo, tôi sẽ gặp phải quy trình xử lý phức tạp: đầu tiên phải tuần tự hóa dữ liệu từ bảng Lua, gửi đến module tương tác MongoDB, giải tuần tự rồi mới chuyển đổi sang định dạng BSON. Sau khi nhận phản hồi từ MongoDB lại phải thực hiện quy trình ngược lại - một quy trình cực kỳ kém hiệu quả.
Với kiến trúc tối ưu, yêu cầu từ phía client sẽ trực tiếp tạo đối tượng BSON, chỉ cần truyền con trỏ của đối tượng này đến module xử lý tương tác (vì Skynet là mô hình đơn tiến trình nên cho phép trao đổi con trỏ C trực tiếp giữa các dịch vụ). Điều này đòi hỏi tôi phải xây dựng một driver MongoDB bằng Lua hoàn toàn mới.
Tôi chính thức bắt tay vào công việc này từ hôm qua. Ban đầu nghĩ rằng đã có sẵn thư viện C Driver chính thức của MongoDB nên công việc chỉ mất khoảng một ngày. Nhưng thực tế hoàn toàn khác biệt.
Điều đầu tiên tôi nghiên cứu là định dạng dữ liệu BSON. Trước đây tôi từng nghe rằng BSON là phiên bản nhị phân của JSON, một định dạng cấu trúc dữ liệu trao đổi phổ thông. Tuy nhiên qua nghiên cứu kỹ lưỡng, tôi nhận ra BSON không phải định dạng đa dụng mà là định dạng được thiết kế riêng cho MongoDB. Dù có ý định trở thành chuẩn chung, nhưng thiết kế giao thức chứa đầy các yếu tố đặc thù của MongoDB. Điều này khiến tôi cảm thấy hơi thất vọng.
Thư viện C Driver của MongoDB do chính nhóm phát triển chính thức duy trì. Ban đầu tôi khá tin tưởng vào chất lượng mã nguồn này, nhưng trải nghiệm thực tế lại vô cùng thất vọng. Vấn đề đầu tiên là tài liệu hướng dẫn lỗi thời, dù điều này không quá nghiêm trọng vì tôi quen với việc đọc trực tiếp file header thay vì tài liệu. Khi cố gắng xây dựng giao diện Lua dựa trên thư viện BSON của C Driver, đến gần cuối tôi mới phát hiện nó thiếu hỗ trợ hai yếu tố quan trọng trong chuẩn BSON là minkey và maxkey. Dù việc bổ sung không tốn nhiều công sức, nhưng tôi không tìm được kênh phản hồi thuận tiện. Tôi đã thử liên hệ qua email với người duy trì trên GitHub nhưng chưa nhận được phản hồi. Dù vậy, cuối cùng tôi vẫn hoàn tất việc phát triển này.
Khi triển khai các phần khác của MongoDB, tôi phát hiện thêm nhiều vấn đề nghiêm trọng. Do tài liệu quá cũ, tôi buộc phải đọc trực tiếp mã nguồn file .c để hiểu cách sử dụng API. Ví dụ như hàm mongo_set_write_concern
cho phép thiết lập đối tượng write concern mặc định, nhưng phần hiện thực chỉ lưu giữ con trỏ mongo_write_concern
mà không quan tâm đến vòng đời của đối tượng này.
Một ví dụ điển hình hơn là đoạn code sau:
|
|
Chuỗi ký tự mode được truyền vào mà không được sao chép. Trong lập trình C thông thường vấn đề này khó lộ diện, nhưng khi xây dựng liên kết với Lua, mode gần như chắc chắn sẽ đến từ một chuỗi Lua truyền qua lua_state. Khi chuỗi này bị thu gom rác (GC), sẽ dẫn đến lỗi con trỏ treo. Hiện tại, trước khi sử dụng bất kỳ API nào tôi đều phải xem xét kỹ phần hiện thực. Trải nghiệm này khiến tôi bắt đầu nghi ngờ liệu có nên từ bỏ ý định đóng gói C Driver mà trực tiếp triển khai từ cấp độ giao thức.
Kết luận: Chất lượng mã nguồn của MongoDB C Driver thực sự rất kém.
Tiếp theo tôi muốn chia sẻ một số vấn đề trong quá trình xây dựng thư viện Lua: Đối với các kiểu dữ liệu phức tạp trong BSON như objectid timestamp, luamongo hiện tại sử dụng full userdata kèm theo metatable. Cách tiếp cận này theo tôi là chưa tối ưu, vì mỗi lần xử lý tin nhắn MongoDB sẽ tạo ra rất nhiều userdata. Để khắc phục hạn chế này, tôi đã nghĩ ra một phương pháp khác.
BSON yêu cầu chuỗi ký tự phải mã hóa theo chuẩn UTF-8, do đó các chuỗi không UTF-8 sẽ phải được mã hóa dưới dạng binary. Tôi tận dụng đặc điểm này để biểu diễn các kiểu đối tượng đặc biệt thông qua chuỗi ký tự không UTF-8.
Cụ thể, tôi quy định tất cả chuỗi bắt đầu bằng hai byte 00 XX đều thuộc kiểu mở rộng, với XX đại diện cho mã loại. Nhờ đó, các kiểu mở rộng cần thiết của BSON như binary, timestamp, date, minkey, maxkey, objectid… đều được mã hóa thành chuỗi Lua bình thường. Việc tạo các kiểu mở rộng này thực chất là xây dựng một chuỗi Lua đặc biệt, chi phí mã hóa/giải mã cực kỳ thấp.
Vì vậy tôi quyết định sẽ triển khai lại hoàn toàn driver MongoDB cho Lua dựa trên chuẩn BSON từ đầu. Do đó, phiên bản driver Lua dựa trên C Driver hiện tại tạm thời sẽ không được công khai mã nguồn.