- Kết hợp logic programming Datalog với hiệu quả của Rust để chia sẻ chi tiết quá trình phát triển một engine Datalog tương tác vừa đơn giản, dễ dùng, vừa chú trọng hiệu năng
- Có thể thêm/mở rộng rule và fact theo thời gian thực trong môi trường CLI trực quan, đồng thời trải nghiệm khả năng nạp dữ liệu khối lượng lớn, nhập rule động và hiệu năng truy vấn nhanh
- Giải thích theo từng bước về parsing, biểu diễn dữ liệu và đánh giá rule (Planning/Evaluation) kèm mã Rust để có thể học cách triển khai thực tế
- Bắt đầu từ một bản cài đặt đơn giản chưa được tối ưu rồi dần cải thiện hiệu năng và cấu trúc, qua đó cũng có thể học các logic nâng cao như xử lý song song dữ liệu và tối ưu join
- Chạy thử các trường hợp phân tích chương trình trên tập dữ liệu lớn (Nullability, Aliasing, v.v.) để chia sẻ kinh nghiệm về vấn đề hiệu năng và bộ nhớ, tối ưu truy vấn và cải thiện join-plan
Mở đầu: thử nghiệm logic programming Datalog trong Rust
- Trong dịp Memorial Day, tại hội nghị Minnowbrook đã diễn ra nhiều buổi thực hành và thảo luận về logic programming (Datalog, v.v.)
- Các công cụ Datalog hiện có (Soufflé, ctdal, v.v.) bộc lộ giới hạn về khả năng sử dụng thực tế, mở rộng và hiệu năng, làm nổi bật nhu cầu về một công cụ thực dụng hơn
- Tác giả quyết định tự triển khai bằng Rust một trình thông dịch Datalog (
datatoad) đáp ứng đồng thời tính đơn giản, khả năng sử dụng và hiệu năng
- Mục tiêu dự án: nạp nhanh dữ liệu lớn trong CLI, thêm/sửa rule động, kiểm tra kết quả theo thời gian thực và vẫn đảm bảo hiệu năng truy vấn
Khái niệm cơ bản về Datalog
- Datalog là một hệ dựa trên các mệnh đề logic dạng rule (Head :- Body), tự động suy ra mọi fact có thể dẫn xuất từ các fact và rule đã cho
- Rule (ví dụ:
tri(a, b, c) :- edge(a, b), edge(b, c), edge(a, c)) được tạo thành từ biến và literal
- fact là giá trị đúng mà không có điều kiện (ví dụ:
edge(1, 2) :- .)
- Điểm mạnh của Datalog: khi thêm rule thì tập thông tin có thể suy luận tăng lên (tính đơn điệu), và bất kể thứ tự của rule/fact thì vẫn suy ra cùng một kết quả (tính hội tụ)
- Trong Rust, rule và fact được biểu diễn bằng các struct Atom/Rule/Term, và tập fact được quản lý theo từng relation
Thiết kế cấu trúc cốt lõi
Biểu diễn dữ liệu
- Fact ban đầu được thiết kế là
Vec<String>, còn tập fact là BTreeMap<String, Vec<Fact>>
- Để tối ưu dữ liệu khối lượng lớn, cấu trúc dữ liệu columnar được đưa vào nhằm giảm tối đa alloc overhead
- FactContainer: tập fact đã được sắp xếp/loại trùng, theo cấu trúc chỉ-append
- FactLSM: quản lý nhiều tầng FactContainer theo kiểu LSM (Log-structured merge-tree) để tối ưu chèn, sắp xếp và hợp nhất
- FactSet: quản lý vòng đời của fact (mới thêm, vừa suy ra gần đây, fact đã ổn định) để tránh tính toán trùng lặp và lãng phí bộ nhớ không cần thiết
Áp dụng rule và suy luận
- Mỗi rule tạo một JoinPlan rồi suy luận theo kiểu merge join dựa trên thứ tự cột/tổ hợp khóa phù hợp
- merge join: sắp xếp các cột khóa theo từng body atom, rồi chỉ suy ra fact mới khi khóa join trùng khớp để tối đa hóa hiệu năng
- Tận dụng cấu trúc stable/recent/to_add của FactSet để tách fact đã suy ra và fact mới, ngăn việc tính lại không cần thiết (đánh giá vi sai)
- Vòng lặp
.update(): lặp áp dụng mọi rule cho đến khi không còn fact mới nào được suy ra nữa, tức đạt fixpoint
Cài đặt parser
- Hỗ trợ cú pháp kiểu Soufflé (
?var, :-, ., ,, v.v.), với tokenizer/parser tự viết bằng Rust
- Khi lỗi xảy ra thì trả về
None an toàn, với thiết kế parser đơn giản phù hợp môi trường thử nghiệm
Tối ưu hiệu năng và ví dụ phân tích thực tế
Phân tích Nullability (Reachability)
- Định nghĩa rule Datalog và đo hiệu năng để theo dõi đường đi sao chép/di chuyển giá trị trên các tập dữ liệu lớn (ví dụ: httpd_df)
- Khác biệt hiệu năng rất lớn tùy vào cách viết rule (binding biến/thứ tự cột/phát sinh temp relation theo join plan, v.v.)
- Tùy dạng dữ liệu đầu vào và chiến lược join, thời gian chạy và mức dùng bộ nhớ có thể chênh nhau hàng chục lần, cho thấy rõ tầm quan trọng của tối ưu truy vấn
- Khi áp dụng tối ưu, hiệu năng được xác nhận cải thiện từ 10 đến hơn 80 lần so với các công cụ nền C++ hiện có (Graspan, v.v.)
Phân tích Aliasing (Points-to)
- Triển khai các mẫu Datalog phức tạp cho phân tích aliasing/theo dõi con trỏ, chạy cùng các truy vấn như trong các bài báo (Graspan, Zheng-Rugina, v.v.)
- Mở rộng rõ ràng các phép lặp (
^*), tùy chọn (^?), chuyển vị (^T) trong rule Datalog thành đệ quy/union tường minh
- Tùy vào cách đặt tên và tái sử dụng kết quả trung gian (relation alias, temp join, v.v.), hiệu quả và mức tiêu thụ tài nguyên của toàn bộ query plan có thể khác biệt rất lớn
- Nếu tạo ra kết quả trung gian lớn mà không tối ưu query plan, hiệu năng sẽ giảm mạnh và bộ nhớ có thể tăng vọt (ví dụ: relation V)
- Có đề cập tới cách tiếp cận "demand-driven" chỉ tạo kết quả cần thiết (biến đổi magic-set), đồng thời cho thấy khả năng cải thiện hiệu năng qua việc biến đổi query plan thực tế
Bài học rút ra từ quá trình tự thử nghiệm bằng Rust
- Yếu tố cốt lõi của hiệu năng engine Datalog nằm ở biểu diễn dữ liệu (columnar/LSM), suy luận vi sai và tối ưu join plan
- Nếu chỉ viết rule một cách máy móc thì sẽ dẫn tới tạo dữ liệu trung gian không cần thiết và lãng phí tài nguyên (cần tối ưu)
- Qua việc trực tiếp thử nghiệm bằng Rust, có thể quản lý hiệu quả hàng triệu đến hàng chục triệu hàng fact trên các tập dữ liệu thực tế, đồng thời đạt được cả khả năng mở rộng lẫn tốc độ suy luận
- Trong môi trường CLI, có thể dễ dàng thử nghiệm từ nạp dữ liệu lớn, thêm rule thời gian thực cho đến kiểm tra kết quả
- Vai trò của query optimizer, bushy-tree join (tận dụng kết quả trung gian), và thói quen tránh tạo ra các relation không cần thiết đều là những gợi ý quan trọng cho việc viết và vận hành Datalog thực tế
Các hướng mở rộng trong tương lai
- Vẫn còn các chủ đề nghiên cứu như spill ra đĩa, mở rộng phân tán với nhiều worker/process, streaming join, tối ưu biên dịch tùy biến, v.v.
- Datalog trong Rust có tiềm năng ứng dụng cao trong các lĩnh vực thực tế như phân tích chương trình quy mô lớn, suy luận đồ thị/quan hệ, static analysis và theo dõi luồng dữ liệu
1 bình luận
Ý kiến Hacker News