paint-brush
Async vs Sync Benchmark (.NET): różnica między metodami asynchronicznymi i synchronicznymiprzez@artemmikulich
1,892 odczyty
1,892 odczyty

Async vs Sync Benchmark (.NET): różnica między metodami asynchronicznymi i synchronicznymi

przez Artem Mikulich5m2024/09/21
Read on Terminal Reader

Za długo; Czytać

W tym artykule pokazano różnicę między metodami asynchronicznymi i synchronicznymi w praktyce. Dwie niezależne instancje Locust działają na dwóch maszynach. Liczba użytkowników rośnie równomiernie do liczby docelowej (*Liczba użytkowników*). Prędkość wzrostu jest kontrolowana przez parametr * Spawn Rate* (liczba unikalnych użytkowników dołączających na sekundę)
featured image - Async vs Sync Benchmark (.NET): różnica między metodami asynchronicznymi i synchronicznymi
Artem Mikulich HackerNoon profile picture

Jedno z moich ulubionych pytań na rozmowie kwalifikacyjnej brzmi: „Co mówią ci takie słowa jak async i await ?”, ponieważ otwiera to okazję do ciekawej dyskusji z osobą udzielającą wywiadu… Albo nie, ponieważ poruszają ten temat. Moim zdaniem, zrozumienie, dlaczego stosujemy tę technikę, jest niezwykle ważne.


Mam wrażenie, że wielu programistów woli polegać na stwierdzeniu, że „to najlepsza praktyka” i stosować metody asynchroniczne w ciemno.


W tym artykule pokazano różnice pomiędzy metodami asynchronicznymi i synchronicznymi w praktyce.

Narzędzia

  • Aplikacja .NET Web API (cel testowy)


  • 2 bazy danych Azure SQL


  • 2 Azure App Service w systemie Windows (obsługuje aplikację)


  • Azure App Insights (do zbierania metryk)


  • framework Locust (do symulacji obciążenia użytkownika).

Konfiguracja

Schemat eksperymentu

Przeprowadzę test porównawczy w następujący sposób. Dwie niezależne instancje Locust są uruchomione na dwóch maszynach. Instancje Locust symulują użytkownika, który wykonuje następujące czynności:

  • Użytkownik z hosta locust 1 trafia do synchronicznego punktu końcowego usługi App Service 1, otrzymuje odpowiedź i pozostaje bezczynny przez 0,5–1 sekundy (dokładne opóźnienie jest losowe). Powtarza się do końca eksperymentu.


  • Użytkownik z hosta Locust 2 zachowuje się dokładnie tak samo, z jedną różnicą — trafia do asynchronicznego punktu końcowego usługi App Service 2.


Pod maską każda usługa App Service łączy się z własną bazą danych i wykonuje zapytanie SELECT, które trwa pięć sekund i zwraca kilka wierszy danych. Zobacz kod kontrolera poniżej, aby uzyskać odniesienia. Użyję Dappera, aby wykonać wywołanie do bazy danych. Chciałbym zwrócić uwagę na fakt, że asynchroniczny punkt końcowy również wywołuje bazę danych asynchronicznie ( QueryAsync<T> ).


Kod usług aplikacji


Warto dodać, że wdrażam ten sam kod w obu usługach aplikacyjnych.


Podczas testu liczba użytkowników rośnie równomiernie do liczby docelowej ( Liczba użytkowników ). Prędkość wzrostu jest kontrolowana przez parametr Spawn Rate (liczba unikalnych użytkowników dołączających na sekundę) — im wyższa liczba, tym szybciej dodawani są użytkownicy. Szybkość odradzania jest ustawiona na 10 użytkowników/s dla wszystkich eksperymentów.


Czas trwania każdego eksperymentu jest ograniczony do 15 minut.


Szczegóły dotyczące konfiguracji maszyny można znaleźć w części artykułu zatytułowanej Dane techniczne.

Metryka

  • żądania na minutę — pokazuje liczbę żądań, które aplikacja faktycznie przetworzyła i zwróciła kod statusu.
  • liczba wątków — pokazuje liczbę wątków zużywanych przez usługę aplikacji.
  • mediana czasu reakcji, ms


Czerwone linie odnoszą się odpowiednio do asynchroniczności, a niebieskie — do punktu końcowego synchronicznego.


To tyle o teorii. Zacznijmy.

Eksperyment nr 1

  • liczba użytkowników : 75 (na usługę)


Widzimy, że oba punkty końcowe działają podobnie — obsługują około 750 żądań na minutę, a mediana czasu odpowiedzi wynosi 5200 ms.


Eksperyment nr 1. Żądania na minutę


Najbardziej fascynującym wykresem w tym eksperymencie jest trend wątków. Można zobaczyć znacznie wyższe liczby dla synchronicznego punktu końcowego (niebieski wykres) — ponad 100 wątków!


Eksperyment nr 1. Liczba nici


To jednak jest oczekiwane i zgodne z teorią — gdy przychodzi żądanie i aplikacja wykonuje wywołanie do bazy danych, wątek jest blokowany, ponieważ musi czekać na zakończenie podróży w obie strony. Dlatego gdy przychodzi kolejne żądanie, aplikacja musi utworzyć nowy wątek, aby je obsłużyć.


Czerwony wykres — liczba wątków asynchronicznych punktów końcowych — pokazuje inne zachowanie. Gdy przychodzi żądanie i aplikacja wykonuje wywołanie do bazy danych, wątek wraca do puli wątków zamiast być blokowanym. Dlatego gdy przychodzi kolejne żądanie, ten wolny wątek jest ponownie używany. Pomimo wzrostu liczby żądań przychodzących, aplikacja nie wymaga żadnych nowych wątków, więc ich liczba pozostaje taka sama.


Warto wspomnieć o 3. metryce — medianie czasu reakcji . Oba punkty końcowe wykazały ten sam wynik — 5200 ms. Nie ma więc różnicy pod względem wydajności.


Eksperyment nr 1. Podsumowanie


Teraz nadszedł czas, aby podnieść stawkę.

Eksperyment nr 2

  • liczba użytkowników : 150


Podwoiliśmy obciążenie. Asynchroniczny punkt końcowy radzi sobie z tym zadaniem pomyślnie — jego szybkość żądania na minutę oscyluje wokół 1500. Synchroniczny brat ostatecznie osiągnął porównywalną liczbę 1410. Ale jeśli spojrzysz na poniższy wykres, zobaczysz, że zajęło to 10 minut!


Powodem jest to, że synchroniczny punkt końcowy reaguje na przybycie nowego użytkownika, tworząc kolejny wątek, ale użytkownicy są dodawani do systemu (przypominamy, że współczynnik odradzania się wynosi 10 użytkowników/s) szybciej, niż serwer WWW jest w stanie się dostosować. Dlatego na samym początku ustawiono w kolejce tak wiele żądań.


Eksperyment nr 2. Żądania na minutę


Nie jest zaskoczeniem, że metryka liczby wątków nadal wynosi około 34 dla asynchronicznego punktu końcowego, podczas gdy wzrosła ze 102 do 155 dla synchronicznego. Mediana czasu odpowiedzi spadła podobnie do szybkości żądania na minutę — synchroniczny czas odpowiedzi był znacznie wyższy na początku eksperymentu. Gdybym trzymał test przez 24 godziny, mediana liczb stałaby się równa.


Eksperyment nr 2. Podsumowanie


Eksperyment nr 3

  • liczba użytkowników : 200


Trzeci eksperyment ma na celu potwierdzenie tendencji ujawnionych podczas drugiego — możemy zaobserwować dalszą degradację punktu końcowego synchronizacji.


Eksperyment nr 3. Podsumowanie


Wniosek

Korzystanie z operacji asynchronicznych zamiast synchronicznych nie poprawia bezpośrednio wydajności ani doświadczenia użytkownika. Po pierwsze, zwiększa stabilność i przewidywalność pod presją. Innymi słowy, podnosi próg obciążenia, dzięki czemu system może przetworzyć więcej, zanim ulegnie degradacji.

Załącznik nr 1. Szczegóły techniczne

  • Azure App Service: B1, 100 ACU , pamięć 1,75 Gb, równoważnik obliczeniowy serii A.
  • Baza danych Azure SQL: Standard S4: 200 jednostek DTU, pamięć masowa 500 Mb.
  • Ustawienia połączenia SQL: Maksymalny rozmiar puli = 200.

Załącznik nr 2. Notatki

Aby uzyskać najczystsze wyniki testów, powinienem uruchomić testy z dwóch maszyn wirtualnych znajdujących się w tej samej sieci, w której znajdują się docelowe usługi aplikacji.


Założyłem jednak, że opóźnienie sieciowe wpłynie na obie aplikacje w mniej więcej podobny sposób. Dlatego nie może zagrozić głównemu celowi — porównaniu zachowania metod asynchronicznych i synchronicznych.

Załącznik nr 3. Eksperyment bonusowy

Co zrobiłem, aby zmusić synchroniczny punkt końcowy do działania niemal tak asynchronicznie i przedstawić poniższy wykres (warunki eksperymentu są takie same, jak w trzecim eksperymencie — 200 użytkowników)?


Eksperyment bonusowy. Żądania na minutę