Một người dùng gửi yêu cầu hỗ trợ: ứng dụng di động chạy rất chậm. Bạn kiểm tra các dashboard của mình. Mức sử dụng CPU vẫn ổn định. Tỷ lệ lỗi bằng không. Các đèn báo APM đều hiển thị màu xanh an tâm. Về mọi phương diện có thể quan sát được, backend đều đang hoạt động tốt. Nhưng người dùng không hề sai. Sự chậm trễ là có thật, và nó đang xảy ra đâu đó trong "hành lang mờ tối" kéo dài giữa thiết bị Android và máy chủ của bạn.
Vấn đề là hầu hết các công cụ observability đều dừng lại ở ranh giới ứng dụng. Chúng đo lường những gì xảy ra sau khi framework của bạn phân tích một yêu cầu. Chúng theo dõi các truy vấn cơ sở dữ liệu, cache hits và các lời gọi dịch vụ hạ nguồn. Những gì chúng bỏ lỡ chính là cơ chế của chính yêu cầu đó: thời gian một lệnh gọi OkHttp tiêu tốn bên trong Android HTTP stack, quá trình truyền tải qua mạng di động không ổn định, quá trình thương lượng TLS handshake, và sự chờ đợi thầm lặng diễn ra bên trong các hàng đợi kernel trước khi mã của bạn thực sự chạy. Những khoảng trống này nuốt chửng hàng miligiây—hoặc thậm chí hàng giây—trong khi APM của bạn vẫn im lìm.
Việc mã hóa khiến sự mù mờ trở nên hoàn toàn. Các ứng dụng Android hiện đại định tuyến mọi thứ qua BoringSSL. Vào thời điểm một gói tin chạm tới kernel network stack, các HTTP header đã được mã hóa. Một lệnh tcpdump tiêu chuẩn hoặc network hook chỉ thấy được các bản ghi TLS mờ đục. Bạn có thể quan sát thấy lưu lượng đang chảy, nhưng không thể đọc được nó. Bạn chắc chắn không thể liên kết một phân đoạn TCP ở cấp độ kernel cụ thể với một lệnh gọi API của một người dùng cụ thể. Ngữ cảnh truy vết (trace context) mà bạn cần đang bị kẹt bên trong văn bản mã hóa (ciphertext).
eBPF thay đổi cục diện vì nó cho phép bạn instrument hệ thống từ trong ra ngoài mà không cần thay đổi mã ứng dụng. Thay vì yêu cầu ứng dụng báo cáo độ trễ của chính nó, bạn gắn các chương trình nhỏ trực tiếp vào kernel và các thư viện userspace quan trọng. Các chương trình này quan sát các sự kiện khi chúng xảy ra, trích xuất những gì bạn cần và gửi chúng đến một ring buffer. Không có sự cồng kềnh của SDK bên trong Android APK ngoài một header nhẹ, và không có agent instrumentation nào phải viết lại các class backend của bạn.
Thiết lập bốn phần
Việc xây dựng pipeline này bao gồm bốn lớp quan sát riêng biệt.
1. Điểm neo traceparent. Trên thiết bị Android, bạn thêm một OkHttp interceptor để chèn header W3C traceparent vào mọi yêu cầu gửi đi. Đây là thay đổi duy nhất cần thiết ở phía di động, và nó rất tối thiểu. Header này di chuyển bên trong payload đã mã hóa cho đến tận backend của bạn. Vì nó nằm bên trong lớp HTTP, nó sẽ tồn tại trong phần văn bản thuần (plaintext) mà engine TLS cuối cùng sẽ hiển thị.
2. Thời điểm arrival của TCP. Trên host backend, bạn sử dụng các eBPF Traffic Control hooks gắn vào giao diện mạng. Các chương trình này kích hoạt khi các phân đoạn TCP riêng lẻ đến nơi. Chúng ghi lại số thứ tự (sequence numbers) và dấu thời gian (timestamps) tại biên. Giờ đây, bạn biết chính xác thời điểm các bit rời khỏi đường truyền và đi vào máy của mình, rất lâu trước khi ứng dụng của bạn đọc được dù chỉ một byte.
3. Các đầu dò giải mã (Decryption probes). Backend của bạn kết thúc TLS bằng cách sử dụng OpenSSL hoặc BoringSSL. Đây là lúc kiến trúc trở nên thú vị. Sử dụng uprobes—các đầu dò userspace động—bạn gắn vào SSL_write và SSL_read bên trong thư viện TLS. Các hàm này kích hoạt ngay lập tức khi dữ liệu plaintext đi qua engine mã hóa. Chương trình eBPF của bạn sẽ đọc buffer đã giải mã đó, quét tìm header traceparent và trích xuất nó. Kernel giờ đây sở hữu một bản đồ trực tiếp giữa một luồng TCP thô và một yêu cầu di động cụ thể, mà bạn không cần phải xử lý chứng chỉ hay khóa trong một công cụ tùy chỉnh nào.
4. Hàng đợi kernel (Kernel queueing). Ngay cả sau khi dữ liệu đã được giải mã và sẵn sàng, ứng dụng của bạn có thể không tiêu thụ nó ngay lập tức. Bạn gắn kprobes vào các hàm kernel liên quan đang xử lý socket buffers và các sự kiện lập lịch (scheduling events). Điều này đo lường độ trễ hàng đợi: thời gian các yêu cầu phải chờ đợi trong vùng kernel vì tiến trình của bạn đang tranh chấp CPU hoặc đơn giản là chưa gọi read() .
Cả bốn nguồn tín hiệu đều ghi các sự kiện vào một eBPF ring buffer. Một tiến trình sidecar chạy trong userspace sẽ rút dữ liệu từ buffer này, tương quan các sự kiện theo traceparent ID và tái cấu trúc thành một dòng thời gian duy nhất, mạch lạc cho mỗi yêu cầu. Những gì trước đây là những tiếng ồn kernel rời rạc, không kết nối nay trở thành một trace có cấu trúc.
Đọc toàn bộ lộ trình
Kết quả tổng hợp thường được hiển thị dưới dạng một flame graph hoặc một cây span có cấu trúc, chia độ trễ thành bốn phần cụ thể:
- Thời gian truyền tải mạng: Khoảng thời gian từ lúc radio của Android gửi byte cuối cùng của yêu cầu cho đến khi NIC ở backend nhận được nó. Đây chính là nơi sự biến động của mạng di động diễn ra.
- Thời gian bắt tay TLS: Thời gian dành cho việc thương lượng đường truyền mã hóa. Trên các mạng chập chờn, thời gian này có thể lớn hơn nhiều so với việc truyền dữ liệu thực tế.
- Độ trễ hàng đợi kernel: Thời gian nằm trong các bộ đệm kernel và hàng đợi của bộ lập lịch sau khi phân đoạn (segment) đến nhưng trước khi không gian người dùng (userspace) tiêu thụ nó.
- Thời gian xử lý ứng dụng: Phần thời gian mà framework backend và logic nghiệp vụ của bạn thực sự tiêu thụ.
Bạn có thể phát hiện ra rằng một yêu cầu di động mất 800ms thì chỉ có 40ms nằm trong bộ phân tích JSON của bạn. 200ms khác biến mất do quá trình bắt tay TLS bị đình trệ trên một kết nối có độ mất gói cao (lossy connection). 300ms khác lại mất hút vào hàng đợi (backlog) của kernel trên một máy chủ đang bị quá tải (oversubscribed host). APM của bạn chỉ báo cáo con số 40ms. Nếu không có khả năng quan sát ở cấp độ kernel, bạn sẽ tối ưu hóa hoàn toàn sai hướng.
Chi phí overhead có khả năng mở rộng thực sự
Các agent APM truyền thống đạt được khả năng quan sát bằng cách chặn các lời gọi bên trong runtime ngôn ngữ của bạn. Chúng bao bọc (wrap) các phương thức, cấp phát các đối tượng span và tuần tự hóa (serialize) dữ liệu telemetry bên trong vùng nhớ heap của tiến trình. Dưới tải trọng lớn, chi phí overhead đó sẽ tích tụ nhanh chóng. Chi phí tuần tự hóa tăng lên. Áp lực thu gom rác (garbage collection) tăng cao. Bạn đang phải trả giá cho khả năng quan sát bằng chính tài nguyên của ứng dụng mình.
Các chương trình eBPF chạy bên trong một máy ảo kernel và được biên dịch JIT sang các lệnh máy gốc. Một trình xác thực (verifier) sẽ kiểm tra tính an toàn của chúng trước khi tải. Mỗi probe chỉ thêm vài micro giây vào lộ trình, chứ không phải mili giây. Các công việc nặng nề như khớp các sự kiện và vẽ biểu đồ diễn ra trong sidecar, nằm ngoài luồng xử lý chính (hot path) của dịch vụ. Bạn không làm phình to việc cấp phát heap. Bạn không thêm "thuế" tuần tự hóa vào quá trình xử lý yêu cầu. Đối với các dịch vụ xử lý hàng nghìn yêu cầu mỗi giây, sự khác biệt đó là cực kỳ quan trọng.
Những gì cần thiết để vận hành
Đây không phải là một giải pháp vạn năng, và nó cũng không phải là một dịch vụ SaaS được quản lý mà bạn có thể bật lên là xong. Bạn cần một kernel backend hỗ trợ eBPF hiện đại, bao gồm cả thông tin kiểu BTF để các probe của bạn có thể duyệt qua các cấu trúc kernel một cách an toàn. Thư viện TLS của bạn phải hiển thị các symbol mà uprobes có thể nhắm tới; nếu bạn phân phối một binary liên kết tĩnh (statically linked) với bản build OpenSSL đã bị lược bỏ (stripped) hoặc tùy chỉnh mạnh, bạn sẽ cần phải tính đến điều đó. Bạn cũng cần xác minh rằng header traceparent của mình vẫn tồn tại qua bất kỳ proxy hoặc edge gateway nào giữa client di động và điểm kết thúc TLS (TLS termination point).
Nhưng đối với các đội ngũ đang kiệt sức vì những ticket "di động chạy chậm" mà không thể phân tích được nguyên nhân gốc rễ, kiến trúc này thay thế việc đoán mò theo thói quen bằng những tín hiệu thực tế. Bạn ngừng suy đoán về "thời tiết mạng" và bắt đầu đo lường các yêu cầu cụ thể thông qua các đường truyền cụ thể.
Bài học thực tế rút ra
Bạn không cần phải coi lưu lượng di động được mã hóa như một luồng dữ liệu mù mờ mà chỉ đột nhiên trở nên rõ ràng khi nó chạm tới framework của bạn. Bằng cách kết hợp một header OkHttp đơn giản với các probe eBPF được đặt một cách chiến lược ở backend, bạn có thể theo dõi một yêu cầu duy nhất từ thiết bị Android qua quá trình giải mã TLS, các hàng đợi kernel và đi vào logic ứng dụng của bạn—mà không cần viết lại ứng dụng di động và không cần phải instrumenting mã backend theo cách mà các APM truyền thống yêu cầu. Đó không chỉ là một sự cải tiến nhỏ. Đó là khả năng quan sát đầu cuối (end-to-end observability) xuyên suốt
