6 min czytaniaInżynieria

Budżet opóźnienia przekierowania: p95 poniżej 15 ms w Elido

Architektura edge-redirect za celem p95 15 ms przy trafieniu w cache: LRU, wspólny cache, origin gRPC, asynchroniczne kliknięcia i czego nie zmierzyliśmy.

Marius Voß
DevRel · edge infra

Przekierowanie blokuje odwiedzającego. Klika krótki link, przeglądarka czeka i nic innego się nie ładuje, dopóki nie wróci 302. Dlatego services/edge-redirect ma budżet opóźnienia zamiast poczucia "wystarczająco szybko": p50 5 ms i p95 15 ms przy trafieniu w cache.

Czytaj to jako cel, a nie wynik. Budżet jest zapisany w README usługi i w runbooku dyżurnego, a na nim zbudowany jest nasz próg akceptacji testu obciążeniowego. Nie mamy zatwierdzonego benchmarku ani pliku wyników testu obciążeniowego, który dowodziłby percentyla per region, więc ten wpis go nie drukuje. Opisuje, na podstawie kodu, co robi gorąca ścieżka i, równie przydatnie, czego odmawia.

TL;DR

  • Edge to usługa w Go na lekkiej alokacyjnie bibliotece HTTP Go, działająca dziś na jednym hoście produkcyjnym za reverse proxy; wieloregionowy edge jest planowany.
  • Wyszukiwanie przechodzi przez LRU w procesie (100 000 wpisów, TTL 60 sekund), potem współdzielony cache w pamięci (TTL 5 minut), potem api-core przez gRPC z limitem czasu 1,5 sekundy.
  • Zdarzenie kliknięcia jest produkowane z osobnej gorutyny. Odwiedzający nigdy na nie nie czeka.
  • Budżet: p50 5 ms, p95 15 ms przy trafieniu w cache. To cele, a zmierzone percentyle nie są tu publikowane.

Dlaczego budżet istnieje

Czas przekierowania dodaje się do wszystkiego, co po nim. Jeśli link kampanii kieruje ruch na stronę docelową, przekierowanie jest częścią czasu do pierwszego bajtu tej strony. Budżet wymusza decyzję, ilekroć ktoś chce dodać pracę na tej ścieżce: mieści się albo przechodzi do workera.

Liczby z services/edge-redirect/README.md:

PercentylTrafienie w cacheChybienie w cache
p505 ms40 ms
p9515 ms100 ms
p9930 ms200 ms

To cel projektowy. Jeśli potrzebujesz zmierzonych liczb do przeglądu dostawcy, uruchom scenariusz k6 opisany na końcu tego wpisu na własnych linkach i zachowaj wynik.

Ścieżka żądania krok po kroku

internal/handler/redirect.go dokumentuje przepływ na początku pliku. W kolejności:

  1. Zamień host na małe litery i weź slug ze ścieżki.
  2. resolver.Resolve przechodzi przez L1, potem L2, potem origin.
  3. Przerwij według stanu: wstrzymana przestrzeń robocza, wygasły link, link chroniony hasłem (odwiedzający trafia na stronę odblokowania i nie jest emitowane kliknięcie), link oznaczony przez url-scanner jako blocked (451) oraz host celu na administracyjnej liście blokad.
  4. Odpal zdarzenie kliknięcia bez czekania na nie.
  5. Odpowiedz 302, chyba że link mówi 301 lub 307.

Wszystko w kroku 3 to pole w zbuforowanym rekordzie linku albo wyszukanie w zbiorze w pamięci. Dopasowanie listy blokad, na przykład, jest synchronizowane z api-core w tle, więc jego sprawdzenie kosztuje kilka wyszukań w mapie, a nie wywołanie bazy danych.

Podglądy linków (slug kończący się na +), żądania agentów AI prosące o JSON i linki szyfrowane end-to-end przechodzą przed przekierowaniem inne gałęzie. Żadna nie dotyka Postgresa.

Gorąca i zimna ścieżka: wyszukiwanie, kontrole stanu i odpowiedź przekierowania dzieją się, zanim odwiedzający dostanie odpowiedź, a zapis kliknięć, skanowanie i analityka działają później w osobnych workerach

Trzy poziomy wyszukiwania

Każdy poziom jest celowo mały i każdy poza ostatnim działa w trybie best-effort.

L1, LRU w procesie. internal/cache/lru.go opakowuje hashicorp/golang-lru/v2/expirable. Rozmiar to L1Size: 100_000 wpisów, a TTL to L1TTL: 60s, oba ustawione w internal/config/config.go. TTL istnieje po to, by wyłączony lub usunięty link nie mógł być serwowany w nieskończoność przez jeden węzeł. Klucz to host + "/" + slug, więc ten sam slug na dwóch domenach nie koliduje.

L2, współdzielony cache. internal/cache zapisuje link jako JSON, kluczowany hostem i slugiem, z L2TTL: 5m. Jest współdzielony przez instancje edge. Nilowy klient jest dozwolony: metody cache zwracają chybienie, a edge dalej działa.

Origin, gRPC do api-core. internal/origin/origin.go obsługuje dwa transporty. REST (GET /v1/lookup) jest domyślny, gdy nic innego nie jest skonfigurowane. gRPC (elido.v1.LookupService.Resolve) jest używany po ustawieniu GRPC_ORIGIN_ADDR i to jest wybór produkcyjny. Wywołanie ogranicza OriginTimeout, 1,5 sekundy.

Resolver jest dość krótki, by zacytować ważną część:

if v, ok := r.l1.Get(key); ok {
    return v, nil
}
if v, ok := r.l2.Get(ctx, key); ok {
    r.l1.Set(key, v)
    return v, nil
}
link, err := r.origin.Resolve(ctx, host, slug)
if err != nil {
    return nil, err
}
r.l1.Set(key, link)
r.l2.Set(ctx, key, link)
return link, nil

Trafienie w L2 odświeża L1. Chybienie w obu idzie do api-core i odświeża oba. Tylko origin może powiedzieć "nie znaleziono"; jeśli to zrobi, resolver ponawia raz z nadrzędnym hostem wildcard (abc.example.com staje się *.example.com) dla własnych domen wildcard.

Kolejność wyszukiwania w edge redirect: LRU w procesie ze 100 000 wpisów i TTL 60 sekund, potem współdzielony cache w pamięci z TTL 5 minut, potem api-core przez gRPC z limitem 1,5 sekundy, a każde trafienie odświeża warstwy powyżej

Czego ten projekt nie ma

Wcześniejsza wersja tego wpisu opisywała rzeczy, których kod nie robi. Wprost:

  • Nie ma grupy singleflight. Dwa równoczesne chybienia dla tego samego zimnego sluga oba docierają do origin.
  • Nie ma subskrypcji pub/sub na edge. Gdy link się zmienia, api-core usuwa klucz L2 (purger w services/api-core/internal/linkmod/adapters.go). L1 nie jest pushowane, więc edytowany link może być serwowany z pamięci węzła do 60 sekund.
  • L1 to LRU ograniczone TTL, a nie cache z kontrolą przyjęć, i jest rozmiarowane liczbą wpisów, nie bajtami.
  • Nie ma endpointu cache-status, nagłówka odpowiedzi per poziom ani histogramu opóźnień eksportowanego przez edge.

Okno 60 sekund jest powodem, dla którego edytowalny link powinien używać przekierowania tymczasowego i dlaczego 301 kontra 302 ma znaczenie dla strony przeglądarki tego samego problemu. Projekt cache jest opisany głębiej w strategii cache dla przekierowań URL.

Jedyna metryka Prometheus, na którą możemy wskazać w kodzie edge, to elido_edge_redirect_total z etykietami fleet i outcome. Runbook wspomina inne nazwy metryk (edge_cache_*, edge_origin_grpc_duration), których nie znaleźliśmy zarejestrowanych w obecnym kodzie, więc ich nie dokumentujemy.

Co zostaje poza gorącą ścieżką

Zapis kliknięć. click.Producer.Emit serializuje zdarzenie i przekazuje je producentowi franz-go, a handler wywołuje go jako go h.clicks.Emit(...). Callback produkcji tylko loguje błąd. Jeśli szyna zdarzeń jest wolna, odwiedzający tego nie zauważa; koszt jest taki, że kliknięcie może zostać utracone, co jest zwykłym kompromisem fire-and-forget. Zapis kliknięć do magazynu analityki to zadanie click-ingester, który zasila API analityki kliknięć.

Wyzwania dla botów. Na przekierowaniu nie ma wyzwania JavaScript. Wykrywanie botów to lista podciągów user-agenta, która decyduje, czy kliknięcie jest liczone, więc crawlery i pobierania podglądów linków nadal dostają przekierowanie, ale nie zawyżają analityki. To z założenia gruby filtr.

Skanowanie. url-scanner publikuje werdykt dla każdego linku przy tworzeniu i według harmonogramu. Edge czyta tylko zapisany werdykt (clean, suspect, blocked), a przekierowanie zatrzymuje tylko blocked.

Ewaluacja reguł. Smart linki podróżują z rekordem linku, więc reguły kraju, urządzenia i podobne działają na zbuforowanej kopii, a wyszukanie GeoIP odbywa się w procesie z lokalnej bazy. Trafienie w cache z regułami nie wymaga wywołania sieciowego do api-core.

Ustawienia serwera są skromne. cmd/server/main.go konfiguruje serwer z 2-sekundowym limitem odczytu i zapisu, 30-sekundowym limitem bezczynności i limitem treści żądania 64 KB. Wybraliśmy go zamiast net/http dla mniejszej alokacji na żądanie, jak zauważa README usługi. Nie opublikowaliśmy bezpośredniego benchmarku naszego handlera, więc to powód projektowy, a nie zmierzony wynik.

Jak zmierzyć to samemu

Repozytorium ma runbook testów obciążeniowych (docs/runbooks/19-load-testing.md) oparty na k6. Jego przedstartowy próg akceptacji dla gorącej ścieżki to 1,5 tys. żądań/s przez 5 minut przy p95 15 ms bez żadnego 5xx oraz 5 tys. żądań/s przez 1 minutę przy p95 30 ms z poniżej 0,1% 5xx. To progi, a nie wyniki.

Test z pobliskiego generatora obciążenia mierzy usługę. Test z odległego hosta mierzy Twoją sieć. Dla opóźnienia po stronie odwiedzającego sonduj z zewnątrz monitorem syntetycznym, jak w monitorowaniu przekierowań z Sentry i Datadog.

Szczerze, jeszcze nie cytowałbym klientowi 15 ms jako pomiaru. Luki, które zamknęlibyśmy przed wydrukowaniem tabeli per region: histogram opóźnień na edge, singleflight przy zimnych chybieniach, push-unieważnianie L1 i zatwierdzony benchmark stojący za wartością 15 ms.

Marius Voß jest DevRel i edge infra w Elido. Źródła: services/edge-redirect, services/api-core/internal/linkmod/adapters.go i docs/runbooks/19-load-testing.md; nie składamy żadnych deklaracji zewnętrznych dostawców.

Powiązane na blogu

Najczęściej zadawane pytania

Czy 15 ms p95 to liczba zmierzona?

Nie. 15 ms p95 (z p50 na poziomie 5 ms) przy trafieniu w cache to budżet opóźnienia usługi edge-redirect i to względem niego napisane są nasze alerty i próg akceptacji testu obciążeniowego. Ten wpis nie publikuje zmierzonych percentyli per region, bo nie mamy zatwierdzonego benchmarku ani pliku wyników, które mogłyby je poprzeć.

Jak przekierowanie Elido rozwiązuje slug?

Edge zagląda najpierw do LRU w procesie, potem do współdzielonego cache w pamięci i dopiero potem pyta api-core przez gRPC. Każda warstwa cache działa w trybie best-effort, więc tylko origin może zwrócić autorytatywny brak wyniku. Trafienie w niższej warstwie jest kopiowane do warstw powyżej.

Co zostaje poza ścieżką przekierowania?

Zapis kliknięć, analityka i kontrole url-scannera. Edge emituje zdarzenie kliknięcia do szyny zdarzeń w osobnej gorutynie i odpowiada odwiedzającemu bez czekania na nie. Filtrowanie botów to sprawdzenie user-agenta, które decyduje, czy kliknięcie jest liczone, a nie wyzwanie, które odwiedzający musi przejść.

Dlaczego Elido domyślnie używa 302?

302 mówi przeglądarkom, by nie traktowały celu jako stałego, więc edytowany link zadziała przy następnej wizycie. Link można ustawić indywidualnie na 301 lub 307. Kompromis omawia nasz wpis o 301 kontra 302.

Gdzie są punkty POP edge?

Dziś produkcja to jeden host z usługą edge za reverse proxy. Wieloregionowy edge (Europa, Ameryka Północna i Azja Południowo-Wschodnia, a w każdym ten sam binarny edge) jest planowany, nie działa. Główne bazy danych i szyna zdarzeń zostają w UE, a w tym projekcie obok węzłów edge leżałyby tylko dane cache przekierowań, jak opisuje ADR-0014.

Czy edytowany link może być serwowany w starej wersji?

Krótko tak. Gdy link się zmienia, api-core usuwa jego wpis we współdzielonym cache, ale kopia w procesie węzła edge żyje do wygaśnięcia 60-sekundowego TTL. Edycja celu może więc dotrzeć do każdego węzła nawet po minucie, co jest jednym z powodów, dla których przekierowania tymczasowe pasują do linków edytowalnych.

Wypróbuj Elido

Wklej URL, otrzymaj krótki link

Bez rejestracji. Link działa 30 dni. Zarejestruj się, aby zachować go na zawsze.

Bez rejestracji. Załóż darmowe konto i korzystaj z 50 linków miesięcznie.

Wypróbuj Elido

Skracarka URL hostowana w UE: własne domeny, głęboka analityka i otwarte API. Darmowy plan - bez karty kredytowej.

Tagi
url shortener performance
edge redirect latency
multi-region url shortener
redirect cache strategy
go redirect service
redirect latency budget

Czytaj dalej