- PgDog, proxy mở rộng cho PostgreSQL, đã áp dụng binding trực tiếp Rust thay cho tuần tự hóa Protobuf để tăng hiệu năng phân tích cú pháp SQL
- Thay thế kiến trúc dựa trên Protobuf trước đây bằng chuyển đổi trực tiếp C–Rust (bindgen + wrapper do Claude tạo), giúp phân tích cú pháp nhanh hơn 5,45 lần, deparse nhanh hơn 9,64 lần
- Điểm nghẽn hiệu năng được phát hiện ở hàm pg_query_parse_protobuf, và sau khi thử cache, nhóm đã thay đổi cấu trúc để có cải thiện mang tính căn bản
- Tận dụng Claude LLM để tự động tạo 6.000 dòng mã chuyển đổi Rust–C, áp dụng cho các hàm chính như
parse, deparse, fingerprint, scan
- Nhờ tối ưu hóa này, mức sử dụng CPU và độ trễ của PgDog giảm xuống, hiệu quả với vai trò proxy mở rộng theo chiều ngang cho PostgreSQL được cải thiện rõ rệt
PgDog và giới hạn của Protobuf
- PgDog là một proxy để mở rộng PostgreSQL, bên trong sử dụng libpg_query để phân tích cú pháp truy vấn SQL
- Được viết bằng Rust, và trước đây giao tiếp với thư viện C thông qua tuần tự hóa/giải tuần tự hóa Protobuf
- Protobuf tuy nhanh, nhưng cách dùng binding trực tiếp còn nhanh hơn
- Nhóm PgDog đã fork
pg_query.rs, loại bỏ Protobuf và triển khai binding trực tiếp C–Rust
- Kết quả là phân tích truy vấn nhanh hơn 5,45 lần, còn deparse nhanh hơn 9,64 lần
Kết quả benchmark
- Benchmark có thể được tái hiện từ kho fork của PgDog
pg_query::parse (Protobuf): 613 QPS
pg_query::parse_raw (C–Rust trực tiếp): 3357 QPS
pg_query::deparse (Protobuf): 759 QPS
pg_query::deparse_raw (Rust–C trực tiếp): 7319 QPS
Phân tích điểm nghẽn hiệu năng và thử nghiệm cache
- Kết quả phân tích thời gian sử dụng CPU bằng profiler samply cho thấy hàm pg_query_parse_protobuf chính là điểm nghẽn
- Nhóm đã thử cải thiện một phần bằng cache
- Sử dụng cache hashmap dựa trên thuật toán LRU, lưu AST với khóa là văn bản truy vấn
- Có thể tái sử dụng trong trường hợp dùng prepared statement
- Tuy nhiên, một số ORM tạo ra hàng nghìn truy vấn duy nhất, hoặc driver PostgreSQL cũ không hỗ trợ prepared statement, khiến hiệu quả cache thấp
Loại bỏ Protobuf với sự hỗ trợ của LLM
- Nhóm PgDog đã tận dụng Claude LLM để tạo ra binding Rust sau khi loại bỏ Protobuf
- AI hoạt động hiệu quả trong phạm vi công việc rõ ràng và có thể kiểm chứng
- Dựa trên đặc tả Protobuf của
libpg_query, Claude đã ánh xạ cấu trúc C sang cấu trúc Rust
- Sau 2 ngày lặp đi lặp lại, hoàn thành 6.000 dòng mã Rust đệ quy
- Áp dụng cho các hàm
parse, deparse, fingerprint, scan và xác nhận hiệu năng tăng 25% theo pgbench
Chi tiết cấu trúc triển khai
- Việc chuyển đổi giữa Rust và C dùng các hàm unsafe để ánh xạ trực tiếp cấu trúc
- Truyền cấu trúc C vào API Postgres để tạo AST, rồi chuyển đổi đệ quy sang Rust
- Mỗi node AST được xử lý bằng hàm convert_node, ánh xạ hàng trăm token của cú pháp SQL
- Có các hàm chuyển đổi riêng cho từng loại node như SELECT, INSERT v.v.
- Kết quả chuyển đổi tái sử dụng cấu trúc Protobuf hiện có (
protobuf::ParseResult), nên có thể kiểm chứng bằng so sánh từng byte khi test
- Thuật toán đệ quy ít cấp phát bộ nhớ hơn và tận dụng cache CPU tốt hơn, nên nhanh hơn cách triển khai dựa trên vòng lặp
- Cách làm dựa trên vòng lặp lại chậm hơn do cấp phát bộ nhớ không cần thiết và tra cứu hashmap
Kết luận
- Giảm overhead của parser Postgres đã giúp PgDog cắt giảm đồng thời độ trễ, bộ nhớ và mức sử dụng CPU
- Nhờ tối ưu hóa này, PgDog tiếp tục phát triển thành proxy mở rộng PostgreSQL nhanh hơn và có chi phí vận hành thấp hơn
- PgDog đang tuyển kỹ sư để cùng xây dựng mở rộng theo chiều ngang (next iteration) cho PostgreSQL
3 bình luận
Có thể là tôi đang hiểu sai bài gốc, nhưng riêng các bài viết liên quan đến Rust thì dường như thường viết theo kiểu như thể nó nhanh hơn là vì "do là Rust", bỏ qua bản chất vấn đề.
Điểm chính của bài này là hiệu năng được cải thiện nhờ giảm phần overhead tuần tự hóa không cần thiết cơ mà.
Giờ nhìn lại thì có vẻ đây cũng không hẳn là một bài ca ngợi Rust như vậy, hay là do những bài khác đã khiến tôi hình thành sẵn ấn tượng tiêu cực rồi nhỉ?
Tôi cũng thấy tiêu đề gốc của bài này, khác với nội dung thực tế, nghiêng về Rust quá mức nên trông như đang nhấn mạnh vào việc cải thiện hiệu năng, vì vậy tôi đã sửa nhẹ lại.
Có vẻ các bài viết về Rust khá thường có xu hướng như vậy, nên có lẽ cần đọc với một chút chọn lọc.
Ý kiến trên Hacker News
Tiêu đề khiến người ta tưởng như Rust mang lại mức cải thiện hiệu năng gấp 5 lần, nhưng điều mỉa mai là thực ra ban đầu nó lại làm chậm đi
Vấn đề là phần mềm viết bằng Rust phải dùng
libpg_queryviết bằng C, nhưng không thể kết nối trực tiếp nên đã dùng binding Rust–C dựa trên ProtobufCách này quá chậm, nên cuối cùng họ đã nhờ LLM hỗ trợ để viết lại một binding mới kém tính di động hơn nhưng được tối ưu tốt hơn nhiều
Nếu ngay từ đầu viết bằng C thì đã không cần bước chuyển đổi đó. Nói cách khác, tiêu đề chính xác hơn phải là “giảm tổn thất hiệu năng do dùng Rust”
Tầng chuyển đổi mang lại tính di động và bảo mật, nhưng rốt cuộc việc lặp đi lặp lại sao chép · chuyển đổi · tuần tự hóa cũng là một trong những nguyên nhân khiến ứng dụng chậm đi
Trong Rust, việc gọi thư viện C rất dễ và cũng đã có nhiều wrapper an toàn
Cấu trúc đặt Protobuf ở giữa thì gần như chưa từng thấy, và đó mới là nút thắt cổ chai
Tiêu đề có vẻ chỉ là một kiểu meme “viết lại bằng Rust” để câu click
Thư viện gốc vốn có thiết kế sai khi lặp đi lặp lại serialize/deserialize, và điểm cốt lõi là họ đã loại bỏ điều đó
Tiêu đề đúng hơn phải là “thay Protobuf bằng API thông thường thì nhanh hơn 5 lần”
Binding C trong Rust là loại dễ nhất, và nếu API không quá lớn thì khá đơn giản
Tôi nghĩ Protobuf là công cụ không phù hợp để trao đổi dữ liệu trong bộ nhớ
Có vẻ trong tương lai nhờ LLM mà việc port sang nhiều ngôn ngữ khác nhau sẽ bùng nổ
Tiêu đề hơi gây hiểu lầm
Về bản chất thì chỉ là “loại bỏ bước tuần tự hóa Protobuf thì nhanh hơn”
Nó cho phép client và server cập nhật độc lập mà vẫn hoạt động, đồng thời giúp giao tiếp đa ngôn ngữ dễ dàng hơn
Trong các hệ thống lớn, kiểu linh hoạt này rất quan trọng
memcpyhaymmapnhanh hơn nhiều, nhưng phía Rust thường ngại những cách làm thiếu an toàn như vậyNguyên nhân chậm có thể không phải do Rust, mà do dùng Protobuf như định dạng lưu trữ tổng quát
Cuối cùng, điểm cốt lõi vẫn là đơn giản hóa theo đúng mục đích cụ thể
Việc đưa Rust vào tiêu đề có vẻ là một lựa chọn để câu click
Tác giả gốc của
pg_querygiải thích bối cảnhBan đầu tại pganalyze, nó được dùng để parse truy vấn Postgres nhằm tìm tham chiếu bảng, đồng thời rewrite và format truy vấn
Lúc đầu dùng JSON, nhưng sau đó đã chuyển sang Protobuf để dễ cung cấp binding có type safety cho nhiều ngôn ngữ khác nhau (Ruby, Go, Rust, Python...)
Với những ngôn ngữ như Rust thì FFI tốt hơn, nhưng với các ngôn ngữ khác, gánh nặng bảo trì lại lớn hơn
Ông ủng hộ nỗ lực của Lev, và trong tương lai dự định sẽ bổ sung các hàm cho phép truy cập trực tiếp
libpg_queryqua FFITuy vậy, trong những trường hợp hiệu năng không quan trọng thì Protobuf vẫn là lựa chọn thuận tiện hơn
Cụm “nhanh hơn 5 lần” làm tôi nhớ tới câu đùa “nhanh vô hạn” của Cap’n Proto
Tiêu đề có hơi cường điệu, nhưng công việc thực tế thì rất ấn tượng
Họ không loại bỏ hoàn toàn Protobuf mà là tối ưu cách sử dụng nó
Câu kiểu “đổi sang X thì nhanh hơn 5 lần” thường có nghĩa là “đã sửa một phần triển khai vốn rất tệ”
Bài học cốt lõi là
serialize/deserializerất dễ trở thành nút thắt cổ chai ẩnRust FFI cũng có overhead, nên thành quả thực sự không nằm ở ngôn ngữ mà ở việc thiết kế lại luồng dữ liệu và nỗ lực tối ưu hóa
FlatBuffers nhanh hơn, nhưng lý do người ta dùng Protobuf là vì nó được các tập đoàn lớn bảo trì
Cuối cùng, suy nghĩ kiểu “do Google làm nên an toàn” thật ra không có cơ sở
code.google.com) rồi nó chết yểuNếu chỉ cần một cấu trúc zero-copy đơn giản với chia sẻ bộ nhớ và trường phiên bản thì tôi thấy không có lý do gì phải dùng Protobuf
Tôi thấy hiệu năng Protobuf ở mức như một trò đùa
Nên dùng định dạng zero-copy nơi việc tuần tự hóa gần như miễn phí
Ví dụ, Lite³ mà tôi tạo ra nhanh hơn FlatBuffers 242 lần
Lý do dùng Protobuf là vì vô số nguyên nhân thực tế như hệ sinh thái, schema, công cụ theo từng ngôn ngữ, v.v.
Thực ra vấn đề không nằm ở Rust hay Protobuf, mà ở triển khai tuần tự hóa kém hiệu quả của tầng trừu tượng PostgreSQL
pgdogđã loại bỏ tầng đó và truyền dữ liệu trực tiếp qua C APIGỡ bỏ tính năng không cần thiết thì dĩ nhiên sẽ nhanh hơn
Nhưng với một số người, tuần tự hóa vẫn là điều cần thiết
Với họ, tiêu đề kiểu “hãy chuyển sang Rust” là một thông điệp sai lệch
Rốt cuộc, trong đa số trường hợp thì JSON là đủ, còn nếu thực sự cần nhanh hơn nữa thì nên tránh bản thân việc tuần tự hóa
Đây là một so sánh không công bằng
Dùng giao thức tuần tự hóa cho giao tiếp IPC đương nhiên sẽ có overhead
Đây đúng là kiểu trường hợp phù hợp với câu nói “nhanh hơn 20% là cải thiện, nhanh hơn 10 lần nghĩa là ngay từ đầu đã làm sai”