Pułapka benchmarków

Osoby publikujące benchmarki nigdy nie pokazują pełnego obrazu. Mierzą handler, który prawie nic nie robi. JSON Hello World. Statyczny ciąg znaków. W takiej próżni Fiber wygrywa bezapelacyjnie. Obsługuje więcej żądań na sekundę i zużywa mniej pamięci niż Gin. To prawdziwa inżynieria. Ale Twoja aplikacja nie działa w próżni.

Buduję aplikację bezpieczeństwa działającą w czasie rzeczywistym w Kenii. Pobiera ona współrzędne GPS od użytkowników w Nairobi i Mombasie, a następnie wysyła powiadomienia o miejscach zagrożonych przestępczością lub wypadkach drogowych. Oznacza to zapisy w PostgreSQL, wywołania Firebase Cloud Messaging i zapytania reverse geocoding. Gdy Twój stos technologiczny obejmuje skoki sieciowe i cykle komunikacji z bazą danych, framework oszczędzający nanosekundy na routingu staje się niewidoczny. Wąskim gardłem nigdy nie jest router. Jest nim przekroczenie czasu oczekiwania bramki SMS. Jest nim zapytanie PostGIS skanujące tabelę raportów o incydentach. Surowa przepustowość wydaje się kusząca, dopóki nie zderzy się z produkcją.

Lojalność wobec biblioteki standardowej

Gin opiera się bezpośrednio na net/http. Ma to większe znaczenie, niż się wydaje.

Każdy programista Go zna net/http. Debugowanie odbywa się przy użyciu znanych stosów wywołań (stack traces). Middleware napisane pięć lat temu nadal można wdrożyć bez zbędnych formalności. Obiekty żądania i odpowiedzi zachowują się dokładnie tak, jak opisuje to dokumentacja Go. Gin pozostaje przewidywalny, ponieważ trzyma się blisko samego języka.

Fiber zastępuje ten fundament przez fasthttp – własny silnik HTTP zoptymalizowany pod kątem wydajności zero-allocation. Aby to osiągnąć, stosuje Request/Response Pooling. Zamiast pozwalać mechanizmowi Garbage Collector na odzyskanie pamięci po każdym żądaniu, Fiber recyklinguje bloki pamięci. Czyści je i przekazuje do następnego nadchodzącego połączenia. Ten trik jest źródłem szybkości. Jest on również źródłem niebezpieczeństwa.

Ukryte niebezpieczeństwo ponownego wykorzystywania pamięci

Pooling brzmi bezpiecznie w teorii. W praktyce wprowadza subtelną umowę: nigdy nie możesz trzymać referencji do danych żądania po powrocie handlera. Jeśli goroutine przetrwa dłużej niż żądanie lub jeśli przechwycisz fragment (slice) ciała żądania do późniejszego przetworzenia, Fiber odzyska tę pamięć i przekaże ją kolejnemu użytkownikowi.

Rezultatem jest zanieczyszczenie danych. Błąd nie objawi się w teście jednostkowym. Objawi się jako współrzędna GPS kierowcy w Nairobi, która nagle pojawia się na mapie w Kisumu. Objawi się jako token JWT jednego użytkownika wyciekający do kontekstu innego użytkownika. Są to awarie czasowe. Znikają, gdy dodasz logowanie, ponieważ samo logowanie alokuje nową pamięć i zmienia timing. Kończysz, goniąc duchy.

W przypadku Gin biblioteka standardowa alokuje świeży obiekt żądania. Nie ma puli, którą można by zatruć. To bezpieczeństwo ma znaczenie, gdy Twoja aplikacja obsługuje rzeczywiste lokalizacje powiązane z realnym bezpieczeństwem.

Grawitacja ekosystemu

Istnieje również cichy podatek od kompatybilności.

Większość middleware w Go zakłada użycie net/http. Walidatory JWT, eksporterzy OpenTelemetry, handlery CORS i ograniczniki prędkości (rate limiters) – wszystkie operują na standardowym interfejsie. Gin rozumie ten interfejs natywnie. Wrzucasz middleware i działa.

Fiber posiada własny typ kontekstu i własną sygnaturę handlera. Potrzebujesz adapterów. Czasami adapter jest oficjalny. Czasami odstaje od biblioteki źródłowej o rok. Czasami zachowuje się nieco inaczej pod obciążeniem. Gdy zarządzasz małym zespołem, nie masz wolnych godzin na debugowanie, dlaczego middleware do uwierzytelniania działa na Twoim laptopie, ale odrzuca poprawne tokeny na serwerze. Chcesz, aby go get było nudne. Gin sprawia, że jest nudny, a to jest dokładnie to, czego potrzebujesz o 3 nad ranem.

Czego aplikacja tak naprawdę potrzebuje

Moja usługa bezpieczeństwa przetwarza sygnały lokalizacji co kilka sekund od wielu użytkowników jednocześnie. Tworzy geofencing dla dróg wysokiego ryzyka. Wyzwala powiadomienia. Powolne powiadomienie jest frustrujące. Błędne powiadomienie jest niebezpieczne. Wysyłanie kogoś w stronę aktywnego miejsca wypadku z powodu uszkodzonej pamięci z puli jest niedopuszczalne.

Niezawodność wygrywa tutaj z przepustowością. Potrzebuję stosów wywołań, które mają sens, gdy podłączam pprof. Potrzebuję profili pamięci, które nie wymagają ode mnie rozumienia wewnętrznego cyklu życia niestandardowej puli pamięci. Potrzebuję, aby kolejny programista, który przejmie ten projekt, mógł wdrożyć się w jedno popołudnie, nie ucząc się modelu obiektowego fasthttp. Gin mi to daje. Operacyjna rozsądność nie jest widoczna w benchmarkach, ale to ona utrzymuje usługę