Pracowałem przy systemie analizującym ruch sieciowy w czasie rzeczywistym w skali całej krajowej sieci telekomunikacyjnej. System parsował protokół GTP — protokół tunelowania używany w sieciach mobilnych do przesyłania danych abonentów. W tym artykule opisuję jak to wygląda od środka: jak ramki trafiają do aplikacji, czym się różni GTP-C od GTP-U, dlaczego korelacja sesji jest trudna i jak doszliśmy do rozwiązania obsługującego 2 miliony ramek UDP na sekundę.Jak ramki trafiają do aplikacji — problem kopii danychPierwszym wyzwaniem jest samo przechwytywanie ruchu. Zanim parser może przetworzyć ramkę, musi ją skądś dostać — i ta kwestia ma ogromny wpływ na wydajność.Standardowa ścieżka pakietu w Linuksie:NIC → DMA → kernel socket buffer → copy → userspace
Przy milionach ramek na sekundę każda dodatkowa kopia między kernelem a przestrzenią użytkownika to realne wąskie gardło — czas CPU, presja na L2/L3 cache, zużycie pamięci.Co rozważałemRozważałem kilka podejść:DPDK — pełny kernel bypass. Sterownik w userspace, DMA prosto do pamięci aplikacji. Najszybsze podejście — ale wymaga wsparcia sterownika NIC. W naszym przypadku karta nie wspierała tego.XDP — program BPF uruchamiany w kernelu na wczesnym etapie przetwarzania pakietu, przed stosem sieciowym. Szybki, ale to nadal kernel space, nie bypass. Również wymagał wsparcia sterownika, którego nie mieliśmy.AF_PACKET z PACKET_MMAP — optymalizacja na poziomie kernela. Kernel tworzy ring buffer i mapuje go do przestrzeni użytkownika przez mmap(). Kernel kopiuje ramki z bufora sterownika do ring buffera, ale ten ring buffer jest mmapowany do userspace — więc aplikacja czyta je bez kolejnej kopii kernel→user.Własna aplikacja C z JNANapisałem własną aplikację w C, która czytała ramki z AF_PACKET ring buffera i przekazywała je do parsera w Javie. Ścieżka pakietu:NIC → DMA → AF_PACKET ring buffer (mmap) → aplikacja C → JNA → Java parser
Do komunikacji C↔Java użyłem JNA (Java Native Access) zamiast JNI. JNA jest dużo prostsze — nie wymaga pisania nagłówków ani ręcznego zarządzania referencjami JVM. Ceną jest overhead: każde wywołanie przechodzi przez warstwę refleksji. JNI byłoby szybsze, ale znacznie bardziej złożone.Testy wypadły obiecująco — ale na produkcję trafiło inne rozwiązanie (opisuję je w sekcji o skalowaniu).Stos protokołów — od ramki do danych abonentaKażda przechwycona ramka to kilka warstw do odkodowania:[ Ethernet frame ]
└── [ IP header ]
└── [ UDP header ]
└── [ GTP header ]
└── [ Inner IP ] ← rzeczywiste dane abonenta
└── [ ICMP / TCP / UDP ]
Parsowanie zaczyna się od surowej ramki ethernetowej (warstwa 2), następnie nagłówek IP (warstwa 3), UDP (warstwa 4) — i dopiero tutaj zaczyna się GTP.GTP (GPRS Tunnelling Protocol) to protokół tunelowania sieci mobilnych. Działa na porcie UDP 2152 (GTP-U) i 2123 (GTP-C).Dwa typy GTP — i dlaczego oba są niezbędneGTP-C (Control Plane)GTP-C to sygnalizacja — zarządza sesjami. Gdy abonent łączy się z siecią, GTP-C tworzy sesję i przypisuje TEID (Tunnel Endpoint Identifier) — unikalny identyfikator tunelu. Kluczowe typy wiadomości:Create Session Request / Response — otwarcie sesjiDelete Session Request / Response — zakończenie sesjiModify Bearer Request / Response — modyfikacja parametrówGTP-U (User Plane)GTP-U przenosi faktyczne dane abonenta. Każdy pakiet zawiera:TEID tunelu (łączy go z sesją z GTP-C)Wewnętrzny pakiet IP z urządzenia abonentaKluczowy problem: pakiet GTP-U sam w sobie jest anonimowy — zawiera tylko numer TEID. Bez tablicy sesji zbudowanej z GTP-C nie wiesz do kogo należy.Problem korelacji GTP-CTo jest serce całego wyzwania technicznego.Sesja GTP-C składa się z par wiadomości. Create Session Request przychodzi od jednego węzła, Create Session Response wraca od drugiego — z zamienionymi adresami src/dst i uzupełnionymi TEID-ami. Dopiero razem dają kompletny obraz sesji.Utrata stanuSesje GTP-C mogą trwać godzinami lub dniami. Jeśli system straci stan (restart, awaria węzła), nie da się poprosić sieci o ponowne wysłanie informacji o sesjach. Trzeba czekać aż abonent sam odnowi połączenie — a to może trwać bardzo długo.Trwałość stanu i odporność na restarty to wymagania absolutnie kluczowe w tego typu systemach.Delete SessionKażda zakończona sesja musi być zwolniona z tablicy korelacji. Delete Session Request/Response sygnalizuje koniec sesji — przy milionach aktywnych sesji brak zwalniania stanu to szybka droga do OOM.Skalowanie i routing sesjiKorelacja GTP-C wymaga by oba kierunki tej samej sesji (request i response) trafiły do tego samego węzła przetwarzającego. Request i response mają zamienione src/dst — naiwne hashowanie po samym IP src skieruje je do różnych węzłów.Rozwiązanie: hashowanie po parze IP niezależnie od kierunku — hash(min(src,dst), max(src,dst)). Oba kierunki zawsze trafiają do tej samej instancji.Rozwiązanie produkcyjne: N instancji + reguły siecioweProdukcyjnym rozwiązaniem było uruchomienie N niezależnych instancji parsera i podział ruchu na poziomie systemu operacyjnego za pomocą reguł sieciowych. Reguły kierowały ruch do konkretnych instancji na podstawie pary IP src/dst. Każda instancja budowała własną tablicę korelacji GTP-C — bez dzielenia stanu między procesami.To podejście rozwiązuje jednocześnie dwa problemy: wydajność (skalowanie poziome) i poprawność korelacji (ta sama para IP zawsze do tej samej instancji).Wczesne odrzucanie śmieciNie każda ramka, która dociera, jest warta przetwarzania. Na sieci krajowej pojawia się ruch, który nie jest GTP — fragmenty, ramki z błędami. Przepuszczanie ich przez cały pipeline to zmarnowany CPU.Aplikacja robiła fast-path rejection na samym wejściu — sprawdzenie portu UDP, wersji GTP i długości nagłówka zanim ramka wejdzie w kosztowne parsowanie. Co nie pasowało, leciało od razu do kosza.WynikiSystem osiągał 2 miliony ramek UDP na sekundę przy N instancjach rozdzielonych regułami sieciowymi. Co miało największy wpływ:Skalowanie poziome — N instancji bez dzielenia stanuPodział ruchu regułami sieciowymi OS — bez dodatkowej warstwy proxyFast-path rejection — większość nieistotnych ramek eliminowana przed parseremBrak dzielenia stanu między instancjami — minimalna synchronizacja