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.
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:
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> ).
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.
Czerwone linie odnoszą się odpowiednio do asynchroniczności, a niebieskie — do punktu końcowego synchronicznego.
To tyle o teorii. Zacznijmy.
Widzimy, że oba punkty końcowe działają podobnie — obsługują około 750 żądań na minutę, a mediana czasu odpowiedzi wynosi 5200 ms.
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!
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.
Teraz nadszedł czas, aby podnieść stawkę.
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ń.
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.
Trzeci eksperyment ma na celu potwierdzenie tendencji ujawnionych podczas drugiego — możemy zaobserwować dalszą degradację punktu końcowego synchronizacji.
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.
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.
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)?