Blue Green Release

Proces releasowania oprogramowania jest chyba jednym z ważniejszych wyzwać w czasach oprogramowania bazującego na usługach. 20 lat temu,  oprogramowanie zwykle było sprzedawane na nośnikach danych i częstotliwość wydawania nowych wersji była bardzo niska (np. dwie aktualizacje na rok).

Myślę, że 5-10 lat temu, standardem stało się releasowanie co iterację, np. raz na dwa tygodnie.  Od kilku lat jednak, wiele firm wdraża zmiany kilkakrotnie dziennie i taki proces można dopiero nazwać “continuous deployment”.

Temat bardzo szeroki, ale dzisiaj chciałbym przyjrzeć się tylko wzorcowi blue-green. Wiemy, że wdrożenie nowego kodu jest bardzo ryzykowne, nawet gdy mamy dobre pokrycie testami. W praktyce, istnieje wiele innych problemów takich jak konfiguracja zapory ogniowej czy odpowiednie uprawnienia.  Skoro chcemy wdrażać często, nie jesteśmy w stanie sobie pozwolić na jakąkolwiek przerwę w działaniu systemu.

Wzorzec blue-green zakłada, że mamy do dyspozycji przynajmniej dwa serwery: blue oraz green. W dowolnym momencie tylko jeden z nich jest aktywny oraz posiada najnowszą wersję. Ponadto do dyspozycji powinniśmy mieć load balancer albo router, który będzie przełączał przekierowania do serwera blue albo green. Na początku oczywiście, przy pierwszym wdrożeniu nie ma znaczenia, który z serwerów wybierzemy. Sytuacja zatem może wyglądać następująco:

1

Blue zawiera pierwszą wersje kodu. Nie ma tu nic specjalnego. Załóżmy jednak, że mamy nową wersję (v2) i chcemy w bezpieczny sposób ją wdrożyć.  Do dyspozycji mamy zapasowy serwer – w tym przypadku green. Umieszczamy tam zatem nową wersję:

2

Mamy teraz dwie wersje systemu, ale użytkownicy wciąż są przekierowani do v1. Mamy teraz czas na sprawdzenie czy nowy kod nie powoduje żadnych problemów. Oba węzły to serwery produkcyjne, zatem mają dostęp do tych samych zasobów. Jeśli jesteśmy pewni, że wszystko działa, zmieniamy konfigurację balancera, aby przekierowywał ruch do serwera zielonego:

3

Jeśli stwierdzimy, że nie wszystko jednak działa tak jak należy i np. musimy dokonać rollback’u, wtedy sewer niebieski wciąż zawiera poprzednią wersję. Wystarczy ponownie zmienić przekierowanie na balancerze i ruch będzie przekierowywany do V1.

Kolejne wdrożenia polegają na tej samej zasadzie. Wersja v3 będzie zainstalowana na serwerze z poprzednią wersją, czyli serwerze niebieskim w tym przypadku:

4

Innymi słowy, po każdym wdrożeniu, balancer będzie zmieniany tak, aby na drugi węzeł wskazywać. W dowolnym momencie, jeden z serwerów będzie zawierał aktualną wersję, a drugi poprzednią. W przypadku wdrażania nowego kodu, serwer z poprzednią wersją można określić jako staging albo preprod. Jak widać,  nie ma znaczenia, który serwer to “blue”, a który “green”. Powyższy wzorzec ma wiele nazw i myślę, że nazwa może być trochę myląca – “green” nie znaczy, że jest to zawsze aktywny serwer. Aktywny serwera, co każde wdrożenie jest przełączany między niebieskim, a zielonym węzłem.

Podsumowując dzięki blue\green deployment zyskujemy:

  • zero downtime deployment – aplikacja zawsze będzie dostępna dla użytkowników.
  • Rollback – wrócenie do poprzedniej wersji sprowadza się wyłącznie do zmiany konfiguracji routera\balancera.
  • Bezpieczne i bezstresowe wdrożenia.

Visual Studio 2015 – okno C# interactive

W Visual Studio 2015 Update 1 można znaleźć nowe okno, a mianowicie C# interactive. Często chcemy przetestować kawałek kodu w izolacji od aplikacji nad którą aktualnie pracujemy. Zwykle korzystałem z LinqPad, ale posiadanie takiej funkcji od razu w Visual Studio jest wygodniejsze. Wystarczy z menu głównego wybrać View->Other Windows->C# Interactive:

1

Następnie możemy wykonać jakikolwiek kod, np.:

2

Jak widzimy, jeśli nie zakończymy linii kodu średnikiem, wtedy zawartość zostanie po prostu wyświetlona. Wpisując #help dostaniemy opis podstawowych instrukcji\operacji:

3

Ponadto, okno wspiera bardzo dobrze Intellisense:

4

Dobrze, że można również deklarować przestrzenie nazw, co nie jest może bardzo intuicyjne, ale za to przydaje się:

5

Nic nie stoi na przeszkodzie, aby zadeklarować własną klasę:

6

Możliwe jest również załadowanie zewnętrznych bibliotek. Jak widać, jest to bardzo przydatny notatnik C#, a do tego jak już piszemy kod w Visual Studuio, odpalenie okna jest dużo szybsze niż załadowanie LinqPad czy stworzenie aplikacji konsolowej w celu przetestowania czegoś.

HTTP – testy wydajnościowe w JMeter

JMeter jest darmową aplikacją bardzo przydatną podczas  wykonywania “load testing”. Interfejs użytkownika co prawda jest bardzo mało intuicyjny, ale po pewnym czasie można przyzwyczaić się. Aplikacja, po uruchomieniu prezentuje się następująco:
1

Oczywiście na oficjalnej stronie można znaleźć pełną dokumentację, więc moim celem nie jest opisywanie każdego elementu. Jako próbkę, po prostu spróbujmy stworzyć test, który będzie łączył się z jakąś stroną (np. Google) i symulował zapytania wykonywane przez użytkownika. W tym celu, najpierw dodajemy tzw. Thread Group:

2

Thread group służy do symulacji ruchu. Po dodaniu elementu, będziemy mogli skonfigurować częstotliwość zapytań:

3

Number of threads oraz ramp-up period to chyba najważniejsze parametry. Pierwszy z nich to liczba użytkowników (wątków). Drugi z kolei to czas w którym zapytania mają zostać wykonane. Jeśli zatem ustalimy Number of threads na 1000, a ramp-up period na 30, wtedy zostanie wykonanych 1000 akcji (np. zapytań) w ciągu 30 sekund.

Kolejny przydatny element to “Sampler”. W naszym przypadku skorzystamy z HTTP Request:

4

Po dodaniu elementu zobaczymy,  że możemy skonfigurować nazwę serwera, jak i przekazywane parametry. Załóżmy, że chcemy wykonać proste zapytanie GET do Google:

5

Z powyższego screena widać, że praktycznie każdą składową można skonfigurować. Nie ma zatem problemu z wysłaniem POST z bardziej zaawansowanymi parametrami.

Po wykonaniu zapytania, warto coś zrobić z dostarczoną odpowiedzią. Pierwszym krokiem może być wyświetlenie wyników np. w formie drzewa:

6

Przydatne jest również wygenerowanie Summary Report:

7

Niezbędna jest również walidacja odpowiedzi. Musimy wiedzieć, kiedy nasz test powinien zakończyć się sukcesem albo błędem. Z tego względu dodajmy asercję odpowiedzi:

8

Załóżmy, że jeśli odpowiedź przyjdzie z kodem HTTP 200, wtedy test zakończyć powinien się sukcesem:

9

Mamy już wszystkie elementy. Wystarczy, że skonfigurujemy Thread Group (liczbę użytkowników oraz ramp-up) i możemy uruchomić testy. Po uruchomieniu i wykonaniu testów, przejdźmy do dodanego wcześniej Results Tree:

10

Każdą odpowiedź możemy zobaczyć, zarówno w formie dokumentu HTML jak i czystego tekstu. Z kolei Summary Report zawiera informacje takie jak średni czas wykonania zapytania czy procent błędów:

11

Z powyższego screena wiemy, że wszystkie zapytania zakończyły się sukcesem, a średni czas wykonania to 481 milisekund.

Oczywiście to tylko namiastka możliwości JMeter. W praktyce będziemy musieli użyć więcej elementów. Zwykle serwisy mają mechanizm autoryzacji i wtedy będziemy musieli przechowywać ciasteczka pomiędzy różnymi zapytaniami. Zamiast wysyłać pojedyncze zapytanie, być możemy będziemy chcieli symulować pewną sekwencję zapytań.

Myślę, że przydatny skrót to CTRL+E, który powoduje wykasowanie aktualnych raportów.

Inną przydatną opcją jest możliwość nagrywania sekwencji za pomocą przeglądarki. Wtedy JMeter będzie służył jako serwer proxy i wszelkie zapytania wykonane przez użytkownika w przeglądarce, będą tworzyć odpowiednie elementy w JMeter.

HTTP 2.0 Server Push

Dzisiaj kolejny element HTTP 2.0, tym razem wymagający zmiany kodu po stronie aplikacji. Tak jak już z wszystkimi opisanymi wcześniej zmianami, ma to na celu zmniejszenie opóźnienia (latency) wynikającego z liczby zapytań.

Doskonale wiemy, że każda strona ma referencje do innych zasobów takich jak CSS czy pliki graficzne. Wcześniej zajęliśmy się już HTTP Multiplexing, który znacząco niweluje problem.

W jednym z poprzednich wpisów pokazałem również jak w HTTP 1.1 programiści radzili sobie z zasobami. Częstym obejściem, było umieszczenie plików graficznych inline. Powodowało to, że jak tylko plik HTML lub CSS został ściągnięty, plik graficzny stawał się od razu dostępny (ponieważ był częścią tego samego pliku).

Z tej techniki tak naprawdę wywodzi się Server Push. Wiemy, że w celu wyświetlenia strony “A”, musimy również wysłać zasób “B”. Dlaczego zatem nie wysłać zasobu B od razu zaraz po A? Inline jest pewną implementacją tego, ale jak wiemy dość kłopotliwą. Server push pozwala z kolei, “wepchnąć” dowolny zasób (nie koniecznie plik graficzny) w strumień danych płynący od serwera do klienta. Oczywiście serwer (np. IIS), nie będzie znał relacji w poszczególnych aplikacjach między zasobami. Wynika z tego, że to nasza rola (ewentualnie framework’a)  jest poinformować o tym klienta.

Służą do tego tzw. “Push Promise”, czyli obietnice klienta odnoście relacji między zasobami. Jeśli projektując system wiemy, że wyświetlając stronę A, zaraz będzie załadowana  biblioteka jQuery,  wtedy wydajemy obietnice. Push promise to prosta metoda, która zwraca listę zależności. Przeglądarka następnie przeczyta taką listę obietnic i stwierdzi czy warto je akceptować. Czasami te zasoby mogą już znajdować się w cache przeglądarki, wtedy należy przerwać taką transmisje. W przeciwnym wypadku przeglądarka zaakceptuje je i w praktyce zostaną one odebrane zaraz po przesłaniu strony – bez zbędnych opóźnień.

W ASP.NET dostępna jest już metoda PushPromise:

public void PushPromise(
	string path,
	string method,
	NameValueCollection headers
)

Drugie przeładowanie jest nieco prostsze w użyciu:

public void PushPromise(
	string path
)

Warto zauważyć, że w taki sposób możemy kontrolować czas życia obiektu. W przypadku inline nie było takiej możliwości – zawsze zasób był przesyłany. Jeśli korzystamy z ServerPush, zasób będący w cache przeglądarki zostanie anulowany i nie przesyłany. Podobnie za pomocą parametru headers (nagłówki), możemy dowolnie aktualizować lub usuwać dany zasób z cache (nagłówek Cache-Control).

Z powyższego opisu wynika jeszcze jeden wniosek – zwracana obietnica zasobu musi nadawać się do cachowania. Przeglądarka ma prawo i będzie wspomniany zasób wykorzystywać w innych podstronach. Tak samo, projektując stronę, nie powinniśmy polegać wyłącznie na Server Push. To jedynie usprawnienie w wydajności, a nie element nowej architektury – wiele przeglądarek wciąż tego nie wspiera.

Server Push to wyłącznie mechanizm uprzedzenia przeglądarki i dostarczania zależności wraz z plikiem głównym. Nie ma to nic wspólnego z Web Sockets i innymi technikami komunikacja dwustronnej.

HTTP 2.0 w IIS (ASP.NET)

IIS w Windows 10 wspiera już od jakiegoś czasu nową wersję protokołu.  W zasadzie prawdopodobnie nic nie musimy robić, jeśli posiadamy prawidłową wersję IIS. Oczywiście użytkownicy muszą posiadać również odpowiednią wersję przeglądarki internetowej. Ich kompatybilność można sprawdzić tutaj. Jak widać, Edge, Chrome, Firefox, Opera czy iOS Safari radzą sobie najlepiej. W przypadku IE, najnowsza wersja wspiera HTTP 2.0 tylko częściowo.

Jeśli jeszcze nie zainstalowaliśmy IIS na Windows 10, wtedy przejdźmy najpierw do “Turn Windows features on or off”, a następnie zaznaczamy Internet Information Service:

1

Aktualnie IIS wspiera wyłącznie HTTPS (TLS). Oznacza to, że jeśli chcemy użyć nieszyfrowanego połączenia to HTTP 1.1 będzie wciąż używany. Dodajmy więc kolejny binding HTTPS:

2

I to wszystko co musimy zrobić… Jak widać,  nic specjalnego nie należy konfigurować. Wystarczy odpowiednia wersja IIS oraz przeglądarki po stronie klienta. Odpalmy zatem stronę zarówno z HTTP, jak i HTTPS. Spodziewamy się, że nieszyfrowane połączenie wciąż będzie HTTP 1.1:

3

W przypadku HTTPS,HTTP 2.0 będzie użyty: 4

Skrót H2 oznacza oczywiście HTTP 2.0

W przyszłym poście opiszę kolejny element HTTP 2.0, w tym przypadku Server Push, który wymaga pewnych modyfikacji w aplikacji ASP.NET. W celu przetestowania tej aplikacji, potrzebny będzie zarówno serwer IIS wspierający HTTP 2.0, jak i odpowiednia przeglądarka (np. Chrome).

HTTP 2 – kompresja danych oraz atak CRIME

HTTP 2.0 jak wiemy z poprzedniego już wpisu jest protokołem binarnym. Wiemy również, że fundamentalne zasady działania HTTP nie zostały zmienione. Oznacza to, że HTTP pozostaje protokołem bezstanowym.  To z kolei w praktyce oznacza, że każde zapytanie musi dostarczać wszelkie informacje potrzebne do odtworzenia stanu aplikacji. Powoduje to, że zarówno ciało zapytania jak i nagłówek po jakimś czasie mogą zawierać dużo informacji, które należy przesyłać w każdym zapytaniu.

Kompresja danych, dostarczona w HTTP 2.0  jest kolejnym usprawnieniem poprawiającym wydajność, a w szczególności opóźnienie. HTTP 2.0 dostarcza również kompresje nagłówków, co kompletnie nie było dostępne w HTTP 1.1 (była możliwość jedynie kompresji ciała).

Kompresja HTTP 2.0 jest również odporna na atak CRIME (Compression Ratio Info-leak Made Easy), na który był podatny nawet klasyczny HTTPS czy poprzednik HTTP 2.0, a mianowicie SPDY. Jak to możliwe, że HTTPS, który jest szyfrowany może cierpieć na niebezpieczny atak ze względu na dodaną kompresję danych?

Jak sama nazwa wskazuje, atak polega na monitorowaniu rozmiaru skompresowanej treści. Prawdopodobnie dodając trochę treści, rozmiar musi się zmienić. Pomimo, że całość jest szyfrowana, to naturalnie rozmiar pakietu jest całkowicie jawny – to informacja, którą atakujący może wykorzystać.

W jaki sposób zatem przedstawiony atak może wyglądać? Załóżmy, że chcemy odgadnąć szyfrowany identyfikator sesji. Gdzieś w ciele zapytania musi być  przechowywana wartość np. “sessionId=252”. Jednym z fragmentów, które możemy kontrolować jest typ i adres zasobu dla zapytań. Jeśli chcemy wysłać GET do strony głównej wtedy zapytanie będzie wyglądać następująco:

GET /home

Wyobraźmy sobie, że wysyłamy jednak  najpierw poniższy pakiet

GET /sessionId=9

Oczywiście cały czas monitorujemy rozmiar pakietu. Następnie kolejny pakiet to:

GET /sessionId=2

Co możemy dowiedzieć się z rozmiarów wysłanych pakietów? Czy będą miały identyczny rozmiar? Jeśli kompresja jest użyta, drugi pakiet (sessionId=2) będzie mniejszy niż pierwszy, ponieważ sessionId=2 występuje już w ciele zapytania, które zawiera sekretny identyfikator sesji. Algorytmy kompresji, w dużym skrócie polegają na zastępowaniu duplikatów pewnymi wskaźnikami – np. skrótami. Naturalne zatem, że po kompresji ciąg  znaków “AABB” będzie miał mniejszy rozmiar niż “ABCD”. Metodą prób i błędów modyfikujemy zatem pakiet (w tym przypadku adres zasobu), aby rozmiar całości redukował się. Jeśli dodanie kolejne cyfry sesji zwiększa rozmiar, to znaczy, że takowa cyfra nie występuje w zaszyfrowanym ciele i stąd kompresja nie przynosi skutków. Jeśli rozmiar jest mniejszy, pewne fragmenty pakietu się powtarzają i oznacza to dla nas, że odgadliśmy kolejną cyfrę.

Widzimy, że przechwycenie zaszyfrowanego pakietu, zawierającego numer sesji może być niebezpieczne dla użytkownika. Pomimo, że nie mamy szans rozszyfrować pakietu, to bazując na rozmiarze możemy spróbować odtworzyć analogiczny pakiet. Podsumowując, do przeprowadzenia CRIME musimy:

  • posiadać próbkę pakietu, zawierającego sekretne dane (np. identyfikator sesji).
  • kompresja danych musi być włączona
  • należy wstrzyknąć pewną treść, np. za pomocą adresu zasobu.

W przypadku HTTP 1.1, polecane jest aby wyłączyć kompresje danych zarówno po stronie klienta (przeglądarka internetowa) jak i serwera. HTTP 2.0 nie jest podatny na ten typ ataku.

Obejścia problemów z HTTP 1.0\HTTP 1.1

Zanim przejdę do kolejnych usprawnień w HTTP 2.0, warto poświęcić chwilę na zastanowienie się jak omijamy problemy HTTP 1.1 dzisiaj. Większość stron wciąż opiera się na HTTP 1.1 i nie najgorzej radzą sobie z wydajnością. Oczywiście sporo jest do poprawy, ale strony działają na urządzeniach mobilnych bez ogromnych opóźnień.

Mimo wszystko, opóźnienie (latency) jest jednym z podstawowych problemów rozwiązanych przez HTTP 2.0.  Zastanówmy się, jak dzisiaj podchodzimy do tego, aby zminimalizować ten problem. Wiemy, że każda strona posiada liczne skrypty JavaScript, arkusze CSS czy pliki graficzne. Każdy zasób jest ładowany pojedynczo. Jeśli zatem mamy jeden plik html, 2 CSS, 5 skryptów JavaScript oraz 10 plików graficznych, razem zostanie wykonanych 18 zapytań do serwera. Jak wiemy z poprzedniego postu, HTTP 1.1 może wspierać co najwyżej HTTP pipelining. Wysyłanie pojedynczych zapytań jest skrajnie wolne na połączeniach satelitarnych czy mobilnych. Nawet jeśli dostawca obiecuje szybki transfer, to wysłanie pojedynczego zapytania i tak będzie wolne. W tym przypadku średni transfer nie ma znaczenia ponieważ zapytania jak i zawartość strony zwykle nie zawierają dużo danych.

Jednym z obejść zbyt dużej liczby plików graficznych jest połączenie ich w jeden wielki plik (spriting). Technika szczególnie popularna w dawnych czasach do tworzenia animacji komputerowych – jeden plik prezentował różne klatki animacji. Następnie w zależności, którą klatkę się chciało wyświetlić, wydzielało się konkretny fragment większego pliku graficznego. Podobną technikę można wykorzystać w web – załadować jeden obrazek (pojedyncze zapytanie), a potem wyświetlać konkretne fragmenty w zależności od potrzeb. Oczywiście rozwiązanie bardzo niewygodne ponieważ należy logicznie scalać obrazki co zwykle jest czasochłonne i trudne.

Kolejna obejście to definiowanie grafiki bezpośrednio w arkuszach CSS. Zamiast odnosić się do zewnętrznych plików graficznych (które wymagają osobnych zapytań), można osadzać dane bezpośrednio w CSS (embedded images).

Programiści ASP.NET z pewnością kojarzą ASP.NET Bundles. Z punktu widzenia jakości kodu, warto rozdzielać kod JavaScript na konkretne moduły. Ma to jednak ogromny wpływ na wydajność – im więcej plików JS tym wolniej załaduje się strona ponieważ należy więcej wysłać zapytań. Rozwiązanie problemu jest proste -scalać wszystkie pliki w jeden wielki skrypt. Wtedy wystarczy, że przeglądarka wyśle jedno zapytanie i wszystko zostanie załadowane.  Kolejną analogiczną techniką jest minifikacja (minification). Polega na usunięciu niepotrzebnych znaków  takich jak np. komentarze, spacje, znaki nowej linii itp. z pliku JavaScript. Nie są one niezbędne do wykonania kodu, a zajmują miejsce.  Dzięki ASP.NET Bundles zostanie to zrealizowane podczas wdrażania aplikacji. Kod zatem pozostanie przejrzyście rozdzielony na różne pliki, a po wdrożeniu, serwer będzie serwował pojedynczy plik.

Kolejną techniką jest jest tzw. sharding. Polega na umieszczaniu tych samych zasobów (np. plików graficznych) na różnych serwerach. Jak wiemy, HTTP 1.1 posiada wyłącznie HTTP pipelining, który w wielu przeglądarkach jest i tak wyłączony ze względu na HOL blocking. Przeglądarki starają się zatem zainicjalizować  wiele połączeń TCP, które stanowią wtedy niezależne kanały komunikacji. Problem w tym, że specyfikacja HTTP mówiła, że klient może nawiązać maksymalnie dwa równoległe połączenia. Dzięki sharding, mamy te same zasoby dostępne z różnych maszyn.  Wtedy okazuje się, że dla każdego serwera możemy nawiązać dwa różne połączenia. Z tego co wyczytałem, dzisiaj można nawiązać więcej równoległych połączeń, ale i tak ze względu na limity korzysta się z sharding.

Niestety wszystkie powyższe techniki mają wady. Oprócz oczywistej, która jest zbyt duża złożoność, wcześniej czy później pojawią się problemy z buforowaniem. Scalanie plików w JS spowoduje, że zmiana w jakimkolwiek pliku wymusi przeładowanie całego skryptu. Podobnie z spriting – modyfikacja jednego obrazka wymaga odświeżenia cache w przeglądarce dla całego sprite’a.

 

HTTP 2.0 Multiplexing, HTTP 1.1 Pipelining oraz HOL blocking

Kilka postów chcę przeznaczyć na temat HTTP 2.0. Zamiast pisać tylko o nowościach w nowej wersji protokołu, warto najpierw zrozumieć jak działała dotychczasowa wersja czyli HTTP 1.1.

Pierwszym problemem HTTP 1.0 oraz HTTP 1.1 była obsługa wielkiej ilości zapytań. Każda strona, aby prawidłowo wyświetlić się potrzebuje załadować mnóstwo zewnętrznych zasobów takich jak skrypty JS, arkusze css czy grafika. Ponadto HTTP oparty jest na TCP więc ponadto mamy do czynienia z TCP handshake zanim połączenie jest zainicjalizowane.  Połączenia internetowe są coraz lepsze, ale szybkość załadowania stron nie zawsze jest satysfakcjonująca. Wyobraźmy sobie ekstremalną sytuację. Strona posiada 10 skryptów JS, 2 arkusze CS oraz 10 plików graficznych. Zasoby w HTTP nie są przesyłane razem. Najpierw ściągany jest plik HTML, a potem każdy zasób zewnętrzny jest ściągany jeden po drugim. Po mimo szybkiej, średniej szybkości łącza internetowego, opóźnienie (latency) wiążące się z wysoká liczbą zapytań sprawia, że strona działa wolno.

W HTTP 1.0 wszystkie zapytania musiałby być wysyłane pojedynczo (szeregowo). Jeśli mamy zatem 20 zapytań, najpierw musieliśmy wysłać pierwsze, czekać na odpowiedź i dopiero potem wysłać drugie. W przypadku internetu satelitarnego czy mobilnego, strony po prostu wczytywały się bardzo wolno, mimo wysokiej średniej szybkości transmisji.

W odpowiedzi na oczywisty problem, w HTTP 1.1 wprowadzono HTTP pipelining. Dzięki temu, możliwe było wysyłanie kilku zapytań w tym samym połączeniu TCP. Korzyści były ogromne. Po pierwsze tylko jeden TCP Handshake. Zamiast czekać na odpowiedź mogliśmy wysłać najpierw wszystkie zapytania, a potem odbierać odpowiedzi jedno po drugim. Niestety musiało to odbywać się na zasadzie FIFO – odpowiedzi musiały przychodzić w tej samej kolejności co wysłane zapytania. Stanowiło to tak ogromne ograniczenie, że większość przeglądarek nie korzysta dziś nawet z HTTP pipelining.

Podstawowy problem to Head Of Line Blocking. Wiemy, że odpowiedzi muszą przychodzić w tej samej kolejności co zapytania. Co jeśli drugie zapytanie trwa bardzo długo? Skutek będzie taki, że będzie to blokowało kolejne odpowiedzi i przyszłe zapytania. Jeśli zatem wszystkie zapytania byłyby wysyłane za pomocą HTTP Pipeline efekt mógłby być odwrotny do zamierzonego – jeśli pierwszy element zajmuję bardzo długo, wtedy kolejne muszą czekać. Co jeśli ten pierwszy element tak naprawdę nie jest niezbędny do wyświetlenia większości zawartości na stronie? HTTP pipelining zniwelował problemy związane z opóźnieniem, ale ryzyko blokady HOB odstraszało wielu twórców przeglądarek od adaptacji tego.

I tutaj przychodzimy do HTTP 2.0 Multiplexing. Protokół HTTP 2.0 jest binarny, w przeciwieństwie do wersji poprzednich. Wysyłane pakiety stanowią ramki składające się z nagłówku i ciała. Wprowadzono definicje strumieni oraz priorytetów.  Dane wysyłane w obrębie dwóch różnych strumieni są niezależne od siebie, a co za tym idzie, mogą się wzajemnie przeplatać.  Innymi słowy, HTTP 2.0 multiplexing to prawidłowa implementacja wysyłania wielu zapaytań w obrębie tego samego połączenia TCP. Odpowiedzi już nie muszą być wysyłane w tej samej kolejności przez serwer i zostanie to prawidłowo obsłużone. Wspomniana kolejka FIFO nie jest już potrzebna. Ustawienia priorytetów umożliwia uzyskanie odpowiedzi w dowolnej kolejności. Serwer po utrzymaniu wielu zapytań naraz może skorzystać z priorytetów, aby przetwarzać je w odpowiedniej kolejności.

HTTP Multiplexing stanowi jedno z podstawowych usprawnień HTTP 2.0, który wpływa bardzo korzystnie na wydajność stron internetowych, szczególnie w środowisku z wysokim opóźnieniem ( połączenie mobilne). W przyszłych wpisach zajmiemy się kolejnymi usprawnieniami. Dobrą wiadomością jest, że jako programiści nie musimy wiele robić. HTTP 2.0 został zaprojektowany tak, aby mógł zostać wprowadzony bez konieczności aktualizacji istniejącego kodu. Wszystkie pryncypalne zasady takie jak wysyłanie ciasteczek, nagłówki, sesje itp mają nadal rację bytu – HTTP 2.0 to nie zmiana architektury.

AKKA.NET – Przykład obsługi błędów

W poprzednim wpisie pokazałem, w jaki sposób możemy zaprojektować obsługę błędów. Jak widać mamy do dyspozycji sporo opcji. Z punktu widzenia AKKA.NET nie jest to jednak tak skomplikowane. Wystarczy przeładować jedną metodę i zwrócić odpowiedni obiekt.

Tak jak w poprzednim wpisie będziemy testować kod na następującym “systemie”:

controller

Dla przypomnienia nasz ApplicationUserActor wygląda następująco:

public class ApplicationUserActor : UntypedActor
    {
        private readonly string _userName;

        public ApplicationUserActor(string userName)
        {
            _userName = userName;
        }

        protected override void OnReceive(object message)
        {
            Console.WriteLine("Received by {0}: {1}", _userName, message);
        }

        protected override void PreStart()
        {
            Console.WriteLine("{0}: PreStart",_userName);

          
            base.PreStart();
        }

        protected override void PostStop()
        {
            Console.WriteLine("{0}: PostStop", _userName);
            base.PostStop();
        }

        protected override void PreRestart(Exception reason, object message)
        {
            Console.WriteLine("{0}: PreRestart", _userName);
            base.PreRestart(reason, message);
        }

        protected override void PostRestart(Exception reason)
        {
            Console.WriteLine("{0}: PostRestart", _userName);
            base.PostRestart(reason);
        }
    }

Póki co niewiele mamy tam kodu – głównie hooking, które pomogą nam w zrozumieniu propagacji błędów.
Zmodyfikujmy metodę OnReceived tak, aby zasymulować wyjątek:

        protected override void OnReceive(object message)
        {
            if (message.ToString() == "error")
                throw new ArgumentException();

            Console.WriteLine("Received by {0}: {1}", _userName, message);
        }

W celu zdefiniowania obsługi błędów wystarczy przeciążyć metodę SupervisorStrategy aktora zarządzającego. Jeśli chcemy więc obsłużyć wyjątek w ApplicationUserActor, wtedy węzeł zarządzający (rodzic) to ApplicationUserControllerActor. Kod:

        protected override SupervisorStrategy SupervisorStrategy()
        {
            return new OneForOneStrategy((exception) =>
            {
                if (exception is ArgumentException)
                    return Directive.Restart;

                return Directive.Escalate;
            });
        }

W przykładzie wybraliśmy strategię OneForOneStrategy, którą opisałem już w poprzednim wpisie. W skrócie, rodzeństwo węzła, który spowodował wyjątek, nie będzie odgrywało tutaj żadnej roli. Wystarczy, że przekażemy wyrażenie lambda, które określa co należy zrobić w zależności od typu wyjątku. W powyższym przykładzie restartujemy aktora. Tak jak napisałem w poprzednim poście, mamy cztery sposoby reakcji:

  public enum Directive
  {
    Resume,
    Restart,
    Escalate,
    Stop,
  }

W celu zaprezentowania efektu, stwórzmy dwóch aktorów i wyślijmy serię wiadomości:

            var system = ActorSystem.Create("FooHierarchySystem");
            IActorRef userControllerActor =
                system.ActorOf<ApplicationUserControllerActor>("ApplicationUserControllerActor");

            userControllerActor.Tell(new AddUser("Piotr"));
            userControllerActor.Tell(new AddUser("Pawel"));
            var actor1 = system.ActorSelection("/user/ApplicationUserControllerActor/Piotr");
            var actor2 = system.ActorSelection("/user/ApplicationUserControllerActor/Pawel");

            Console.ReadLine();
            actor1.Tell("Sample message I");
            Console.ReadLine();
            actor1.Tell("error");
            Console.ReadLine();
            actor1.Tell("Sample message II");

            Console.ReadLine();

1
Widzimy, że w momencie wystąpienia błędu, aktor został zrestartowany. Ze screenu również można zauważyć, że kolejne wiadomości zostaną przetworzone. Stan wewnętrzny został zrestartowany, ale nie kolejka wiadomości. W celu zademonstrowania, że stan wewnętrzny faktycznie jest wymazywany (ponieważ tworzona jest nowa instancja), dodajmy prywatne pole do klasy:

    public class ApplicationUserActor : UntypedActor
    {
        private readonly string _userName;
        private string _internalState;
      ...
    }

InternalState jest wyświetlany i ustawiany w OnReceive:

        protected override void OnReceive(object message)
        {
            Console.WriteLine("{0}:Internal State: {1}",_userName,_internalState);

            if (message.ToString() == "error")
                throw new ArgumentException();

            _internalState = message.ToString();

            Console.WriteLine("Received by {0}: {1}", _userName, message);
        }

Teraz widzimy, że po wystąpieniu wyjątku, InternalState będzie pusty:
2

Analogicznie, spróbujmy zmienić dyrektywę restart na resume:

        protected override SupervisorStrategy SupervisorStrategy()
        {
            return new OneForOneStrategy((exception) =>
            {
                if (exception is ArgumentException)
                    return Directive.Resume;

                return Directive.Escalate;
            });
        }

Po uruchomieniu programu, przekonamy się, że stan wewnętrzny nie jest usuwany:
3

Zmieńmy również strategię na AllForOneStrategy:

        protected override SupervisorStrategy SupervisorStrategy()
        {
            return new AllForOneStrategy((exception) =>
            {
                if (exception is ArgumentException)
                    return Directive.Restart;

                return Directive.Escalate;
            });
        }

Efekt będzie taki, że wszystkie węzły podrzędne zostaną zrestartowane:
4

Jeśli w jakimś aktorze nie zdefiniujemy strategii obsługi błędów, wtedy domyślna będzie użyta:

   protected virtual SupervisorStrategy SupervisorStrategy()
    {
      return SupervisorStrategy.DefaultStrategy;
    }

Domyślna strategia to z kolei OneForOneStrategy.
Warto również przyjrzeć się  innym przeciążeniom konstruktora, np.:

    ///
<summary>
    /// Applies the fault handling `Directive` (Resume, Restart, Stop) specified in the `Decider`
    ///                 to all children when one fails, as opposed to <see cref="T:Akka.Actor.OneForOneStrategy"/> that applies
    ///                 it only to the child actor that failed.
    /// 
    /// </summary>

    /// <param name="maxNrOfRetries">the number of times a child actor is allowed to be restarted, negative value means no limit,
    ///                 if the limit is exceeded the child actor is stopped.
    ///             </param><param name="withinTimeRange">duration of the time window for maxNrOfRetries, Duration.Inf means no window.</param><param name="localOnlyDecider">mapping from Exception to <see cref="T:Akka.Actor.Directive"/></param>
    public OneForOneStrategy(int? maxNrOfRetries, TimeSpan? withinTimeRange, Func<Exception, Directive> localOnlyDecider)
      : this(maxNrOfRetries.GetValueOrDefault(-1), (int) withinTimeRange.GetValueOrDefault(Timeout.InfiniteTimeSpan).TotalMilliseconds, localOnlyDecider, true)
    {
    }

Widzimy, że oprócz wspomnianego wyrażenia lambda, możemy określić maksymalną liczbę prób oraz przedział czasowy.