Czasami więcej znaczy mniej. Zmniejszenie obciążenia powoduje zwiększenie opóźnień

Jak w większość postów, występuje problem z usługą rozproszoną, nazwijmy ją Alvin. Tym razem nie sam odkryłem problem, poinformowali mnie goście ze strony klienta.

Któregoś dnia obudził mnie niezadowolony e-mail z powodu dużych opóźnień w firmie Alvin, którą planowaliśmy uruchomić w najbliższej przyszłości. W szczególności klient doświadczył opóźnienia na poziomie 99. percentyla w zakresie 50 ms, czyli znacznie powyżej naszego budżetu opóźnień. Było to zaskakujące, ponieważ intensywnie testowałem usługę, szczególnie pod kątem opóźnień, które są częstą skargą.

Zanim umieściłem Alvina w testach, przeprowadziłem wiele eksperymentów z 40 tys. zapytań na sekundę (QPS), a wszystkie wykazały opóźnienia mniejsze niż 10 ms. Byłem gotowy oświadczyć, że nie zgadzam się z ich wynikami. Ale patrząc ponownie na list, zauważyłem coś nowego: nie przetestowałem dokładnie warunków, o których wspominali, ich QPS było znacznie niższe niż moje. Testowałem przy 40 tys. QPS, ale oni tylko przy 1 tys. Aby ich uspokoić, przeprowadziłem kolejny eksperyment, tym razem z niższym QPS.

Ponieważ piszę o tym na blogu, prawdopodobnie już zorientowałeś się, że ich liczby były prawidłowe. Wielokrotnie testowałem mojego wirtualnego klienta z tym samym rezultatem: mała liczba żądań nie tylko zwiększa opóźnienie, ale zwiększa liczbę żądań z opóźnieniem większym niż 10 ms. Innymi słowy, jeśli przy 40k QPS około 50 żądań na sekundę przekraczało 50 ms, to przy 1k QPS było 100 żądań powyżej 50 ms na sekundę. Paradoks!

Czasami więcej znaczy mniej. Zmniejszenie obciążenia powoduje zwiększenie opóźnień

Zawężanie poszukiwań

W przypadku problemu opóźnień w systemie rozproszonym składającym się z wielu komponentów pierwszym krokiem jest utworzenie krótkiej listy podejrzanych. Zagłębmy się nieco w architekturę Alvina:

Czasami więcej znaczy mniej. Zmniejszenie obciążenia powoduje zwiększenie opóźnień

Dobrym punktem wyjścia jest lista ukończonych przejść we/wy (połączenia sieciowe/wyszukiwanie dysku itp.). Spróbujmy dowiedzieć się, gdzie jest opóźnienie. Oprócz oczywistych operacji we/wy z klientem Alvin wykonuje dodatkowy krok: uzyskuje dostęp do magazynu danych. Jednak ta pamięć działa w tym samym klastrze co Alvin, więc opóźnienie powinno być tam mniejsze niż w przypadku klienta. A więc lista podejrzanych:

  1. Połączenie sieciowe od klienta do Alvina.
  2. Połączenie sieciowe od Alvina do magazynu danych.
  3. Wyszukaj na dysku w magazynie danych.
  4. Połączenie sieciowe z hurtowni danych do Alvina.
  5. Połączenie sieciowe od Alvina do klienta.

Spróbujmy przekreślić kilka punktów.

Przechowywanie danych nie ma z tym nic wspólnego

Pierwszą rzeczą, którą zrobiłem, było przekonwertowanie Alvina na serwer ping-ping, który nie przetwarza żądań. Po otrzymaniu żądania zwraca pustą odpowiedź. Jeśli opóźnienie maleje, błąd w implementacji Alvina lub hurtowni danych nie jest niczym niezwykłym. W pierwszym eksperymencie otrzymujemy następujący wykres:

Czasami więcej znaczy mniej. Zmniejszenie obciążenia powoduje zwiększenie opóźnień

Jak widać, nie ma poprawy podczas korzystania z serwera ping-ping. Oznacza to, że hurtownia danych nie zwiększa opóźnień, a lista podejrzanych zostaje skrócona o połowę:

  1. Połączenie sieciowe od klienta do Alvina.
  2. Połączenie sieciowe od Alvina do klienta.

Świetnie! Lista szybko się kurczy. Myślałem, że już prawie odkryłem przyczynę.

gRPC

Nadszedł czas, aby przedstawić Wam nowego gracza: gRPC. Jest to biblioteka typu open source firmy Google do komunikacji w procesie RPC, Chociaż gRPC dobrze zoptymalizowany i szeroko stosowany, użyłem go po raz pierwszy w systemie tej wielkości i spodziewałem się, że moja implementacja będzie co najmniej nieoptymalna.

dostępność gRPC na stosie zrodziło nowe pytanie: może to moja implementacja, albo ja gRPC powoduje problem z opóźnieniami? Dodanie nowego podejrzanego do listy:

  1. Klient dzwoni do biblioteki gRPC
  2. biblioteka gRPC wykonuje połączenie sieciowe z biblioteką na kliencie gRPC na serwerze
  3. biblioteka gRPC kontaktuje się z Alvinem (brak operacji w przypadku serwera ping-pongowego)

Aby dać ci wyobrażenie o tym, jak wygląda kod, moja implementacja klient/Alvin nie różni się zbytnio od implementacji klient-serwer przykłady asynchroniczne.

Uwaga: Powyższa lista jest nieco uproszczona, ponieważ gRPC umożliwia wykorzystanie własnego (szablonu?) modelu wątków, w którym przeplata się stos wykonawczy gRPC i wdrożenie użytkownika. Dla uproszczenia będziemy trzymać się tego modelu.

Profilowanie wszystko naprawi

Po przekreśleniu magazynów danych myślałem, że już prawie skończyłem: „Teraz to proste! Zastosujmy profil i dowiedzmy się, gdzie występuje opóźnienie.” I wielkim fanem precyzyjnego profilowania, ponieważ procesory są bardzo szybkie i najczęściej nie stanowią wąskiego gardła. Większość opóźnień ma miejsce, gdy procesor musi przerwać przetwarzanie, aby zająć się czymś innym. Dokładne profilowanie procesora właśnie to robi: dokładnie rejestruje wszystko przełączniki kontekstu i wyjaśnia, gdzie występują opóźnienia.

Wziąłem cztery profile: z wysokim QPS (niskie opóźnienie) i z serwerem ping-pongowym z niskim QPS (duże opóźnienie), zarówno po stronie klienta, jak i po stronie serwera. I na wszelki wypadek wziąłem też przykładowy profil procesora. Porównując profile, zwykle szukam nietypowego stosu wywołań. Na przykład wadą związaną z dużym opóźnieniem jest znacznie więcej przełączników kontekstu (10 razy lub więcej). Ale w moim przypadku liczba przełączeń kontekstu była prawie taka sama. Ku mojemu przerażeniu nie było tam nic istotnego.

Dodatkowe debugowanie

Byłem zdesperowany. Nie wiedziałem, jakich innych narzędzi mógłbym użyć, więc mój następny plan polegał zasadniczo na powtórzeniu eksperymentów z różnymi odmianami, zamiast jasnej diagnozy problemu.

Co jeśli

Od samego początku obawiałem się specyficznego opóźnienia wynoszącego 50 ms. To bardzo ważny czas. Zdecydowałem, że wytnę fragmenty kodu, dopóki nie będę mógł dokładnie ustalić, która część powoduje ten błąd. Potem przyszedł eksperyment, który zadziałał.

Jak zwykle z perspektywy czasu wydaje się, że wszystko było oczywiste. Umieściłem klienta na tej samej maszynie co Alvin i wysłałem żądanie do localhost. I wzrost opóźnień zniknął!

Czasami więcej znaczy mniej. Zmniejszenie obciążenia powoduje zwiększenie opóźnień

Coś było nie tak z siecią.

Nauka umiejętności inżyniera sieci

Muszę przyznać: moja wiedza na temat technologii sieciowych jest straszna, zwłaszcza biorąc pod uwagę fakt, że pracuję z nimi na co dzień. Ale głównym podejrzanym była sieć i musiałem nauczyć się ją debugować.

Na szczęście Internet kocha tych, którzy chcą się uczyć. Kombinacja ping i tracert wydawała się wystarczająco dobrym początkiem do debugowania problemów z transportem sieciowym.

Po pierwsze uruchomiłem PsPing do portu TCP Alvina. Użyłem ustawień domyślnych - nic specjalnego. Z ponad tysiąca pingów żaden nie przekroczył 10 ms, z wyjątkiem pierwszego, który był wykonywany podczas rozgrzewki. Jest to sprzeczne z obserwowanym wzrostem opóźnienia o 50 ms na 99. percentylu: w tym przypadku na każde 100 żądań powinniśmy zobaczyć około jednego żądania z opóźnieniem 50 ms.

Potem próbowałem tracert: Może występować problem w jednym z węzłów na trasie pomiędzy Alvinem a klientem. Ale tropiciel również wrócił z pustymi rękami.

Zatem to nie mój kod, implementacja gRPC ani sieć spowodowały opóźnienie. Zacząłem się martwić, że nigdy tego nie zrozumiem.

Teraz na jakim systemie operacyjnym jesteśmy

gRPC szeroko stosowany w systemie Linux, ale egzotyczny w systemie Windows. Postanowiłem przeprowadzić eksperyment, który zadziałał: stworzyłem maszynę wirtualną z Linuksem, skompilowałem Alvina dla Linuksa i wdrożyłem go.

Czasami więcej znaczy mniej. Zmniejszenie obciążenia powoduje zwiększenie opóźnień

I oto co się stało: Linuxowy serwer ping-pongowy nie miał takich samych opóźnień jak podobny host Windows, chociaż źródło danych nie było inne. Okazuje się, że problem tkwi w implementacji gRPC dla Windows.

Algorytm Nagle’a

Przez cały ten czas myślałem, że brakuje mi flagi gRPC. Teraz rozumiem, co to naprawdę jest gRPC Brak flagi systemu Windows. Znalazłem wewnętrzną bibliotekę RPC, co do której miałem pewność, że będzie dobrze działać dla wszystkich ustawionych flag Winsock. Następnie dodałem wszystkie te flagi do gRPC i wdrożyłem Alvin w systemie Windows na załatanym serwerze ping-pongowym Windows!

Czasami więcej znaczy mniej. Zmniejszenie obciążenia powoduje zwiększenie opóźnień

Prawie Gotowe: zacząłem usuwać dodane flagi pojedynczo, aż regresja powróciła, aby móc określić przyczynę. To było niesławne TCP_NODELAY, przełącznik algorytmu Nagle’a.

Algorytm Nagle’a próbuje zmniejszyć liczbę pakietów wysyłanych przez sieć, opóźniając transmisję wiadomości do momentu, gdy rozmiar pakietu przekroczy określoną liczbę bajtów. Chociaż może to być miłe dla przeciętnego użytkownika, jest destrukcyjne dla serwerów czasu rzeczywistego, ponieważ system operacyjny opóźni niektóre wiadomości, powodując opóźnienia przy niskim QPS. U gRPC ta flaga została ustawiona w implementacji systemu Linux dla gniazd TCP, ale nie w systemie Windows. jestem tym poprawione.

wniosek

Większe opóźnienia przy niskim QPS były spowodowane optymalizacją systemu operacyjnego. Z perspektywy czasu profilowanie nie wykryło opóźnienia, ponieważ zostało wykonane w trybie jądra, a nie w tryb użytkownika. Nie wiem, czy algorytm Nagle'a można zaobserwować poprzez przechwytywanie ETW, ale byłoby to interesujące.

Jeśli chodzi o eksperyment z hostem lokalnym, prawdopodobnie nie dotknął on rzeczywistego kodu sieciowego, a algorytm Nagle'a nie działał, więc problemy z opóźnieniami zniknęły, gdy klient skontaktował się z Alvinem za pośrednictwem hosta lokalnego.

Następnym razem, gdy zauważysz wzrost opóźnienia w miarę zmniejszania się liczby żądań na sekundę, algorytm Nagle'a powinien znaleźć się na Twojej liście podejrzanych!

Źródło: www.habr.com

Dodaj komentarz