Giảm 90% chi phí CPU của eBPF nhờ memoization: câu chuyện từ một security agent mã nguồn mở

Phần mềm14 tháng 9, 2026·5 phút đọc

Một nhóm kỹ sư đã tối ưu hóa eBPF security agent bằng cách cache policy theo inode thay vì duyệt lại toàn bộ đường dẫn mỗi lần mở file. Kết quả là chi phí CPU phía kernel giảm khoảng 90%, từ 28 tỷ chu kỳ xuống còn 3,03 tỷ chu kỳ trong bài benchmark 200.000 lần mở file.

Giảm 90% chi phí CPU của eBPF nhờ memoization: câu chuyện từ một security agent mã nguồn mở

Giảm 90% chi phí CPU của eBPF nhờ memoization: câu chuyện từ một security agent mã nguồn mở

Có những tối ưu hóa không cần đến phép màu của trí tuệ nhân tạo, chỉ cần hiểu rõ chỗ nào đang ngốn tài nguyên. Một nhóm kỹ sư phát triển eBPF security agent đã phát hiện ra rằng phần đắt đỏ nhất trong hệ thống của họ không phải là việc cho phép hay từ chối truy cập, mà là xác định xem policy nào áp dụng cho một lần mở file cụ thể. Bằng cách cache kết quả theo inode, họ đã cắt giảm chi phí CPU phía kernel khoảng 90%.

Vấn đề: duyệt đường dẫn mỗi lần mở file

Các policy của agent này dựa trên đường dẫn (path-based). Mỗi khi có file được mở, agent sẽ dùng LSM hook để kích hoạt, sau đó tái tạo đường dẫn, đi ngược lên các parent dentry và kiểm tra xem file hoặc bất kỳ thư mục cha nào có policy khớp hay không.

Cách làm này đúng về mặt logic, nhưng lại lặp lại rất nhiều công việc đã làm trước đó. Ví dụ, Postgres truy xuất liên tục các file trong /var/lib/postgres/data/base/123, .../234, .../345 — mỗi lần như vậy đều phải duyệt lại toàn bộ cây dentry. Trong bài viết, tác giả gọi con đường xử lý này là "slow path".

Với những ứng dụng như cơ sở dữ liệu, nơi các file path được truy cập lặp đi lặp lại với tần suất cực cao, slow path trở thành nút thắt cổ chai thực sự.

Giải pháp: cache theo inode

Ý tưởng ban đầu là dùng dentry làm khóa cache, nhưng dentry chỉ là con trỏ và con trỏ không thể lưu trong eBPF map. Nếu muốn dùng, phải đóng gói nội dung dentry vào một struct làm khóa — và cấu trúc đó sẽ khá nặng.

Thay vào đó, nhóm chọn cache theo inode với khóa gồm ba trường:

  • mount namespace ID: đảm bảo entry không bị dùng nhầm sang namespace khác
  • mount ID: xác định cây mount mà file được quan sát qua
  • số inode: định danh file

Không thể chỉ dùng số inode vì inode chỉ duy nhất trong một cây mount cụ thể. Nếu một policy bao phủ nhiều cây mount, các inode có thể trùng nhau.

Giá trị cache gồm hai phần: access_index (vị trí bit của policy đường dẫn, do policy được lưu dưới dạng bitmask để tiết kiệm dung lượng) và state (trạng thái cache, ví dụ không có policy, chỉ đọc toàn cục, v.v.).

Luồng xử lý mới trở nên đơn giản hơn nhiều:

  • Dựng khóa cache từ mount namespace, mount ID và inode
  • Tra khóa trong LRU hash map
  • Nếu trúng cache, áp dụng policy ngay từ kết quả đã lưu
  • Nếu trượt, chạy slow path rồi ghi kết quả vào cache

Sơ đồ luồng xử lý của agent eBPFSơ đồ luồng xử lý của agent eBPF

Kết quả benchmark: từ 28 tỷ xuống 3,03 tỷ chu kỳ

Trong bài kiểm thử, nhóm mở cùng một file 200.000 lần và đo bằng sự kiện cycles:k của perf để tính chi phí CPU phía kernel.

  • Không có cache: 28 tỷ chu kỳ. Hàm tail_call_security_check xuất hiện trên stack 89,2% thời gian, is_restricted_filepath 81,9% và path_check_callback 63,7%.
  • Có cache: 3,03 tỷ chu kỳ. is_restricted_filepathpath_check_callback chỉ còn khoảng 0,02%, gần như biến mất khỏi flamegraph sau lần tra cứu đầu tiên.

Flamegraph trước khi thêm cacheFlamegraph trước khi thêm cache

Flamegraph sau khi thêm cacheFlamegraph sau khi thêm cache

Điểm đáng chú ý là toàn bộ thay đổi diễn ra bên trong agent. Người dùng không cần sửa policy để hệ thống chạy nhanh hơn — một lợi thế lớn khi triển khai ở quy mô lớn.

Cache theo inode có một cạm bẫy rõ ràng: nhiều đường dẫn có thể trỏ tới cùng một inode. Hardlink là ví dụ điển hình nhất.

Vì độ chính xác của kết quả quan trọng hơn hiệu năng cache, nhóm xử lý bằng cách đọc i_nlink (số đường dẫn trỏ tới inode). Nếu lớn hơn 1, entry cache đó bị bỏ qua và hệ thống quay về slow path.

Đây là một sự đánh đổi — chấp nhận mất một phần độ phủ cache để đổi lấy tính chính xác.

Cách xử lý này mang tính workaround hơn là giải pháp triệt để, nhưng với một security agent thì việc quyết định đúng allow/deny quan trọng hơn tốc độ.

Vì sao câu chuyện này đáng chú ý

eBPF đang trở thành nền tảng phổ biến cho các giải pháp observability và security ở tầng kernel, từ Cilium, Falco cho tới các agent thương mại. Nhưng sức mạnh đi kèm cái giá: code eBPF chạy trong kernel, và mọi chu kỳ CPU tiêu tốn ở đó đều ảnh hưởng trực tiếp tới toàn bộ hệ thống.

Bài học ở đây khá cổ điển nhưng luôn hiệu quả: đo trước khi tối ưu. Nhóm này không viết lại thuật toán matching policy, không thêm phần cứng, không dùng mô hình học máy — họ chỉ nhận ra công việc nào đang bị lặp lại và cache nó lại.

Mã nguồn của agent đã được mở tại github.com/bomfather/agent, nên ai quan tâm tới eBPF, LSM hook hay tối ưu hóa hiệu năng kernel đều có thể đọc trực tiếp phần triển khai.

Với các đội DevOps và security tại Việt Nam đang cân nhắc triển khai runtime security agent trên hạ tầng production, đây là một case study đáng tham khảo: chi phí CPU của lớp bảo vệ có thể là yếu tố quyết định giữa một agent "chạy được" và một agent "chạy được mà không ai phàn nàn".

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗