Kiedy reverse proxy jest dobrym rozwiązaniem, a kiedy zakrywa problem

When a Reverse Proxy Is the Right Answer and When It Hides a Problem

Mało który element infrastruktury trafia do wdrożenia tak bezrefleksyjnie jak reverse proxy. Pojawia się w pierwszym sprincie nowego wdrożenia, przejmuje certyfikat TLS, dostaje jedną regułę routingu, a potem przez lata po cichu zbiera kolejne dyrektywy. Większość z nich to rozsądne decyzje infrastrukturalne. Część to blizny: obejście, które ktoś napisał o trzeciej w nocy, żeby uciszyć alert, i do którego nikt nie wrócił, gdy tylko presja opadła. Odróżnienie jednego od drugiego to jeden z bardziej opłacalnych audytów, jakie może przeprowadzić zespół inżynierski, bo proxy, które ukrywa usterki, będzie je ukrywać do momentu, aż popsuje się coś większego.

Co reverse proxy naprawdę robi

Jeśli odrzucić żargon, reverse proxy jest pośrednikiem. Kończy połączenie klienta i otwiera własne połączenie do jednej lub kilku usług upstream. Klient myśli, że rozmawia z twoją aplikacją. Rozmawia z programem, który decyduje, gdzie żądanie powędruje dalej. Forward proxy stoi po stronie klienta i reprezentuje ruch wychodzący użytkowników. Load balancer rozkłada ruch na równorzędne backendy. API gateway dokłada uwierzytelnianie, limity i kwestie schematu. Implementacje mocno się pokrywają - ta sama binarka często pełni kilka z tych ról naraz - ale intencja jest inna, a mieszanie intencji to prosta droga do konfiguracji, której nikt nie umie przeczytać.

Zestaw podstawowych funkcji jest niewielki i niewiele się zmienił: terminacja TLS, routing żądań po hoście lub ścieżce, przepisywanie nagłówków, pula połączeń do upstreamu, buforowanie odpowiedzi i cache. Jakiekolwiek oprogramowanie wstawisz w to miejsce, zajmuje ono tę samą pozycję w architekturze. A pozycja ta stała się domyślnym pierwszym przystankiem, bo prawie każde wdrożenie prędzej czy później potrzebuje co najmniej dwóch z tych funkcji, a nikt nie chce budować ich po raz drugi w kodzie aplikacji. Jeśli konfigurujesz je od zera, instrukcja krok po kroku oszczędza odkrywania tych samych ustawień domyślnych metodą prób i błędów.

Sytuacje, w których reverse proxy jest właściwym narzędziem

Terminacja TLS w jednym miejscu to argument najmocniejszy, bezdyskusyjnie. Certyfikaty, polityka szyfrów i automatyczne odnawianie żyją poza kodem aplikacji, więc aktualizacja środowiska uruchomieniowego języka nigdy nie zagrozi konfiguracji handshake’u. Drugi to serwowanie kilku aplikacji spod jednego publicznego adresu i portu, z routingiem po nazwie hosta albo prefiksie. Trzeci, niedoceniany, to buforowanie: backend przywiązany do wątku lub procesu, zajęty przez klienta sączącego bajty przez połączenie komórkowe, to zmarnowana moc przerobowa, a pośrednik, który przyjmie na siebie wolnego klienta, oddaje tę moc z powrotem.

Poza tym proxy nie budzi kontrowersji w takich sytuacjach:

  • Zasoby statyczne, żądania zakresu bajtów i kompresja, obsługiwane przez oprogramowanie napisane specjalnie do tego
  • Wdrożenia blue-green i canary, które przełączają ruch bez ruszania logiki aplikacji
  • Wygaszanie trwających połączeń podczas wdrożenia, żeby nikt nie zobaczył zerwanej sesji
  • Limitowanie ruchu i polityka dostępu na poziomie IP, która musi zadziałać przed uruchomieniem jakiegokolwiek kodu
  • Zebranie logów dostępu z różnorodnych usług w jednym miejscu

Wskazówka: trzymaj konfigurację proxy w tym samym repozytorium i tym samym procesie przeglądu co kod aplikacji. Konfiguracja, która omija przegląd, to konfiguracja, której nikt nie pamięta.

Maskowanie objawów: proxy jako środek przeciwbólowy

Oto antywzorzec, który widuję raz za razem. Endpoint zwalnia, więc ktoś podnosi timeout odczytu na proxy. Alert milknie. W rzeczywistości szybka i czytelna awaria zamieniła się w długie zawieszenie, a każdy klient trzyma teraz otwarte połączenie znacznie dłużej, podczas gdy wolne zapytanie leży sobie nietknięte. Ponawianie skonfigurowane na warstwie proxy tylko pogarsza sprawę: i tak już przeciążony upstream dostaje zdublowany ruch, a w sprzyjających warunkach wolumen ponowień zaczyna się sam napędzać.

Na szczególną nieufność zasługuje cache. Zakeszowanie odpowiedzi, która jest zepsuta albo niedeterministyczna, nie naprawia jej. Sprawia tylko, że usterka wychodzi wyłącznie przy chybieniu cache’a, a trudniej odtwarzalnego profilu błędu raczej nie ma. Przepisywanie nagłówków i ścieżek, które kompensuje generowanie złych adresów przez aplikację, przenosi błąd z miejsca objętego testami do miejsca bez testów. Buforowanie potrafi ukryć backend źle obsługujący strumieniowanie - dokładnie do chwili, gdy ładunek przekroczy rozmiar bufora i zachowanie zmieni się bez żadnego ostrzeżenia.

Jak odróżnić naprawę od plastra

Trzy pytania oddzielają realną politykę infrastrukturalną od zamiatania pod dywan. Zadaj je, zanim jakakolwiek zmiana w proxy trafi do gałęzi głównej.

  1. Czy zmiana dotyka tylko obserwowanego zachowania, czy samej przyczyny? Jeśli backend nadal robi coś źle, a klient po prostu przestaje to zauważać, masz plaster.
  2. Czy usunięcie tej dyrektywy natychmiast przywróciłoby pierwotną awarię? Uczciwe „tak” oznacza, że dyrektywa jest nośna dla usterki, a nie dla architektury.
  3. Czy to decyzja polityczna, czy obejście? Minimalna wersja TLS, limity ruchu i maksymalny rozmiar treści to polityka. Timeout podniesiony pod jeden endpoint już nie.

Wskazówka: każdej dyrektywie będącej obejściem przypisz właściciela i warunek usunięcia, a nie samą datę. Daty mijają po cichu, warunki da się przetestować.

Obserwowalność: dlaczego proxy jednocześnie pomaga i zaślepia

Proxy to najlepszy punkt obserwacyjny dla liczby żądań, rozkładu kodów statusu i opóźnienia end-to-end, bo widzi każde żądanie niezależnie od tego, czy backend je przeżył. Ta zaleta ma swój odpowiednik po stronie wad. Odrębne awarie upstreamu zlewają się w ogólne statusy bramy, więc odmowa połączenia, timeout i zniekształcona odpowiedź lądują na dashboardzie z tym samym numerem. Pierwotna przyczyna znika, chyba że świadomie ją zachowasz.

Adres klienta, protokół i oryginalny host też znikają, chyba że nagłówki przekazywania są poprawnie ustawione i - co równie ważne - honorowane tylko od znanych pośredników. Ta sama ślepa plamka pojawia się w metadanych infrastruktury: co zdradzają twoje rekordy PTR o wdrożeniu, bywa jedyną zewnętrzną wskazówką, który host faktycznie stoi za danym adresem. Kontekst śledzenia trzeba propagować jawnie, inaczej każde żądanie w rozproszonych śladach wygląda, jakby zaczęło się na proxy, co przerywa łańcuch dokładnie tam, gdzie jest najważniejszy. Porównanie opóźnienia widzianego przez proxy z opóźnieniem widzianym po stronie upstreamu ujawnia kolejkowanie. Gdy różnica rośnie, żądania gdzieś czekają, a ta różnica mówi więcej niż każda z tych liczb osobno.

Wskazówka: loguj czas nawiązania połączenia z upstreamem, czas do nagłówków i pełny czas odpowiedzi jako osobne pola, a nie jeden zbiorczy czas trwania.

Awarie, które dokłada sama warstwa proxy

Dodanie warstwy dodaje zależność, w tym zależność od tego, jak zachowuje się ona przy przeładowaniu konfiguracji. Poza tym kilka trybów awarii wynika wprost z zajmowanej przez nią pozycji:

  • Niedopasowane timeouty. Gdy proxy poddaje się przed upstreamem, a upstream przed bazą danych, praca mieli się dalej dla żądania, na które nikt już nie czeka.
  • Keepalive i ponowne użycie połączeń. Połączenia z puli potrafią trafić z żądaniem do backendu, który już zaczął się wygaszać.
  • Limity rozmiaru. Ograniczenia nagłówków, treści i buforów wywalają się tylko przy największych poprawnych żądaniach, czyli przechodzą przez każdy zestaw testów.
  • Rozbieżne ramkowanie. Gdy proxy i upstream inaczej rozpoznają granice żądań, otwiera się możliwość przemycania żądań.
  • Przypadki brzegowe certyfikatów i SNI. Uderzają w część klientów, często tych starszych, i nigdy w twojej przeglądarce.

Wskazówka: ustaw timeouty upstreamu krócej niż timeouty proxy, żeby to źródło padało pierwsze i mówiło dlaczego, zamiast zostawiać ci goły błąd bramy.

Praktyczna procedura przeglądu istniejących konfiguracji proxy

Zacznij od spisania każdej dyrektywy odbiegającej od domyślnych ustawień producenta, a potem zapytaj, jaki incydent stoi za każdą z nich. Jeśli nikt nie pamięta, to już jest ustalenie. Posortuj wyniki na trzy kategorie - świadoma polityka, strojenie wydajności i obejście - a tę trzecią potraktuj jak dług z realnym kosztem. Zespoły, które nie mają czasu robić tego same, mogą oddać audyt komuś, kto robi to w ramach przeglądu infrastruktury.

Usunięcie podejrzanych obejść przetestuj na środowisku staging, używając realistycznych rozmiarów ładunku i wolnych klientów, a nie porządnych syntetycznych żądań. Większość zamaskowanych usterek wraca dopiero w realistycznych warunkach. To, co przetrwa, śledź w tym samym backlogu co błędy aplikacji, żeby te pozycje walczyły o realny priorytet, zamiast siedzieć w dokumencie infrastrukturalnym, którego nikt nie otwiera.

Wskazówka: gdy obejście musi zostać, podepnij pod nie alert, który odpali, jeśli maskowany stan się pogorszy. Podniesiony timeout jest do zniesienia, jeśli dowiesz się, kiedy opóźnienie u podstawy się podwoi.

Wybieraj świadomie, nie odruchowo

Małe wdrożenia mogą spokojnie odłożyć tę warstwę do czasu, aż uzasadni ją złożoność routingu albo certyfikatów. Dodanie jej wcześnie kupuje elastyczność, której możesz nie potrzebować, i powierzchnię, którą i tak trzeba utrzymywać. Kiedykolwiek już ją dołożysz, trzymaj każdą dyrektywę przy jednym standardzie: nowy inżynier powinien w niecałą minutę wywnioskować, po co ona tam jest. Wszystko, co tego testu nie przechodzi, jest albo nieudokumentowaną polityką, albo usterką w przebraniu, a jedno i drugie warto znaleźć, zanim znajdzie je za ciebie incydent. Więcej notatek o codziennych decyzjach infrastrukturalnych trzyma się tej samej zasady.

Przewijanie do góry