Scott Tiger Tech Blog

Blog technologiczny firmy Scott Tiger S.A.

Riak

Autor: Piotr Karpiuk o wtorek 3. Marzec 2015

Jedną z najczęściej spotykanych w świecie programistów strukturą danych jest słownik (mapa, tablica asocjacyjna, hasz), w którym każdemu kluczowi przyporządkowujemy jakąś wartość i oferujemy szybki (w czasie niemal stałym) dostęp do wartości poprzez klucz. W dzisiejszym świecie taka struktura może przybierać duże rozmiary — wystarczy zauważyć że całą światową sieć WWW możemy w pewnym uproszczeniu traktować jak wielki rozproszony słownik, w którym łańcuchowi (URL) przyporządkowujemy wartość (dokument HTML, obrazek, film itp.). Po jakie rozwiązanie technologiczne sięgnęlibyśmy, aby we własnym centrum danych lub w chmurze zrobić trwałą kopię całej sieci WWW lub jej części?

NoSQL-owe bazy danych typu key-value, takie jak omawiany dziś Riak wydają się być stworzone właśnie do takich wielkich zadań, a wiele rozwiązań czerpią z klasycznego już produktu Amazon Dynamo.

Szybkie podsumowanie głównych cech Riaka:

  • Najważniejszy nacisk:
    • masowa obsługa wielu żądań dostępu, z małym opóźnieniem (czyli maksymalna dostępność),
    • skalowalność (pozioma, czyli realizowana poprzez dostawianie kolejnych maszyn do klastra),
    • odporność na awarie (brak pojedynczego miejsca narażonego na awarię, duża tolerancja awarii pojedynczych maszyn); nawet w razie rozpadu klastra na kilka części (ang. split brain) dane mogą być wciąż zapisywane w sąsiednich maszynach innych niż pierwotnie dla tych danych przeznaczone, minimalizując negatywny wpływ na funkcjonalność bazy.
  • Z zasady bazy tego rodzaju nie wnikają czym są przechowywane wartości, traktując je jak ciąg bitów — w praktyce najczęściej będą to dokumenty tekstowe (TXT, JSON, XML), binarne (Word, Excel, PDF) i multimedia (filmy, obrazki).
  • Jak możemy przeczytać na Wikipedii, Riak jest obecnie używany w tysiącach firm na świecie, w szczególności w 25% firm z listy Fortune 50, np. AT&T, Comcast, UK National Health Services (NHS), czy The Weather Channel.
  • Dostępna jest wersja otwartoźródłowa.
  • Riak jest zaimplementowany w języku Erlang, a funkcje składowane, funkcje map/reduce i triggery mogą być pisane w Erlangu lub JavaScripcie (baza ma wbudowany silnik JavaScriptu SpiderMonkey, ten sam co w przeglądarce Mozilla Firefox).
  • Najprostszy jest dostęp do bazy poprzez interfejs REST, co pozwala przeglądać rekordy i wykonywać proste zapytania z poziomu dowolnej przeglądarki internetowej — wystarczy odpowiednio skonstruować URLa. Oficjalne sterowniki pozwalają na dostęp do bazy z języków programowania Java, Ruby, Erlang i Python, ale społeczność skupiona wokół Riaka udostępnia sterowniki również dla wielu innych języków.

Tak jak znakomita większość baz NoSQL, Riak nie ma wbudowanej obsługi transakcji (jest to cena jaką trzeba płacić za skalowalność i wydajność) i zapewnia atomowość wyłącznie dla operacji CRUD (Create, Read, Update, Delete) wykonywanych na pojedynczych rekordach (para klucz-wartość).

Ponieważ w praktyce w bazie danych przechowuje się obiekty o różnej strukturze i/lub różnych typów, więc Riak pozwala podzielić przestrzeń kluczy na rozłączne klasy abstrakcji — co jest odpowiednikiem tabel w relacyjnej bazie danych, czy jednopoziomowej struktury katalogów w systemie plików. Takie przestrzenie nazw zwane są kubełkami (ang. buckets), a dalej również żartobliwie wiadrami.

Z uwagi na skalowalność nie ma też języka zapytań ad hoc, który przypominałby coś w rodzaju SQLa w relacyjnych bazach danych. Nie oznacza to jednak, że funkcjonalność odczytu sprowadza się jedynie do wyciągnięcia wartości na podstawie klucza:

  • Z każdym rekordem można związać metadane w postaci nagłówek-wartość, na których można założyć indeks wtórny (ang. secondary index) i wyszukiwać po konkretnej wartości nagłówka.
  • Zawartość bazy można przetwarzać wsadowo za pomocą dobrze znanego w świecie BigData mechanizmu MapReduce — niestety wymaga to pisania własnych funkcji map i reduce które nie są tak intuicyjne jak zapytania SQLa.
  • Riak jest bazą rozszerzalną i jednym z dostępnych modułów jest opcja tworzenia indeksu odwróconego (ang. inverted index) na wartościach typu JSON i XML, i mechanizmu pełnotekstowego przeszukiwania wartości (wykorzystywane jest tu narzędzie Apache Solr).
  • Z każdym rekordem można związać metadane w postaci linków do innych rekordów (klucz docelowego obiektu z etykietką powiązania), a Riak pozwala zapytać o wszystkie rekordy danego typu (lub z daną etykietką) dostępne poprzez linki z danego rekordu.

Odpowiednikami wyzwalaczy z relacyjnych baz danych są tzw. pre- i post commit hooks, definiowane na poziomie wiadra. Pierwsze mogą być pisane w JavaScripcie lub Erlangu i mogą przekształcić dane rekordu przed ostatecznym zapisem lub odrzucić operację, a drugie mogą być pisane tylko w Erlangu i wykonać dodatkową akcję (np. wysłanie maila lub zarejestrowanie czegoś w logu).

Odporność na awarie i związana z tym potrzeba redundancji danych, w połączeniu z wymogami wydajnościowymi rodzą dość ciekawe problemy synchronizacyjne. Ponieważ klienci mogą modyfikować rekordy na dowolnej maszynie klastra, łatwo może dojść do sytuacji gdy dwaj różni klienci zmodyfikują ten sam rekord, zduplikowany na kilku maszynach. Aby rozstrzygać tego rodzaju konflikty nie używa się stempli czasowych (byłoby to trudne do osiągnięcia w systemie rozproszonym, i wymagałoby zarządcy podatnego na awarie), lecz tzw. wektorów wersji (ang. vector clocks). W istocie dla każdej operacji na danych przechowuje się informacje takie jak kto modyfikował dany rekord i w jakiej kolejności. Przypomina to działanie systemów kontroli wersji takich jak Subversion czy Git.

Załóżmy że stopień redundancji każdego rekordu bazy wynosi N=3 (wartość domyślna), co znaczy że każdy rekord istnieje w 3 kopiach na 3 różnych maszynach klastra. Powstaje pytanie jak przebiega operacja modyfikacji rekordu przez klienta — czy kończy się ona sukcesem z chwilą przyjęcia zlecenia przez serwer, utrwalenia wartości na dysku, a może dopiero po rozesłaniu duplikatów do jednego lub dwóch innych serwerów? Otóż można o tym zadecydować na poziomie kubełka, a nawet przy każdej operacji odczytu/zapisu. Zauważmy że jeśli operacja modyfikacji będzie czekać na zapis rekordu przynajmniej na dwóch maszynach (W=2), to również późniejszy odczyt będzie wymagał odczytu i porównania zawartości rekordu z dwóch maszyn (R=2). Z kolei jeśli zażyczymy sobie aby zapis kończył się dopiero po utworzeniu wszystkich trzech kopii (W=3), wówczas mamy szybszy odczyt — wystarczy pobrać rekord z jednej maszyny (R=1). Ogólnie musi być spełniony warunek R+W>N.

W praktyce

Sposób instalacji i uruchamiania Riaka do zabawy opisałem na końcu artykułu. W tym rozdziale garść przykładów interakcji z bazą danych. Szczególnie sympatycznym aspektem pracy z Riakiem jest jego interfejs REST pozwalający na wykonanie w zasadzie wszystkich operacji CRUD (POST=Create, GET=Read, PUT=Update, DELETE=Delete) z wiersza poleceń przy pomocy tradycyjnego polecenia curl i przeglądanie zawartości bazy za pomocą przeglądarki internetowej.

Załóżmy że tworzymy bazę hotelu dla psów. Do wiadra animals (którego nawet nie musimy jawnie tworzyć) wstawiamy rekord JSON reprezentujący psa o kluczu „ace”:

    $ curl -v -X PUT http://localhost:10018/riak/animals/ace \
      -H "Content-type: application/json" \
      -d '{"nickname": "The Wonder Dog", "breed": "German Shepherd"}'

Gdybyśmy nie podali klucza, zostałby wygenerowany automatycznie jakiś losowy klucz w rodzaju 6VZc2o7zKxq2B34kJrm1S0ma3PO, i zwrócony w nagłówku Location odpowiedzi HTTP. Teraz możemy obejrzeć nowy rekord w przeglądarce pod URLem http://localhost:10018/riak/animals/ace.

Listę wszystkich wiader otrzymamy wykonując polecenie

    $ curl -X GET http://localhost:10018/riak?buckets=true

A listę wszystkich kluczy w wiadrze:

    $ curl -X GET http://localhost:10018/riak/animals?keys=true

Z danymi o psie powiążmy jego zdjęcie (zdjęcia przechowujemy w wiadrze photos)

    $ curl -X PUT http://localhost:10018/riak/photos/ace.jpg \
      -H "Content-type: image/jpeg" \
      -H "Link: </riak/animals/ace>; riaktag=\"photo\"" \
      --data-binary @ace_image.jpg

Tutaj zdjęcie zawiera jednokierunkowe dowiązanie (ang. link) do rekordu psa.

Polecenie:

    $ curl -X GET http://localhost:10018/riak/photos/ace.jpg/_,_,_

zwróci listę rekordów, do których prowadzą wszystkie dowiązania z rekordu photos/ace.jpg. Lista przecinkowa oznacza kolejno: nazwę wiadra rekordów docelowych, etykietkę powiązania, flaga keep; znak podkreślenia oznacza: dowolny. Możemy więc zapytać o powiązane rekordy z określonego wiadra, lub rekordy na końcu powiązań o określonej nazwie. Odpowiednio rozszerzając URLa można przechodzić po 2 i więcej powiązaniach, przy czym flaga keep mówi czy w odpowiedzi zwracać rekordy pośrednie.

Usunięcie rekordu to nic prostszego:

    $ curl -X DELETE http://localhost:10018/riak/photos/ace.jpg

Z rekordem można wiązać dowolne metadane w postaci nagłówka HTTP, np. aby znacznikowi Color nadać wartość white można w poleceniu curl dodać opcję -H "X-Riak-Meta-Color: white". Na podobnej zasadzie działa mechanizm indeksu wtórnego. Wymaga on włączenia odpowiedniej opcji w konfiguracji klastra, a potem można korzystać z nagłówka HTTP: -H "x-riak-index-color_bin: black" aby dodać rekord do indeksu „color” z wartością „black”. Teraz można wyszukać wszystkie czarne zwierzęta, wystarczy wykonać:

    $ curl -X GET http://localhost:10018/riak/animals/index/color_bin/black

Wartości N, R i W, o których była mowa w poprzednim rozdziale, można ustawić na poziomie kubełka:

    $ curl -X PUT http://localhost:10018/riak/animals \
      -H "Content-Type: application/json" \
      -d '{"props": {"n_val": 4, "w": 2, "r": 3}}'

ale także przy każdej operacji:

    $ curl -X GET http://localhost:10018/riak/animals/ace?r=3

MapReduce

Wyobraźmy sobie bazę 10.000 pokoi w wielkim hotelu: 100 pięter po 100 pokoi na piętrze. Identyfikatorem pokoju jest jego numer z przedziału 0..9999, a wartością jest obiekt JSON zawierający pola: style oznaczające rodzaj pokoju (jednoosobowy, dwuosobowy, apartament), i capacity (liczba łóżek w pokoju). Chcemy sporządzić zestawienie ile łącznie łóżek jest w pokojach każdego rodzaju, przy czym interesują nas wyłącznie pokoje o numerach < 1000. Czyli wynik byłby postaci:

    [ {"single": 225, "double": 155, "suite": 620 } ]

W tym celu użyjemy mechanizmu MapReduce. Wystarczy oczywiście curl:

    $ curl -X POST -H "content-type: application/json" http://localhost:10018/mapred --data-binary @myconf.js

gdzie lokalny plik myconf.js ma następującą zawartość:

    {
      "timeout": 60000,
      "inputs": {
        "bucket": "rooms",
        "key_filters": [["string_to_int"], ["less_than", 1000]]
      },
      "query":[
        {"map":{
          "language":"javascript",
          "source": "function(v) {
            /* From the Riak object, pull data and parse it as JSON */
            var parsed_data = JSON.parse(v.values[0].data);
            var data = {};
            /* Key capacity number by room style string */
            data[parsed_data.style] = parsed_data.capacity;
            return [data];
          }"
        },
        {"reduce": {
          "language": "javascript",
          "source": "function(v) {
            var totals = {};
            for( var i in v ) {
              for( var style in v[i] ) {
                if( totals[style] ) { totals[style] += v[i][style]; }
                else { totals[style] = v[i][style]; }
              }
            }
            return [totals];
          }"
        }}
      }]
    }

Zauważmy, że można zdefiniować czas (w ms) po którym nastąpi timeout.

Instalacja i uruchamianie

Poniżej polecenia shella jakie wykonałem w Linuksie Ubuntu 14.04, aby zainstalować Riaka i uruchomić klaster trzech serwerów:

    # apt-get install erlang libpam0g-dev
    $ curl -O http://s3.amazonaws.com/downloads.basho.com/riak/2.0/2.0.5/riak-2.0.5.tar.gz
    $ tar zxvf riak-2.0.5.tar.gz
    $ cd riak-2.0.5
    $ make devrel

    $ dev/dev1/bin/riak start
    $ dev/dev2/bin/riak start
    $ dev/dev3/bin/riak start
    $ dev/dev2/bin/riak-admin join -f dev1@127.0.0.1
    $ dev/dev3/bin/riak-admin join -f dev2@127.0.0.1

Od tej chwili można już się komunikować z Riakiem z poziomu przeglądarki, na przykład za pomocą URLa

    http://localhost:10018/stats

lub np. z wiersza poleceń Linuksa:

    $ curl http://localhost:10018/ping

Share and Enjoy:
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Śledzik
  • Blip
  • Blogger.com
  • Gadu-Gadu Live
  • LinkedIn
  • MySpace
  • Wykop

Zostaw komentarz

XHTML: Możesz użyć następujących tagów: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>