wtorek, 28 lutego 2012

Nieblokujące serwery sterowane zdarzeniami cz. 6: Technologie

W poprzedniej części cyklu opisałem problemy jakie może przyporzyć stosowanie architektury nieblokujących serwerów sterowanych zdarzeniami. W tej części pokrótce opiszę technologie jakich można użyć w celu zaimplementowania takiego systemu. Tym razem obędzie się bez rozwlekłych opisów - krótko i na temat - nazwa technologii, język programowania oraz krótki opis.

Serwery www do tworzenia aplikacji:

  • Node.js (JavaScript) - najpopularniejsza i najszybciej rozwijająca się technologia; wykorzystuje tylko jeden proces; oparty na wydajnej (jak na JavaScript) implementacji silnika JavaScript o nazwie V8
  • G-WAN web server (C, C++, Objective-C) - ultra-wydajny; wykorzystuje tyle procesów ile rdzeni/procesorów; dla zaawansowanych programistów (kod serwletów uruchamiany wewnątrz procesu co sprawia, że trzeba uważać na bezpieczeństwo).
  • Jetty (Java, dowolny inny język na JVM) - standardowo od wesji 3.0 JavaServlet; można też użyć kontynuacji (cecha charakterystyczna dla tego serwera niebędąca częścią specyfikacji JavaServlet)
  • Tomcat 7+ (Java, dowolny inny język na JVM) - podobnie jak w przypadku Jetty trzeba tworzyć aplikację w oparciu o JavaServlet 3.0
  • JBoss Web server (Java, dowolny inny język na JVM) - j/w; bazuje na Netty (patrz niżej)

Narzędzia do tworzenia własnych serwerów (oraz klientów):

  • epoll (C, C++) - niskopoziomowe API dla Linuxa do nasłuchiwania zdarzeń we-wy; czyli pozwala na monitorowanie wielu deskryptorów plików, w celu dowiedzenia się czy są gotowe na I/O (czyli czy na przykład można czytać z gniazdka); najwydajniejsze API jeśli liczba nasłuchiwanych plików liczona jest w tysiącach 
  • Node.js (JavaScript) - możliwość tworzenia serwerów/klientów TCP oraz UDP w uproszczony sposób w JavaScript;
  • NIO i NIO.2 (Java) - najbardziej "niskopoziomowe" API w Javie; wykorzystuje m.in. epoll (implementacja maszyny na Linuxa)
  • NettyApache Mina (Java) - warstwy abstrakcji nad NIO; systematyzują proces tworzenia asynchronicznych serwerów i klientów ułatwiając późniejsze utrzymanie kodu

Inne serwery www wykorzystujące nieblokujące I/O:
  • Nginx - buforowanie wczytanych wcześniej plików w celu zwiększenia przepustowości spoczywa na systemie operacyjnym; reverse proxy; można wykorzystać do load balancingu oraz buforowania odpowiedzi serwerów (odpowiedzi są zapisywane na dysku); pozwala na pisanie modułów uruchamianych wewnątrz procesu (podobnie jak w G-WAN to dość niebezpieczne ale najbardziej wydajne)
  • Varnish Cache - reverse proxy buforujący odpowiedzi serwerów; alokuje pamięć bezpośrednio (co zapewnia większą kontrolę niż w przypadku bufora systemu plików jak w przypadku Nginx); może służyć jako load balancer

Jak widać lista technologii jest mocno skrócona - opisałem tylko te, które znam i są gotowe do użycia na produkcji. Jeśli znasz jakieś inne narzędzia zapraszam do dodania komentarza, a ja uzupełnię wtedy wpis. Myślę, że na tym poście zakończę cykl traktujący o asynchronicznych serwerach sterowanych zdarzeniami. Jeśli jednak interesuje Cię ten temat i chciał(a)byś dowiedzieć się więc to zachęcam do dodania komentarza pod postem :)

niedziela, 19 lutego 2012

Nieblokujące serwery sterowane zdarzeniami cz. 5: Problemy

Początkowo część ta miała być tylko podsumowaniem cyklu jednak po rozmowach ze znajomymi stwierdziłem, że do tej pory nie napisałem o problemach, które może sprawiać architektura asynchronicznych serwerów sterowanych zdarzeniami. Postanowiłem więc opisać najważniejsze problemy, gdyż z punktu widzenia architekta lub programisty są bardzo istotne i mogą mieć decydujący wpływ na wybór tej architektury.

Nierzadko przeglądając strony promujące technologie tworzenia aplikacji www można odnieść wrażenie, że jedyne co wystarczy zrobić aby zapewnić dużą przepustowość jest dobór odpowiedniego serwera www i można już zacząć prace nad aplikacją. Cześć osób (niesłusznie) podejmuje decyzje w kategorii produktów a nie architektury. Jeśli serwer wygrywa w benchmarkach to znaczy, że jest najlepszy. Problem w tym, że jeśli serwer jest zdecydowanie wydajniejszy niż inne serwery to coś musi się za tym kryć. Nie można liczyć na to, że programiści danej implementacji potrafią pisać wydajniejszy kod od innych. W większości kod udostępniany jest na zasadach Open Source, dlatego serwery oparte na podobnej architekturze często osiągają podobne wyniki. O wydajności decyduje przede wszystkim architektura.

W przypadku dużej liczby równoległych zapytań liczonych w tysiącach na sekundę serwery nieblokujące sterowane zdarzeniami są w stanie zapewnić większą przepustowość od "klasycznego" podejścia. Nie ma jednak nic za darmo. Posiadają one dwie poważne wady: konieczność wykorzystania dedykowanych API oraz niedogodności związane z funkcjami zwrotnymi (callbacks).

Architektura serwerów asynchronicznych sterowanych zdarzeniami wymusza na programiście pisanie kodu, który nie blokuje wątku w oczekiwaniu na zasoby. Dlatego każda operacja - np. przesłanie zapytania do bazy danych, kolejki, wysłanie żądania HTTP musi być asynchroniczna. Oczywiście w dzisiejszych czasach nikt nie pisze sterowników do bazy danych, kolejek czy klientów HTTP samemu. Wykorzystuje się sprawdzone i dobrze znane biblioteki. Niestety większość z nich ze względu na prostotę obsługi korzysta z podejścia blokującego. Przykładowo wątek zablokowany jest do czasu, gdy program otrzyma odpowiedź z bazy danych. Liczba bibliotek asynchronicznych ciągle się powiększa jednak potrzeba jeszcze sporo czasu aby były one na tyle stabilne co ich blokujące odpowiedniki. Najdobitniejszym tego przykładem są dostępne dziś sterowniki baz danych (nie tylko tych relacyjnych). Przykładowo standard JDBC z założenia jest blokujący, dlatego wszystkie sterowniki baz relacyjnych dla Javy są blokujące. Owszem są próby stworzenia asynchronicznych sterowników (np. ADBCJ), jednak dużo czasu upłynie zanim taki sterownik będzie na tyle stabilny aby można było go użyć na produkcji. Także sterowniki najnowszych baz NoSQL często pisane są w oparciu o model blokujący - np. oficjalny sterownik MongoDB dla Javy jest synchroniczny. Problem nie tylko dotyczy Javy, a praktycznie każdego języka programowania. Dlatego przed zdecydowaniem się czy chcemy wykorzystać model asynchroniczny trzeba sprawdzić czy mamy do tego niezbędne narzędzia,

Teraz będzie trochę o kodzie jaki przyjdzie tworzyć programiście. Obecnie najbardziej spopularyzowanym sposobem pisania programów asynchronicznych jest wykorzystanie tzw. callbacks. Funkcje zwrotne (callbacks) to specjalny rodzaj funkcji, który uruchamiany jest dopiero po jakimś zdarzeniu np. gdy zapytanie w bazie danych zwróci wyniki i zostaną one przesłane do naszego serwera. Funkcje zwrotne są przekazane do API, które np. uruchamia zapytania w bazie danych. W JavaScript można przekazać obiekt funkcji, w Groovy closure, w Javie obiekt klasy (często anonimowej) itp. Aby zilustrować różnice pomiędzy kodem wykonywanym synchronicznie a tym asynchronicznym poniżej zamieszczam 2 przykłady kodu w JavaScript - pierwszy w podejściu blokującym, drugi w nieblokującym:

try {
  var result = connection.query("SELECT * FROM test1");
  for(var i=0; i<result.records.length; i++) {
// rób coś z rekordami przesłanymi z bazy
  };
} catch (e) {
    sys.puts("Error: " + e);
}


connection.query("SELECT * FROM test1",
    function(result) {
        for(var i=0; i<result.records.length; i++) {
        // rób coś z rekordami przesłanymi z bazy
        };
    },
    function(error) {
        sys.puts("Error: " + error);
    });


Przykład z rozwiązaniem blokującym jest bardzo prosty. Uruchamiane jest zapytanie, które blokuje wątek dopóki baza nie odpowie. Następnie wyniki są przetwarzane. W przypadku błędu zostanie wyrzucony wyjątek. Przykład z rozwiązaniem nieblokującym jest już bardziej złożony. Metoda query powoduje przesłanie zapytania do bazy danych. Rejestruje ona chęć odebrania odpowiedzi z gniazda utworzonego z bazą danych w systemie operacyjnym (np. za pomocą biblioteki epoll). Dzięki temu nie blokuje wykonania wątku. Gdy baza danych odpowie i wyśle odpowiedź to zostanie uruchomiona funkcja przekazana w powyższym przykładzie jako drugi argument. Gdy pojawi się jakiś błąd to zostanie uruchomiona funkcja przekazana jako ostatni argument. Co dzieje się z wątkiem, który został użyty do uruchomienia zapytania na bazie? Oczywiście będzie on mógł być wykorzystany do obsługi następnego żądania przesłanego do naszego serwera.

Z wykorzystaniem funkcji zwrotnej wiążą się trzy problemy: czytelność, problemy w znajdowaniu błędów oraz problemy z wyciekami pamięci. Pierwszy z nich uwidacznia się szczególnie gdy w ramach jednego żądania trzeba zadeklarować kilka a nawet kilkanaście takich zagnieżdżonych funkcji. Oto przykład z zaledwie trzema zapytaniami:


connection.query("SELECT * FROM test1",
    function(result) {
        for(var i=0; i<result.records.length; i++) {
         connection.query("SELECT * FROM test2",
            function(result) {
              for(var i=0; i<result.records.length; i++) {
            connection.query("SELECT * FROM test3",
                function(result) {
                  for(var i=0; i<result.records.length; i++) {
         // rób coś z rekordami przesłanymi z bazy
                  };
                 },
                function(error) {
                  sys.puts("Error: " + error);
                });               };
             },
            function(error) {
              sys.puts("Error: " + error);
            });
        };
    },
    function(error) {
        sys.puts("Error: " + error);
    });


Drugi problem to kłopoty z debugowaniem kodu. W synchronicznym podejściu jeśli pojawi się jakiś błąd to z przechwyconego wyjątku można wyciągnąć ślad stosu (czyli po kolei nazwy funkcji, które były wykonywane od momentu przyjścia żądania do serwera). W przypadku architektury asynchronicznej ślad stosu będzie skrócony - tzn. nie będziemy widzieć funkcji, które były uruchamiane wcześniej przez wykonaniem jakieś operacji nieblokującej. Przykładowo zamiast takiego śladu stosu:

WyjątekBazyDanych
Callback.onError : Linia 6
klientBazyDanych.uruchomZapytanie: Linia 232
aplikacja.Akcja.obslużŻądanie: Linia 3
jakiś.Serwer.przekażŻądanieDoAplikacji : Linia 15
jakis.Serwer.obslużŻądanie : Linia 10

Będziemy mieli taki:

WyjątekBazyDanych
Callback.onError : Linia 6
jakis.Serwer.pętlaZdarzeń: Linia 241

O ile kod napisaliśmy w sposób jaki przedstawiłem na przykładzie powyżej (tzn. z wykorzystaniem funkcji anonimowych, za pomocą closure lub za pomocą klas anonimowych) to sprawa jest uproszczona - ze śladu stosu wiemy dokładnie, która linijka powoduje błąd. Sytuacja komplikuje się, gdy np. jedna funkcja zwrotna wykorzystywana jest w wielu miejscach np.:

var callback = function(result) {
        for(var i=0; i<result.records.length; i++) {
         // rób coś z rekordami przesłanymi z bazy
        };
    };
1: connection.query("SELECT * FROM test1", callback);
2: connection.query("SELECT * FROM test2", callback);


W tym przykładzie nie wiemy które zapytanie spowodowało uruchomienie funkcji zwrotnej. Nie trudno się domyślić, że często może przysporzyć to nam problemów w znajdywaniu błędów. Jest to szczególnie uciążliwe gdy na przykład otrzymaliśmy logi z błędami z produkcji i nie wiemy tak naprawdę co generuje błąd.

Ostatni już problem to raczej nie jest problem charakterystyczny dla podejścia nieblokującego - występuje on w obu podejściach jednak wykorzystując serwery sterowane zdarzeniami można o nim łatwo zapomnieć (lub inaczej - ciężko go zrozumieć :)). Chodzi o potencjalne niebezpieczeństwo związane z wyczerpaniem przydzielonej pamięci. W chwili oczekiwania na zasoby (np. odpowiedź z bazy danych) wątek wykorzystywany jest do obsługi innego żądania. Nie oznacza to jednak, że cały fragment pamięci wykorzystywany do tej pory w wątku jest czyszczony. W poprzednich częściach pisałem co nieco o stosie i stercie. Stos dotyczył wątku, sterta całego procesu. W sytuacji gdy wątek może być wykorzystany na obsługę innego żądania to stos nie jest już potrzebny - można go "wyzerować". Co innego sterta - obiekty przechowywane na stercie są czyszczone przez garbage collector dopiero gdy nikt nie ma do nich referencji.  Mogłoby się wydawać, że skoro stos został "wyzerowany" to nie ma już żadnych referencji do obiektów na stercie. Nic bardziej mylnego - klasy anonimowe, closures czy funkcje anonimowe to obiekty, które mogą posiadać referencje do niektórych obiektów ze sterty - wszystko zależy od tego, z których zmiennych korzystamy. Przykładowo:

var tablica = [1, 2, 3, 4, 5, 6];
connection.query("SELECT * FROM test1",
    function(result) {
      var e =  tablica[0];
    });

Powyższy kod wykorzystuje tablicę, więc nie może być ona usunięta przez garbage collector. Niektóre języki programowania (takie jak Java) ze względu na statyczne typowanie są w stanie już na etapie kompilacji wydedukować, które zmienne muszą zostać w pamięci (a które nie). Dzięki temu przechowywane są tylko te rzeczywiście używane. Implementacje innych języków np. JavaScript, Ruby mogą już z tym mieć problemy, przez co często trzeba przechowują wszystkie zmienne ze stosu! W obu przypadkach trzeba pamiętać o zagrożeniu bo w sytuacji gdy aplikacja będzie wykorzystywać bardzo dużą ilość pamięci RAM na stertę to może się okazać, że będziemy w stanie obsłużyć tylko niewielką liczbę równoległych żądań. 

Na tym skończę opisywanie problemów związanych z wykorzystaniem nieblokujących serwerów sterowanych zdarzeniami. W następnej części podsumuję cały cykl i wypunktuję obecnie najpopularniejsze technologie tworzenia aplikacji asynchronicznych. 

sobota, 4 lutego 2012

Nieblokujące serwery sterowane zdarzeniami cz. 4: Zastosowanie

Zgodnie z obietnicą w tej części cyklu opiszę jakiego typu aplikacje mogą skorzystać z dobrodziejstw podejścia asynchronicznego sterowanego zdarzeniami. W poprzedniej części napisałem, że aby poczuć różnicę między podejściem blokującym a nieblokującym to ilość równoległe obsługiwanych żądań musi być liczona w tysiącach. W sytuacji wykorzystania liczby wątków odpowiadającej tak dużej liczbie żądań może się okazać, że procesor przez większość czasu będzie wykorzystywany do operacji context switch, a pamięć RAM do przechowywania stosu wątków. Może zabraknąć więc mocy obliczeniowej na wykonywanie algorytmów i pamięci RAM na przechowywanie danych. Jakie aplikacje mają jednak takie wymagania? O tym poniżej.

Pierwszym najbardziej rozpowszechnionym zastosowaniem serwerów asynchronicznych sterowanych zdarzeniami są serwery serwujące statyczne pliki z dysku, czyli np. serwery www takie jak Nginx. Ich głównym zadaniem jest wczytywanie plików i przesyłanie ich do klienta - czyli w większości wypadków przeglądarki www. Czytanie zawartości plików z dysku trwa jakiś czas, dlatego serwer musi na nie czekać. W podejściu nieblokującym serwer nie będzie blokował wątku do czasu odczytu fragmentu pliku i przejdzie do obsługi następnego żądania. Serwer jest więc w stanie obsłużyć wiele tysięcy żądań w ciągu sekundy. Oczywiście przepustowość zależy także (a raczej przede wszystkim) od szybkości dysku oraz tego, czy pliki są w buforze systemu operacyjnego. Taki serwer potrzebuje niewielkiej ilości pamięci  oraz jest oszczędny jeśli chodzi o czas procesora. Dlatego tak dużo firm decyduje się na użycie Nginx lub innych podobnych serwerów.

Oprócz serwerów serwujących statyczne pliki z podejścia nieblokującego mogą korzystać także serwery proxy, na przykład te będące serwerami buforującymi (cacheującymi odpowiedzi w niepoprawnym polsko-angielskim ;) ). Do takich zaliczyć można Varnish lub wspomniany już Nginx. Także w tym wypadku serwer przez większość czasu czeka na zasoby - konkretnie na odpowiedź proxowanego serwera lub odczyt wcześniej zapisanej odpowiedzi z dysku (tylko w przypadku Nginx).

Innym przykładem są serwery kolejek wiadomości. Szczególnie chodzi o wyciąganie wiadomości z kolejki - klient chcący pobrać wiadomość jest blokowany dopóki wiadomość nie pojawi się w kolejce.  Wykorzystując nieblokujące podejście serwer może obsługiwać bardzo dużą liczbę słuchaczy nie marnując przy tym zasobów. Przykładowo silnik HornetQ może wykorzystywać Netty (framework służący do tworzenie nieblokujących serwerów w Javie) w celu ograniczenia liczby wykorzystanych wątków zapewniając przy tym większą przepustowość.

Powyżej opisywane serwery są raczej systemami ogólnego zastosowania. Dlatego szansa, że sam będziesz taki serwer pisać jest raczej mała (ze względu na fakt, że napisano już ich setki i nie ma sensu wymyślać koła na nowo ;) ). Są jednak bardziej specyficzne problemy, które może rozwiązać architektura asynchroniczna. Na przykład - serwer chatu. Problem znany od dawna, jednak wciąż sprawiający problemy. Ilość przesyłanych wiadomości między użytkownikami nie musi być wcale duża w porównaniu do liczby użytkowników zalogowanych do chatu. Część użytkowników może w ogóle nie być przy komputerze, część tylko czyta wiadomości itp. Aby użytkownik widział pojawiające się wiadomości w czasie rzeczywistym to musi być cały czas podłączony do serwera. Korzystając z podejścia blokującego trzeba wykorzystać tyle wątków ile użytkowników. Sam serwer robi jednak niewiele - najczęściej jest  tylko pośrednikiem między klientem (np. przeglądarką) a serwerem kolejek (np. typu topic). Dlatego jeśli mamy zamiar obsługiwać co najmniej 10 tysięcy użytkowników lub więcej jednocześnie to lepiej wykorzystać nieblokujące podejście sterowane zdarzeniami. Trzeba jednak pamiętać, aby API za pomocą, którego porozumiewamy się z serwerem kolejek też działało w sposób nieblokujący (tak aby można było zwolnić wątek).

Do serwera chatu podobne są serwery gier - czyli serwery, gdzie każda czynność gracza jest natychmiast wysyłana od klienta. Zasada w sumie identyczna, aczkolwiek sam serwer gier może wykonywać bardziej skomplikowane algorytmy niż chat co z kolei powoduje, że liczba równoległych żądań jest ograniczona bez względu na użyte podejście: blokujące czy nie. Współczesne gry społecznościowe działają nieco inaczej - np. Playfish w swoich grach buforuje po stronie klienta czynności gracza po czym wysyła cały pakiet czynności w jednym żądaniu. Po stronie serwera zaś operacje uruchamiają się w sposób asynchroniczny (algorytmy uruchamiają się później na innych maszynach), dzięki czemu serwer przyjmujący akcje użytkownika nie zajmuje się "ciężkimi operacjami".

Kolejnym przypadkiem wykorzystania podejścia nieblokującego sterowanego zdarzeniami jest pisanie serwera strumieniującego, np. serwera pozwalającego na streaming wideo. Serwer taki także czeka na dane np. z dysku i przesyła je fragmentami do klienta. W podejściu blokującym znowu liczba wątków będzie równa liczbie użytkowników oglądających film. W nieblokującym będzie można to zrobić nawet na jednym wątku.

Oczywiście przykłady, które podałem nie wyczerpują tematu. Z pewnością jest o wiele więcej zastosowań. Zapraszam więc do dodania komentarza, do czego jeszcze można użyć tą architekturę. W następnej części cyklu opiszę na jakie problemy można natrafić korzystać z serwerów asynchronicznych. Mam nadzieję, że do tej pory nie zanudziłem i chociaż trochę rozjaśniłem temat nieblokujących serwerów sterowanych zdarzeniami :)

niedziela, 29 stycznia 2012

Nieblokujące serwery sterowane zdarzeniami cz. 3: Zyski

Nieblokujące serwery sterowane zdarzeniami mogą mieć większą przepustowość od klasycznego synchronicznego (czyli blokującego) podejścia z wykorzystaniem puli wątków. Do tej pory skupiłem się na wprowadzeniu w temat (część pierwsza cyklu) oraz opisałem jak można zapewnić równoległą obsługę żądań (część druga). Teraz nasuwa się pytanie ile (dokładnie) możemy na tym zyskać i jakie konkretne problemy rozwiązać. Mylne jest bowiem założenie, że dowolną tworzoną aplikację powinniśmy tworzyć w oparciu o tą a nie inną architekturę. Każdy dobry programista/projektant/architekt wie, że zawsze powinniśmy starać się dobierać narzędzia odpowiednie do problemu, który chcemy rozwiązać. W poprzednich częściach cyklu napisałem, że wykorzystując asynchroniczne serwery sterowane zdarzeniami oszczędzamy na takich zasobach jak pamięć oraz procesor. W tym poście opiszę ile można oszczędzić i w jakich przypadkach.

W poprzednim poście opisałem sposoby równoległej obsługi żądań. Podejście nieblokujące sterowane zdarzeniami w porównaniu do pozostałych charakteryzowało się tym, że wykorzystywało znacznie mniejszą liczbę wątków lub procesów (od jednego do kilku wątków, na ogół nie więcej niż liczba dostępnych procesorów/rdzeni). Pomimo tak małej liczby wątków serwery te potrafiły obsługiwać bardzo duże ilości żądań równoległych o liczbie wielokrotnie większej niż ilość użytych wątków, oszczędzając przy tym na zasobach. Ile jednak można zaoszczędzić i czy gra jest warta świeczki?

Aby zrozumieć ile można zyskać trzeba znowu zejść o wiele warstw niżej - aż do systemu operacyjnego i sposobu w jaki obsługuje on procesy oraz wątki. Procesy od wątków różnią się między sobą znacznie, jednak nie zamierzam tu opisywać wszystkich różnic. Skupię się tylko na wykorzystaniu przez nich zasobów (tj. pamięci oraz czasu procesora).

Wykorzystanie pamięci RAM

Zarówno proces jak i wątek posiadają na własność jakąś część pamięci wirtualnej. Istotne tu jest słowo "na własność", które oznacza, że dostęp do tej pamięci ma tylko dany proces lub dany wątek. Inne procesy i wątki nie mogą jej odczytywać ani modyfikować.  Każdy proces posiada na własność m.in. stertę, stos oraz kod programu. Wątki posiadają na własność tylko stos - stertę oraz kod współdzielą z innymi wątkami. Można wykonać obliczenia, ile więcej pamięci wirtualnej pochłonie przykładowe 1000 wątków/procesów w stosunku do 10 wątków/procesów. W przypadku wątków obliczenie ilości konsumowanej pamięci wirtualnej jest stosunkowo proste. Przy tworzeniu każdego wątku deklaruje się bowiem rozmiar stosu. Dokładnie taki rozmiar zostanie alokowany przez system operacyjny. Nieważne jak bardzo zagnieżdżone będą wywołania w kodzie - stos wątku przez cały cykl życia wątku zajmuje tyle samo pamięci. Teraz trzeba zadać pytanie ile pamięci potrzeba na stos wątku. Wielkość ta zależna jest od wykorzystanego języka programowania. Różne języki pozwalają przechowywać różne rzeczy na stosie. W przypadku języka C/C++ rozsądną liczbą jest 1MB (dla 32-bitowego systemu), w przypadku Javy stos może być mniejszy, bo tutaj większość struktur przechowywana jest na stercie (512KB domyślnie na Oracle JVM na x86-32). Ktoś mógłby się przyczepić, że przecież program w C/C++ może podobnie jak program w Javie przechowywać na stosie tylko referencje i typy proste - jeśli tak jest to można założyć, że Java i C/C++ będą potrzebowały stosu o podobnej wielkości. Ale czy tak to wygląda w rzeczywistości? Jeśli programujesz w C/C++ zapraszam do dodania swojego komentarza :) Wracając do założeń dotyczących wielkości stosu wykonajmy prostą matematykę:

Dla programu w C/C++:
10 wątków - 10MB
1000 wątków - 1GB

Dla programu w Java:
10 wątków      - 5MB
1000 wątków  - 500MB

500MB-1GB to nawet w architekturze 32-bitowej nie stanowi przesadnie wiele. Załóżmy, że obecnie za niewielkie pieniądze można kupić średniej klasy serwer wyposażony w 16GB RAM działający na 64-bitowym systemie operacyjnym. Mnożąc te liczby razy 2 (takie bardzo naiwne założenie, że architektura 64-bitowa pochłania 2 razy więcej RAMu) to w przypadku C/C++ program pochłaniał będzie 2GB RAMu, w przypadku Java 1GB. Myślę, że przy tej konfiguracji sprzętowej wartości te są dopuszczalne - nie powinny stanowić większego problemu. Problem jednak jest jeśli chcemy móc obsługiwać jeszcze większą liczbę żądań równocześnie. Np. 10000 wątków. W tym momencie program będzie wymagał porządnej maszyny, która raczej nie zaliczać się będzie do tych przeciętnej klasy. Pytanie tylko, po co nam aż 10000 wątków? O dziwo są aplikacje, które mają takie wymagania, opiszę je jednak dopiero w następnej części cyklu. Tymczasem wróćmy do rozważań na temat wykorzystania zasobów.

Jeśli chodzi o wykorzystanie pamięci wirtualnej przez procesy to niestety sprawa już nie jest taka prosta. Rozmiar pamięci wirtualnej wykorzystywanej przez proces zmienia się w czasie. Każde wywołanie malloc lub podobnej funkcji powoduje alokację pamięci dla sterty. Stos z kolei zwiększa się wraz z kolejno odkładanymi ramkami i w większości współczesnych systemów operacyjnych raczej pozostaje w tym rozmiarze (tzn. nie zmniejsza się już pomimo, że np. skończyliśmy wywoływać funkcję rekurencyjną). Ciężko jest jednak obliczyć ile proces będzie potrzebował pamięci. Nie tylko rozmiar stosu ma tu znaczenie, ale przede wszystkim rozmiar sterty i kod programu. Jakby tego było mało system operacyjny może optymalizować wykorzystanie pamięci przez proces i np. wybrane strony w pamięci wirtualnej współdzielić między procesy (o ile żaden z procesów ich nie modyfikuje). Ciężko jest więc obliczyć jaki narzut na zużycie pamięci mają procesy. Być może Ty masz jakiś pomysł jak to obliczyć :) ?

Czytając fragment dotyczący wykorzystania wątków można by odnieść wrażenie, że tylko liczba wątków ma znaczenie - im więcej wątków tym większe zużycie pamięci. Jednak w rzeczywistości liczy się tak naprawdę liczba żądań. Nawet wykorzystując jeden wątek do obsługi wszystkich żądań trzeba liczyć się z tym, że każde żądanie przechowuje jakieś dane na stercie (np. wyniki zapytania potrzebne do kolejnego kroku w kodzie obsługującym żądanie). Czy kod czeka na nie czy nie to i tak musi przechowywać jakieś dane na stercie. Dlatego w wielu aplikacjach (o ile nie w większości aplikacji bazujących na jakieś bazie danych) problemem może być zbyt duże wykorzystane pamięci na potrzeby sterty. W takiej sytuacji może się okazać, że bez względu na ilość użytych wątków maksymalna liczba równolegle obsługiwanych żądań jest na tyle mała, że nie ma nawet sensu zastanawiać się czy chcemy wykorzystywać synchroniczny czy asynchroniczny serwer (w jednym z systemów, przy których przyszło mi pracować w przeszłości liczba równoległych wątków dla konkretnego żądania nie mogła być większa od pięciu, bo kończyła się pamięć).

Wykorzystanie CPU


Teraz czas na drugi zasób - procesor. Sposób w jaki liczba wątków wpływa na procesor jest znacznie bardziej skomplikowany aniżeli sposób w jaki wpływa na pamięć. Mogłoby się wydawać, że liczba uruchomionych wątków lub procesów nie wpływa znacząco na procesor. Otóż nie, o ile liczba wątków lub procesów nie jest liczona w wielu tysiącach. Czemu jednak liczba ma znaczenie? Powód jest jeden - jeśli liczba procesów i wątków jest większa od liczby rdzeni/procesorów w serwerze to zadaniem systemu operacyjnego (schedulera) jest zarządzanie przydzielaniem czasu procesora dla danego procesu lub wątku. Oczywiście wskazane jest, żeby system operacyjny robił to w taki sposób, aby użytkownik miał wrażenie, że wszystkie te operacje wykonywane są równocześnie (za pomocą tzw. wywłaszczania). System operacyjny przydziela więc w krótkich odstępach czasowych czas procesora dla wybranych wątków lub procesów, które mają coś do zrobienia. To co spowalnia to tzw. context switch, czyli przełączenie procesora aby wykonywał kod z następnego wątku lub procesu. Operacja polega na skopiowaniu pewnych danych opisujących wątek lub proces z pamięci RAM do procesora i na odwrót. Przykładowo wątek, który aktualnie jest uruchomiony na procesorze wykorzystuje rejestry procesora oraz licznik rozkazów. W momencie przełączenia na inny wątek - trzeba skopiować te rejestry oraz licznik do pamięci, a z pamięci skopiować dane o następnym wątku do procesora. Operacja ta powinna być błyskawiczna - liczona w kilku/kilkunastu mikrosekundach. Jednak jeśli jest wykonywana tysiące razy w ciągu sekundy to jest w stanie spowolnić czas obsługi żądań. Context switch wykonywany jest w kilku przypadkach. Wykonuje go sam scheduler podczas wywłaszczania, wykonuje go program, który w wyniku uruchomienia polecenia systemowego czeka na dane (np. czyta z pliku). Context switch uruchamiany jest także wtedy gdy wykonywana jest funkcja systemowa sleep. Podsumowując im więcej wątków tym większa szansa na context switch. Aplikacje, które nie wykonują obliczeń, a jedynie zbierają wyniki z różnych zewnętrznych zasobów (np. bazy danych) nie korzystają zanadto z czasu procesora - w ich przypadku to właśnie context-switch może najbardziej obciążać procesor. Ile jednak operacji context-switch musi się uruchomić, żeby miało to wpływ na ogólną pracę serwera? Zakładając, że jeden context-switch to 10us to wychodzi, że musiałby być uruchomiony co najmniej 50000 razy w ciągu sekundy (50k switchów trwa około 500ms). Liczba taka jest jak najbardziej osiągalna jeśli napiszemy program wykorzystujący 10000 wątków i wykonujący operację sleep w każdym z nich co 100ms. Przykładowy kod w Java zamiesciłem na GitHub. Polecenie sleep ma emulować czekanie wątku na zasoby. Założyłem, że będzie to 100ms. W rzeczywistości czas oczekiwania może bardzo różny np. dla prostego zapytania w bazie trwać kilkanaście sekund do nawet kilku sekund oczekiwania na odpowiedź zdalnego serwera www. Ciężko mi jednak znaleźć jakąś średnią - wszystko zależy od tworzonej aplikacji. Przykładowy program można uruchomić na swoim komputerze i użyć narzędzia do mierzenia context switch'ów (np. sar pod Linuxem). Przykład powoduje na moim komputerze wykorzystanie 50% czasu procesora na sam context-switch co daje do myślenia i nietrudno się domyśleć, że może mieć wpływ na ostateczną przepustowość serwera. Trzeba jednak pamiętać, że liczba wątków naprawdę musi być duża tzn liczona w dziesiątkach tysięcy (przy pauzach 100ms) lub w tysiącach (jeśli pauza wynosi 10ms).

Opisując czas potrzebny do wykonania context switch skupiłem się na wątkach. Zrobiłem to celowo, ponieważ context switch w przypadku wątków jest znacznie bardziej efektywny od tej samej operacji w przypadku procesu. Dzieję się tak z jednego powodu - pamięci wirtualnej. System operacyjny musi bowiem pamiętać tablicę stron dla procesu - czyli odwzorowania adresów logicznych na adresy fizyczne. Tablice te przechowywane są w pamięci cache procesora, aby czas translacji adresów był bardzo szybki. Wykonanie context switch procesu sprawia, że tablica ta staje się nieaktualna (trzeba tu wgrać nową tablicę). W przypadku wątków tablica stron jest współdzielona, więc context switch jest szybszy. Dlatego decydując się na wykorzystanie procesów dla  zapewniania  równoległej obsługi żądań trzeba liczyć się z tym, że procesor będzie bardziej obciążony (jak bardzo zależy tak naprawdę od samego procesu i wielkości pamięci wirtualnej, którą wykorzystuje).

Myślę, że ten post pozwolił Ci zrozumieć potrzebę wykorzystania asynchronicznych serwerów sterowanych zdarzeniami. Przydają się one przede wszystkim w sytuacjach, gdy serwer ma obsługiwać bardzo dużą liczbę równoległych żądań liczonych w tysiącach, przy założeniu jednak, że ilość zasobów potrzebnych na obsługę żądań jest niewielka (to znaczy każde żądanie nie pochłania dużej ilości pamięci lub czasu procesora). Przydatne są także wtedy gdy każde żądanie trwa długi czas (przez kilka sekund) czekając przez większość czasu na zewnętrzne zasoby (takie jak zewn. serwer czy dysk twardy). W następnej części cyklu opiszę konkretne zastosowania.

sobota, 7 stycznia 2012

Nieblokujące serwery sterowane zdarzeniami cz. 2: Równoległa obsługa żądań

W poprzedniej części streściłem w wielkim skrócie czym są sterowane zdarzeniami nieblokujące serwery. W tym jednak chciałbym wrócić do korzeni i opisać skąd w ogóle wziął się pomysł na coś takiego.

Obecnie wykorzystywane serwery muszą obsługiwać wiele żądań jednocześnie aby zwiększyć swoją przepustowość. Potrzeba ta zaistniała, z kilku powodów. Po pierwsze wielordzeniowe procesory i maszyny wieloprocesorowe muszą być wykorzystane w pełni. Wykonywanie tylko jednej operacji w danym czasie jest oczywistym marnotrawieniem wynalazków techniki. Po drugie większość aplikacji nie jest zwykłym algorytmem, który korzystając tylko i wyłącznie z przydzielonej mu pamięci RAM wykonuje kolejne kroki algorytmu. Programy te komunikują się z innymi procesami, np. bazami danych, innymi serwerami lub wykonują operacje na dysku. W chwili gdy oczekują na te tzw. "zewnętrzne zasoby" mogłyby obsłużyć inne żądania. Sposobów równoległej obsługi wielu żądań jest wiele. Oto najważniejsze z nich:

  1. Wątek tworzony dla każdego połączenia
  2. Pula wątków
  3. Jeden wątek sterowany zdarzeniami wykonujący nieblokujące operacje we/wy
  4. Jeden wątek szefa (boss) i wiele wątków pracowników (workers)
Zanim wejdę w szczegóły chciałbym nadmienić, że pisząc o wątkach mam na myśli zarówno wątki jak i procesy. To znaczy, że aby zapewnić równoległe przetwarzanie żądań można użyć jednego z nich (lub obu jeśli ktoś zapragnie). O różnicach między wątkami i procesami napisano już naprawdę wiele, dlatego nie będę tu wchodził w takie szczegóły.

Pierwszy sposób jest najbardziej oczywisty. Metodą obsługi wielu żądań jednocześnie jest stworzenie wątku dla każdego zaakceptowanego połączenia. Na przykład dla każdego połączenia HTTP serwer tworzy wątek, wykonuje przetwarzanie w wątku, zwraca wyniki w postaci strony HTML a następnie usuwa wątek.  Rozwiązanie proste jednak najmniej efektywne (o tym za chwilę).

Drugi sposób - pula wątków - to rozwinięcie pierwszego. Powstał z dwóch powodów.  Pierwszy powód to zagrożenie jakim jest zbyt duża liczba utworzonych wątków, która może spowodować błąd braku pamięci lub spowolnienie pracy serwera. Każdy wątek tworzy i przetwarza swoje dane (na stosie i stercie - o tym napiszę w kolejnych częściach cyklu), które przechowywane są w pamięci. Zbyt duża liczba utworzonych wątków może sprawić, że zabraknie pamięci RAM. Wpływa to także na przepustowość serwera. Procesor ma ograniczoną liczbę rdzeni, a wątków mogą być tysiące. W takim wypadku system operacyjny zarządza, który wątek ma się teraz uruchamiać. Aby wątek nie miał monopolu to system musi co chwila dawać szansę innym wątkom dokonując tzw. przełączania kontekstu. Operacja ta jest bardzo szybka, jednak wykonując ją setki tysięcy razy można znacznie spowolnić działanie serwera. Drugi nieco mniej ważny powód to długi czas tworzenia wątku lub procesu. Zamiast tworzyć nowy wątek za każdym razem gdy nawiązywane jest połączenie wykorzystywany jest już wcześniej utworzony wątek. Po wykonaniu wszystkich operacji w wątku jest on zwalniany i trafia z powrotem do puli. Teraz można go ponownie wykorzystać.

Trzeci sposób jest zdecydowanie bardziej innowacyjny od poprzednich. Metoda ta zrywa z wielowątkowości całkowicie, a mimo to jest w stanie obsłużyć żądania wielu użytkowników jednocześnie. Jak to możliwe? Wspominałem na początku tego posta, że serwery w kółko na coś czekają - na bazę danych czy na dysk. W czasie tego oczekiwania można by wykorzystać ten pojedynczy wątek na obsługę kolejnego żądania. Nie jest to jednak proste z technicznego punktu widzenia. Wszystkie funkcje, które wykonują jakiekolwiek operacje we/wy np. funkcja odczytu pliku, funkcja uruchamiająca zapytanie na bazie danych musiałaby zlecić wykonanie operacji nie blokując jednak wątku w oczekiwaniu na odpowiedź. Natomiast resztę kodu obsługi żądania musiałaby uruchomić później. Poniżej zamieszczam pseudkod, który pokazuje jako mogłaby wyglądać funkcja obsługi żądania:

funkcja obslugujŻądanie() {
  uruchomZapytanie( { 
    kod ktory ma się uruchomić po otrzymaniu odpowiedzi z bazy
  } );
}

Dowolna funkcja wykonująca operacje we/wy powinna zgłosić systemowi operacyjnemu chęć otrzymania zdarzenia (tzw. gotowości zasobu). Przykładowo funkcja uruchamiająca zapytanie w bazie danych z przykładu powyżej oczekuje odpowiedzi z bazy w postaci listy rekordów. Dane będą dostępne dopiero gdy baza danych wykona zapytanie i zacznie wysyłać pierwsze bajty odpowiedzi poprzez gniazdko. Funkcja ta powinna więc nasłuchiwać zdarzenia gotowości do odczytu z tego gniazdka. 

Po uruchomieniu funkcji obslugujŻądanie serwer wraca do początkowego stanu i może obsłużyć kolejne żądanie. Następnym razem serwer sprawdza czy nie pojawiły się nowe zdarzenia. Okazuje się, że baza danych przesyła odpowiedź. W tym momencie serwer uruchamia kod, który wcześniej został oznaczony do uruchomienia po odpowiedzi z bazy danych.

Poza tymi komplikacjami model numer 3. nie jest też pozbawiony innych wad. Jedną z nich jest niewykorzystanie procesorów wielordzeniowych, gdyż wątek jest zawsze jeden. W tej sytuacji trzeba ręcznie uruchomić np. tyle procesów ile jest rdzeni, co nieco komplikuje kwestie administracji - nie chodzi tu bynajmniej o napisanie skryptu, który uruchamia na przykład 4 procesy zamiast jednego. Każdy proces to serwer nasłuchujący na wskazanym porcie. Nie mogą jednak nasłuchiwać na jednym porcie. Dlatego konieczne jest wykorzystanie jakiegoś serwera "z przodu" (np. load balancera), który przychodzące żądania pokieruje do któregoś z procesów.

Drugą wadą - znacznie poważniejszą jest dość nietypowy sposób pisania kodu. Nie pisze się bowiem programów w standardowy sekwencyjny sposób, linijka po linijce. Zamiast tego trzeba używać tzw. callback'ów czyli funkcji, które uruchamiają się na dane zdarzenie (np. gdy przyjdzie odpowiedź z bazy danych, gdy zostanie przesłane żądanie HTTP itp.). Powyższy przykład pseudo-kodu ilustruje problem. Kod przekazany do funkcji uruchomZapytanie to argument będący blokiem kodu, który nie uruchamia się natychmiast, a dopiero po otrzymaniu zdarzenia z systemu operacyjnego informującym o tym, że baza danych przesłała odpowiedź.

Co jednak zyskujemy korzystając z serwera opartego na jednym wątku sterowanym zdarzeniami? Przede wszystkim dwie rzeczy: mniejsze zużycie pamięci RAM oraz większą przepustowość w sytuacji gdy liczba równoległych żądań jest liczona w setkach a nawet tysiącach. Ile tak naprawdę tej pamięci zyskujemy oraz czasu procesora zostanie opisane w następnej części cyklu.

Metoda numer cztery to zmodyfikowana wersja metody pojedynczego wątku. Owszem, tu także jest jeden wątek, który wyciąga zdarzenia informujące o przesyłaniu żądań do serwera. Jednak po nawiązaniu połączenia wykorzystywana jest pula pracowników (workers). Każdy wątek z puli obsługuje jedno połączenie. Rozwiązanie to jest o tyle dobre, że wykorzystane są procesory wielordzeniowe a także w kodzie, który obsługuje żądanie nie ma konieczności wykorzystywania nietypowych rozwiązań z funkcjami typu callback, gdyż uśpienie wątku w oczekiwaniu na odpowiedź z bazy danych nie powstrzymuje serwera od obsługi innych żądań (usypianie ma swoje wady o czym w następnej części).

W praktyce stosuje się różne przedstawione tu metody, w zależności od potrzeb dotyczących wydajności a także łatwości utrzymania. Pierwsza metoda z tworzeniem wątku dla każdego zaakceptowanego połączenia jest raczej książkowa i nie powinna być używana w produkcyjnym kodzie. Użycie puli jest już wiele lepsze i niektóre ze starszych serwerów realizują wielozadaniowość właśnie w ten sposób. Pojedynczy wątek wykorzystywany jest zaś przez ostatnio bardzo popularny Node.js. Serwery NginxTomcat lub Jetty wykorzystują podejście z jednym wątkiem głównym i wieloma pracownikami. W następnej części porównam wszystkie te rozwiązania pod kątem wydajności oraz zużycia pamięci.

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.

środa, 14 grudnia 2011

Rozpoczęcie działalności TryCatch


Od 1. grudnia firma TryCatch jest oficjalnie zarejestrowaną firmą w Polsce. Została założona przeze mnie aby sprostać wymaganiom wszystkich firm, chcących tworzyć aplikacje www obsługujące duże wolumeny użytkowników. Ze względu na popularyzację internetu a przede aplikacji www (takich jak np. serwisy społecznościowe) zapotrzebowanie na wysoce skalowalne aplikacje rośnie. TryCatch to firma, która staje naprzeciw aktualnym wymaganiom rynku. Chcemy pomóc firmom w dobraniu odpowiednich technologii, zaprojektowaniu architektury systemu a także samym developmencie. Nasz cel jest prosty: chcemy pomóc firmom tworzyć rozwiązania wydajne, skalowalne i przede wszystkim tanie z punktu widzenia inwestycji w sprzęt.

Od 7 lat zajmuję się tworzeniem aplikacji. Przez większość czasu implementowałem aplikacje www - niezależnie od tego czy były to aplikacje dla szerokiego spektrum odbiorców czy aplikacje wykorzystywane wewnątrz jednej firmy. Scentralizowana architektura tego typu aplikacji stawia niebyle wyzwania przed twórcami oprogramowania. Ze względu na wykorzystanie przeglądarek internetowych większość obliczeń musi być wykonywana po stronie serwera - operacje na bazie danych, algorytmy logiki biznesowej, generowanie dynamicznej treści HTML to tylko niektóre obowiązki serwerów. Problem zaczyna się w momencie gdy liczba żądań lub użytkowników aplikacji jest tak duża, że serwer nie jest w stanie ich obsłużyć w rozsądnym czasie (tj. czasie, po którym użytkownik nie straci cierpliwości). Aby poprawić czasy odpowiedzi można zainwestować w sprzęt wymieniając maszyny na mocniejsze lub dokupując macierz dyskową. Jednak tego typu inwestycje przestają być opłacalne wraz z coraz większym wzrostem obciążenia. Możliwości maszyn są ograniczone, a koszt maszyny wraz ze wzrostem jej możliwości obliczeniowych rośnie wręcz parabolicznie. Dlatego tworząc skalowalne systemy trzeba znaleźć złoty środek pomiędzy inwestycjami w sprzęt a inwestycjami w oprogramowanie, będące w stanie wykorzystać słaby sprzęt ale w większej ilości (tj. zamiast jednego bardzo drogiego superkomputera możemy kupić kilka serwerów przeciętnej klasy). Najczęściej odwiedzane serwisy internetowe na świecie takie jak: Google, Amazon, Facebook, EBay lub Twitter wybrały właśnie tą drogę - wykorzystanie wielu serwerów i stworzenie takiej architektury oprogramowania, która umożliwi ich efektywne wykorzystanie.

Strategia TryCatch jest jasna - wspomóc rodzime biznesy dostarczając wiedzy i technologii. Będziemy zajmować się tworzeniem architektur i implementacją systemów. Chcielibyśmy także stworzyć własne autorskie serwisy. Być może kiedyś któryś z nich okaże się hitem na miarę Facebooka ;)