Testy wydajności

5/5 - (1 vote)

W tym rozdziale zostaną przedstawione wyniki przeprowadzonych testów wydajności nowego klastra. Przy projektowaniu testów były brane pod uwagę dwa zasadnicze aspekty: poprawność oraz szybkość działania. Testy zostały wykonane w laboratorium komputerowym na Wydziale Matematyki, Informatyki i Mechaniki Uniwersytetu Warszawskiego. Dostępne były dwa typy maszyn o następującej charakterystyce:

Parametr Maszyna typu A Maszyna typu B
Procesor 350 MHz 700 MHz
RAM 64 MB 128 MB
Sieć 100 Mb/s 100 Mb/s
SO Linux Linux

Do testowania został wykorzystany specjalnie w tym celu napisany program, którego zadaniem było generowanie odpowiedniej liczby równoległych żądań do systemu, uwzględniających przesłany przez system identyfikator sesji. Aby wyniki były bardziej miarodajne, program testujący był uruchamiany równolegle na kilku maszynach (w ten sposób wyniki zostały uniezależnione od wydajności konkretnej maszyny testującej, co miało szczególne znaczenie przy testowaniu połączeń szyfrowanych).

Po stronie serwera do testów zostały wykorzystane dwa typy aplikacji. Pierwsza aplikacja (dalej nazywana aplikacją sesyjną) działała w następujący sposób:

  1. Pobierała z żądania nazwę parametru oraz j ego wartość.
  2. Wstawiała tę parę do sesji.
  3. Zwracała wszystkie pary klucz-wartość znajdujące się w sesji.

Aplikacja sesyjna została napisana głównie w celu praktycznego udowodnienia poprawności działania klastra. Program testujący generując kolejne żądania, odwołujące się do tej samej sesji, sprawdzał czy wynik żądania zgadza się z oczekiwaniami. Jeżeli w wyniku nie znajdował się jeden z dodanych przez niego parametrów, to podnoszony był wyjątek i test kończył się niepowodzeniem. Należy zwrócić uwagę na fakt, iż z wydajnościowego punktu widzenia aplikacja jest skrajnie pesymistycznym przypadkiem wykorzystania klastra. Przetworzenie żądania powoduje znikome obciążenie serwera, jednocześnie zmuszając klaster do replikowania wciąż rosnących sesji.

Druga aplikacja (dalej nazywana aplikacją XSL) działała w następujący sposób:

  1. Przy pierwszym żądaniu ustawiała w sesji dwa parametry:
  2. ścieżkę do pliku zawierającego dane zapisane w formacie XML,
  3. ścieżkę do pliku zawierającego przekształcenie XSL prezentujące dane z pliku XML.
  4. Przy drugim żądaniu aplikacja pobierała oba parametry z sesji i wykonywała przekształcenie XSL na danych z pliku XML.

Aplikacja nieco lepiej odwzorowuje praktyczne zastosowanie serwera Tomcat. Co prawda nie komunikuje się z bazą danych, co ma miejsce w większości prawdziwych zastosowań serwerów aplikacyjnych, niemniej jednak wykonuje obliczenia mające na celu uatrakcyjnienie wyświetlanych informacji. Wykorzystuje bardzo modne ostatnio technologie XML oraz przekształcenia XSL oddzielające warstwę danych od warstwy prezentacji.

Każdy z przeprowadzonych scenariuszy testowych był wykonywany kilkakrotnie w celu zwiększenia wiarygodności wyników. Tabele wynikowe prezentują uśredniony czas. Podczas badań system zachowywał się stabilnie – standardowe odchylenie oscylowało pomiędzy 5%, a 15%. Ponieważ różnice w wynikach były nieznaczne, nie zajmowałem się badaniem odchylenia samego w sobie.

Test poprawności

Test poprawności został przeprowadzony na maszynach typu A. Program testujący generował kolejne żądania do aplikacji sesyjnej sprawdzając zgodność danych. Przy każdym żądaniu wielkość sesji była zwiększana o około 130 bajtów. Charakterystyka testu:

  • Serwer zarządca znaj dowal się wewnątrz jednego z serwerów Tomcat.
  • Przy każdym kolejnym żądaniu program testujący wybierał losowo jeden z węzłów, do którego przesyłał żądanie.
30 w. 50 ż. 60 w. 50 ż. 90 w. 50 ż. 30 w. 100 ż.
bez klastra 5 10 17 14
2 węzły 7 15 21 18
3 węzły 8 23 39 40
4 węzły 17 28 45 50
5 węzłów 35 45 74 100

Rys. 5.1. Wykres wyniku testów przy losowym wyborze serwera

Wykres na rys. 5.1 prezentuje czasy trwania kolejnych testów mierzone w sekundach. Zapis „30 w. 50 ż.” oznacza, że zostało wygenerowanych 30 wątków, z których każdy wysyłał kolejno bez opóźnień 50 żądań, sukcesywnie zwiększających sesję (każdy wątek miał osobny identyfikator sesji).

Podstawowym zadaniem testu było pokazanie poprawności działania modułu co zostało osiągnięte. Mimo skrajnie trudnych warunków pracy w jakich przeprowadzono test (tzn. ciągła zmiana kontekstu wykonania żądań bez jakiegokolwiek opóźnienia) moduł działał poprawnie. Testy kończyły się z 99,9 procentową poprawnością. Przy bardzo dużym obciążeniu czasami zdarzało się, że klaster przekazywał błąd (status odpowiedzi na żądanie 500), który był spowodowany przekroczeniem maksymalnego czasu oczekiwania na zajęcie sesji. Niemniej jednak nie wystąpiła sytuacja, w której program testujący zaobserwowałby niezgodność danych – czyli moduł nie dopuścił do odczytu (modyfikacji) nieaktualnej sesji.

Jak można się było spodziewać zwiększanie liczby węzłów w klastrze powodowało zwiększanie czasu trwania testu. Nie jest to wielkim zaskoczeniem zważywszy na charakterystykę przeprowadzonego testu. Nie powinno również być wielkim zaskoczeniem, że czas trwania testu na pojedynczym serwerze był najniższy. Pojedynczy serwer odwołuje się do danych bezpośrednio w pamięci operacyjnej i nie musi ich przesyłać przez sieć. Jeżeli dodatkowo żądanie nie generuje prawie żadnego obciążenia na serwerze, to obsługa nawet kilku tysięcy żądań będzie trwała bardzo krótko.

Drugi test został przeprowadzony w bardzo podobnych warunkach, z tym że system wykorzystywał zintegrowany serwer ruchu. Każdy z wątków testujących łączył się z serwerem ruchu, który na podstawie przesyłanego identyfikatora sesji wybierał węzeł aktualnie zajmujący sesję. Każdy z węzłów miał ustawiony czas opóźnienia wysyłania replik oraz opóźnienia zwolnienia sesji na 2 sekundy.

Rys. 5.2. Wykres wyniku testów przy zastosowaniu zintegrowanego serwera ruchu

Wykres na rys. 5.2 prezentuje wyniki testów mierzone w sekundach. Jak można zauważyć zastosowanie zintegrowanego serwera ruchu bardzo mocno polepszyło wydajność klastra. Prawdopodobnie zysk byłby nawet jeszcze większy przy odpowiednio dobrym zaimplementowaniu serwera ruchu. Z testów wynikało, że tak naprawdę wąskim gardłem tego testu mógł okazać się właśnie serwer ruchu. Niemniej jednak można zauważyć, że przy tej architekturze zwiększanie liczby węzłów w klastrze przestało negatywnie wpływać na wydajność systemu. W zasadzie różnice czasu po dodaniu kolejnych węzłów stają się nieznaczące.

Test wydajności

Test wydajności został przeprowadzony na maszynach typu B. Każdy wątek testowy wykonywał pierwsze żądanie do aplikacji XSL w celu ustawienia w sesji ścieżek do plików. W kolejnym żądaniu odbierał rezultat wykonania przekształcenia na pliku XML. Należy zwrócić uwagę na fakt, że w przeprowadzanym teście również były odwołania do replikowanych sesji, a nie tylko generowanie obliczeń. Charakterystyka testu:

  • Serwer zarządca znaj dował się w j ednym z serwerów Tomcat.
  • Testy były przeprowadzane bez użycia zintegrowanego serwera ruchu.
  • Aplikacja testująca wybierała kolejne węzły w sposób losowy.

Rys. 5.3. Wyniki testów aplikacji XSL

Wykres na rys. 5.3 prezentuje wyniki testów. Jak można zaobserwować zysk z zastosowania klastra jest niebagatelny w stosunku do pracy pojedynczego serwera. Dodanie kolejnych węzłów obniża czas trwania testu, co jest szczególnie widoczne przy obsłudze bardzo wielu żądań jednocześnie. Zysk z zastosowania klastra najlepiej widać dla 150 oraz 210 wątków. Pojedynczy serwer Tomcat bez klastra w ogóle nie przeszedł testu – serwer przestał odpowiadać i trzeba było go zrestartować. Czyli zainstalowanie klastra pozwoliło na obsługę znacznie większej liczby klientów jednocześnie, co tak naprawdę jest najważniejszym wyznacznikiem wydajności systemu.

Prawdopodobnie zastosowanie dobrze zaimplementowanego serwera ruchu pozwoliłoby na uzyskanie jeszcze lepszych wyników, szczególnie przy większej liczbie węzłów.

Test HTTPS

Ostatni z testów miał na celu pokazanie skuteczności działania klastra w przypadku stosowania połączeń szyfrowanych. Obliczenia związane z deszyfrowaniem połączeń zostały rozrzucone na wszystkie węzły w klastrze (dokładniejszy opis konfiguracji znajduje się w p. 4.4.2). Do testów wykorzystano maszyny typu B. Charakterystyka testu:

  • Serwer zarządca znaj dował się wewnątrz j ednego z serwerów Tomcat.
  • Do testów wykorzystano aplikację sesyjną.

Rys. 5.4. Wyniki testów przy wykorzystaniu połączeń szyfrowanych

Wykres na rys. 5.4 prezentuje wyniki przeprowadzonych testów. Co ciekawe przy wykorzystaniu połączeń szyfrowanych okazuje się, że zysk z zastosowania klastra osiąga się nawet dla skrajnie pesymistycznych przypadków z punktu widzenia klastra (aplikacja sesyjna). Obliczenia związane z szyfrowaniem i deszyfrowaniem są tak kosztowne, że złączenie wielu fizycznych maszyn rekompensuje straty związane z przesyłaniem replik sesji w sieci. Jak widać na wykresie pojedynczy serwer jest dużo wolniejszy od klastra, a szczególnie wyraźnie widać to przy dużym obciążeniu. Podczas testu z 90 wątkami serwer przestał odpowiadać i trzeba było go zrestartować.

Warto zwrócić uwagę na kształtowanie się różnic czasów pomiędzy klastrem złożonym z 3 węzłów a klastrem złożonym z 4 węzłów. Dla małej liczby równoległych żądań (15 w. 100 ż.) klaster z trzema węzłami okazuje się szybszy od tego z 4. Przy małym obciążeniu systemu wąskim gardłem staje się strata związana z przesyłaniem replik w sieci. Natomiast wraz ze wzrostem liczby równoległych żądań zysk z doinstalowania kolejnego węzła staje się coraz bardziej widoczny. Jest to bardzo ważna obserwacja z punktu widzenia administratora systemu. Zwiększenie liczby węzłów w klastrze musi iść w parze ze zwiększeniem liczby użytkowników systemu; w przeciwnym przypadku dodanie węzła spowoduje spadek wydajności systemu.

Test został przeprowadzony dla aplikacji sesyjnej, aby uwidocznić zysk jaki można osiągnąć przy instalacji klastra do obsługi połączeń szyfrowanych. Jeżeli test zostanie przeprowadzony dla bardziej wymagającej aplikacji, to można oczekiwać, że wyniki będą jeszcze korzystniejsze.

image_pdf

Lokalne sieci komputerowe

5/5 - (1 vote)

Specyfika sieci lokalnych

Sieci lokalne posiadają swoją specyfikę przede wszystkim w warstwach najniższych modelu OSI. W celu uzyskania dużych szybkości transmisji oraz małej stopy błędów stosuje się specyficzne rodzaje łączy i techniki transmisji, co znajduje swoje odzwierciedlenie w warstwach: fizycznej i liniowej. Wszystkie stacje dołączone do LSK(lokalna siec komputerowa) korzystają ze wspólnego medium transmisyjnego. Pojawia się, więc tutaj problem bezkolizyjnego dostępu do tego medium (np. te same stacje nie mogą zacząć jednocześnie nadawania). W celu rozwiązania tego problemu zdecydowano się na rozbicie warstwy liniowej na dwie podwarstwy:

  • podwarstwę dostępu, niższą (odpowiedzialną za bezkonfliktowy dostęp do łączy),
  • podwarstwę łącza logicznego, wyższą (realizującą pozostałe funkcje).

Normy ISO sieci komputerowych

Normy ISO dla sieci komputerowych stanowią zbiór standardów, które określają zasady i metody stosowane przy projektowaniu, budowie i zarządzaniu sieciami komputerowymi. Międzynarodowa Organizacja Normalizacyjna (ISO) opracowała wiele norm, które mają na celu zapewnienie zgodności i efektywności w komunikacji sieciowej. Jednym z najważniejszych standardów w tej dziedzinie jest model OSI (Open Systems Interconnection), który definiuje sposób, w jaki urządzenia sieciowe komunikują się ze sobą.

Model OSI jest teoretyczną architekturą, która dzieli procesy komunikacyjne na siedem warstw. Każda z tych warstw ma określoną funkcję, zaczynając od warstwy fizycznej, odpowiedzialnej za transmisję bitów, aż po warstwę aplikacji, która zapewnia interakcję użytkownika z aplikacjami sieciowymi. Dzięki temu modelowi możliwe jest zrozumienie i zarządzanie skomplikowanymi procesami komunikacji w sieci komputerowej, a także rozwiązywanie problemów związanych z przesyłaniem danych.

Norma ISO/IEC 7498-1 określa ogólną architekturę sieci komputerowych, bazując na modelu OSI. Dokument ten stanowi fundament dla wielu innych norm i wytycznych w dziedzinie projektowania sieci, zapewniając wspólne zasady dla różnych technologii komunikacyjnych. Jest to kluczowy standard, który pozwala na interoperacyjność systemów oraz tworzenie skalowalnych i elastycznych rozwiązań sieciowych.

Ważnym dokumentem w kontekście sieci komputerowych jest również ISO/IEC 8802, który definiuje standardy dotyczące technologii Ethernet i innych sieci lokalnych (LAN). Norma ta szczegółowo opisuje fizyczne aspekty sieci, takie jak rodzaje kabli i urządzeń, oraz zasady działania warstwy łącza danych. Jest to szczególnie ważne w kontekście projektowania infrastruktury sieciowej, która musi być kompatybilna z różnymi technologiami transmisji danych.

W dzisiejszych czasach, oprócz norm dotyczących samej infrastruktury, istotne są również standardy związane z bezpieczeństwem sieci. ISO/IEC 27001, choć skupia się głównie na zarządzaniu bezpieczeństwem informacji, ma także kluczowe znaczenie dla ochrony sieci komputerowych przed zagrożeniami. Norma ta określa wymagania dotyczące systemu zarządzania bezpieczeństwem informacji (ISMS), pomagając organizacjom w ochronie danych przechowywanych i przesyłanych w sieci komputerowej.

Normy ISO są fundamentem dla budowania bezpiecznych, efektywnych i interoperacyjnych sieci komputerowych. Dzięki tym standardom, różnorodne technologie mogą współpracować, a dane mogą być przesyłane w sposób niezawodny i bezpieczny. Normy te pomagają także w zarządzaniu rozwojem infrastruktury sieciowej, co jest niezbędne w obliczu rosnących wymagań związanych z przepustowością, bezpieczeństwem i skalowalnością nowoczesnych systemów sieciowych.

Niektóre zasady dostępu do łącza wymagają dodatkowych, specjalnych usług warstwy fizycznej. Nie można, łączyć w dowolny sposób rozwiązań odnośnie podwarstwy dostępu i warstwy fizycznej. Zalecenia i normy dotyczące LSK (obecnie są to już standardy) zebrano w dokumentach ISO o numerach 8802.X. X oznacza poszczególne warianty.

Rodzaje transmisji, elementarne konfiguracje łączy

Transmisja w paśmie podstawowym (baseband) – polega na przesłaniu ciągu impulsów uzyskanego na wyjściu dekodera (i być może lekko zniekształconego). Widmo sygnału jest tutaj nieograniczone. Jest to rozwiązanie dominujące w obecnie istniejących LSK.

Transmisja szerokopasmowa (broadband) polega na tym, że za pomocą przebiegu uzyskanego na wyjściu dekodera jest modyfikowany (modulowany) sygnał sinusoidalny o pewnej częstotliwości (zwanej częstotliwością nośną). Modulacji może podlegać dowolny parametr przebiegu sinusoidalnego: amplituda, częstotliwość lub faza. Tak zmodulowany przebieg sinusoidalny jest przekazywany w tor transmisyjny. Widmo takiego przebiegu mieści się w pewnym ściśle określonym przedziale częstotliwości, którego środkiem jest częstotliwość nośna, a szerokość nie przekracza dwukrotnej szybkości sygnalizacji (częstotliwości sygnału modulującego). Istnieją rozwiązania, które pozwalają jeszcze zawęzić to pasmo. Każde łącze charakteryzuje się pewnym pasmem przenoszenia sygnałów. Pasmo to dzieli się na części (kanały), a w każdej z nich przesyła się sygnał o innej częstotliwości nośnej. Można więc w jednym łączu przesyłać sygnał telewizyjny, informację cyfrową itd.

W każdej takiej konfiguracji może odbywać się transmisja:

  1. jednokierunkowa (simplex) – gdy łącze umożliwia propagację sygnału tylko w jednym kierunku. Odbiornik nie może przesłać odpowiedzi. Często ten rodzaj transmisji wykorzystywany jest w układach typu master-slave. Przykładem może być transmisja radiowa;
  2. dwukierunkowa (duplex) – w tym przypadku wyróżnia się transmisję naprzemienną (half duplex) – przesyłanie w dowolnym kierunku, ale tylko w jednym w danej chwili, wykorzystuje się system sygnalizacji wskazujący, że jedno urządzenie zakończyło nadawanie lub odbiór, transmisję w tym trybie można zrealizować przy użyciu kabla dwuprzewodowego (np. skrętka), typowy przykład takiej transmisji to komunikacja za pomocą CB – oraz transmisję równoczesną (full duplex) – możliwe jest przesyłanie jednoczesne sygnału w dwóch kierunkach bez jego zniekształcania, w sieciach cyfrowych konieczne są dwie pary przewodów do utworzenia połączenia.

W konfiguracjach wielopunktowych może się zdarzyć sytuacja, w której kilka nadajników zacznie równocześnie emisję sygnału, co spowoduje wzajemne zniekształcenie nadawanych sygnałów. Taka sytuacja nazywa się kolizją. W chwili kolizji całkowita moc sygnału w łączu znacznie się zwiększa, zarówno nadajnik jak i odbiornik muszą być odpowiednio przygotowane do takich warunków pracy. W niektórych rozwiązaniach LSK wprowadza się układy umożliwiające wykrycie kolizji. Działają one na ogół według jednej z dwóch zasad:

  1. analizowana jest moc sygnału odbieranego. Stwierdzenie przekroczenia przez tę moc pewnego poziomu progowego świadczy o wystąpieniu kolizji. Metoda ta jest zawodna w przypadku, gdy w miejscu zainstalowania odbiornika moc kolidujących sygnałów znacznie się różni;
  2. porównywany jest sygnał emitowany przez nadajnik z sygnałem odbieranym. Metoda ta jest możliwa do zastosowania tylko przez uczestników kolizji. Chcąc zapewnić jednoczesne wykrycie kolizji przez wszystkich uczestników korzystających z łącza narzuca się wymaganie emitowania specjalnego sygnału przez stację, która wykryła kolizję;

Wyposażenie do transmisji danych

Istnieje duże różnorodność sprzętu służącego do transmisji danych, określanego skrótem DCE (Data Communication Equipment) Najczęściej są one powiązanie z urządzeniami końcowymi DTE (Data Terminal Equipment). Urządzenia DCE znajdują się pomiędzy urządzeniami DTE i linią lub kanałem transmisyjnym, umożliwiają podłączenie urządzeń DTE do sieci komunikacyjnej oraz pełnią funkcję terminatora łącza i zapewniają w nim synchronizację. Przykładem urządzenia DCE jest modem.

Interfejsy urządzeń DCE i DTE zdefiniowane są w warstwie fizycznej modelu OSI. W urządzeniach DCE/DTE najpowszechniej stosowane są standardy przyjęte przez EIA: RS-232-C i RS-232-D oraz V.24 komitetu CCITT. Inne standardy to: EIA RS-366-A, CCITT X.20, X.21 i V.35.

image_pdf

Implementacja

5/5 - (1 vote)

Mimo bardzo podobnej architektury i koncepcji rozwiązania przy implementacji w zasadzie nie udało się wykorzystać kodu napisanego przez Hanika. W skład nowego modułu weszło jedynie kilka plików źródłowych z poprzedniej implementacji, zawierających mechanizm podłączenia modułu do serwera. W szczególności są to klasy:

  • apache.catalina.cluster.session.SimpleTcpReplicationManager,
  • apache.catalina.cluster.session.ReplicatedSession,
  • apache.catalina.cluster.tcp.ReplicationValve.

Nakład pracy jaką należałoby włożyć w celu dostosowania kodów źródłowych starego modułu do celów nowego projektu byłby porównywalny ze stworzeniem całości od nowa. Dlatego bardziej sensowne okazało się zaimplementowanie całego modułu od początku.

Moduł został napisany w języku Java (wersja 1.4), przy użyciu standardowych bibliotek, dostarczanych wraz z instalacją środowiska wykonywalnego Javy lub z dystrybucją serwera Jakarta-Tomcat. Wybór języka programowania został zdeterminowany przez technologię serwera Tomcat. Ponieważ cały system został zaimplementowany w języku Java, nie było powodu, aby wprowadzać inny język.

Kod modułu był pisany pod kątem maksymalizowania wydajności. Ponieważ zazwyczaj w tego typu systemach wąskim gardłem staje się przepustowość sieci, główna część uwagi podczas tworzenia projektu była poświęcona kwestii minimalizowania ilości przesyłanych informacji. Wiąże się to z wyeliminowaniem zbędnych pakietów (por. p. 4.3.1, 4.3.2) oraz z mechanizmami buforowania (por. p. 4.3.4). Dodatkowym utrudnieniem podczas pisania było silnie zrównoleglone środowisko pracy modułu. Serwer WWW pracuje wielowątkowo, obsługując nawet kilkaset żądań jednocześnie. Dobrze napisany moduł klastra musi być zabezpieczony przed równoległym dostępem, jednocześnie pozwalając na wykonywanie możliwie jak największej liczby operacji w tym samym czasie. Zbyt mała liczba rozłącznych sekcji krytycznych spowolni działanie modułu, tworząc wąskie gardła. Z kolei zbyt częste wywołania wejścia i wyjścia z sekcji krytycznej może negatywnie wpływać na wydajność i utrudniać wykrywanie błędów.

Dodatkową ważną kwestią, decydującą o sposobie budowania modułu była potrzeba działania w dwóch różnych trybach:

  1. Jako klient, zgłaszający się do serwera zarządcy.
  2. Jako serwer zarządca.

Zakłada się, że w drugim trybie moduł może pracować zarówno wewnątrz serwera Tomcat, z wykorzystaniem części klienckiej mechanizmu synchronizacji, jak i w niezależnej aplikacji (por. p. 4.3.7), wykorzystującej tylko część serwerową mechanizmu. Dokładniejszy opis zastosowanego rozwiązania znajduje się w p. 4.3.6.

W dalszej części pracy opisuję sposób implementowania poszczególnych części systemu, zaczynając od mechanizmu transportowania danych w sieci, a kończąc na algorytmie synchronizacji oraz równoważenia obciążenia. W celu uproszczenia notacji został zastosowany skrót org. . oznaczający pakiet

org.apache.catalina.cluster.

Warstwa transportująca

Ponieważ nowy klaster ma być rozwiązaniem tanim i łatwym w instalacji, jako mechanizm transportujący został wykorzystany protokół TCP/IP, przy użyciu którego komputery mogą się komunikować niemalże w każdej dostępnej sieci. Drugim bardzo ważnym czynnikiem, który spowodował wybór tej technologii jest powszechność gotowych, sprawdzonych implementacji (obsługa TCP/IP jest zaimplementowana w standardowych klasach Javy). Charakterystyka architektury klastra (połączenia każdy z każdym) sugerowałaby wykorzystanie protokołu rozgłoszeniowego, niemniej jednak brak mechanizmów gwarantujących dostarczenie danych oraz sprawdzenia ich poprawności, spowodował wykluczenie tego rozwiązania z projektu. Do poprawności działania klastra niezbędna jest gwarancja dostarczenia danych w niezmienionej postaci. Taką gwarancję daje protokół TCP/IP.

Na implementację warstwy transportującej składają się następujące klasy:

  1. .transport.DestinationManager,
  2. .transport.DestinationAddress,
  3. .transport.socket.ServerSocketListener,
  4. .transport.socket.ClientSocketListener,
  5. .transport.socket.LoopbackSocketListener.

Kluczową rolę w mechanizmie transportującym odgrywa klasa DestinationManager. Zawiera ona metody umożliwiające dodawanie/odejmowanie adresów dostępnych komputerów oraz umożliwia odbieranie i wysyłanie danych.

Każdy serwer w klastrze identyfikowany jest poprzez unikatowy klucz. Na klucz składa się adres IP oraz numer portu. Klasa DestinationAddress reprezentuje adresy serwerów w module, jednocześnie implementując interfejs org. .Member. W klasie została nadpisana metoda eąuais ( , która sprawdza zgodność adresu IP oraz portu porównywanych obiektów. Poza tym została zaimplementowana metoda compareTo (DestinationAddress), w której porównywane są adresy IP lub w przypadku ich równości numery portów. Metoda ta jest wykorzystywana podczas wybierania strony aktywnej/biernej przy nawiązywaniu połączenia (zostanie to dokładniej opisane w dalszej części punktu).

Obowiązkiem obiektu DestinationManager jest obsługa wszelkich problemów związanych z połączeniem i przesyłaniem danych. Sporą wadą protokołu TCP/IP jest łatwość zerwania połączenia – każde połączenie jest uznawane za aktywne dopóki są otrzymywane pakiety.

Niestety, wystarczy, że z powodu przeciążenia sieci lub systemu operacyjnego pakiety zaczną się znacząco opóźniać i połączenie może zostać zerwane. Ponieważ w zastosowanej architekturze spójność klastra jest kontrolowana za pomocą aktywności połączeń, aby ograniczyć liczbę omyłkowych decyzji o awarii węzłów, implementacja warstwy transportującej umożliwia przezroczyste odtwarzanie chwilowo zerwanych połączeń. Po utracie połączenia system przez pewien czas próbuje odtworzyć kanał komunikacyjny; jeżeli się to nie powiedzie, to informuje wszystkich zarejestrowanych słuchaczy o utracie adresata.

Mechanizm powiadamiania o utracie połączenia jest wykorzystywany przez klaster do podejmowania decyzji o awarii węzłów. W przypadku zwykłego węzła znaczenie ma tylko informacja o zerwaniu połączenia z serwerem zarządcą – węzeł musi zerwać wszystkie pozostałe połączenia i próbować odnowić komunikację z zarządcą. Z kolei w przypadku serwera zarządcy informacja o zerwaniu połączenia powoduje podjęcie decyzji o jego odrzuceniu z klastra.

Ważną cechą obiektu klasy DestinationManager jest fakt, że dany adresat będzie uznawany przez obiekt jako aktywny dopóki nie zostanie sxplicits wywołane RemoveDestination (DestinationAddress). Ten fakt ułatwia implementację mechanizmu replikacji – węzeł wysyła dane do wszystkich pozostałych węzłów, niezależnie od tego czy są one dla niego dostępne czy nie. Obiekt DestinationManagei po prostu będzie bez końca ponawiał próbę wysłania lub stwierdzi fakt poprawnego zakończenia, jeżeli w międzyczasie adresat zostanie usunięty z listy aktywnych.

Aby uniknąć aktywnego oczekiwania na grupie połączeń, każdy kanał komunikacyjny ma swój własny wątek, który go obsługuje. Istnieją trzy typy połączeń:  ServerSocketListene: ,    ClientSocketListener      oraz

LoopbackSocketListener. Diagram na rys. 4.8 przedstawia hierarchię klas.

Rys. 4.8. Hierarchia klas warstwy transportującej

W celu zminimalizowania liczby przesyłanych pakietów w sieci, pomiędzy każdymi dwoma węzłami otwierane jest tylko jedno połączenie. Wyróżnia się stronę bierną i aktywną. Strona bierna, czyli obiekt klasy ServerSocketListene , nasłuchuje na porcie identyfikującym węzeł, oczekując na zainicjowanie połączenia przez stronę aktywną. Strona aktywna, czyli obiekt klasy clientSocketListene , nawiązuje komunikację łącząc się na odpowiedni port adresata.

Ponieważ strona bierna nie może zidentyfikować strony aktywnej (nie jest w stanie poznać numeru portu, na którym nasłuchuje strona aktywna, a jedynie adres IP, z którego się łączy), więc węzeł inicjujący połączenie wysyła w pierwszej kolejności numer swojego portu. Rola w połączeniu jest ustalana na podstawie wartości, którą przekaże metoda compareTo (DestinationAddress). Poniższy kod jest wykonywany podczas tworzenia połączenia W obiekcie DestinationManager:

if (oAddr.compareTo(oLocalAddr) < 0)

oCL = new ServerSocketListener(oAddr, this); else if (oAddr.compareTo(oLocalAddr) > 0)

oCL = new ClientSocketListener(this.oLocalAddr, oAddr, this); else

throw new RuntimeException(„Will not add localhost”);

Do prawidłowego działania systemu bardzo istotne jest, aby obie strony w tym samym momencie zaprzestały prób odtworzenia zerwanego połączenia. Sytuacja, w której strona aktywna dochodzi do wniosku, że połączenie jest zerwane, natomiast strona bierna wciąż oczekuje na odtworzenie kanału komunikacyjnego, może doprowadzić do błędnego działania systemu. Strona aktywna mogłaby przedsięwziąć pewne kroki związane z zerwaniem połączenia, a następnie ponowić próbę nawiązania komunikacji. Wtedy strona bierna odtworzy połączenie, niestety bez powiadamiania słuchaczy o zerwaniu.

Schemat procesu nawiązywania połączenia jest przedstawiony na rys. 4.9.

Rys. 4.9. Diagram nawiązywania połączenia

Obiekt DestinationManagei podczas tworzenia otwiera port do nasłuchu. W momencie próby nawiązania połączenia (powrót z metody accept ()), menedżer sprawdza czy istnieje obiekt konektora obsługujący adres zgłaszającego się komputera. Jeżeli istnieje, to wywołuje na nim metodę Setchannel (socket , tym samym kończąc procedurę nawiązywania połączenia. Jeżeli nie istnieje, to wywołuje na obiekcie klasy '4ainSyncServer (dokładniej o tej klasie będzie mowa w p. 4.3.6), metodę canAccept (DestinationAddress). Jeżeli metoda przekaże wartość pozytywną, to menedżer wywołuje sam dla siebie metodę CreateDestination z nowym adresatem jako argumentem. W tym momencie cała procedura nawiązywania połączenia zaczyna się na nowo. Taki mechanizm umożliwia serwerowi zarządcy akceptowanie połączeń z nowymi, nieznanymi wcześniej węzłami.

Jedyna różnica w konektorach biernym i aktywnym występuje w metodzie Connect (). Konektor bierny w tej metodzie przechodzi w stan oczekiwania na wywołanie metody Setchannel (Socketchannel , natomiast konektor aktywny nawiązuje połączenie i wysyła jako pierwsze 4 bajty swój numer portu. Oba konektory w przypadku przechwycenia jakiegokolwiek wyjątku wywołują metodę ReConnect ( , która próbuje odnowić zerwane połączenie.

Poza dwoma podstawowymi typami połączeń zostało jeszcze stworzone połączenie zwrotne, obiekt klasy LoopbackSocketListener. Wszystkie wysyłane przez niego dane od razu przekierowywane są do funkcji obsługi wiadomości sieciowych, nie wywołując niepotrzebnie funkcji systemowych. Jest on wykorzystywany podczas przesyłania danych od serwera zarządcy do serwera Tomcat w przypadku, gdy serwer Tomcat jest jednocześnie serwerem zarządcą. Architektura rozwiązania wymusiła taki mechanizm – serwer zarządca nie wie nic o istnieniu serwera Tomcat. Jego zadanie sprowadza się jedynie do odbierania i wysyłania odpowiednich komunikatów. Takie podejście umożliwia odseparowanie serwera zarządcy od Tomcata (por. p. 4.3.7).

Odbieranie danych z sieci

Każdy konektor posiada swoją pulę buforów, do których może wpisywać pobrane z sieci informacje (maksymalna liczba dostępnych buforów jest konfigurowalna). Po odebraniu danych z sieci konektor pobiera kolejny wolny bufor, przepisuje do niego wiadomość i wykonuje na obiekcie DestinationManager metodę:

AddReadyBuffer(DestinationAddress oFromAddr, ExtendableByteBuffer oData) .

Po zakończeniu obsługi wiadomości na obiekcie ExtendableByteBuffer musi zostać wywołana metoda ReturnToQueue (), aby konektor mógł ponownie wykorzystać bufor do odebrania danych. Jeżeli liczba wolnych buforów spadnie do zera, to moduł wstrzyma odbiór danych, tym samym chroniąc system przed nadmiernym zużyciem pamięci.

Implementacja warstwy transportującej jest bardzo silnie zależna od protokołu TCP/IP. Przenośność i uogólnianie części transportującej nie były głównymi czynnikami decydującymi o kształcie końcowego kodu. Najważniejsza była wydajność tej części modułu, ponieważ to od niej zależała wydajność całego projektu. Takie zabiegi jak tworzenie pojedynczych połączeń, osobne wątki dla każdego kanału komunikacyjnego czy kolejki buforów miały na celu optymalizację modułu klastra. Mechanizm odtwarzania połączeń miał na celu zabezpieczenie klastra przed przypadkowym rozbiciem oraz ułatwienie implementacji samego mechanizmu replikowania sesji.

Komunikacja w klastrze

Komunikacja w klastrze odbywa się za pomocą warstwy transportującej. Aby możliwe było przesłanie danych konieczne jest wcześniejsze utworzenie adresata. W celu optymalizacji został stworzony model komunikacji bezstanowej – to znaczy wysłany komunikat zawiera pełen zbiór informacji potrzebnych do jego przetworzenia (analogicznie do modelu komunikacji w serwerach HTTP). Nie było potrzeby tworzenia skomplikowanych w implementacji dialogów między stronami. Przesyłane wiadomości mają następujący format:

  • 32 bity – długość przesyłanej wiadomości.
  • 8 bitów – typ wiadomości.
  • Kolejne bity zawierają dane zależne od typu wiadomości.

Typy wiadomości można podzielić na dwa zbiory:

  1. Wiadomości przesyłane między dwoma zwykłymi węzłami.
  2. Wiadomości przesyłane między zwykłym węzłem i serwerem zarządcą.

Wiadomości przesyłane między dwoma zwykłymi węzłami sessions_add – dodanie nowej lub modyfikacja istniejącej sesji. W sekcji danych znajduje się identyfikator sesji, kontekst oraz zserializowane dane sesji.

Wiadomości przesyłane między zwykłym węzłem i serwerem zarządcą Wiadomości przesyłane od serwera zarządcy do zwykłego węzła:

  • destinations_ade – dodanie nowego węzła do klastra. W sekcji danych przesyłany jest adres nowego węzła.
  • destination_remove – usunięcie węzła z klastra. W sekcji danych przesyłany jest adres węzła.
  • obtained_session – sesja została przyznana węzłowi. W sekcji danych znajduje się identyfikator sesji, kontekst oraz aktualny numer wersji (jeżeli węzeł posiada mniejszy numer oznacza to, że z jakiś powodów nie otrzymał repliki ostatnich zmian i musi wstrzymać modyfikację sesji do czasu otrzymania najświeższych danych).
  • session_existance – informacja czy podana sesja istnieje. W sekcji danych znajduje się identyfikator sesji, kontekst oraz 1 bajt z informacją czy sesja istnieje w klastrze czy nie. Komunikat jest wysyłany w odpowiedzi na

CHECK_SESSION_EXISTANCE.

  • check_session_version – prośba o odesłanie numeru wersji sesji o podanym identyfikatorze. W sekcji danych przesyłany jest identyfikator sesji oraz kontekst. Komunikat jest wysyłany w momencie próby odzyskania sesji zajmowanych przez węzeł, który doznał awarii. Każdy węzeł w klastrze odsyła numer wersji, a serwer zarządca na tej podstawie wybiera węzeł, który zreplikuje swoją kopię danych do pozostałych członków.
  • s e s s i on s_remove – usunięcie sesji. W sekcji danych znajduje się tablica identyfikatorów sesji oraz kontekstów. Komunikat jest wysyłany w momencie wygaśnięcia sesji. Serwer zarządca podejmuje decyzję o zakończeniu życia sesji analogicznie do pojedynczego serwera Tomcat. Jeżeli przez odpowiednio długi okres żaden serwer nie poprosi o dostęp do sesji, to zostaje ona uznana za nieaktywną.
  • sessions_replicate_with_sync – wymuszenie procesu replikacji. W sekcji danych znajduje się adres węzła, do którego należy wysłać replikę (jeżeli jest pusty, to należy rozesłać repliki do wszystkich węzłów) oraz tablica identyfikatorów sesji, które należy replikować. Węzeł, który odbierze komunikat po zakończeniu replikacji musi zwolnić wszystkie sesje, ponieważ serwer zarządca przed wysłaniem wiadomości ustawia blokady na adresata. Mechanizm ten zabezpiecza przed niebezpieczeństwem wprowadzania zmian w sesjach przez inny węzeł w czasie replikowania. Wysłane repliki mogłyby nadpisać nowo wprowadzone zmiany.

Wiadomości przesyłane od zwykłego węzła do serwera zarządcy:

  • obtain_session – zajęcie sesji. W sekcji danych znajduje się identyfikator zajmowanej sesji oraz kontekst.
  • release_session – zwolnienie sesji. W sekcji danych znajduje się identyfikator zwalnianej sesji, kontekst, numer wersji sesji oraz liczba zwolnień (zazwyczaj 1). Może się zdarzyć, że węzeł posiadając blokadę na sesji otrzyma od serwera zarządcy komunikat sessions_replicate_with_sync (na przykład z powodu zgłoszenia się nowego węzła do klastra). Wtedy węzeł musi zwolnić sesję więcej niż raz, wysyłając w tym celu liczbę zwolnień.
  • new_session_member – zgłoszenie się nowego węzła w klastrze. W sekcji danych znajduje się słownik par: identyfikator sesji (wraz z kontekstem), numer wersji. Komunikat jest wysyłany za każdym razem, gdy nastąpi zerwanie połączenia między zwykłym węzłem a serwerem zarządcą. W przypadku zupełnie nowego węzła słownik będzie pusty.
  • session_created – utworzenie sesji. W sekcji danych znajduje się identyfikator sesji oraz kontekst. Komunikat jest wysyłany zaraz po utworzeniu sesji. Wysłanie komunikatu jest konieczne, aby pozostałe węzły mogły się dowiedzieć poprzez komunikat check_session_existance o istnieniu tej sesji zanim twórca zakończy proces replikacji.
  • check_session_existance – sprawdzenie istnienia sesji. W sekcji danych znajduje się identyfikator sesji oraz kontekst. Komunikat jest wysyłany, jeżeli węzeł otrzyma żądanie odwołujące się do nieistniejącej sesji. Ponieważ sesja mogła być stworzona na innym węźle, ale jeszcze nie doszła replika, serwer wpierw sprawdza u zarządcy czy identyfikator jest aktywny.

Po odebraniu komunikatu z sieci tworzone jest zadanie (więcej o zadaniach będzie mowa w p. 4.3.3):

  • . task. SessionReceivedTask — W przypadku, gdy moduł działa wewnątrz serwera Tomcat  (zadanie tworzone jest w obiekcie org..transport.TransportCluster).
  • . task.MessageReceivedTask — W przypadku, gdy moduł działa jako niezależny serwer zarządca (zadanie tworzone jest w obiekcie org..transport.DestinationManager).

Zadanie SessionReceivedTask dziedziczy po MessageReceivedTask dokładając kilka możliwych typów wiadomości, które może obsługiwać. W szczególności są to wiadomości odbierane przez zwykłe węzły od serwera zarządcy lub wiadomości wymieniane między zwykłymi węzłami. Zakłada się, że serwer zarządca nie musi nic wiedzieć o menedżerze sesji. Natomiast iessionReceivedTask posiada atrybut wskazujący na menedżera sesji – niezbędne podczas przetwarzania takich komunikatów jak chociażby sessions_add.

Zadania tworzone na potrzeby przetwarzania komunikatów posiadają specjalną kolejkę zadań, gwarantującą, że komunikaty będą przetwarzane w kolejności, w której nadeszły (lub ewentualnie równolegle).

Mechanizm kolejki zadań zależnych

Na potrzeby projektu została stworzona wyspecjalizowana kolejka do przetwarzania zadań. Kolejka posiada swoją własną pulę wątków przetwarzających instrukcje. W ten sposób istnieje możliwość zlecania zadań, które mają się wykonać asynchronicznie (jak np. replikacja sesji). Wątki tworzone są raz, w momencie tworzenia kolejki, a ich praca sprowadza się do oczekiwania na nadejście kolejnego zadania, które mogłyby wykonać. Jeżeli zadań nie ma, to wątki przechodzą w stan uśpienia. Zmieniając liczbę wątków w kolejce można kontrolować zużycie zasobów systemowych przez moduł klastra (szerzej o konfigurowaniu klastra będzie mowa w p. 4.4).

Kolejka zadań posiada dodatkowe, bardzo przydatne możliwości:

  1. definiowania zadań, które zostaną wykonane z opóźnieniem (np. zwolnienie sesji przy wykorzystaniu buforowania);
  2. definiowania zależności między zadaniami – czyli zadanie zostanie wystartowane dopiero po wykonaniu innych zadań (np. zwolnienie sesji po zakończeniu wykonywania zadań replikacji);
  3. restartowania zadania, czyli ponownego wrzucenia zadania do kolejki, w przypadku błędu (np. ponawianie prób replikowania sesji do danego węzła).

Każde zadanie musi dziedziczyć po abstrakcyjnej klasie org. .task.Task. Klasa zawiera w szczególności abstrakcyjną metodę internalRun (), w której podklasy umieszczają kod zadania. Klasa zawiera metodę AddListener (Listener), poprzez którą można dołączyć słuchaczy do zadania. Słuchacze implementują interfejs

org..task.Listener:

public interface Listener {

public void WakeUp(boolean bError);

}

W momencie zakończenia wykonywania zadania dla każdego dołączonego słuchacza wywoływana jest metoda wakeup (boolean bError), z argumentem informującym czy zadanie zakończyło się z błędem czy nie (błąd jest wynikiem zgłoszenia przez zadanie wyjątku). Ten mechanizm jest wykorzystywany przy implementacji zależności między zadaniami. Po prostu klasa Task implementuje interfejs słuchacza, a w metodzie WakeUp (boolean) zmniejsza licznik zadań, na które oczekuje. W momencie, gdy licznik spadnie do zera, zadanie samo się inicjuje, dodając się do kolejki:

public void WakeUp(boolean bError) { this.DecTasks () ;

}

public synchronized void DecTasks() { nTasks–;

Start();

}

public void Start()     {

if (nTasks <= 0)       {

if (this.nTimeoutFromStart > 0)

this.SetTime(System.currentTimeMillis()               +

nTimeoutFromStart); oTaskQueue.AddTask(this);

}

}

Jeżeli zadanie ma się wykonać tylko w przypadku poprawnego zakończenia wszystkich zadań zależnych, to należy przeimplementować w podklasie metodę WakeUp (boolean , zmniejszając licznik zależności tylko w przypadku braku błędu. Ten mechanizm został wykorzystany przy implementacji zadania zwalniania sesji (klasa org. .task. ReleaseSessionTask):

public void WakeUp(boolean bError) { if (! bError)

super.WakeUp(bError) ;

}

Zadanie zwolnienia sesji zależy od zadań replikacji sesji do poszczególnych węzłów. Może się ono wykonać tylko w przypadku poprawnego zakończenia wszystkich zadań replikacyjnych (co jest równoznaczne z poprawnym rozesłaniem replik do wszystkich węzłów w klastrze). Jeżeli wystąpi nieoczekiwany błąd podczas wykonywania zadań replikujących (np. brak pamięci w systemie), to sesja nie zostanie zwolniona, co zabezpieczy system przed groźbą utraty danych.

Na rys. 4.10 znajduje się hierarchia klas najważniejszych zadań zdefiniowanych w systemie.

Rys. 4.10. Diagram hierarchii zadań

Opis zadań:

  1. sendingTask – abstrakcyjna klasa, zawierająca metodę Send (DestinationAddress, ByteBuffer), która umożliwia wysłanie bufora z danymi do konkretnego adresata. Można skonfigurować zadanie w dwóch różnych trybach:
  2. Operacja wysyłania ma być blokująca – tzn. powrót z metody 3end () nastąpi dopiero po faktycznym wysłaniu danych.
  3. Błąd podczas wysyłania ma powodować restart zadania.
  4. BufferSendingTast – klasa w metodzie internaiRun () wywołuje metodę send () z nadklasy. Klasa jest wykorzystywana do generowania zadań wysyłających proste komunikaty (np. komunikat o typie j bt ai u se s s i oh ).
  5. sessionPropagateTask – klasa jest odpowiedzialna za wysłanie repliki konkretnej sesji do konkretnego adresata. W metodzie InternaiRun () serializuje obiekt sesji i próbuje wysłać dane do węzła (wywołując metodę Send () z nadklasy). Klasa wykorzystuje tryb restartowania w przypadku błędu. W ten sposób, jeżeli z jakiegoś powodu wysłanie danych zaczyna się opóźniać, to nie blokuje niepotrzebnie pamięci zserializowanymi danymi sesji i co ważniejsze przy kolejnym uruchomieniu zadania ponawia próbę replikacji, ale już być może z nowszą wersją sesji, zmniejszając w ten sposób liczbę przesyłanych informacji (tworzenie zadań replikacyjnych zostanie opisane w p. 4.3.5). Ponadto, jeżeli system wykorzystuje buforowanie, to przy definicji zadania ustala się opóźnienie w wykonaniu (por. p. 4.3.4).
  6. ReieaseSessionTask – zadania tej klasy są odpowiedzialne za zwalnianie sesji w serwerze zarządcy. System podczas tworzenia ustawia zależność tego zadania od wszystkich zadań replikacyjnych sesji. Proces zwolnienia jest uruchamiany dopiero po poprawnym przesłaniu replik do wszystkich węzłów w klastrze. W przypadku wykorzystywania buforowania system dodatkowo opóźnia start zadania już po wykonaniu wszystkich replikacji (szerzej na ten temat będzie mowa w p. 4.3.4).
  7. MessageReceivedTask – zadanie tej klasy tworzone jest po odebraniu wiadomości z sieci. Zadanie zawiera dane komunikatu oraz adres węzła, który je przysłał. Przetwarzanie wiadomości wykonywane jest w odrębnym wątku, co powoduje, że nie następuje blokowanie wątku odbierającego dane z sieci. W ten sposób zapewniane jest maksymalne zrównoleglenie obliczeń wykonywanych przez moduł klastra, co może nie być bez znaczenia w przypadku serwerów wieloprocesorowych.
  8. SessionReceivedTask — klasa dziedziczy po MessageReceivedTask dodając w metodzie internaiRun () obsługę komunikatów charakterystycznych w sytuacji, gdy moduł jest zanurzony wewnątrz serwera Tomcat, jak np. komunikat

W systemie zostały stworzone dwie odrębne kolejki zadań:

  • dla zadań przetwarzających odebrane z sieci komunikaty oraz
  • dla pozostałych zadań występujących w systemie (czyli w przeważającej liczbie zadań wysyłających komunikaty).

Potrzeba odizolowania tych zadań wynikła z faktu, że zadania przetwarzania wiadomości odebranych nie mogą być blokowane przez zadania wysyłające dane.

Można sobie wyobrazić sytuację, gdy wszystkie wątki w kolejce zostały wstrzymane przy próbie wysłania danych i nie ma wolnego wątka, który opróżniłby bufory z odebranymi wiadomościami. Bardzo często to właśnie szybkie przetworzenie odebranych informacji będzie miało krytyczny wpływ na wydajność systemu. W pakietach odbieranych będą informacje o przydzieleniu sesji (komunikat obtained_sessio: ) – zinterpretowanie tego komunikatu powoduje bezpośrednie wznowienie wykonywania zgłoszenia wygenerowanego przez użytkownika systemu.

Moduł klastra umożliwia konfigurowanie liczby wątków dołączonych do każdej z kolejek (opis konfiguracji znajduje się w p. 4.4).

Implementacja kolejki zadań

Do implementacji kolejki zostało wykorzystane drzewo (standardowa klasa Javy j ava. utii. TreeSei) z wartościami posortowanymi zgodnie z czasem wykonania. Zadania, które mają być wykonane bez opóźnienia wstawiane są do drzewa z małymi wartościami, natomiast te, które mają się wykonać po upływie pewnego czasu są wstawiane z liczbą milisekund określającą czas wykonania.

Każdy wątek podłączony do drzewa w nieskończonej pętli pobiera zadanie z kolejki (JorkerQueue. GetTask () – wywołanie metody jest blokujące i w przypadku braku zadań powoduje wstrzymanie wykonywania). Następnie dla pobranego zadania wywołuje metodę Run ( , która obudowuje wywołanie tnternaiRun ().

Głównym powodem stworzenia kolejki zadań zależnych była potrzeba realizacji procesu zwalniania sesji dopiero po udanym zakończeniu rozsyłania kopii do wszystkich węzłów klastra. Drugim, nie mniej ważnym powodem było umożliwienie realizacji pewnych zadań z opóźnieniem, jak np. wysłanie repliki po pewnym czasie, aby ewentualnie umożliwić użytkownikowi wprowadzenie kolejnych zmian i ominąć wysłanie wcześniejszej wersji.

Skuteczność i wydajność systemów rozproszonych w bardzo dużym stopniu zależy od zastosowanych metod buforowania. Wszelkiego rodzaju opóźnianie wysłania danych w celu ich zagregowania czy eliminowanie przesyłania niepotrzebnych wiadomości powodują, że system może w znacznym stopniu przyspieszyć swoje działanie. Stworzony moduł klastra również zawiera mechanizmy usprawniające jego działanie.

Buforowanie

W systemie zostało zastosowane buforowanie w dwóch różnych etapach:

  1. Opóźnianie momentu replikacji sesji.
  2. Opóźnianie momentu zwalniania sesji.

Oba typy buforowania są możliwe do zastosowania tylko w przypadku wykorzystania zintegrowanego serwera ruchu. Jeżeli serwer rozdzielający zadania na poszczególne węzły nie będzie posiadał informacji o węźle aktualnie zajmującym sesję, to buforowanie na poziomie sesji może jedynie niepotrzebnie opóźniać działanie klastra. Jeżeli natomiast posiada takie informacje, to będzie przekierowywał żądania do węzła, który aktualnie okupuje sesję, co umożliwia węzłom opóźnianie momentu przekazania sesji.

Opóźnianie momentu replikacji sesji

Każde zadanie replikujące posiada numer sesji oraz adres węzła, do którego ma wysłać replikę. Moduł dla każdego węzła przechowuje informację o stworzonych, ale nie rozpoczętych zadaniach replikujących, z możliwością wyszukiwania po identyfikatorze sesji (słownik, w którym kluczami są identyfikatory). W momencie zakończenia przetwarzania żądania, system tworząc zadania replikujące sprawdza czy nie istnieje już identyczne zadanie, które jeszcze nie zdążyło się wykonać. Jeżeli istnieje, to nie ma potrzeby tworzenia kolejnego zadania, bo istniejące tak czy owak w momencie wykonania pobierze najświeższe dane sesji. Należy tylko do zbioru słuchaczy istniejącego zadania dodać obiekt klasy ReieaseSessionTas , aby w odpowiednim momencie zwolnić sesję.

Takie podejście gwarantuje, że nawet jeżeli wiele wątków jednocześnie będzie przetwarzało żądania odwołujące się do tej samej sesji, to tak czy owak zostanie stworzone tylko jedno zadanie replikujące, zmniejszając w ten sposób liczbę przesyłanych informacji. Dodatkowo mechanizm pozwala na łatwą realizację koncepcji opóźniania procesu replikacji. Wystarczy, że podczas tworzenia zadania replikującego moduł wstawi opóźnienie w jego wykonanie. Jeżeli w czasie oczekiwania zostanie przetworzone kolejne żądanie (zmieniające dane sesji), to moduł ominie wysłanie wcześniejszej wersji. W ten sposób doprowadza się do sytuacji, gdzie wydajność klastra przestaje zależeć od częstotliwości zgłaszania się użytkowników. W rezultacie sesja będzie replikowane co pewien czas, niezależnie od tego czy użytkownik zgłosi się do systemu raz czy bardzo wiele razy. Dobór długości opóźnienia nie jest rzeczą zupełnie trywialną – jeżeli będzie za małe, to buforowanie może nie przynosić oczekiwanych rezultatów. Z kolei jeżeli opóźnienie będzie za duże, to system straci swoją główną zaletę – czyli dynamiczne równoważenie obciążenia. Doprowadzi to do sytuacji, gdzie każda sesja jest na stałe przypisana do konkretnego węzła – czyli analogicznie do koncepcji zastosowanej w serwerze Bea Weblogic 8.0 (por. p. 2.3.2.1), a cały nakład związany z synchronizowaniem sesji okaże się zbędny.

Opóźnianie wysłania zmienionych danych zwiększa niebezpieczeństwo utraty zmian. Jeżeli nastąpi awaria węzła, to może to doprowadzić do utraty nie tylko właśnie przetwarzanych sesji, ale również tych, które były przetworzone wcześniej, ale oczekiwały na moment replikacji. Problem ten został rozwiązany przez tworzenie jednego zadania replikacyjnego bez opóźnienia. To znaczy moduł po przetworzeniu żądania wybiera losowo jeden węzeł, dla którego tworzy zadanie wysłania repliki z natychmiastowym czasem wykonania. W ten sposób w każdej chwili najświeższe dane sesji znajdują się przynajmniej na dwóch maszynach. W razie awarii dane sesji zostaną odzyskane z drugiej maszyny (patrz opis komunikatów

CHECK_SESSION_VERSION oraz SESSION_VERSION W p. 4.3.2).

Opóźnianie momentu zwalniania sesji

Poza opóźnianiem wysłania pakietów z kopią najnowszej wersji sesji można również opóźniać moment zwolnienia sesji. Wydajność klastra jest mierzona szybkością reakcji systemu na zgłoszenie użytkownika. Im reakcja jest szybsza, tym lepiej. Niestety architektura zaimplementowanego klastra wymaga, aby przed modyfikacją danych użytkownika system zablokował sesję w serwerze zarządzającym. Operacja ta jest dosyć kosztowna – wymaga przesłania dwóch pakietów w sieci (prośba o zablokowanie i odpowiedź). Jeżeli węzeł opóźni moment oddania sesji, to może przetworzyć kilka kolejnych żądań bez potrzeby ponownego zajmowania sesji. Oczywiście nie można przesadzić z długością opóźnienia, aby nie doprowadzić do sytuacji opisanej w poprzednim punkcie, gdy klaster zaczyna się zachowywać tak, jakby istniały stałe przypisania sesja – węzeł macierzysty.

Warto zwrócić uwagę na fakt, że blokada jest przyznawana całemu węzłowi i wszystkie wątki wewnątrz serwera mogą korzystać z danych sesji bez potrzeby ponownego wysyłania prośby o jej przyznanie. W danym momencie tylko jeden wątek wysyła komunikat z prośbą o zablokowanie sesji. Wszystkie kolejne wątki, odwołujące się do sesji będą czekały na zakończenie wcześniej rozpoczętej procedury. Podobnie wygląda proces zwalniania – dopiero ostatni zwalniający wątek wyśle faktycznie komunikat do serwera zarządcy. Wszystkie wcześniejsze po prostu zmniejszą licznik odwołań.

Znając podstawowe mechanizmy działające w module można przeanalizować algorytm przetwarzania żądania. Ścieżka wykonania algorytmu rozpoczyna się w momencie zgłoszenia przez użytkownika żądania, a kończy w momencie wysłania do serwera zarządcy wiadomości zwalniającej sesję.

Algorytm przetwarzania żądania

Opisywana ścieżka przetwarzania żądania rozpoczyna się już w konkretnym węźle, czyli serwerze Tomcat. Sposób działania systemu na poziomie zarządcy ruchu zostanie opisany w p. 4.3.7. Koniec opisywanego procesu wyznaczany jest przez wysłanie danych z serwera Tomcat oraz przez zwolnienie blokady sesji użytkownika.

Na rys. 4.11 znajduje się diagram przepływów ukazujący najbardziej charakterystyczny przypadek obsługi żądania. Diagram przedstawia zgłoszenie klienta z numerem sesji, którą należy zablokować w serwerze zarządzającym.

Rys. 4.11. Diagram przepływ u podczas obsługi żądania

Klient generuje zgłoszenie, które zostaje wysłane do serwera Tomcat. Serwer inicjuje wykonanie strumienia przetwarzania żądań (por. p. 3.3.2). Wśród załączonych wyzwalaczy występuje również wyzwalacz klasy ReplicationValve (analogicznie do implementacji Filipa Hanika). W metodzie invoke(Request, Response, Context) wyzwalacz wznawia przetwarzanie strumienia w celu wykonania Zgłoszenia. Po powrocie Z metody invokeNext (Reąuest, Response, Context) program sprawdza czy żądanie posiada sesję oraz czy sesja jest dzielona przez cały klaster. Jeżeli tak, to zwiększa wersję sesji, tworzy dla każdego węzła w klastrze zadanie replikacyjne oraz tworzy zadanie odblokowania sesji, które wykona się po rozesłaniu wszystkich kopii. Następnie kończy działanie, tym samym kończąc proces przetwarzania żądania. Wszystkie stworzone zadania wykonane zostaną asynchronicznie przez wątki obsługujące kolejkę zadań.

Oczywiście zadania replikacyjne zostaną stworzone tylko pod warunkiem, że w systemie nie występowały już wcześniej zdefiniowane identyczne zadania. Po wykonaniu replikatorów (zadań replikacyjnych) do kolejki trafia zadanie odblokowania sesji. W czasie uruchomienia sprawdza czy inne wątki aktualnie nie używają sesji. Jeżeli nie, to ustawia w strukturach danych, że sesja nie jest zablokowana i wysyła komunikat do serwera zarządcy w celu faktycznego odblokowania. Od tego momentu każdy wątek, który spróbuje się odwołać do sesji będzie musiał najpierw zablokować ją u zarządcy. Tutaj warto zwrócić uwagę na implementację procesu przydzielania i zwalniania zasobu. Nawet jeżeli zostaną wysłane równolegle dwa pakiety: jeden z prośbą o zwolnienie, a drugi o zablokowanie sesji, to ponieważ serwer zlicza ile razy semafor został podniesiony i opuszcza go dokładnie tyle razy ile wysłany pakiet to specyfikuje, nie będzie niebezpieczeństwa wejścia do sekcji krytycznej bez podniesienia semafora. Po prostu jeden pakiet zmniejszy licznik, a drugi go zwiększy – kolejność nie będzie odgrywała roli.

Proces tworzenia sesji

Jeżeli wygenerowane żądanie nie posiadało wcześniej utworzonej sesji, a aplikacja odwoła się do niej, to serwer Tomcat standardowo wywoła dla menedżera sesji metodę createSession (). Ponieważ dla aplikacji zadeklarowanych jako rozproszone (tag <distributable/> w pliku web.xml) menedżerem jest obiekt klasy org. . server. SessionManage , wywołanie to zostanie przechwycone przez moduł klastra. Menedżer utworzy sesję (analogicznie jak w zwykłych menedżerach), ale sesja będzie instancją klasy org. . session. ReplicatedSession (podklasa org . apache . catalina . session . StandardSessic ). Dodatkowo menedżer wygeneruje zadanie wysłania wiadomości session_createe do serwera zarządcy informujące o stworzeniu nowego identyfikatora (zadanie będzie wykonywane w tle). Serwer zarządca przetwarzając tę wiadomość przy okazji podniesie semafor dla nowej sesji a konto węzła, który ją stworzył (aby nie trzeba było wysyłać kolejnego pakietu z prośbą o przyznanie sesji).

Oprócz wysłania wiadomości menedżer ustawi w globalnych strukturach danych, że sesja została przydzielona temu węzłowi. Na tym wywołanie metody createSession () się kończy. Dalej następuje przetwarzanie analogiczne do sytuacji, gdy sesja była utworzona wcześniej.

Proces sprawdzania istnienia sesji

Kolejnym przypadkiem jaki może się wydarzyć jest odwołanie do sesji, która nie jest zarejestrowana w menedżerze. Są dwie możliwości zaistnienia takiej sytuacji (nie licząc złośliwych żądań generowanych przez włamywaczy):

  1. Sesja już wygasła w klastrze.
  2. Węzeł nie otrzymał jeszcze pakietu sessions_add od twórcy sesji.

O ile w pierwszym przypadku menedżer przez pewien czas może zachowywać informacje o wygasłych sesjach i bez potrzeby komunikowania się z zarządcą po prostu tworzyć nową sesję, o tyle w drugim przypadku konieczne jest wysłanie zapytania.

Obiekt klasy SessionManager w metodzie findSession(String sessionID)

wykonuje następujący kod:

public Session findSession(String sSessionID) throws java.io.IOException { return findSession(sSessionID, true);

}

public Session findSession(String sSessionID, boolean bCreate) throws java.io.IOException { if (sSessionID == null) return null;

Session oSession = super.findSession(sSessionID); if (oSession == null && sSessionID != null) {

// sprawdzamy czy sesja istnieje w klastrze

if (oSyncServer.CheckExistance(new SessionContext(sSessionID, this.getName())))  {

synchronized (sessions) {

oSession = super.findSession(sSessionID);

if ( oSession == null)  {

// sesja istnieje, ale my wciąż jej nie mamy // dlatego tworzymy pustą, aby wątek mógł // przy odwołaniu rozpocząć procedurę // blokowania sesji. oSession =

this.createSession(sSessionID, false);

}

}

}

return oSession;

Obiekt oSyncServer jest instancją klasy org. .server.SyncManager (klasa została szerzej opisana w p. 4.3.6). Każdy węzeł klastra zawiera dokładnie jeden obiekt tej klasy, współdzielony przez wszystkie menedżery sesji. Metoda checkExistance (SessionContext) działa analogicznie do procedury zajmowania sesji. To znaczy pierwszy wątek inicjuje faktyczną procedurę sprawdzania u serwera zarządcy (wysyła komunikat check_session_existanc ), a wszystkie kolejne jedynie czekają na odpowiedź. Jeżeli okaże się, że w klastrze istnieje sesja o podanym identyfikatorze, to menedżer stworzy pusty obiekt i pozwoli na dalsze wykonywanie wątku. Nie istnieje tu niebezpieczeństwo rozspójnienia, ponieważ mimo stworzenia pustej sesji nie została ustawiona na niej blokada. Czyli wątek, który będzie się odwoływał do danych sesji, tak czy owak będzie musiał ją najpierw zablokować. Z kolei, aby blokada się udała musi zakończyć się proces replikacji, który spowoduje, że pusty do tej pory obiekt sesji zostanie w końcu zasilony danymi.

Należy tu zwrócić uwagę na fakt, że w przypadku wykorzystania zintegrowanego serwera ruchu węzły nie będą miały potrzeby generowania dodatkowych zapytań do serwera zarządcy. Jeżeli jakaś sesja będzie w danym momencie zajęta, to żądanie będzie przekierowane do zajmującego ją węzła. Jeżeli nie będzie przez nikogo zajęta, to będzie to oznaczało, że tak czy owak wszystkie węzły posiadają kopię tej sesji i nie będą musiały się pytać serwera czy istnieje.

Proces blokowania sesji poprzez odwołanie

W celu zminimalizowania niepotrzebnych operacji blokowania oraz replikowania sesji moment wejścia do sekcji krytycznej został przeniesiony w miejsce faktycznego odwołania do danych sesji. Czyli jeżeli użytkownik wygeneruje żądanie, które nie będzie zaglądało do danych sesji (nie zostaną wywołane na sesji metody

getAttribute(String), setAttribute(String, Object) itp.), to nie spowoduje

to żadnego ruchu po stronie klastra.

W dalszej części tego punktu opisano sposób zaimplementowania tego mechanizmu.

Obiekt klasy ReplicatedSession nadpisuje wywołania wszystkich metod związanych z pobieraniem lub ustawianiem danych w sesji (czyli m.in. getAttribute ( String , setAttribute(String, Object ). W każdej Z ty ch metod zanim zostanie wykonana metoda z nadklasy sprawdzane jest czy odwołujący się wątek zablokował już tę sesję. Jeżeli nie, to wywoływana jest metoda obtainSession(string sessioniD) na menedżerze sesji. To wywołanie z kolei blokuje sesję u serwera zarządcy (synchronicznie) lub zwiększa licznik odwołań do już zajętej sesji. Po przetworzeniu żądania (w wyzwalaczu ReplicationValv ) sprawdzane jest czy wątek zajmował sesję. Jeżeli tak, to inicjowana jest replikacja oraz zwolnienie sesji.

Niestety istnieje tu hipotetyczne niebezpieczeństwo, że jeżeli aplikacja przetwarzając żądanie stworzy nowy wątek, który odwoła się do sesji, to później sesja ta nie zostanie zwolniona. Wątek założy blokadę, której później nie będzie w stanie zdjąć. Jednak sytuacja taka jest na tyle specyficzna, że raczej istnieje małe prawdopodobieństwo, że programiści zdecydują się na jej realizację w rzeczywistych aplikacjach. Nawet gdyby ktoś chciał zaimplementować taką architekturę rozwiązania, to wystarczy, że do nowego wątku przekaże już pobrane z sesji atrybuty, a po zakończeniu jego wykonywania wpisze je z powrotem.

Tak czy owak w najgorszym wypadku węzeł po prostu nie zwolni sesji, co przy wykorzystaniu zintegrowanego serwera ruchu nie spowoduje zaprzestania działania aplikacji. Wszystkie żądania będą kierowane na jedną maszynę bez możliwości zmiany kontekstu wykonania. Atutem zastosowania klastra będzie natomiast wciąż trwający proces replikowania sesji, co może okazać się nie bez znaczenia w przypadku awarii tego węzła.

Cała logika związana z blokowaniem, zwalnianiem czy replikowaniem sesji znajduje się w klasie menedżera sesji (org.. server. sessionManagei) oraz klasie samej sesji ( rg. . session. ReplicatedSessio ). Niemniej jednak większość kodu niezbędnego do komunikacji z serwerem zarządcą jak i kod samego serwera zarządcy znajduje się W dwóch klasach: org. . server .MainSyncServer oraz

org..server.SyncManager.

Serwer zarządca

Głównym celem implementacji serwera zarządcy była możliwość wykorzystania tego samego kodu w dwóch przypadkach:

  1. Serwer zarządca jest jednym z węzłów klastra (zamieszczony wewnątrz Tomcata).
  2. Serwer zarządca jest osobnym programem, który nie ma nic wspólnego z klasami Tomcata.

Aby uzyskać taką dwoistość modułu, zastosowano mechanizm dziedziczenia w podejściu obiektowym. Implementacja bazowa serwera zarządcy opiera się na klasie org. . server.MainSyncServer oraz klasie implementującej warstwę transportującą — org. . server. DestinationManager. Zastosowanie modułu wewnątrz serwera Tomcat staje się możliwe poprzez dodanie klas dziedziczących po klasach bazowych.

W wywołaniach nadpisanych metod został dodany kod specyficzny dla serwera Tomcat. W szczególności został zaimplementowany mechanizm zdalnego lub lokalnego (w zależności od konfiguracji) odwoływania się do serwera zarządcy. Jeżeli moduł ma działać jako klient zdalnego serwera zarządcy, to wszystkie odwołania do
metod z klasy SyncManager powodują wygenerowanie odpowiednich komunikatów w sieci. Natomiast w przypadku, gdy moduł wewnątrz Tomcata pełni jednocześnie rolę serwera zarządcy, wtedy klasa SyncManager wywołuje kod z nadklasy, czyli MainSyncServer. Czyli tak naprawdę zainstalowanie pojedynczego serwera w klastrze (w trybie serwera zarządcy) nie spowoduje znaczącego spadku wydajności w stosunku do sytuacji, gdy serwer ten będzie działał w ogóle bez modułu klastra. Taka informacja może mieć duży wpływ na podjęcie decyzji o instalacji środowiska rozproszonego. Administrator instalując klaster nie jest zmuszony do natychmiastowego podłączenia wielu komputerów, w celu zrekompensowania spadku mocy obliczeniowej. Pojedyncza maszyna będzie działała równie wydajnie jak przed podłączeniem modułu.

Wydajność klastra w dużej mierze zależy od oprogramowania rozdzielającego zadania na poszczególne jego węzły. Szczególnie istotne jest to w przypadku przedstawionego w pracy rozwiązania. Algorytm, który nie wykorzystuje informacji o aktualnych przydziałach sesji, spowoduje osłabienie mocy przerobowej systemu i uniemożliwi wykorzystanie buforowania.

Serwer ruchu

Zasadniczym celem pracy było napisanie i przedstawienie działającego modułu klastra w serwerze Tomcat. Niemniej jednak, aby móc w całości ukazać działanie rozwiązania konieczne stało się napisanie zintegrowanego serwera przekierowującego żądania do poszczególnych maszyn klastra. Niestety nie udało się znaleźć gotowego programu, który odznaczałby się następującymi cechami:

  • Napisany w języku Java z dostępnym kodem źródłowym.
  • Bardzo wydajny (przekierowania na poziomie TCP/IP).
  • Buforujący pulę połączeń.

Dlatego w ramach pracy został napisany serwer ruchu jednocześnie działający jako serwer zarządca. Implementację należy traktować jako wzorcowy przykład rozwiązania, a nie jako docelowy i w pełni działający serwer do przeki ero wy wania żądań.

Serwer został w całości napisany w języku Java z wykorzystaniem klas bazowych z modułu klastra. Schemat działania programu jest stosunkowo prosty:

  1. Przy starcie otwiera port, na którym będzie przyjmował żądania od klientów.
  2. Tworzy i inicjuje obiekty serwera zarządcy.
  3. W momencie, gdy jakiś węzeł klastra zgłosi się do serwera zarządcy jednocześnie dodawany jest do listy aktywnych serwerów Tomcat.
  4. Przy zgłoszeniu klienta program wybiera serwer Tomcat z listy aktywnych i zaczyna się zachowywać jak serwer pośredniczący, przekazując dane między klientem a serwerem.
  5. Jeżeli któraś ze stron zakończy połączenie, to program automatycznie kończy połączenie z drugiej strony.
  6. W momencie zgłoszenia awarii węzła przez oprogramowanie serwera zarządcy jest on automatycznie usuwany z listy aktywnych serwerów Tomcat.

Szczegóły implementacyjne

Dla każdego aktywnego serwera Tomcat trzymana jest pula połączeń (jej wielkość jest konfigurowalna). Każde połączenie jest zadaniem, które w momencie uruchomienia powoduje rozpoczęcie czytania z kanału komunikacyjnego. Jednocześnie obiekt zadania posiada metodę write (ByteBuffer , która umożliwia pisanie danych do połączenia. Przed ponownym uruchomieniem zadania (wywołaniem metody Restart o) ustawiane są referencje między zadaniem obsługującym połączenie z serwerem, a zadaniem obsługującym połączenie z klientem. W ten sposób później wszystkie odebrane informacje przez jeden obiekt zostają przesłane do drugiego (tworząc pomost dla danych). Różnice między zadaniami klienta a serwera występują w zachowaniu na początku i końcu.

Zadanie połączenia klienta na początku, zanim zostanie sparowane z zadaniem serwerowym, czyta nagłówek żądania w celu sprawdzenia czy nie odwołuje się ono do konkretnej sesji. Jeżeli tak, to program sprawdza czy podana sesja nie jest aktualnie zajmowana przez jeden z węzłów. Jeżeli jest zajmowana, to zadanie jako odbiorcę wybiera połączenie z puli tego węzła. W przeciwnym przypadku zadanie wybiera serwer posiadający najwięcej nieużywanych połączeń.

Z kolei zadanie serwerowe po skończeniu obsługiwania klienta odnawia połączenie z serwerem, aby przy obsłudze kolejnego żądania nie trzeba było tracić czasu na nawiązywanie połączenia. Istotne jest, aby odpowiednio skonfigurować połączenia w serwerach – tzn. dopuszczalny czas bezruchu na połączeniu musi być odpowiednio długi, aby połączenia nie były zbyt szybko zrywane przez serwer. Jeżeli połączenie zostanie zerwane zanim jakiś klient zacznie je wykorzystywać, to obsługa żądania zostanie wydłużona o czas odnowienia połączenia.

Wykorzystanie zadań umożliwiło dynamiczny przydział wątków do obsługi wszystkich połączeń ze wszystkich serwerów jednocześnie. W ten sposób można zwiększyć liczbę oczekujących połączeń bez zwiększania liczby wątków. Dynamiczny przydział wątków ze wspólnej puli powoduje, że rozwiązanie dużo lepiej adaptuje się do aktualnego zapotrzebowania na połączenia.

Warto zwrócić uwagę, że zastosowane rozwiązanie umożliwia wykorzystanie opcji protokołu HTTP 1.1, Connection: Keep-Aiive. Opcja pozwala przeglądarce na wykorzystanie raz otwartego połączenia dla obsługi kilku osobnych żądań. Po prostu żadna ze stron nie zerwie połączenia, tym samym zachowując aktywny pomost w serwerze ruchu.

Wadą zaproponowanego rozwiązania jest brak interpretera nagłówka protokołu HTTP. Gdyby program był w stanie poprawnie interpretować nagłówek mógłby zachowywać otwarte połączenia z każdym z węzłów, bez potrzeby ich odnawiania (do tego niezbędna byłaby kontrola długości przesyłanych danych). Wtedy również w przypadku trzymania połączeń typu Keep-Alive można by było dynamicznie zmieniać serwer, który obsłuży kolejne żądanie. Niestety stopień skomplikowania takiego interpretera wykracza poza ramy tej pracy.

Zarówno serwer ruchu jak i sam moduł klastra zawierają szereg parametrów, które umożliwiają dostrojenie systemu w zależności od zastosowania. W kolejnym punkcie opisano parametry oraz przedstawiono różne dodatkowe wskazówki, którymi mogą się kierować administratorzy klastra.

image_pdf

Protokoły sieciowe

5/5 - (1 vote)

Protokół (ang. protocol) – Zbiór sygnałów używanych przez grupę komputerów podczas wymiany danych (wysyłania, odbierania i kontroli poprawności informacji). Komputer może używać kilku protokołów. Np. jednego do komunikacji z jednym systemem, a drugiego z innym.

Protokołem w sieci komputerowej nazywamy zbiór powiązań i połączeń jej elementów funkcjonalnych. Tylko dzięki nim urządzenia tworzące sieć mogą się porozumiewać. Podstawowym zadaniem protokołu jest identyfikacja procesu, z którym chce się komunikować proces bazowy. Z uwagi na to, że zwykle w sieci pracuje wiele komputerów, konieczne jest podanie sposobu określania właściwego adresata, sposobu rozpoczynania i kończenia transmisji, a także sposobu przesyłania danych. Przesyłana informacja może być porcjowana – protokół musi umieć odtworzyć informację w postaci pierwotnej. Ponadto informacja może z różnych powodów być przesłana niepoprawnie – protokół musi wykrywać i usuwać powstałe w ten sposób błędy. Różnorodność urządzeń pracujących w sieci może być przyczyną niedopasowania szybkości pracy nadawcy i odbiorcy informacji – protokół powinien zapewniać synchronizację przesyłania danych poprzez zrealizowanie sprzężenia zwrotnego pomiędzy urządzeniami biorącymi udział w transmisji. Ponadto z uwagi na możliwość realizacji połączenia między komputerami na różne sposoby, protokół powinien zapewniać wybór optymalnej – z punktu widzenia transmisji – drogi.

Protokoły sieciowe – zapewniają usługi łączy systemów komunikacyjnych, obsługują adresowanie, informacje routingu, weryfikację błędów oraz żądania retransmisji. Obejmują także procedury dostępu do sieci, określone przez wykorzystywany rodzaj sieci. Najpopularniejsze protokoły sieciowe to:

  • IP (Internet Protocol), część zestawu protokołów TCP/IP,
    • APPN (Advanced Peer-to-Peer Networking) firmy IBM,
    • CONS (OSI Connection-Oriented Network Service),
    • CLNS (OSI Connectionless Network Service),
    • IPX, część zestawu protokołów SPX/IPX firmy Novell,
    • Interfejsy Microsoft NetBEUI,
    • AppleTalk DDP (Datagram Delivery Protocol).
    Trzy najczęściej używane protokoły w sieciach lokalnych i w internecie to TCP/IP, SPX/IPX i NetBEUI

TCP/IP

OSI – model OSI, czyli powiązania między protokołami TCP/IP. Chyba najczęściej używany, zarówno dla sieci lokalnych jak i połączenia z internetem.

• TCP – Protokół sterowania transmisją (ang. Transmission Control Protocol) jest protokołem obsługi połączeniowej procesu użytkownika, umożliwiającym niezawodne i równoczesne (ang. full-duplex) przesyłanie strumienia bajtów. W większości internetowych programów użytkowych stosuje się protokół TCP. TCP korzysta z protokołu IP, więc całą rodzinę protokołów nazywamy TCP/IP.
• UDP – Protokół datagramów użytkownika (komunikaty przesyłane między systemami jeden niezależnie od drugiego) (ang. User Datagram Protocol) jest protokołem obsługi bezpołączeniowej procesów użytkownika. W odróżnieniu od protokołu TCP, który jest niezawodny, protokół UDP nie daje gwarancji, że datagramy UDP zawsze dotrą do celu.
• ICMP – Protokół międzysieciowych komunikatów sterujących (ang. Internet Control Message Protocol) obsługuje zawiadomienia o błędach i informacje sterujące między bramami (ang. gateway) a stacjami (ang. host). Chociaż komunikaty ICMP są przesyłane za pomocą datagramów IP, są one zazwyczaj generowane i przetwarzane przez oprogramowanie sieciowe TCP/IP, a nie przez procesy użytkownika.
• IP – Protokół międzysieciowy (ang. Internet Protocol) obsługuje doręczanie pakietów dla protokołów TCP, UDP oraz ICMP. Procesy użytkownika normalnie nie muszą komunikować się z warstwą IP.
• ARP – Protokół odwzorowania adresów (ang. Address Resolution Protocol) służy do odwzorowania adresów internetowych na adresy sprzętowe. Ten protokół i protokół RARP nie jest używany we wszystkich sieciach, lecz tylko w niektórych.
• RARP – Protokół odwrotnego odwzorowywania adresów (ang. Reverse Address Resolution Protocol) służy do odwzorowywania adresów sprzętowych na adresy internetowe.

IPX/SPX

IPX/SPX – jest to zestaw protokołów firmy Novell, bierze on nazwę od swoich dwóch głównych protokołów: międzysieciowej wymiany pakietów IPX i sekwencyjnej wymiany pakietów SPX. Ten firmowy stos protokołów został oparty na protokole systemów sieciowych firmy Xerox, wykorzystywanym w pierwszej generacji Ethernet. Wymiana IPX/SPX zyskała na znaczeniu we wczesnych latach 80, jako integralna część systemu Novell Netware. Netware stał się faktycznym standardem sieciowego systemu operacyjnego dla sieci lokalnych pierwszej generacji. Protokół IPX w dużym stopniu przypomina IP. Jest bezpołączeniowym protokołem datagramowym, który nie wymaga ani nie zapewnia potwierdzenia każdego transmitowanego pakietu. Protokół IPX polega na SPX w taki sam sposób, w jaki protokół IP polega na TCP w zakresie porządkowania kolejności i innych usług połączeniowych warstwy 4. Stos protokołów IPX/SPX obejmuje cztery warstwy funkcjonalne: dostępu do nośnika, łącza danych, Internetu i aplikacji. Głównym protokołem warstwy aplikacji jest protokół rdzenia NetWare ( NCP). Protokół NCP można bezpośrednio sprzęgnąć zarówno z protokołem SPX, jak i IPX. Jest wykorzystywany do drukowania, współdzielenia plików, poczty elektronicznej i dostępu do katalogów. Innymi protokołami warstwy aplikacji są: protokół informacyjny trasowania, firmowy protokół ogłoszeniowy usługi i protokół obsługi łącza systemu NetWare. Protokół warstwy Internetu SPX jest protokołem połączeniowym i może być wykorzystywany do przesyłania danych między klientem serwerem, dwoma serwerami czy dwoma klientami. Tak jak w przypadku TCP, protokół SPX zapewnia niezawodność transmisjom IPX, zarządzając połączeniem i udostępniając sterowanie strumieniem danych, kontrolę błędów i porządkowanie kolejnych pakietów.

NetBEUI

NetBEUI – interfejs NetBEUI został opracowany przez IBM i wprowadzony na rynek w 1985 roku. Jest stosunkowo małym ale wydajnym protokołem komunikacyjnym LAN. NetBEUI jest wyłącznie protokołem transportu sieci LAN dla systemów operacyjnych Microsoft. Nie jest trasowany. Dlatego jego implementacje ograniczają się do warstwy 2, w których działają wyłącznie komputery wykorzystujące systemy operacyjne firmy Microsoft. Aczkolwiek staje się to coraz mniejszą przeszkodą, to jednak ogranicza dostępne architektury obliczeniowe i aplikacje technologiczne. Zalety korzystania z protokołu NetBEUI są następujące: Komputery korzystające z systemów operacyjnych lub oprogramowania sieciowego firmy Microsoft mogą się komunikować. NetBEUI jest w pełni samodostrajającym się protokołem i najlepiej działa w małych segmentach LAN. Ma minimalne wymagania odnośnie pamięci. Zapewnia doskonałą ochronę przed błędami transmisji, a także powrót do normalnego stanu w razie ich wystąpienia. Wadą protokołu NetBEUI jest fakt, że nie może być trasowany i niezbyt dobrze działa w sieciach WAN.

image_pdf

Serwer Jakarta-Tomcat 5.0

5/5 - (1 vote)

Serwer Jakarta-Tomcat [8] jest darmowym serwerem WWW z ogólnodostępnym kodem źródłowym. Umożliwia on tworzenie dynamicznych stron w oparciu o standardy Java (serwlety i strony JSP). Pomimo bardzo prężnego rozwoju serwera wciąż brakuje w nim w pełni funkcjonalnego i działającego klastra. Twórcy Tomcata dostrzegli już jak duży wpływ na decyzję o wyborze kontenera aplikacji ma jego skalowalność i odporność na awarie. W wersji 5.0 wprowadzili standard klastra w kodach źródłowych serwera oraz podłączyli bardzo prosty moduł replikujący stan sesji między węzłami klastra. Przykładowa implementacja modułu nie rozwiązuje jednak wielu problemów, które mogą spowodować wadliwe działanie klastra. Stąd zrodziła się idea stworzenia nowego klastra dla serwera Jakarta-Tomcat.

W kolejnych punktach zostanie opisany serwer Jakarta-Tomcat oraz przedstawiona implementacja pierwszego modułu klastra napisana przez Filipa Hanika (por. p. 3.4).

  • Rozwój serwera Jakarta-Tomcat

Pierwszym pomysłodawcą i twórcą Tomcata był James Duncan Davidson (projektant i programista z firmy Sun). Pod koniec lat dziewięćdziesiątych rozpoczął on prace nad wzorcową implementacją kontenera dla serwletów i stron JSP. Po napisaniu sporej części systemu przekazał kody źródłowe organizacji Apache Software Foundation, aby dalszy rozwój serwera odbywał się pod jej patronatem. Organizacja ochrzciła projekt nazwą Jakarta-Tomcat i w 1999 roku opublikowała wersję 3.0, jako wtyczkę do serwera WWW – Apache. Kolejna wersja (4.0) była już w pełni niezależnym serwerem. Pojawiła się obsługa takich standardów jak JDBC, SSL czy Realms.

Serwer Jakarta-Tomcat stał się bardzo popularnym kontenerem serwletów oraz stron JSP, głównie za sprawą ogólnodostępnego kodu źródłowego oraz jego dobrej wydajności. Prosta implementacja ograniczonego podzbioru standardów J2EE [3] umożliwiła stworzenie bardzo szybkiego i „lekkiego” serwera, który mógł być bardzo dobrą alternatywą dla serwerów ze stronami pisanymi w języku PHP. W wielu projektach nie ma potrzeby wykorzystywania skomplikowanych mechanizmów EJB czy JMS, a do pełnej realizacji zadań wystarczą serwlety z możliwością dostępu do bazy danych. Do tego typu projektów idealnie nadawał się serwer Jakarta-Tomcat. Wiele projektów i firm korzysta z Tomcata jako jednego z komponentów, jak choćby Jonas [7] czy Hyperion Analyzer [5].

  • Główne założenia koncepcyjne

Serwer Jakarta-Tomcat jest przede wszystkim kontenerem serwletów (wersja 2.4) i stron JSP (wersja 2.0). Intencją twórców projektu nie było stworzenie pełnej implementacji standardu J2EE [3], ale szybkiego i stabilnego kontenera serwującego strony dynamiczne napisane w języku Java. Ponieważ cały serwer został napisany w języku Java, więc może działać w zasadzie na dowolnej platformie, posiadającej implementację maszyny wirtualnej.

W kolejnych wersjach doszło sporo różnego rodzaju dodatków, ułatwiających pracę programistom i administratorom systemu.

Autoryzacja

Doszło wsparcie dla obiektów autoryzacji (czyli tzw. recilmś). Administrator może stworzyć własne implementacje mechanizmu uwierzytelniania lub skorzystać z trzech gotowych komponentów (w szczególności dostępny jest mechanizm korzystający z bazy danych). Ponadto obsługę żądań można tunelować w protokole HTTPS.

Drzewa JNDI, JDBC, JavaMail

Tomcat udostępnia proste mechanizmy rejestrowania obiektów w drzewie nazw (JNDI). Programista może pobierać w kodzie obiekty, wyszukując je po nazwie. Najczęściej stosuje się ten mechanizm przy korzystaniu z połączeń do bazy danych (obiekty typu javax. sql. DataSourc )    – administrator może modyfikować

parametry połączenia bez potrzeby zaglądania czy modyfikowania kodu aplikacji.

Analogicznie można korzystać z obiektów umożliwiających wysyłanie listów pocztą elektroniczną [4],

Menedżer bezpieczeństwa

Tomcat w wersji 5.0 posiada wsparcie dla menedżera bezpieczeństwa (ang. security manager). Administrator może definiować poziom izolacji przy wykonywaniu kodu napisanego przez programistów. W ten sposób można obronić się przed wykonaniem niebezpiecznego z punktu widzenia serwera kodu w aplikacjach,

np.

System.exit(0);

co powodowałoby koniec pracy całego systemu.

  • Opis implementacji

Jakarta-Tomcat jest w całości napisany w języku Java.

Źródła systemu są podzielone na trzy grupy:

  1. jakarta-tomcat-catalina – część, w której znajduje się faktyczna implementacja kontenera serwletów,
  2. jakarta-tomcat-connectors – implementacja podstawowych typów połączeń stosowanych w systemie (między klientem a serwerem),
  3. jakarta-tomcat-jasper – implementacja kompilatora do stron JSP.

Z punktu widzenia klastrów najciekawsza jest pierwsza część, ponieważ tu znajduje się implementacja jądra systemu, a także moduł do tworzenia klastra.

Tomcat został zaimplementowany w postaci hierarchicznego drzewa obiektów, w którym każdy węzeł może zostać przedefiniowany przez odpowiednie wpisy w plikach konfiguracyjnych. Takie podejście ułatwia modyfikację systemu, co miało duży wpływ na spopularyzowanie serwera.

  • Hierarchia obiektów

Serwer Jakarta-Tomcat zazwyczaj składa się z następującej hierarchii komponentów:

– Korzeniem drzewa jest maszyna (obiekt implementujący interfejs org. apache. catalina. Engin ). Reprezentuje ona niezależny serwer.

  • Maszyna posiada węzły (obiekty implementujące interfejs apache. catalina.Host), które reprezentująwirtualne węzły.
  • Węzeł posiada konteksty (obiekty implementujące interfejs apache. catalina. Contexi), które reprezentują aplikacje internetowe (ang. w eh applications). Aplikacją może być plik z rozszerzeniem .war lub podkatalog w katalogu webapps.
  • Kontekst składa się z serwletów zadeklarowanych przez programistę w pliku opisującym aplikację (plik web.xml) oraz menedżera sesji.
  • Menedżer sesji (obiekt implementujący interfejs apache. catalina. Manage ) zarządza sesjami użytkowników.

Taka hierarchia nie jest obligatoryjna – dla przykładu w sytuacji, gdy instalujemy Tomcat jako wtyczkę w serwerze Apache, drzewo degraduje się do ostatniego poziomu, czyli samych obiektów aplikacji. Wszystkie pozostałe stają się zbędne, gdyż obsługą żądania na wyższym poziomie zajmuje się macierzysty serwer.

Konfiguracja hierarchii obiektów znajduje się w pliku server.xml. Każdy poziom jest definiowany przez odpowiedni znacznik w języku XML. W szczególności, standardowa dystrybucja Tomcata dostarcza trzy różne rodzaje menedżerów sesji, które w zależności od zapotrzebowania można ustawiać w pliku konfiguracyjnym. Ze względu na rolę jaką odgrywa menedżer sesji w klastrze (przechowuje stan sesji), w p. 3.3.1.1 zostaną zaprezentowane dostępne implementacje tego obiektu.

  • Implementacje menedżera sesji

W standardowej dystrybucji serwera Tomcat dostępne są trzy rodzaje menedżera sesji:

  1. standardowy (ang. StandardManager),
  2. plikowy (ang. PersistentManager),
  3. bazodanowy (ang. JDBCManager).

Pierwszy z nich udostępnia podstawową funkcjonalność menedżera sesji – czyli po prostu przechowuje dane w pamięci operacyjnej jednej maszyny.

Drugi pozwala na periodyczne zapisywanie stanu sesji do pliku i odzyskanie tych danych po restarcie maszyny. Jest również przydatny w sytuacji, gdy nie wszystkie sesje mieszczą się w pamięci operacyjnej maszyny. Wtedy menedżer zapewnia nam dodatkowy mechanizm wymiany.

Trzecie rozwiązanie działa analogicznie do drugiego z tą różnicą, że zapis odbywa się w bazie danych, w odpowiednio zdefiniowanym schemacie.

  • Klaster w serwerze Tomcat

W wersji 5.0 została dodana obsługa klastra. Administrator może zdefiniować klasę implementującą klaster (interfejs org. apache. catalina. ciuster) w znaczniku Cluster, w pliku konfiguracyjnym serwera. Klaster jest definiowany na poziomie węzła, dzięki czemu można łatwo rozdzielić aplikacje, które mają działać w trybie rozproszonym od tych działających na pojedynczym serwerze. Klaster jest odpowiedzialny za obsługę wszystkich aplikacji (kontekstów) zainstalowanych w obrębie węzła. Implementacja musi zapewnić mechanizmy replikacji i synchronizacji w obrębie grupy połączonych maszyn oraz może udostępniać metody instalowania i odinstalowywania aplikacji w całym klastrze (metody installContext (string contextPath, URL war), start(String contextPath) oraz stop(String contextPath) ).

Dodatkowo rozszerzono specyfikację pliku opisującego aplikację (web.xml [3]) o znacznik :distributable/>, określający czy aplikacja ma być obsługiwana przez klaster. Jeżeli znacznik zostanie wstawiony, to system podłączy do kontekstu menedżer sesji utworzony przez implementację klastra. W przeciwnym przypadku do kontekstu zostanie podłączony standardowy menedżer.

Każdy obiekt tworzony w drzewie hierarchii Tomcata będzie miał skopiowane wartości parametrów z pliku konfiguracyjnego, poprzez wywołanie metod setxxx (string value) (gdzie XXXjest nazwą parametru oraz jednocześnie nazwą atrybutu w pliku XML). Dodatkowo w specjalny sposób traktowane są obiekty implementujące interfejs klasy org.apache . catalina . Lifecycle.

  • Obiekty klasy Lifecycle

Jeżeli tworzone na poziomie serwera obiekty implementują interfejs org.apache. catalina. Lifecycl , to w momencie startu systemu wywoływana jest dla nich metoda starto. Obiekty tego interfejsu zostaną powiadomione o zamknięciu systemu poprzez wywołanie dla nich metody stop(). Dla przykładu obiekt implementujący interfejs ciuster w module klastrowania jednocześnie implementuje interfejs Lifecycl , aby w metodach start i stój wykonać czynności przygotowujące do pracy oraz czynności kończące pracę klastra.

Każde żądanie obsługiwane przez serwer Tomcat przechodzi przez odpowiedni strumień wywołań procedur. Jest to najbardziej newralgiczne miejsce systemu, ze względu na częstość wywoływania jego kodu.

  • Strumień przetwarzania żądań

Strumień przetwarzania żądań w serwerze Tomcat składa się z następujących wywołań:

  1. Strumień jest inicjowany przez konektor, dostarczający żądanie klienta.
  2. Lokalizowany jest odpowiedni węzeł, a w nim kontekst, do którego odwołuje się żądanie.
  3. Wykonywany jest ciąg wyzwalaczy (tzw. valve), które obudowują wywołanie serwletu (szerzej o wyzwalaczach będzie mowa w p. 3.3.3).
  4. Uruchamiany jest serwlet
  5. Wyzwalacze kończą działanie.
  6. Konektor przekazuje odpowiedź do klienta.
  • Wyzwalacze

Serwer Tomcat umożliwia podpięcie wyzwalaczy obudowujących wykonanie żądania przez serwlet. Zasada działania jest podobna do serwletów filtrujących [3], z tym że wyzwalacze definiuje się na poziomie węzła. Wyzwalacz musi być obiektem klasy implementującej interfejs org. apache . catalina .Valve.

W metodzie invoke (Reąuest, Response, Context) programista może zdefiniować akcje, które będą wykonywane podczas obsługi każdego żądania. Programista decyduje czy żądanie ma być przetwarzane dalej, czy ma zostać przerwane, wywołując lub nie metody invokeNext (Reąuest, Response).

Przykładowo implementacja kontenera autoryzującego opiera się na wyzwalaczach. Przed wywołaniem odpowiedniej strony obsługi żądania sprawdzane jest czy klient posiada wystarczające uprawnienia.

Wyzwalacze umożliwiają tworzenie dzienników serwera lub stosowanie globalnych dla całego serwera filtrów przychodzących żądań.

W szczególności mechanizm ten jest również wykorzystywany przy implementacji klastra (patrz p. 3.4).

3.4 Implementacja klastra w serwerze Tomcat 5.0

W wersji 5.0 klaster dla serwera Tomcat został ustandaryzowany. Dołączono odpowiedni mechanizm podłączania i konfiguracji odrębnych implementacji mechanizmu rozpraszania obliczeń. Osobą odpowiedzialną za rozwój standardu, jak również dołączenie pierwszego w pełni działającego modułu klastra w oficjalnej dystrybucji serwera, jest Filip Hanik – członek zespołu programistów Tomcata.

Wybór Filipa Hanika jako osoby odpowiedzialnej za klaster nie był przypadkowy. Już wcześniej zaimplementował on działającą wtyczkę do Tomcata wersji 4.0 umożliwiającą stworzenie klastra.

  • Klaster dla wersji 4.0

Hanik zaimplementował w pełni funkcjonalny moduł dla Tomcata w wersji 4.0, który po podpięciu do serwera umożliwiał stworzenie klastra. Jego pomysł polegał na podmianie klasy menedżera sesji, w której dodał mechanizm replikowania zmian zachodzących w sesjach użytkowników. Jako warstwy transportującej użył biblioteki JavaGroups [6] dostarczającej mechanizmów ułatwiających programowanie w środowisku rozproszonym. Konfiguracja JavaGroups dopuszcza komunikację w trybie rozgłoszeni owym (ang. multicast), minimalizującą liczbę przesyłanych pakietów (niestety implementacja nie posiada mechanizmów zapewniających niezawodność transmisji, jaka jest w protokole TPC/IP).

Rozwiązanie Filipa Hanika było tylko zewnętrzną nadbudówką do serwera, który sam w sobie nie posiadał jeszcze wtedy żadnego wbudowanego wsparcia dla klastrów. Wymuszało to tworzenie odrębnych klastrów dla każdej aplikacji, co oczywiście powodowało zbędny narzut.

W wersji 5.0 moduł Filipa Hanika został przepisany i dołączony do oficjalnej dystrybucji.

  • Architektura klastra w wersji 5.0

Klaster jest całkowicie zdecentralizowany, każdy kolejny węzeł dołącza się rozsyłając odpowiedni komunikat w trybie rozgłoszeni owym, w lokalnej sieci. Sesje replikowane są do wszystkich węzłów – w szczególności zakłada się, że węzły posiadają identyczny stan każdej sesji. Klaster nie zakłada istnienia zintegrowanego mechanizmu równoważącego obciążenie – każde kolejne żądanie może trafić do dowolnego serwera w klastrze i będzie obsłużone tak, jakby cała interakcja odbywała się na jednej fizycznej maszynie. Takie podejście pozwala na zastosowanie dynamicznego równoważenia obciążenia, z czym wiąże się minimalizacja wariancji czasu obsługi żądania.

Klaster jest zaimplementowany jako moduł (plik z rozszerzeniem .jar), który można opcjonalnie dołączyć do serwera Tomcat.

  • Implementacja klastra w wersji 5.0

Kody źródłowe modułu można podzielić na trzy części:

  1. warstwa transportuj ąca (por. p. 3.4.3.1),
  2. warstwa związana z serwerem Tomcat: menedżer sesji, obiekt sesji (por. p. 3.4.32),
  3. klasy pomocnicze (por. p. 3.4.3.3).
  • Warstwa transportująca

Hanik zrezygnował z korzystania z biblioteki JavaGroups – jak sam twierdzi z powodu braku gwarancji poprawności przesyłanych informacji [11], Zaimplementował warstwę opartą na protokole TCP/IP, służącą do wymiany danych między węzłami klastra. Każdy węzeł trzyma otwarte połączenia do wszystkich pozostałych węzłów. Lista pozostałych węzłów uaktualniana jest na podstawie periodycznie wysyłanych komunikatów o istnieniu węzła (w trybie rozgłoszeniowym). Jeżeli któryś węzeł przestanie wysyłać komunikat, to pozostałe komputery uznają, że nastąpiła w nim awaria i wyrzucą go z listy członków klastra. Niestety pojawia się tu problem rozspójnienia klastra – może zdarzyć się, że część węzłów uzna, że dany komputer przestał działać (np. z powodu nadmiernego obciążenia sieci, bądź procesora), a część pozostawi go na liście aktywnych węzłów. Wtedy podczas replikacji uaktualniona wersja sesji nie dotrze do wszystkich komputerów. Przy założeniu, że cały klaster posiada spójną wersję sesji, może okazać się to dużym zagrożeniem utraty danych. Wystarczy, że kolejne żądanie zostanie przekierowane do węzła nie posiadającego najnowszej wersji sesji.

Każdy węzeł przy starcie otwiera jeden port, na którym będzie oczekiwał na połączenia od pozostałych węzłów. Połączenie nawiązywane jest tylko raz, a przy wysyłaniu danych korzysta się z wcześniej otwartego połączenia. Niestety między każdymi dwoma węzłami otwierane są dwa osobne połączenia, co negatywnie wpływa na wydajność sieci.

Każdy węzeł otwiera jeden port w trybie rozgłoszeniowym, w celu periodycznego wysyłania komunikatu o swoim istnieniu. W komunikacie znajduje się informacja o węźle oraz o porcie, na którym nasłuchuje. Jeżeli odbiorca komunikatu nie posiada otwartego połączenia do ogłaszającego się węzła, to inicjowane jest nowe połączenie.

Wadą wysyłania komunikatów w trybie rozgłoszeniowym jest uniemożliwienie pracy dwóch serwerów Tomcat na jednej fizycznej maszynie, tak aby oba należały do tego samego klastra. Tylko jeden z serwerów będzie w stanie otworzyć konkretny port rozgłoszeniowy.

Wszelkie informacje wymieniane między węzłami klastra przesyłane są za pomocą wiadomości. Komunikacja jest całkowicie bezstanowa – to znaczy po wysłaniu komunikatu nadawca nie oczekuje na odpowiedź, minimalizując ryzyko potencjalnych zakleszczeń (ang. deadlock). Format rozsyłanych wiadomości jest następujący:

  • 7 bajtów – preambuła,
  • 1 bajt – długość wiadomości,
  • dane wiadomości,
  • 7 bajtów-zakończenie wiadomości.

Na dane składa się zserializowana postać klasy 3essionMessage. Klasa zawiera typ wiadomości, identyfikator sesji, dane sesji, identyfikator kontekstu oraz adres węzła wysyłającego wiadomość.

Występują następujące typy wiadomości:

  1. evt_session_created – została utworzona nowa sesja lub istniejąca sesja została zmieniona;
  2. evt_session_expired_wonotify – wygasła sesja, ale nie należy powiadamiać o tym słuchaczy (ang. listner)\
  3. evt_session_expired_wnotify – wygasła sesja i należy powiadomić słuchaczy;
  4. evt_session_accessed – użytkownik odwołał się do sesji, ale jej nie zmieniał;
  5. evt_get_all_s e s s i on s – nowy węzeł pobiera wszystkie do tej pory stworzone sesje;
  6. EVT_ALL_SESsioN_DATi – przesyłane są dane sesji.

W aktualnej wersji rozwiązania zdarzenia evt_session_expired_wonotify oraz evt_session_expired_wnotify są obsługiwane jednakowo, z powiadamianiem słuchaczy.

Zapisywanie i odtwarzanie danych sesji

Dane sesji są serializowane za pomocą wywołania standardowej metody

writeObjectData(ObjectOutputStream stream) z klasy StandardSession, a deserializowane za pomocą metody readObjectData (Objectlnputstream stream). Przy odtwarzaniu danych sesji z tablicy bajtów, tworzony jest strumień wejściowy Replicationstrea , który czyta obiekty korzystając z mechanizmu ładowania klas dostarczanego przez kontekst aplikacji, do której należy sesja. W ten sposób przesyła się obiekty klas stworzonych w ramach danej aplikacji.

  • Warstwa związana z serwerem Tomcat

Moduł Hanika podłączany jest za pomocą klasy simpleTcpCluste , którą definiuje się w znaczniku <ciuster: , w pliku konfiguracyjnym serwera. Klasa implementuje standardowy interfejs org. apache. catalina. ciuste:, dostarczając niezbędnych metod do pracy systemu. W aktualnej wersji implementacja nie wspiera metod instalacji oraz deinstalacji aplikacji w całym klastrze.

Klasa SimpleTcpCluster implementuje interfejs org.apache.catalina. Lifecycle, wykorzystując metody starto oraz stop() do inicjowania swoich struktur danych.

Przy starcie systemu w metodzie start () obiekt tworzy warstwę transportującą, przekazując do niej parametry pobrane z pliku konfiguracyjnego server.xml (dokładniej o parametrach klastra będzie mowa w p. 3.4.4).

W metodzie createManager(string name;, odpowiadającej za tworzenie menedżera sesji, przekazywany jest obiekt instancji klasy SimpleTcpReplicationManagei, będącej nadklasą klasy StandardManager. Klasa przeimplementowuje metodę createSession ( , w której tworzy i przekazuje obiekt typu ReplicatedSession zamiast standardSession. Obiekty replikowanych sesji przechwytują wywołania metod

  • setAttribute(String name, Object value),
  • removeAttribute(String name),
  • expire(boolean notify)

z poziomu aplikacji w celu ustawienia bitu informującego o przeprowadzonych zmianach w sesji. Po zakończeniu przetwarzania żądania system podejmuje decyzję o replikacji na podstawie wartości tego bitu.

Inicjowanie procesu replikacji zachodzi w odpowiednim wyzwalaczu (obiekt klasy ReplicationValve), podpiętym pod wywołania żądań. Po wykonaniu żądania przez aplikację obsługującą dany kontekst, wyzwalacz wywołuje metodę

reąuestcompieted(string sessionid) w obiekcie zarządcy sesji. Metoda przekazuje wiadomość, zawierającą zserializowane dane sesji, która zostaje rozesłana do wszystkich węzłów w klastrze.

Rozsyłanie wiadomości do węzłów może być wykonywane w dwóch trybach: synchronicznym oraz asynchronicznym. Przy pierwszym trybie wątek obsługujący żądanie jest równocześnie odpowiedzialny za rozesłanie pakietów w obrębie klastra. Powoduje to oczywiście wydłużenie czasu obsługi żądania, co może negatywnie wpływać na wydajność systemu.

W trybie asynchronicznym tworzone jest zadanie replikacji sesji, które zostanie wykonane przez jeden ze specjalnych wątków – zazwyczaj już po zakończeniu obsługi żądania. Przy takim podejściu klient nie musi niepotrzebnie czekać na zakończenie zadania replikacji sesji; proces ten wykonywany jest w tle, w czasie przesyłania odpowiedzi do klienta. Niestety implementacja tego mechanizmu ma dużą wadę – nie jest w żaden sposób sprawdzane czy węzeł zdążył rozesłać nową wersję sesji przy kolejnym odwołaniu. Czyli może zdarzyć się następująca sytuacja:

  1. Klient wysyła żądanie do klastra i zostaje przekierowany do maszyny A.
  2. Podczas obsługi żądania sesja zostaje zmodyfikowana (na maszynie A).
  3. Klient otrzymuje odpowiedź i natychmiastowo wysyła kolejne żądanie.
  4. Serwer równoważący obciążenie przekierowuje żądanie do maszyny B.
  5. Kod obsługujący żądanie odwołuje się do sesji, której maszyna A nie zdążyła jeszcze wysłać do maszyny B.

Taka sytuacja może z łatwością zajść przy nadmiernym obciążeniu danego węzła. Z powodu braku mocy obliczeniowej (lub przeciążenia sieci) wysyłanie wiadomości ze zmienionym stanem sesji zaczyna się opóźniać, co może doprowadzić do rozspójnienia klastra i w konsekwencji utraty danych. W praktycznych zastosowaniach tryb asynchroniczny jest rozwiązaniem niedopuszczalnym, właśnie z powodu ryzyka utraty danych.

Kolejnym poważnym problemem implementacji jest brak synchronizacji dostępu do sesji w obrębie klastra. Klaster nie posiada żadnego mechanizmu kontrolującego równoczesny dostęp do tej samej sesji. Jeżeli klient wyśle równolegle dwa żądania, modyfikujące te same zasoby, to doprowadzi to do utraty danych. Co gorsze, może okazać się, że część węzłów będzie posiadała sesję zmienioną przez jedno żądanie, a część przez drugie – jeżeli wiadomości z replikami będą docierały w różnej kolejności.

Taka sytuacja może mieć miejsce w przypadku stron HTML posiadających ramki. Po odświeżeniu strony przeglądarka wysyła równolegle wiele żądań do tych samych zasobów, automatycznie generując równoległe odwołania do tej samej sesji.

  • Klasy pomocnicze

Buforowanie

Został zaimplementowany prosty mechanizm buforowania przesyłanych w sieci informacji. W przypadku pracy w trybie asynchronicznym po przetworzeniu żądania kolejkowane jest zadanie replikacji sesji. Jeżeli zadanie dotyczące tej samej sesji znajdowało się już w kolejce, to zostanie ono nadpisane przez nowszą wersję. W ten sposób eliminuje się niepotrzebny ruch w sieci. Niemniej jednak taka sytuacja ma miejsce tylko przy dużym obciążeniu sieci, kiedy węzeł nie jest w stanie wystarczająco szybko wysłać wiadomości do pozostałych węzłów. Niestety zysk z wykorzystania tego mechanizmu jest iluzoryczny, ponieważ jest mało prawdopodobne, że kolejne żądanie trafi do tego samego węzła. Skoro zakłada się, że mogą występować tak duże opóźnienia w rozsyłaniu replik zmienionej sesji, to klaster bardzo szybko stanie się niespójny i zacznie tracić dane.

Pula wątków

W celu minimalizowania narzutu związanego z tworzeniem wątków została zaimplementowana pula wątków, obsługująca przychodzące wiadomości. Jeżeli wątek nasłuchujący na otwartych połączeniach z pozostałymi węzłami odbierze dane, to budzi jeden z wątków z puli i przydziela mu gniazdo (ang. socket), z którego należy wczytać dane. Wątek wczytuje kolejne wiadomości przetwarzając je synchronicznie. Po zakończeniu zwraca gniazdo i przechodzi w stan oczekiwania.

  • Konfiguracja klastra w wersji 5.0

Klaster włącza się poprzez odkomentowanie sekcji klastra w pliku server.xml:

<Cluster

className=”org.apache.catalina.cluster.tcp.SimpleTcpCluster”

name=”FilipsCluster”

debug=”10″

serviceclass=”org.apache.catalina.cluster.mcast.McastService”

mcastAddr=”228.0.0.4″

mcastPort=”45564″

mcastFrequency=”500″

mcastDropTime=”3000″

tcpThreadCount=”2″

tcpListenAddress=”auto”

tcpListenPort=”4001″

tcpSelectorTimeout=”100″

printToScreen=”false”

expireSessionsOnShutdown=”false”

useDirtyFlag=”true”

replicationMode=”synchronous”

/>

oraz sekcji wyzwalacza wykorzystywanego przez klaster:

<Valve

className=”org.apache.catalina.cluster.tcp.ReplicationValve”

filter=”.*\.gif;.*\.js;.*\.jpg;.*\.htm;.*\.html;.*\.txt;”

/>

Znaczenie odpowiednich atrybutów w sekcji klastra:

  1. name – nazwa klastra (wartość w zasadzie nie wykorzystywana),
  2. debug – poziom szczegółowości generowanych przez implementację zapisów systemowych,
  3. serviceclass – klasa służąca do dostarczania informacji o dostępnych węzłach w klastrze (musi implementować interfejs

org.apache.catalina.cluster.MembershipService),

  1. mcastAddr – adres rozgłoszeniowy, na którym węzły będą powiadamiały się nawzajem o swoim istnieniu,
  2. mcastPort – port używany przy rozgłaszaniu,
  3. mcastFreąuency – częstotliwość rozsyłania informacji o istnieniu węzła (w milisekundach),
  4. mcastDropTime – czas po jakim węzeł zostaje uznany za niesprawny od momentu otrzymania ostatniego pakietu o jego istnieniu,
  5. tcpThreadCount – liczba wątków obsługujących przychodzące wiadomości w klastrze,
  6. tcpListenAddress – adres, na którym węzeł spodziewa się połączeń od pozostałych węzłów (jeżeli ustawiona jest wartość „auto”, to zostanie użyty domyślny adres komputera). Wykorzystuje się go, jeżeli komputer posiada więcej niż jedną kartę sieciową,
  7. tcpListenPort – port, na którym nasłuchuje węzeł,
  8. tcpSelectorTimeout – czas w milisekundach po jakim wątek nasłuchujący na otwartych połączeniach ma sprawdzić czy serwer nie jest zamykany,
  9. printToScreen – czy strumień diagnostyczny ma być przekierowany do standardowego wyj ścia,
  10. expireSessionsOnShutdown – czy podczas zamykania serwera sesje mają zostać zdezaktualizowane,
  11. useDirtyFlag – czy sesja ma być replikowana tylko po wywołaniu metod

setAttribute(..), removeAttribute(..), expire(), invalidate(), setPrincipal(..), setMaxInactiveInterval(..),

  1. replicationMode — przyjmuje wartość synchronous lub asynchronous i

oznacza tryb w jakim sesje będą replikowane.

Wyzwalacz przyjmuje atrybuty:

  1. filter – zawiera wyrażenia regularne oddzielone znakiem średnika, definiujące adresy, podczas przetwarzania których na pewno sesja nie będzie zmieniana. Przy obsłudze tych żądań mechanizm replikacji nie będzie wywoływany, nawet gdy flaga iseDirty będzie ustawiona na fałsz.
  • Zalety rozwiązania

Zaletą przedstawionego rozwiązania jest jego całkowite zdecentralizowanie. Klaster nie posiada wyróżnionego węzła, co zwiększa stabilność i odporność na

awarie konkretnej maszyny. Dołączanie kolejnego węzła wiąże się jedynie z włączeniem go do sieci.

Kolejną cechą wyróżniającą klaster w serwerze Tomcat jest całkowita niezależność od algorytmu równoważenia obciążenia. Rozwiązanie nie wymaga specjalnego oprogramowania interpretującego identyfikatory sesji czy zapamiętującego przypisania węzeł-sesja. Administrator może wykorzystać dowolny mechanizm przekierowywania (programowy czy sprzętowy).

Pełna replikacja (tzn. każdy węzeł replikuje swoje sesje do wszystkich pozostałych węzłów) zapewnia dużą odporność klastra na awarie. Wystarczy, że chociaż jeden z serwerów pozostanie sprawny, aby nie utracić żadnych informacji. Poza tym umożliwia zastosowanie wyrafinowanych algorytmów równoważenia obciążenia, które nie będą ograniczane stałymi przypisaniami sesji do węzłów. W szczególności po dodaniu kolejnego węzła do klastra bardzo szybko możemy przerzucić na niego obciążenie z pozostałych komputerów, a nie tylko przekierowywać nowo przybyłych użytkowników.

Niestety rozwiązanie oprócz zalet ma również wady. Niektóre z nich są na tyle poważne, że uniemożliwiają wykorzystanie serwera w komercyjnych zastosowaniach. Rozwiązanie nie gwarantuje poprawnego działania systemu.

  • Wady rozwiązania

Główną wadą rozwiązania jest brak synchronizacji przy dostępie do sesji oraz brak kontroli nad spójnością klastra. Założenie o pełnej replikacji pociąga za sobą konieczność zapewnienia mechanizmu synchronizacji przy wprowadzaniu zmian czy chociażby odczycie sesji. Jeżeli każdy komputer może uchodzić za serwer macierzysty dowolnej sesji (czyli może obsługiwać wszelkie żądania, jakie docierają do klastra), to należy uniemożliwić wprowadzanie równoczesnych zmian na różnych maszynach. Niestety rozwiązanie Filipa Hanika ignoruje zagrożenie równoległych zmian, tak samo jak ignoruje możliwość opóźnień w replikacji sesji. Jeżeli komputer z powodu nadmiernego obciążenia opóźni rozesłanie nowej wersji sesji, to przy kolejnym żądaniu klienta, przekierowanym do innego węzła, aplikacja będzie korzystała z nieaktualnej wersji danych, nadpisując tym samym poprzednie zmiany. Co gorsze w takiej sytuacji nie ma pewności, która wersja sesji trafi do pozostałych węzłów, ponieważ będzie to zależało tylko od kolejności w jakiej odbiorą one repliki z dwóch różnych źródeł.

Nie mniej ważne jest niebezpieczeństwo rozspójnienia klastra. Zakłada się, że wszystkie węzły w klastrze posiadają identyczną wersję dowolnej sesji, ale co się stanie jeżeli węzeł chwilowo uzna inny komputer za niesprawny i nie prześle mu repliki zmienionej sesji? Taka sytuacja może zaistnieć przy dużym obciążeniu sieci lub maszyny – wystarczy, że na czas nie dotrze do któregoś węzła pakiet rozgłoszeniowy informujący o istnieniu innego węzła. Ten odrzuci go, sądząc, że węzeł przestał działać i nie będzie wysyłał do niego wiadomości. Za chwile pakiet o istnieniu znowu dotrze i węzeł ponownie zostanie włączony do listy aktywnych członków, ale sesja nie zostanie już zaktualizowana.

Korzystanie z trybu rozgłoszeniowego w sieci uniemożliwia zainstalowanie dwóch węzłów klastra na jednej fizycznej maszynie – tylko jeden serwer otworzy port rozgłoszeniowy i będzie mógł z niego korzystać. To może okazać się sporą niedogodnością w niektórych topologiach klastrów. Czasami administrator może chcieć zainstalować więcej niż jeden serwer na fizycznej maszynie zapewniając większą odporność klastra na awarie oprogramowania. Niestety przedstawione rozwiązanie wyklucza taki model.

Architektura rozwiązania powoduje generowanie dużego ruchu w sieci – każdy węzeł rozsyła repliki sesji do wszystkich pozostałych. Przy dużym obciążeniu sieć może okazać się wąskim gardłem całego klastra. Powoduje to spore ograniczenia na liczbę węzłów jednocześnie działających w klastrze. Liczba przesyłanych komunikatów jest kwadratowo zależna od liczby podłączonych węzłów.

Reasumując można stwierdzić, że Hanik przygotował bardzo stabilne podstawy do implementacji modułu klastra dla serwera Jakarta-Tomcat. Wyłonił się standard jaki muszą spełniać moduły, aby mogły poprawnie działać przy każdej kolejnej dystrybucji Tomcata oraz został naszkicowany schemat podłączenia modułu do serwera. Wzorcowa implementacja dostarcza wiele wskazówek, które mogą ułatwić pracę twórcom kolejnych rozwiązań. Niestety nie spełnia ona wymagań stawianych przez komercyjne zastosowania i może służyć jedynie jako przykład.

Celem tej pracy jest stworzenie modułu, bazującego na koncepcji Hanika, wykorzystującego zalety rozwiązania, ale przede wszystkim eliminującego jego wady i zagrożenia. Główną zaletą, która została zidentyfikowana w bazowym rozwiązaniu, jest możliwość dynamicznego sterowania obciążeniem poszczególnych jednostek klastra. Główną wadą jest brak synchronizacji podczas odwoływania się do sesji oraz brak kontroli nad spójnością klastra. Drugim bardzo ważnym celem jest optymalizacja rozwiązania, aby jego wydajność nie stała się powodem rezygnacji ze stosowania klastra w serwerze Jakarta-Tomcat. W rozdziale 4 przedstawię nową, autorską implementację klastra, a w rozdziale 5 opiszę wyniki testów wydajnościowych zaproponowanego rozwiązania.

image_pdf