wtorek, 20 grudnia 2011

Nieblokujące serwery sterowane zdarzeniami cz. 1: Wstęp

Jednym ze sposobów tworzenia bardzo wydajnych aplikacji www jest wykorzystanie sterowanych zdarzeniami serwerów używających nieblokujących operacji wejścia/wyjścia. Brzmi dość skomplikowanie, jednak w rzeczywistości podejście to jest całkiem proste do zrozumienia. Zanim jednak opiszę co i jak chciałbym wprowadzić w temat schodząc niemal na sam dół drabinki technologicznej - do protokołu TCP. Nie obawiaj się jednak - nie będę opisywał jak zbudowany jest pakiet TCP :)

Serwery oparte o protokół TCP, a więc m.in. serwery www (protokół HTTP) lub przekazywania poczty elektronicznej (protokół SMTP) wykorzystują gniazdka do przesyłania lub odbierania danych. Gdy przykładowo przeglądarka www chce ściągnąć zawartość podanej strony to tworzy tzw. połączenie z serwerem. Do utworzenia takiego połączenia serwer udostępnia właśnie takie gniazdko.  Po nawiązaniu komunikacji przeglądarka pyta się serwer o dane wysyłając żądania. Przyjęło się, że taki sposób komunikacji implementuje się za pomocą wątków. Nazwijmy ten model "wątek per połączenie". Jest to prosty i sprawdzony sposób, jednak jak się za chwilę okaże nie do końca efektywny. Jeden rdzeń procesora jest w stanie uruchamiać jedną komendę w jednostce czasu, w związku z czym dla każdego tworzonego połączenia między przeglądarką a serwerem tworzony jest wątek po stronie serwera. Dlatego im więcej użytkowników korzysta właśnie z serwisu tym więcej uruchomionych jest wątków.

W przypadku zapytań o zasoby generowane dynamicznie czyli np. strony HTML serwer musi najpierw uruchomić kod programisty (jakąś akcję, skrypt itp.), a następnie zwrócić do przeglądarki odpowiedź. Nierzadko czas jaki przeglądarka musi spędzić na oczekiwaniu na odpowiedź trwa kilka lub kilkanaście sekund i jest to spowodowane długim czasem uruchamiania właśnie tego kodu. Powodów tak długiego czasu przetwarzania może być wiele, jednak w dużej liczbie przypadków jest to spowodowane oczekiwaniem na tzw. "zewnętrzne zasoby", czyli np. oczekiwanie na zakończenie operacji na dysku twardym, zakończenie wykonywania się zapytania w bazie danych czy wreszcie oczekiwanie na odpowiedź usługi zdalnej. Model "wątek per połączenie", który opisuję zakłada, iż pomimo tego, że w czasie oczekiwania na zewnętrzny zasób kod nie wykonuje żadnej operacji to i tak zajmuje cenne miejsce w pamięci RAM, a nawet spowalnia pracę serwera.

Aby zrozumieć potrzebę wykorzystania serwerów sterowanych zdarzeniami trzeba najpierw wyobrazić sobie sytuacje, w której nasz serwer ma za zadanie obsłużyć bardzo wiele żądań jednocześnie - niech będzie to 1000 żądań w ciągu jednej sekundy. W modelu wątek per połączenie z początku utworzonych zostanie tysiąc wątków. Załóżmy, że kod programisty podczas obsługi żądania uruchamia zewnętrzną usługę - niech to będzie jakaś usługa udostępniania przez Facebook. Niech czas odpowiedzi tych usług wynosi średnio 5 sekund. Wynika z tego, że ów 1000 żądań nie ma szans uruchomić się w ciągu jednej sekundy, ponieważ żądanie się nie zakończy dopóki zewnętrzna usługa nie odpowie. Dlatego po pięciu sekundach liczbą wątków będzie już wynosić 5 tysięcy i mniej więcej na tym poziomie będzie się utrzymywać zakładając, że tysiąc żądań będzie uruchamianych w każdej sekundzie. Wykorzystywanych jest więc 5 tysięcy wątków, pomimo, iż kod ten nie robi nic poza czekaniem na odpowiedź z zewnętrznej usługi. Do obsługi tak dużej ilości wątków potrzeba jest już sporej ilości pamięci RAM, co znacznie obciąża  serwer. Poniżej wykres prezentujący wykorzystanie wątków na przestrzeni czasu.

Wykorzystanie liczby wątków na przestrzeni czasu

Można jednak wykorzystać bardziej optymalną architekturę. Współczesne systemy operacyjne udostępniają mechanizm przesyłania/odbioru danych w trybie asynchronicznym (czyli nieblokującym). Oznacza to tyle, że wysyłając dane z klienta do serwera lub na odwrót, czytając dane z pliku lub odpowiedź z bazy danych nie oczekujemy na zakończenie operacji i kontynuujemy w wątku uruchamianie naszego kodu. Czyli w naszym przypadku wysyłamy żądanie do usługi Facebook, ale nie blokujemy wątku do czasu aż Facebook wyśle pełną odpowiedź tylko dalej wykonujemy swój kod. Co jednak w sytuacji gdy kod nie może być dalej wykonywany bo zależy od odpowiedzi z Facebook? W tym wypadku moglibyśmy poinformować nasz serwer, że oczekujemy na odpowiedź i oddajemy swój wątek do wykorzystania przez inne żądanie. Teraz prześledźmy jeszcze raz nasz przykład: w ciągu pierwszych 100 milisekund przychodzi 100 żądań, tworzonych jest 100 wątków, kod w każdym z wątków uruchamia usługę Facebook a następnie oddaje swój wątek do wykorzystania. Kolejne 100 żądań wykonywane jest w tych wątkach. Gdy po 5 sekundach przyjdzie odpowiedź z Facebooka to serwer z powrotem przydziela wątek danemu połączeniu generując tzw. zdarzenie. Podsumowując: nasz serwer jest w stanie obsługiwać 1000 żądań za pomocą jedynie 100 wątków (w przeciwieństwie do 5 tysięcy wątków modelu wątek per połączenie). Mało tego, jest w stanie obsługiwać więcej żądań w ciągu sekundy nie marnując przy tym pamięci operacyjnej.


Mam nadzieję, że opisałem problem w miarę prosty sposób. Jeśli nie to zachęcam do komentarzy. Na podstawie uwag postaram się udoskonalić ten post. W następnej części opiszę jak w praktyce działają takie serwery i zaprezentuję dane, które udowodnią, że w wielu przypadkach wykorzystanie tego typu serwerów jest naprawdę opłacalne.

3 komentarze:

  1. Hej Jacku, interesujący tekst. Polinkuj ze sobą wszystkie wpisy z serii. Będzie nam łatwiej nawigować :)

    OdpowiedzUsuń
    Odpowiedzi
    1. Dzięki za komentarz :) Dodałem linki między wpisami. Myślę, że zabrakło tu też jakiegoś głównego posta, który linkowałby do wszystkich części w jednym miejscu (do czasu jak go dodam można użyć etykiety: http://blogger.trycatch.pl/search/label/nieblokuj%C4%85ce%20serwery%20sterowane%20zdarzeniami )

      Usuń