Użytkownik otwiera zgłoszenie: aplikacja mobilna działa powoli. Sprawdzasz swoje dashboardy. Wykorzystanie CPU jest stabilne. Wskaźniki błędów wynoszą zero. Twoje narzędzia APM świecą uspokajającą zielenią. Backend, według każdego widocznego miernika, jest zdrowy. Ale użytkownik ma rację. Spowolnienie jest rzeczywiste i dzieje się gdzieś w długim, mrocznym korytarzu między urządzeniem z Androidem a Twoim serwerem.

Problem polega na tym, że większość narzędzi do observability kończy się na granicy aplikacji. Mierzą one to, co dzieje się po tym, jak Twój framework przekaże żądanie do analizy. Śledzą zapytania do bazy danych, trafienia do pamięci cache i wywołania usług downstream. To, co omijają, to mechanika samego żądania: czas, jaki wywołanie OkHttp spędza wewnątrz stosu HTTP systemu Android, tranzyt przez niestabilną sieć mobilną, negocjacja uścisku dłoni TLS oraz ciche oczekiwanie w kolejkach jądra, zanim Twój kod w ogóle zostanie uruchomiony. Te luki pochłaniają milisekundy — a nawet całe sekundy — podczas gdy Twoje APM milczy.

Szyfrowanie dopełnia tej ślepoty. Nowoczesne aplikacje na Androida kierują cały ruch przez BoringSSL. Zanim pakiet trafi do stosu sieciowego jądra, nagłówki HTTP są już zaszyfrowane. Standardowe narzędzie tcpdump lub hook sieciowy widzi jedynie nieprzejrzyste rekordy TLS. Możesz zaobserwować, że ruch płynie, ale nie możesz go odczytać. Z pewnością nie możesz powiązać konkretnego segmentu TCP na poziomie jądra z konkretnym wywołaniem API danego użytkownika. Kontekst śladu (trace context), którego potrzebujesz, jest uwięziony w szyfrogramie.

eBPF zmienia postać rzeczy, ponieważ pozwala na instrumentację systemu „od wewnątrz”, bez modyfikowania kodu aplikacji. Zamiast prosić aplikację o raportowanie własnego opóźnienia, podpinasz małe programy bezpośrednio do jądra oraz do krytycznych bibliotek w przestrzeni użytkownika (userspace). Programy te obserwują zdarzenia w momencie ich wystąpienia, wyodrębniają potrzebne dane i przesyłają je do bufora pierścieniowego (ring buffer). Wewnątrz Twojego pliku APK nie ma żadnej nadmiarowości SDK poza lekkim nagłówkiem, a żaden agent instrumentujący nie przepisuje klas Twojego backendu.

Czteroetapowa konfiguracja

Budowa tego potoku obejmuje cztery odrębne warstwy obserwacji.

1. Kotwica traceparent. Na urządzeniu z Androidem dodajesz interceptor OkHttp, który wstrzykuje nagłówek W3C traceparent do każdego wychodzącego żądania. Jest to jedyna wymagana zmiana po stronie mobilnej i jest ona minimalna. Nagłówek podróżuje wewnątrz zaszyfrowanego ładunku aż do Twojego backendu. Ponieważ znajduje się w warstwie HTTP, przetrwa wewnątrz tekstu jawnego (plaintext), który silnik TLS ostatecznie udostępni.

2. Czas przybycia TCP. Na hoście backendowym używasz hooków eBPF Traffic Control podpiętych do interfejsu sieciowego. Programy te uruchamiają się, gdy docierają poszczególne segmenty TCP. Rejestrują one numery sekwencyjne i znaczniki czasu na brzegu sieci. Wiesz teraz dokładnie, kiedy bity opuściły kabel i weszły do Twojej maszyny, na długo przed tym, zanim Twoja aplikacja odczyta choćby jeden bajt.

3. Sondy deszyfrujące. Twój backend kończy sesję TLS, używając OpenSSL lub BoringSSL. To tutaj architektura staje się interesująca. Używając uprobes — dynamicznych sond w przestrzeni użytkownika — podpinasz się pod SSL_write i SSL_read wewnątrz biblioteki TLS. Funkcje te uruchamiają się w momencie, gdy dane tekstowe przechodzą przez silnik szyfrujący. Twój program eBPF odczytuje ten odszyfrowany bufor, skanuje go w poszukiwaniu nagłówka traceparent i go wyodrębnia. Jądro posiada teraz bezpośrednie mapowanie między surowym przepływem TCP a konkretnym żądaniem mobilnym, bez konieczności ręcznego obsługiwania certyfikatów czy kluczy w niestandardowym narzędziu.

4. Kolejkowanie w jądrze. Nawet po odszyfrowaniu i przygotowaniu danych, Twoja aplikacja może nie skonsumować ich natychmiast. Podpinasz kprobes do odpowiednich funkcji jądra obsługujących bufory gniazd (socket buffers) i zdarzenia planowania (scheduling). Pozwala to zmierzyć opóźnienie kolejkowania: czas, jaki żądania spędzają czekając w przestrzeni jądra, ponieważ Twój proces rywalizuje o CPU lub po prostu nie wywołał jeszcze funkcji read().

Wszystkie cztery źródła sygnałów zapisują zdarzenia w buforze pierścieniowym eBPF. Proces sidecar działający w przestrzeni użytkownika opróżnia ten bufor, koreluje zdarzenia według identyfikatora traceparent i rekonstruuje pojedynczą, spójną oś czasu dla każdego żądania. To, co wcześniej było rozproszonym szumem jądra, staje się ustrukturyzowanym śladem (trace).

Odczytywanie pełnej ścieżki

Zgromadzone dane są zazwyczaj renderowane jako flame graph lub ustrukturyzowane drzewo spanów, które dzieli opóźnienie na cztery konkretne części:

  • Czas przesyłu sieciowego: Czas od momentu wysłania ostatniego bajtu żądania przez radio Androida do momentu odebrania go przez kartę sieciową (NIC) backendu. To tutaj kryje się zmienność sieci komórkowej.
  • Czas trwania uścisku dłoni TLS: Czas poświęcony na negocjację szyfrowanego tunelu. W niestabilnych sieciach może on znacznie przewyższać czas właściwego transferu danych.
  • Opóźnienie kolejkowania w jądrze: Czas spędzony w buforach jądra i kolejkach szeregowca po odebraniu segmentu, ale przed jego przetworzeniem przez przestrzeń użytkownika (userspace).
  • Czas przetwarzania przez aplikację: Część czasu, którą faktycznie zużywają Twój framework backendowy i logika biznesowa.

Możesz odkryć, że żądanie mobilne trwające 800 ms spędza tylko 40 ms w parserze JSON. Kolejne 200 ms znika w zawieszonym uścisku dłoni TLS na połączeniu z dużymi stratami pakietów. Inne 300 ms przepada w kolejce (backlog) jądra na przeciążonym hoście. Twoje APM raportowało tylko te 40 ms. Bez widoczności na poziomie jądra zoptymalizowałbyś zupełnie nie to, co trzeba.

Narzut, który faktycznie się skaluje

Tradycyjne agenty APM uzyskują widoczność poprzez przechwytywanie wywołań wewnątrz środowiska uruchomieniowego Twojego języka. Owijają metody, alokują obiekty typu span i serializują dane telemetryczne wewnątrz sterty (heap) Twojego procesu. Pod obciążeniem narzut ten szybko narasta. Koszty serializacji rosną. Rośnie presja na odśmiecanie pamięci (Garbage Collection). Płacisz za widoczność zasobami własnej aplikacji.

Programy eBPF działają wewnątrz maszyny wirtualnej jądra i są kompilowane metodą JIT do natywnych instrukcji maszynowych. Przed załadowaniem weryfikator sprawdza ich bezpieczeństwo. Każda sonda (probe) dodaje do ścieżki mikrosekundy, a nie milisekundy. Ciężka praca związana z dopasowywaniem zdarzeń i renderowaniem wykresów odbywa się w sidecarze, poza krytyczną ścieżką (hot path) Twojej usługi. Nie zwiększasz alokacji na stercie. Nie nakładasz „podatku” od serializacji podczas obsługi żądań. Dla usług obsługujących tysiące żądań na sekundę ta różnica ma znaczenie.

Czego wymaga uruchomienie

To nie jest magiczne rozwiązanie ani zarządzana usługa SaaS, którą można po prostu włączyć. Potrzebujesz jądra backendu ze wsparciem dla nowoczesnego eBPF, w tym informacji o typach BTF, aby Twoje sondy mogły bezpiecznie przechodzić przez struktury jądra. Twoja biblioteka TLS musi udostępniać symbole, do których mogą celować uprobes; jeśli dostarczasz binaria statycznie linkowane z odchudzonym (stripped) lub mocno zmodyfikowanym buildem OpenSSL, będziesz musiał to uwzględnić. Musisz również zweryfikować, czy nagłówek traceparent przetrwa przejście przez wszelkie proxy lub bramy brzegowe (edge gateways) między klientem mobilnym a punktem terminacji TLS.

Jednak dla zespołów wyczerpanych zgłoszeniami typu „aplikacja mobilna działa wolno”, których nie da się przeanalizować pod kątem przyczyn źródłowych, ta architektura zastępuje rytualne zgadywanie twardymi danymi. Przestajesz spekulować na temat „pogody sieciowej” i zaczynasz mierzyć konkretne żądania przez konkretne kanały.

Kluczowy wniosek

Nie musisz traktować zaszyfrowanego ruchu mobilnego jako nieprzejrzystego strumienia, który magicznie staje się widoczny dopiero po dotarciu do Twojego frameworka. Łącząc prosty nagłówek OkHttp ze strategicznie rozmieszczonymi sondami eBPF na backendzie, możesz śledzić pojedyncze żądanie od urządzenia z Androidem, przez deszyfrowanie TLS i kolejki jądra, aż do logiki aplikacji — bez konieczności przepisywania aplikacji mobilnej i bez instrumentowania kodu backendowego w sposób wymagany przez tradycyjne APM. To nie jest tylko stopniowa poprawa. To pełna obserwowalność (end-to-end observability) obejmująca