Jak w , pojawił się problem z rozproszoną usługą, nazwijmy tę usługę Elwin. Tym razem nie odkryłem problemu sam, poinformowali mnie o tym chłopaki z części klienckiej.
Pewnego dnia obudziłem się od niezadowolonego e-maila z powodu dużych opóźnień u Elwina, którego planowaliśmy uruchomić w najbliższym czasie. W szczególności klient napotkał opóźnienie 99. percentyla rzędu 50 ms, znacznie wyższe niż nasz budżet opóźnień. To było zaskakujące, ponieważ starannie testowałem usługę, szczególnie pod kątem opóźnień, gdyż to temat częstych skarg.
Zanim oddałem Elwina do testów, przeprowadziłem wiele eksperymentów z 40 tys. zapytań na sekundę (QPS), wszystkie wykazały opóźnienie poniżej 10 ms. Byłem gotów stwierdzić, że nie zgadzam się z ich wynikami. Ale jeszcze raz spojrzawszy na e-mail, zwróciłem uwagę na coś nowego: dokładnie nie testowałem warunków, które wspomnieli, ich QPS był znacznie niższy niż mój. Testowałem przy 40k QPS, a oni tylko przy 1k. Przeprowadziłem jeszcze jeden eksperyment, tym razem z niższym QPS, po prostu żeby ich zadowolić.
Ponieważ piszę o tym na blogu - prawdopodobnie już zrozumiałeś: ich liczby okazały się prawidłowe. Sprawdzałem swojego wirtualnego klienta raz za razem, wciąż z tym samym wynikiem: niska liczba zapytań nie tylko zwiększa opóźnienie, ale też zwiększa liczbę zapytań z opóźnieniem powyżej 10 ms. Innymi słowy, przy 40k QPS około 50 zapytań na sekundę przekraczało 50 ms, natomiast przy 1k QPS każdą sekundę było 100 zapytań powyżej 50 ms. Paradoks!

Zawężamy krąg poszukiwań
Stykając się z problemem opóźnienia w rozproszonej systemie z wieloma komponentami, najpierw należy sporządzić krótki wykaz podejrzanych. Zbadajmy nieco głębiej architekturę Elwina:

Dobrą punktą wyjścia jest lista zrealizowanych przejść wejścia-wyjścia (wywołania sieciowe/wyszukiwanie na dysku itp.). Spróbujmy dowiedzieć się, gdzie występuje opóźnienie. Oprócz oczywistych wejść-wyjść z klientem, Elwin wykonuje dodatkowy krok: odwołuje się do magazynu danych. Jednak to magazyn działa w tym samym klastrze co Elwin, więc opóźnienie tam powinno być mniejsze niż z klientem. A więc, lista podejrzanych:
- Wywołanie sieciowe od klienta do Elwina.
- Wywołanie sieciowe od Elwina do magazynu danych.
- Wyszukiwanie na dysku w magazynie danych.
- Wywołanie sieciowe z magazynu danych do Elwina.
- Wywołanie sieciowe od Elwina do klienta.
Spróbujmy wykreślić niektóre punkty.
Magazyn danych nie ma w tym nic wspólnego.
Na początku przekształciłem Elwina w serwer ping-ping, który nie przetwarza żądań. Otrzymując żądanie, zwraca pustą odpowiedź. Jeśli opóźnienie maleje, to oznacza błąd w implementacji Elwina lub magazynu danych – nic nadzwyczajnego. W pierwszym eksperymencie uzyskujemy taki wykres:

Jak widać, korzystając z serwera ping-ping, nie zaobserwowano żadnych usprawnień. Oznacza to, że magazyn danych nie zwiększa opóźnienia, a lista podejrzanych zostaje zredukowana o połowę:
- Wywołanie sieciowe od klienta do Elwina.
- Wywołanie sieciowe od Elwina do klienta.
Super! Lista szybko się skraca. Myślałem, że prawie odkryłem przyczynę.
gRPC
Teraz nadszedł czas, aby przedstawić wam nowego gracza: . To biblioteka z otwartym źródłem od Google do komunikacji wewnątrz procesów. . Choć gRPC jest dobrze zoptymalizowana i powszechnie używana, to pierwszy raz stosuję ją w systemie tej skali, a moja realizacja wydawała mi się nieoptymalna – delikatnie mówiąc.
Obecność gRPC w stosie spowodowało nowe pytanie: może to moja realizacja lub sam gRPC wywołuje problem opóźnienia? Dodajemy do listy nowego podejrzanego:
- Klient wywołuje bibliotekę,
gRPC - Biblioteka
gRPCna kliencie realizuje wywołanie sieciowe biblioteki,gRPCna serwerze - Biblioteka
gRPCzwraca się do Elwina (brak operacji w przypadku serwera ping-pong).
Abyś zrozumiał, jak wygląda kod, moja realizacja klienta/Elwina niewiele różni się od przykładów klient-serwer. .
Uwaga: powyższa lista jest trochę uproszczona, ponieważ
gRPCumożliwia wykorzystanie własnego (szablonowego?) modelu wątków, w którym przeplatają się stos wykonaniagRPCi realizacja użytkownika. Dla prostoty będziemy trzymać się tego modelu.
Profilowanie wszystko naprawi.
Po wykluczeniu magazynów danych pomyślałem, że prawie skończyłem: „Teraz łatwo! Zastosujemy profil i dowiemy się, gdzie występuje opóźnienie”. Jestem ponieważ CPU jest bardzo szybkie i najczęściej nie stanowi wąskiego gardła. Większość opóźnień występuje, gdy procesor musi zatrzymać przetwarzanie, aby zrobić coś innego. Dokładne profilowanie CPU zostało dokładnie zaprojektowane do tego celu: precyzyjnie rejestruje wszystkie i pozwala zrozumieć, gdzie występują opóźnienia.
Wybrałem cztery profile: dla wysokiego QPS (niska latencja) oraz z serwerem ping-pong przy niskim QPS (wysoka latencja), zarówno po stronie klienta, jak i po stronie serwera. Dla pewności wziąłem też próbkę profilu procesora. Porównując profile, zazwyczaj szukam anormalnego stosu wywołań. Na przykład, po złej stronie z wysoką latencją występuje znacznie więcej przełączeń kontekstu (dziesięciokrotnie i więcej). Jednak w moim przypadku liczba przełączeń kontekstu była niemal identyczna. Ku mojemu przerażeniu, nie znaleziono nic istotnego.
Dodatkowe debugowanie
Byłem w rozpaczy. Nie wiedziałem, jakich narzędzi można jeszcze użyć, a mój kolejny plan polegał zasadniczo na powtórzeniu eksperymentów z różnymi wariantami, zamiast na precyzyjnym diagnozowaniu problemu.
Co jeśli
Od samego początku niepokoił mnie konkretny czas opóźnienia 50 ms. To bardzo duży czas. Zdecydowałem, że będę wycinał fragmenty z kodu, aż dowiem się, która część powoduje ten błąd. Następnie przeprowadziłem eksperyment, który zadziałał.
Jak to zwykle bywa, z perspektywy czasu wszystko wydaje się oczywiste. Umieściłem klienta na jednej maszynie z Elvinem - i wysłałem zapytanie do localhost. I opóźnienie zniknęło!

Coś było nie tak z siecią.
Opanowując umiejętności inżyniera sieciowego
Muszę przyznać: moja wiedza o technologiach sieciowych jest straszna, zwłaszcza biorąc pod uwagę, że pracuję z nimi na co dzień. Ale sieć była głównym podejrzanym, więc musiałem nauczyć się, jak ją debugować.
Na szczęście, internet jest przyjazny dla tych, którzy chcą się uczyć. Połączenie ping i tracert wydawało się dobrym początkiem do debugowania problemów z transportem sieciowym.
Po pierwsze, uruchomiłem na porcie TCP Elvina. Użyłem domyślnych ustawień - nic nadzwyczajnego. Z ponad tysiąca pingów żaden nie przekroczył 10 ms, poza pierwszym dla rozgrzewki. To jest w sprzeczności z zaobserwowanym wzrostem opóźnienia 50 ms w 99. percentylu: przy 100 zapytaniach powinniśmy zobaczyć około jednego zapytania z opóźnieniem 50 ms.
Następnie spróbowałem : być może problem leży na jednym z węzłów na trasie między Elvinem a klientem. Ale tracert również wrócił z pustymi rękami.
W ten sposób przyczyną opóźnienia nie był mój kod, nie implementacja gRPC ani sieć. Już zaczynałem się martwić, że nigdy tego nie zrozumiem.
Na jakim systemie operacyjnym się znajdujemy
gRPC jest szeroko stosowany w systemie Linux, ale w przypadku Windows to egzotyka. Postanowiłem przeprowadzić eksperyment, który się powiódł: stworzyłem maszynę wirtualną Linux, skompilowałem Elvina dla Linux i uruchomiłem ją.

I oto, co się stało: w serwerze ping-pong na Linuxie nie było opóźnień, jak w analogicznym węźle Windows, chociaż źródło danych się nie różniło. Okazało 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 zrozumiałem, że tak naprawdę brakuje mi flagi Windows. Znalazłem wewnętrzną bibliotekę RPC, w której byłem pewien, że działa poprawnie dla wszystkich zainstalowanych flag gRPC Winsock Prawie

gotowe: zacząłem usuwać dodane flagi jedną po drugiej, aż regresja wróciła, więc mogłem dokładnie określić jej przyczynę. Był to nieszczęsny , przełącznik algorytmu Nagle'a. próbuje zmniejszyć liczbę pakietów wysyłanych przez sieć przez opóźnienie przesyłania wiadomości, aż 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. Ten
flaga była ustawiona w implementacji Linux dla gniazd TCP, ale nie dla Windows. Ja to gRPC Duże opóźnienia przy niskim QPS były spowodowane optymalizacją systemu operacyjnego. Patrząc wstecz, profilowanie nie wykryło opóźnienia, ponieważ było przeprowadzane w trybie jądra, a nie w .
Podsumowanie
trybie użytkownika. Jeśli chodzi o eksperyment localhost, prawdopodobnie nie dotyczył rzeczywistego kodu sieciowego, więc algorytm Nagle'a się nie uruchomił, dlatego problemy z opóźnieniem zniknęły, gdy klient skontaktował się z Elvinem przez localhost.
Następnym razem, gdy zauważysz wzrost opóźnienia przy spadku liczby żądań na sekundę, algorytm Nagle'a powinien być na twojej liście podejrzanych!
Jak w większości postów, pojawił się problem z rozproszoną usługą, nazwijmy tę usługę Elvin.
Źródło: habr.com
