Chcąc zapewnić jak najlepszy user-experience na bieżąco inwestujemy w udoskonalanie naszych usług. Rozwijamy je, poprawiamy błędy czy też przenosimy na nowy bardziej wydajny sprzęt. Wiele z naszych usług zależy od zewnętrznych systemów, takich jak bazy danych, czy API udostępnianych przez naszych partnerów. W środowisku, w którym pracują aplikacje zachodzą ciągłe zmiany: migracja na nowsze serwery, migracja do innych lokalizacji, rekonfiguracja oprogramowania. Nie możemy sobie pozwolić, aby taka operacja powodowała przerwę w działaniu systemu.

Tu pojawia się wyzwanie – jak zmienić konfigurację, gdy aplikacja działa w wielu instancjach (czasami licząc w dziesiątkach). Gdy musimy brać pod uwagę to, że systemy pracują pod ciągłym obciążeniem, nie możemy po prostu dokonać zmian i wymusić restart aplikacji. Użytkownik końcowy nie może przecież nagle zostać uraczony komunikatem błędu lub też pustą stroną. Wprowadzanie zmian w konfiguracji każdego serwera, wypinanie z klastra i jego restart i ponowne wpięcie jest mrówczą pracą pochłaniającą dużą ilość czasu. Idealnym rozwiązaniem byłoby mieć możliwość wprowadzenia zmian w konfiguracji we wszystkich działających instancjach z praktycznie zerowym opóźnieniem. W zrobieniu tak fajnej rzeczy przychodzi nam z pomocą Apache Zookeeper.

Większość z was pomyśli sobie teraz: „hola, hola, ale co to jest ten ZooKeeper”. Juz śpieszę z wytłumaczeniem. Zacznijmy od bardzo podstawowych kwestii.

Apache ZooKeeper obecnie jest utrzymywany przez ASF (Apache Software Foundation), wcześniej był rozwijany w ramach Hadoopa – jednak został wyciągnięty z niego i obecnie traktowany jest jako „Top Level Project” fundacji Apache. Serwer ZooKeepera został w całości zaimplementowany w Javie, dodatkowo też zostało udostępnione API w języku C – tak abyśmy mogli zaimplementować obsługę ZooKeepera we własnym oprogramowaniu. ZooKeeper jest intensywnie używany przez Yahoo, Netflixa jak i w innych projektach ASF: HBase, Solr.

Zarządzanie konfiguracją to nie jedyne możliwe zastosowanie ZooKeepera, dzięki jego elastycznemu modelowi danych, który bardzo przypomina system plików stosowany w systemach Unix możemy ZooKeepera użyć do synchronizowania stanu aplikacji, zarządzania aplikacjami czy też obsługi kolejek.

Wybrane możliwości zookeepera

  • hierarchiczna struktura danych – która składa się z tzw. znode’ów (węzłów) powiązanych ze sobą w formie drzewa. Każdy z nich może przechowywać w sobie dane jak i posiadać dzieci, inne znode’y – taki sposób zarządzania danymi bardzo przypomina (o czym juz wcześniej napomniałem) system plików, z tą jednak różnicą, że znode może być naraz plikiem i katalogiem.
  • efemeryczne znode’yznode’y, które istnieją tylko na czas sesji klienta, gdy klient się rozłączy stworzone przez niego efemeryczne znode’y zostają automatycznie usunięte.
  • powiadomienia/watchery – dzięki nim możemy dostać informacje (PUSH) o zmianie danych w znodzie, czy też o dodaniu znode’a (dziecka) do interesującego nas znode’a (rodzica).
  • lider – w klastrze naraz tylko jeden serwer może pełnić role lidera, zaś pozostałe działają jako tzw. follower-y. Jeśli nie ma lidera lub aktualny lider przestaje odpowiadać, pozostałe serwery w próbują wybrać nowego lidera, do wybrania nowego serwera konieczne jest kworum (Quorum) głosów (diagram pochodzi z dokumentacji).

    Lider odpowiedzialny jest za: 
    • wszystkie zapisy w obrębie klastra (follower-y przekierowują do niego zapisy)
    • dbanie o zachowanie kolejności operacji
  • quorum – wybór lidera odbywa się poprzez Quorum, n/2 + 1 serwerów musi wspólnie wybrać nowego lidera – w związku z czym nie jest zalecane aby klaster składał się z parzystej liczby serwerów, gdyż nie zwiększy to naszej odporności na awarię.

Podsumowując ZooKeeper daje nam:

  • prostość użycia (proste API, hierarchiczna struktura danych)
  • replikację danych
  • zachowanie kolejności operacji
  • szybkość działania
  • powiadomienia

Wydajność i zastosowanie w nodejs

Nie jest tajemnicą że w naszej chmurze korzystamy z aplikacji napisanych w oparciu o coraz bardziej popularny node.js. Jak na razie istnieje tylko jedna biblioteka pozwalająca nam skorzystać z dobrodziejstw ZooKeepera (
https://github.com/yfinkelstein/node-zookeeper
).
Biblioteka rozwijana jest od około roku, została napisana częściowo w JS (cześć kliencka) oraz w C (rdzeń), dodatkowo wykorzystuje libev dzięki czemu wszystkie operacje są w pełni asynchroniczne. Biblioteka rozwijana jest jako OpenSource (licencja MIT) – co daje nam możliwość wzięcia udziału w rozwoju biblioteki – zgłaszamy błędy oraz poprawki do kodu.

Wydajność – problem który może położyć na plecy niejedno świetnie zapowiadające się oprogramowanie. Cóż by nam by było z tych wszystkich fajnych funkcjonalności, gdyby okazało się ZooKeeper jest powolny? Na szczęście w ZooKeeperze wydajność w przypadku najbardziej typowych zastosowań stoi na bardzo wysokim poziomie. Z racji architektury (pamiętacie co napisałem o liderze?) w środowisku w którym zdecydowanie góruje ilość zapisów niż odczytów wydajność może być niższa.

Poniżej zamieściłem wykres wydajności (pożyczony z dokumentacji ZooKeepera) przedstawiający wydajność porównując ilość serwerów w klastrze w stosunku do procentowej ilości odczytów/zapisów.

Powyższe testy zostały wykonane na maszynie wyposażonej w 2Ghz dwurdzeniowy procesor Xeon oraz dwa dyski 15000rpm.

Przykładowe zastosowanie oraz jak się do niego zabrać.
Aby w pełni wykorzystać możliwości ZooKeepera konieczne jest zaprojektowanie aplikacji w odpowiedni sposób, konieczne jest asynchroniczne działanie (z racji asynchronicznych powiadomień) oraz możliwość przeładowania konfiguracji w locie, bez konieczności restartu.

Załóżmy że piszemy dowolną usługę która do swojego działania potrzebuje połączenia z jakąś zdalną bazą czy też inną usługą. Jak to zrobić biorąc pod uwagę fakt, że owa usługa będzie działać na 20 serwerach?
Aplikacja podczas startu łączy się z ZooKeeperem, pobiera z ustalonego znode’a swoją konfigurację (przykładowo:/moja-usluga/konfiguracja-bazy-danych), w której znajdują się dane potrzebne do połączenia z bazą danych (adres serwera, użytkownik, hasło etc.) oraz dodatkowo zaczyna nasłuchiwać dalsze na dalsze zmiany tego znode’a. Następnie aplikacja kontynuuje swoje działanie. Gdy konieczna jest migracja używanej przez nas bazy danych na inna maszynę, sytuacja wydaje się problematyczna – jednak dzięki ZooKeeperowi z takiej sytuacji wychodzimy z tarczą. Chcąc przełączyć się na już przemigrowaną bazę danych wystarczy, że uaktualnimy naszego znode’a w którym trzymamy konfigurację bazy (/moja-usluga/konfiguracja-bazy-danych). Do naszej usługi (na wszystkich serwerach) ZooKeeper wyśle powiadomienie o zmianie danych w znodzie, co nasza aplikacja odczyta jako: „tadam, zmieniła się konfiguracja bazy – muszę się do niej połączyć”. Tutaj oczywiście aplikacja weryfikuje co się zmieniło w konfiguracji i odpowiednio reaguje, jeśli konieczne jest połączenie się z bazą na innym hoście aplikacja kończy przetwarzać obecne żądania. Następnie, kolejkuje przychodzące żądania, łączy się do nowej bazy danych (jeśli nie jest to możliwe wraca do starej bazy danych) oraz wznawia działanie.

W wyżej omówiony sposób zapewniliśmy ciągłe działanie aplikacji, nawet w wypadku stosunkowo ciężkiego problemu jakim jest migracja bazy danych – wysoką dostępność, bezproblemową zmianę konfiguracji w mgnieniu oka oraz zabezpieczyliśmy się na wypadek wycieku kodu – ponieważ nie przechowujemy w konfiguracji aplikacji żadnych newralgicznych danych.

Prawda że proste?

Jakub Lekstan
starszy programista