Who moved my 99th percentile latency?

Coauthor: Cuong Tran

Długie opóźnienia wpływają na członków każdego dnia, a poprawa czasów odpowiedzi systemów nawet na 99. percentylu jest krytyczna dla doświadczenia członka. Może być wiele przyczyn, takich jak powolne aplikacje, powolny dostęp do dysku, błędy w sieci i wiele innych. Napotkaliśmy główną przyczynę ruchu microbursting, która nie może być łatwo rozwiązana przez strategię hedging your bet, tj. wysyłanie tego samego żądania do wielu serwerów w nadziei, że jeden z serwerów nie będzie miał wpływu na opóźnienia longtail. W poniższym poście podzielimy się naszą metodologią w celu ustalenia przyczyny opóźnień typu „longtail”, doświadczeniami i wnioskami.

Opóźnienia sieciowe pomiędzy maszynami w centrum danych mogą być niskie. Ogólnie rzecz biorąc, cała komunikacja trwa kilka mikrosekund, ale raz na jakiś czas niektóre pakiety zajmują kilka milisekund. Pakiety, które zajmują kilka milisekund, zazwyczaj należą do 90 percentyla lub wyżej latencji. Latencje longtail występują, gdy te wysokie percentyle zaczynają mieć wartości, które wykraczają daleko poza średnią i mogą być o wiele większe niż średnia. Tak więc średnie opóźnienia dają tylko połowę historii. Poniższy wykres pokazuje różnicę pomiędzy dobrym rozkładem opóźnień a tym z długim ogonem. Jak widać, 99 percentyl jest 30 razy gorszy od mediany, a 99,9 percentyl jest 50 razy gorszy!

Długie ogony naprawdę mają znaczenie!

Opóźnienie 99 percentyla wynoszące 30 ms oznacza, że co 1 na 100 żądań doświadcza 30 ms opóźnienia. Dla witryny o dużym natężeniu ruchu, takiej jak LinkedIn, może to oznaczać, że dla strony z 1 milionem odsłon dziennie wtedy 10 000 z tych odsłon doświadcza opóźnienia. Jednak większość systemów w dzisiejszych czasach to systemy rozproszone i 1 żądanie może w rzeczywistości tworzyć wiele żądań w dół. Więc 1 żądanie może utworzyć 2 żądania, lub 10, lub nawet 100! Jeśli wiele żądań downstream uderzy w pojedynczą usługę dotkniętą latencją longtail, nasz problem staje się straszniejszy.

Aby zilustrować, powiedzmy, że 1 żądanie klienta tworzy 10 żądań downstream do podsystemu dotkniętego latencją longtail. I załóżmy, że ma on 1% prawdopodobieństwo powolnego reagowania na pojedyncze żądanie. Następnie prawdopodobieństwo, że co najmniej 1 z 10 żądań downstream są dotknięte opóźnieniami longtail jest równoważne uzupełnieniu wszystkich żądań downstream odpowiadających szybko (99% prawdopodobieństwo odpowiadania szybko na każde pojedyncze żądanie), które jest:

To jest 9,5 procent! Oznacza to, że 1 żądanie klienta ma prawie 10 procent szans na bycie dotkniętym przez powolną odpowiedź. Jest to równoznaczne z oczekiwaniem, że 100 000 żądań klientów zostanie dotkniętych z 1 miliona żądań klientów. To jest dużo członków!

Jednakże nasz poprzedni przykład nie bierze pod uwagę, że aktywni członkowie zazwyczaj przeglądają więcej niż jedną stronę i jeśli ten pojedynczy użytkownik wykonuje to samo żądanie klienta wiele razy, prawdopodobieństwo, że użytkownik jest dotknięty problemami z opóźnieniami wzrasta dramatycznie. Dlatego bardzo aktywna usługa backendowa dotknięta przez opóźnienia typu longtail może mieć poważny wpływ na całą witrynę.

Studium przypadku

Mieliśmy ostatnio okazję zbadać jeden z naszych systemów rozproszonych, który doświadczył opóźnień sieciowych typu longtail. Problem ten czaił się od kilku miesięcy, a pobieżne badania nie wykazały żadnych oczywistych przyczyn opóźnień sieciowych. Postanowiliśmy przeprowadzić bardziej dogłębne dochodzenie, aby znaleźć przyczynę problemu. W tym wpisie na blogu chcieliśmy podzielić się naszym doświadczeniem i metodologią, której użyliśmy do zidentyfikowania pierwotnej przyczyny poprzez następujące studium przypadku.

Krok 1: Posiadanie kontrolowanego i uproszczonego środowiska

Początkowo skonfigurowaliśmy środowisko testowe rzeczywistego systemu produkcyjnego. Uprościliśmy system do kilku maszyn, które mogły odtworzyć opóźnienia sieciowe typu longtail. Ponadto, wyłączyliśmy logowanie i persystencję danych cache na dysku, aby wyeliminować IO-stres. To pozwoliło nam skupić naszą uwagę na kluczowych komponentach, takich jak procesor i sieć. Upewniliśmy się również, że skonfigurowaliśmy symulowane przebiegi ruchu, które mogliśmy powtarzać w celu uzyskania powtarzalnych testów podczas przeprowadzania eksperymentów i dostrajania systemów. Poniższy diagram pokazuje nasze środowisko testowe, które składało się z warstwy API, serwera cache oraz małego klastra baz danych.

Na wysokim poziomie, żądania z zewnętrznych usług przychodzą do rozproszonego systemu poprzez warstwę API. Żądania są następnie kierowane do serwera cache w celu realizacji zapytań. Jeśli dane nie znajdują się w pamięci podręcznej, serwer pamięci podręcznej wykona żądania do klastra bazy danych, aby utworzyć odpowiedź na zapytanie.

Krok 2: Pomiar opóźnień end-to-end

Kolejnym krokiem było przyjrzenie się szczegółowym opóźnieniom end-to-end. W ten sposób mogliśmy spróbować wyizolować nasze opóźnienia typu longtail i zobaczyć, który komponent w naszym rozproszonym systemie miał wpływ na obserwowane przez nas opóźnienia. Podczas symulacji ruchu, użyliśmy narzędzia ping pomiędzy różnymi parami pomiędzy hostem warstwy API, hostem serwera cache i jednym z hostów klastra bazy danych w celu zmierzenia opóźnień. Poniżej pokazano 99 percentyl latencji pomiędzy parą hostów:

Z tych wstępnych pomiarów wywnioskowaliśmy, że serwer pamięci podręcznej miał problem z latencją długiego ogona. Eksperymentowaliśmy dalej, aby zweryfikować te ustalenia i stwierdziliśmy, co następuje:

  1. Głównym problemem były opóźnienia 99 percentyla dla ruchu przychodzącego do serwera pamięci podręcznej.
  2. Opóźnienia 99. percentyla zostały zmierzone do innych hostów znajdujących się w tej samej szafie co serwer pamięci podręcznej i nie miało to wpływu na żadne inne hosty.
  3. 99 percentyl latencji został zmierzony również z ruchem TCP, UDP i ICMP i cały ruch przychodzący do Serwera Cache był dotknięty.

Kolejnym krokiem było rozbicie sieci i stosu protokołów podejrzanego serwera cache. Robiąc to, mieliśmy nadzieję wyizolować część serwera cache, która miała wpływ na opóźnienia longtail. Nasze pomiary opóźnień od końca do końca pokazane są poniżej:

Wykonaliśmy te pomiary implementując prostą aplikację UDP request/response w C i wykorzystaliśmy znaczniki czasu dostarczone przez system Linux dla ruchu sieciowego. Możesz zobaczyć przykład w dokumentacji jądra dla funkcji w pliku timestamping.c, aby uzyskać szczegółowe informacje o tym, kiedy pakiety trafiają na kartę interfejsu sieciowego i gniazda. Warto również zauważyć, że niektóre karty interfejsu sieciowego zapewniają sprzętowy timestamping, który pozwala na uzyskanie informacji o tym, kiedy pakiety faktycznie przechodzą przez kartę sieciową; jednakże nie wszystkie karty to obsługują. Więcej informacji na ten temat można znaleźć w dokumencie RedHat. Użyliśmy również tcpdumps w systemie, aby móc zobaczyć, kiedy żądania/odpowiedzi są przetwarzane na poziomie protokołu przez system operacyjny.

Krok 3: Eliminacja i eksperymenty

Po tym, jak zidentyfikowaliśmy, że problem opóźnienia był pomiędzy sprzętem karty interfejsu sieciowego a warstwą protokołu systemu operacyjnego, skupiliśmy się mocno na tych częściach systemu. Ponieważ karta interfejsu sieciowego (NIC) mogła stanowić potencjalny problem, zdecydowaliśmy się zbadać ją w pierwszej kolejności i pracować w górę stosu, aby wyeliminować różne warstwy. Przyglądając się każdemu komponentowi, pamiętaliśmy o następujących kwestiach: Fairness, Contentions i Saturation. Te trzy kluczowe obszary pomagają znaleźć potencjalne wąskie gardła lub problemy z opóźnieniami.

  • Sprawiedliwość: Czy podmioty w systemie otrzymują swój sprawiedliwy udział czasu lub zasobów do przetwarzania lub zakończenia? Na przykład, czy każda aplikacja w systemie otrzymuje odpowiednią ilość czasu na uruchomienie na procesorach, aby wykonać swoje zadania? Jeśli nie, to czy niesprawiedliwość lub uczciwość powoduje problem? Na przykład, może aplikacja o wysokim priorytecie powinna być faworyzowana w stosunku do innych; wideo w czasie rzeczywistym wymaga więcej czasu na przetwarzanie niż zadanie w tle, które pozwala na tworzenie kopii zapasowych plików w usłudze w chmurze.
  • Spory: Czy podmioty w systemie walczą o ten sam zasób? Na przykład, jeśli dwie aplikacje zapisują na jednym dysku twardym, obie aplikacje muszą walczyć o przepustowość dysku. Ma to duży związek z uczciwością, ponieważ konflikty muszą być rozwiązywane za pomocą jakiegoś algorytmu uczciwości. Sprzeczności mogą być łatwiejsze do znalezienia zamiast pytania o uczciwość.
  • Nasycenie: Czy zasób jest ponad lub całkowicie wykorzystany? Jeśli zasób jest ponad lub całkowicie wykorzystany, możemy trafić na jakieś ograniczenie, które tworzy spory lub opóźnienia, ponieważ podmioty muszą ustawić się w kolejce, aby korzystać z zasobów, gdy stają się one dostępne.

Kiedy zajęliśmy się NIC, skupiliśmy się głównie na sprawdzeniu a) czy kolejki nie są przepełnione, co byłoby widoczne jako odrzuty i wskazywałoby na możliwe ograniczenia w wykorzystaniu pasma lub b) czy były jakieś źle sformatowane pakiety wymagające retransmisji, co mogłoby spowodować opóźnienia. W trakcie naszych eksperymentów do NIC trafiło 0 odrzutów i 0 złośliwych pakietów, a wykorzystanie pasma wynosiło około 5-40 MB/s, co jest niską wartością na naszym sprzęcie 1 Gbps.

Następnie skupiliśmy się na poziomie sterownika i protokołu. Te dwie części były trudne do oddzielenia; jednakże, spędziliśmy sporą część naszego dochodzenia przyglądając się różnym tuningom systemu operacyjnego, które dotyczyły planowania procesów, wykorzystania zasobów dla rdzeni, planowania obsługi przerwań i powinowactwa przerwań dla wykorzystania rdzeni. Te kluczowe obszary mogą potencjalnie powodować opóźnienia w przetwarzaniu pakietów sieciowych, a my chcieliśmy mieć pewność, że żądania i odpowiedzi są obsługiwane tak szybko, jak tylko maszyna jest w stanie to wytrzymać. Niestety, większość naszych eksperymentów nie przyniosła żadnej pierwotnej przyczyny.

Objawy, które widzieliśmy na początku, zdawały się sugerować, że system ma ograniczoną przepustowość. Kiedy wytwarzane jest duże natężenie ruchu, opóźnienia wzrastają z powodu opóźnień w kolejkach. Jednak, gdy spojrzeliśmy na warstwę NIC, nie zauważyliśmy takiego problemu. Ale po wyeliminowaniu prawie wszystkiego w stosie, zdaliśmy sobie sprawę, że nasze wskaźniki wydajności mierzą z dokładnością do 1 sekundy lub 1000 milisekund. Przy opóźnieniach rzędu 30 ms, jak mogliśmy mieć nadzieję na wychwycenie problemu?

Microbursts, oh my!

Wiele naszych systemów ma karty interfejsów sieciowych o przepustowości 1 Gb/s. Kiedy przyjrzeliśmy się ruchowi przychodzącemu, zobaczyliśmy, że Serwer Cache generalnie doświadczył ruchu na poziomie 5 – 40 MB/s. Takie wykorzystanie pasma nie wzbudza żadnych zastrzeżeń, jednak co by się stało, gdybyśmy spojrzeli na wykorzystanie pasma na milisekundę! Pierwszy wykres poniżej to wykorzystanie pasma na sekundę i pokazuje niskie wykorzystanie, podczas gdy drugi wykres to wykorzystanie pasma na milisekundę i pokazuje zupełnie inną historię.

Aby zmierzyć przychodzący ruch pasma na milisekundę, użyliśmy tcpdump, aby zebrać ruch przez określony okres czasu. Wymagało to obliczeń offline, ale ponieważ tcpdump ma znaczniki czasowe na poziomie mikrosekund, byliśmy w stanie obliczyć wykorzystanie pasma przychodzącego na milisekundę. Wykonując te pomiary, byliśmy w stanie zidentyfikować przyczynę opóźnień w sieci longtail. Jak widać na powyższych wykresach, wykorzystanie pasma na milisekundę pokazuje krótkie wybuchy kilkuset milisekund w czasie, które osiągają blisko 100 kB/ms. Takie tempo 100 kB/ms utrzymujące się przez pełną sekundę byłoby równoważne 100 MB/s, co stanowi 80% teoretycznej przepustowości kart sieciowych 1 Gbps! Te wybuchy są znane jako mikrowybuchy i są tworzone przez rozproszone klastry baz danych odpowiadające na serwer cache wszystkie naraz, tworząc w ten sposób w pełni wykorzystane łącze przez czas poniżej sekundy. Poniżej znajduje się wykres wykorzystania pasma jako procent prędkości 1 Gbps w stosunku do latencji mierzonych w tym samym przedziale czasowym. Jak widać, istnieje wysoka korelacja pomiędzy skokami opóźnień a ruchem burstowym:

Wykresy te pokazują jak ważne są pomiary sub-sekundowe! Chociaż trudno jest utrzymać pełną infrastrukturę z takimi danymi, przynajmniej w przypadku dogłębnych badań, powinna to być granularność, ponieważ w wydajności milisekundy naprawdę mają znaczenie!

Impact of Root Cause

Ta podstawowa przyczyna ma interesujący wpływ na nasz system rozproszony. Generalnie systemy lubią wysoką przepustowość, więc ekstremalnie wysokie wykorzystanie jest dobrą rzeczą. Ale nasz serwer buforujący ma do czynienia z dwoma rodzajami ruchu: (1) dane o dużej przepustowości z bazy danych (2) małe zapytania z API Layer. Przyznajemy, że żądania z warstwy API mogą powodować wysoką przepustowość danych z bazy danych, ale tutaj jest klucz: jest to potrzebne tylko wtedy, gdy żądanie nie może być spełnione przez pamięć podręczną. Jeśli żądanie jest w pamięci podręcznej, serwer buforujący powinien zwrócić dane szybko, bez konieczności oczekiwania na obliczenia bazy danych. Ale co się stanie, jeśli zbuforowane żądanie nadejdzie podczas odpowiedzi microburst dla nie zbuforowanego żądania? Mikroburst może spowodować 30 ms opóźnienia dla każdego innego ruchu przychodzącego i dlatego buforowane żądanie może doświadczyć dodatkowych 30 ms opóźnienia, które jest całkowicie niepotrzebne!

Krok 4: Prototyp i walidacja

Po odkryciu prawdopodobnej przyczyny źródłowej, chcieliśmy zweryfikować nasze wyniki. Ponieważ to nagłe zużycie pasma może powodować opóźnienia w trafieniach do pamięci podręcznej, mogliśmy odizolować te żądania od zapytań serwera cache do klastra bazy danych. W tym celu skonfigurowaliśmy środowisko eksperymentalne, w którym pojedynczy host serwera cache posiada dwa NIC, każdy z własnym adresem IP. Przy takiej konfiguracji, wszystkie żądania API Layer do serwera cache przechodzą przez jeden interfejs, a wszystkie zapytania serwera cache do klastra baz danych przechodzą przez drugi interfejs. Poniższy diagram ilustruje to:

Przy takiej konfiguracji zmierzyliśmy następujące latencje i jak widać, latencje pomiędzy warstwą API a serwerem cache są właściwie takie, jakich oczekujemy – zdrowe i poniżej 1 ms. Latencji z klastrem bazy danych nie da się uniknąć bez poprawy sprzętu; ponieważ chcemy zmaksymalizować przepustowość, zawsze będą występować bursty, a co za tym idzie pakiety będą ustawiane w kolejce na interfejsie.

W związku z tym, różny ruch zasługuje na różne priorytety i może być idealnym rozwiązaniem do obsługi ruchu microbursting. Inne rozwiązania obejmują ulepszenie sprzętu, takie jak użycie sprzętu 10 Gbps, kompresja danych, a nawet użycie jakości usług.

Znalezienie pierwotnej przyczyny długich ogonów może być trudne.

Pierwotna przyczyna opóźnień długich ogonów może być trudna do znalezienia, ponieważ są one efemeryczne i mogą wymykać się pomiarom wydajności. Większość metryk wydajnościowych, które zbieramy w LinkedIn, jest na poziomie 1 sekundy, a niektóre na poziomie 1 minuty. Jednak biorąc to pod uwagę, opóźnienia longtail, które trwają 30 ms mogą być łatwo przeoczone przez pomiary o granulacji nawet 1000 ms (1 sekunda). Nie tylko to, opóźnienia typu longtail mogą być spowodowane różnymi problemami w sprzęcie lub oprogramowaniu i mogą być dość trudne do wykorzenienia w złożonym systemie rozproszonym. Niektórymi przykładami przyczyn może być wykorzystanie zasobów sprzętowych związane z uczciwością, rywalizacją i nasyceniem, lub problemy z wzorcem danych, takie jak dystrybucja wielowęzłowa lub użytkownicy o dużej mocy obliczeniowej powodujący opóźnienia typu longtail dla swoich obciążeń.

Podsumowując, gorąco zachęcamy do zapamiętania tych czterech kroków naszej metodologii dla przyszłych badań:

  1. Posiadaj kontrolowane i uproszczone środowisko.
  2. Wykonaj szczegółowe pomiary opóźnień end-to-end.
  3. Eliminować i eksperymentować.
  4. Prototypuj i waliduj.

Wyciągnięte wnioski

  • Długie opóźnienia nie są tylko szumem! Może być spowodowane różnymi rzeczywistymi powodami, a żądania 99 percentyla mogą mieć wpływ na resztę dużego systemu rozproszonego.
  • Nie dyskontuj 99. percentyla problemów z opóźnieniami jako użytkowników mocy; jako użytkownicy mocy mnożą się, więc będą problemy.
  • Zabezpieczenie zakładu, chociaż ogólnie dobra strategia, w której system wysyła to samo żądanie dwa razy w nadziei na jedną szybką odpowiedź, nie pomaga, gdy opóźnienia longtail są wywołane przez aplikację. W rzeczywistości, to tylko pogarsza system poprzez dodanie więcej ruchu do systemu, co w naszym przypadku spowodowałoby więcej mikrowybuchów do wystąpienia. Gdybyśmy wdrożyli tę strategię bez dokładnej analizy, bylibyśmy rozczarowani, ponieważ wydajność systemu uległaby pogorszeniu i zmarnowano by znaczną ilość wysiłku na wdrożenie takiego rozwiązania.
  • Podejścia typu scatter/gather mogą łatwo spowodować mikrowybuchy wykorzystania pasma, powodując opóźnienia w kolejkach na poziomie milisekund.
  • Niezbędne są pomiary o granulacji podsekundowej.
  • Czasami ulepszenia sprzętowe są najbardziej efektywnym kosztowo sposobem na złagodzenie problemów, ale do tego czasu, nadal istnieją interesujące łagodzenia, które deweloperzy mogą zrobić, takie jak kompresja danych lub bycie selektywnym na to, jakie dane są wysyłane lub używane.

Na koniec, najważniejszą lekcją, jakiej się nauczyliśmy, było podążanie za metodologią. Metodologie nadają kierunek dochodzeniom, zwłaszcza gdy sprawy stają się zagmatwane lub zaczynają przypominać podróż przez Śródziemie.

Podziękowania

Chciałbym podziękować Andrew Carterowi za jego pracę i współpracę podczas dochodzenia oraz Stevenowi Callisterowi za zapewnienie wsparcia operacyjnego i informacji zwrotnych. Dziękuję również Badriemu Sridharanowi, Haricharanowi Ramachandrze, Riteshowi Maheshwariemu i Zhenyunowi Zhuangowi za ich opinie i sugestie dotyczące tego opracowania.

.