eBPF: Giải pháp hiệu suất cao cho khả năng quan sát Linux Kernel

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

Tại sao các công cụ giám sát Linux truyền thống thất bại khi mở rộng quy mô

Các công cụ tiêu chuẩn như top, netstat, và tcpdump đã là trụ cột của quản trị viên hệ thống trong nhiều thập kỷ. Tuy nhiên, chúng thường gặp khó khăn trong các môi trường hiện đại đầy rẫy microservices và container mật độ cao. Bạn có thể thấy CPU của máy chủ chạm mức 90%, nhưng top lại không hiển thị tiến trình cụ thể nào gây ra việc đó. Điều này xảy ra vì các công cụ cũ thường bỏ lỡ các “micro-bursts” hoặc các tiến trình ngắn hạn bắt đầu và kết thúc giữa các khoảng thời gian thăm dò (polling).

Hầu hết các tiện ích giám sát sống ở user-space. Chúng lấy dữ liệu từ filesystem /proc, giống như việc hỏi kernel về trạng thái mỗi giây một lần. Cách tiếp cận này mang tính thụ động và tạo ra độ trễ về khả năng hiển thị. Để biết chính xác lý do tại sao một gói tin bị hủy hoặc một syscall read() mất bao lâu, trước đây bạn có hai lựa chọn: sửa đổi mã nguồn kernel hoặc tải một kernel module đầy rủi ro. Cả hai đều chậm chạp và nguy hiểm cho môi trường production.

Đây là lúc eBPF (extended Berkeley Packet Filter) thay đổi cuộc chơi. Nó cho phép bạn chạy các chương trình đóng gói (sandboxed) bên trong kernel mà không cần thay đổi bất kỳ dòng mã nguồn nào hay khởi động lại máy. Nó biến kernel thành một công cụ có thể lập trình được.

Kernel Modules và eBPF: So sánh về độ an toàn

Để hiểu giá trị của eBPF, bạn cần hiểu cách mở rộng kernel truyền thống: Linux Kernel Modules (LKM).

Rủi ro của các Module truyền thống

Kernel module rất mạnh mẽ nhưng vốn dĩ mong manh. Chúng chạy với đầy đủ đặc quyền. Một lỗi con trỏ null (null-pointer) hoặc rò rỉ bộ nhớ nhỏ cũng có thể gây ra “Kernel Panic”, làm sập toàn bộ hệ thống ngay lập tức. Hơn nữa, các module gắn chặt với các phiên bản kernel cụ thể. Nếu bạn cập nhật kernel từ 5.10 lên 5.15, các module thường yêu cầu biên dịch lại hoàn toàn để duy trì tính tương thích.

Lưới an toàn eBPF

Ngược lại, eBPF chạy các chương trình trong một máy ảo (VM) hạn chế bên trong kernel. Trước khi bất kỳ mã nào được thực thi, nó phải đi qua một Verifier. “Người gác cổng” này sẽ kiểm tra các vòng lặp vô hạn và truy cập bộ nhớ trái phép. Nếu mã có vẻ không an toàn, kernel sẽ từ chối nó. Bạn có được hiệu suất ở cấp độ kernel với sự an toàn của user-space. Theo kinh nghiệm của tôi, điều này giảm thiểu rủi ro gián đoạn hệ thống production xuống gần như bằng không so với các module C tùy chỉnh.

Tính năng Kernel Modules (LKM) eBPF
Độ an toàn Thấp (Có thể làm sập hệ thống) Cao (Được xác thực bởi kernel)
Hiệu suất Bản địa (Native) Gần như bản địa (<1% overhead)
Dễ sử dụng Lập trình C phức tạp Dễ tiếp cận (Python/Go/C)
Tính di động Phụ thuộc vào phiên bản Cao (thông qua BTF và CO-RE)

Những đánh đổi thực tế khi áp dụng eBPF

Mặc dù eBPF rất hiệu quả, nhưng nó không phải là giải pháp vạn năng. Triển khai nó trong production đòi hỏi phải hiểu một vài hạn chế cụ thể.

Ưu điểm

  • Khả năng quan sát chi tiết: Bạn có thể theo dõi bất kỳ lời gọi hàm nào, từ quá trình chuyển đổi stack mạng đến I/O đĩa. Điều này mang lại cái nhìn toàn diện (full-stack) về hành vi hệ thống.
  • Hiệu quả: Các công cụ như tcpdump sao chép mọi gói tin sang user-space để phân tích, điều này có thể làm giảm hiệu suất nghiêm trọng trên đường truyền 10Gbps. eBPF xử lý dữ liệu trực tiếp trong kernel, loại bỏ những gì bạn không cần trước khi nó chạm tới CPU.
  • Bảo mật thời gian thực: Bạn có thể viết các chính sách chặn các syscall độc hại ngay lập tức thay vì chỉ ghi nhật ký sau khi sự việc đã xảy ra.

Hạn chế

  • Yêu cầu Kernel hiện đại: Bạn cần Linux Kernel 4.18 trở lên cho các tính năng cơ bản. Đối với networking nâng cao và trải nghiệm lập trình tốt nhất, phiên bản 5.4 hoặc mới hơn là tiêu chuẩn.
  • Độ phức tạp kỹ thuật: Ngay cả với các thư viện hỗ trợ, bạn vẫn cần hiểu các hook của kernel như kprobes và tracepoints. Đây không phải là giải pháp “cắm vào là chạy” cho các đội ngũ thiếu kiến thức về nội tại (internals) của Linux.

Bắt đầu: BCC và bpftrace

Viết bytecode thô là công việc của các chuyên gia. Hầu hết các kỹ sư sử dụng các framework để đơn giản hóa các công việc nặng nhọc. Nếu bạn mới bắt đầu, hãy tập trung vào hai công cụ này:

  1. BCC (BPF Compiler Collection): Sử dụng công cụ này để xây dựng các công cụ giám sát phức tạp, lâu dài bằng Python hoặc Lua.
  2. bpftrace: Đây là một ngôn ngữ cấp cao hoàn hảo cho việc khắc phục sự cố tức thời (ad-hoc). Nếu bạn biết AWK, bạn sẽ cảm thấy rất quen thuộc.

Cài đặt

Trên Ubuntu hoặc Debian, bạn có thể thiết lập môi trường trong vài giây:

sudo apt update
sudo apt install -y bpfcc-tools linux-headers-$(uname -r) bpftrace

Một lời cảnh báo nhanh: luôn kiểm tra các script của bạn trong môi trường staging. Mặc dù VM của eBPF an toàn, một script viết kém ghi nhật ký mọi gói tin trên giao diện 10Gbps lưu lượng cao vẫn có thể làm đầy đĩa cứng hoặc gây ra các đỉnh CPU nhỏ. Hãy bắt đầu từ quy mô nhỏ và tinh chỉnh các bộ lọc của bạn.

Triển khai thực tế: Theo dõi việc xóa tệp

Hãy tưởng tượng bạn có một tiến trình bí ẩn đang xóa các tệp cấu hình. Các log tiêu chuẩn có thể không cho bạn biết ai đã làm việc đó. eBPF có thể giải quyết vấn đề này ngay lập tức.

Lệnh bpftrace một dòng

Chạy lệnh này để xem mọi hệ thống gọi unlink (xóa tệp) khi nó xảy ra trong thời gian thực:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_unlink* { printf("%s (PID %d) đang xóa một tệp\n", comm, pid); }'

Lệnh này hook vào tracepoint của kernel dành cho việc xóa tệp. Nó in ra tên tiến trình (comm) và ID (pid) ngay lập tức. Không còn phải đoán mò nữa.

Giám sát tùy chỉnh với BCC

Đối với các logic phức tạp hơn, như lọc theo thư mục hoặc gửi dữ liệu đến ELK stack, hãy sử dụng BCC. Đây là một đoạn mã Python kích hoạt mỗi khi một tiến trình mới bắt đầu (syscall execve):

from bcc import BPF

# Mã kernel eBPF
program = """
int hello(void *ctx) {
    bpf_trace_printk("Tiến trình đã bắt đầu!\n");
    return 0;
}
"""

# Gắn vào syscall execve
b = BPF(text=program)
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="hello")

print("Đang giám sát... Nhấn Ctrl+C để dừng.")
b.trace_print()

Kernel thực thi mã C bên trong chuỗi program mỗi khi một tiến trình khởi chạy. Wrapper Python sau đó đọc các thông điệp đó từ trace pipe của kernel và hiển thị chúng.

Các lưu ý quan trọng để thành công

Chuyển logic giám sát của bạn vào kernel là một sự nâng cấp đáng kể. Để giữ cho hệ thống ổn định, hãy tuân theo các nguyên tắc sau:

  • Ưu tiên Tracepoints: Sử dụng Tracepoints thay vì Kprobes bất cứ khi nào có thể. Tracepoints ổn định hơn. Kprobes hook vào các hàm nội bộ có thể thay đổi khi bạn cập nhật kernel.
  • Theo dõi chi phí tài nguyên (Overhead): Sử dụng offcputime từ bộ công cụ BCC để xác định nơi các tiến trình đang bị kẹt khi chờ đợi, thay vì chỉ nhìn vào mức sử dụng CPU hoạt động.
  • Tận dụng BTF: Sử dụng BPF Type Format (BTF) để đảm bảo các công cụ của bạn hoạt động trên các phiên bản kernel khác nhau mà không cần biên dịch lại cho từng phiên bản.

Bằng cách sử dụng eBPF, bạn không còn coi kernel là một chiếc hộp đen nữa. Bạn có được sự minh bạch cần thiết để gỡ lỗi các vấn đề hiệu suất phức tạp mà các công cụ cũ đơn giản là không thể nhìn thấy.

Share: