NoSQL. Kompendium wiedzy (, ) -

Poznaj fascynujący świat baz danych NoSQL! Bazy danych NoSQL są coraz popularniejsze. Pozwalają na przechowywanie gigant

964 48 4MB

Polish Pages [163] Year 2014

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

NoSQL. Kompendium wiedzy (, ) -

Table of contents :
Spis treści
Przedmowa
Część I. Zrozumienie
Rozdział 1. Dlaczego NoSQL?
1.1. Wartość baz relacyjnych
1.1.1. Przechowywanie trwałych danych
1.1.2. Współbieżność
1.1.3. Integracja
1.1.4. Ustandaryzowany (przeważnie) model
1.2. Niezgodność impedancji
1.3. Bazy aplikacji i integracji
1.4. Atak klastrów
1.5. Pojawienie się baz NoSQL
1.6. Najważniejsze kwestie
Rozdział 2. Agregacyjne modele danych
2.1. Agregacje
2.1.1. Przykłady relacji i agregacji
2.1.2. Konsekwencje orientacji na agregacje
2.2. Modele klucz – wartość i dokumentów
2.3. Magazyny rodziny kolumn
2.4. Podsumowanie baz zorientowanych na agregacje
2.5. Dalsza lektura
2.6. Najważniejsze kwestie
Rozdział 3. Więcej szczegółów na temat modelów danych
3.1. Relacje
3.2. Bazy grafowe
3.3. Bazy danych bez schematu
3.4. Widoki zmaterializowane
3.5. Modelowanie z myślą o dostępie do danych
3.6. Najważniejsze kwestie
Rozdział 4. Modele dystrybucyjne
4.1. Pojedynczy serwer
4.2. Współdzielenie
4.3. Replikacja master-slave
4.4. Replikacja peer-to-peer
4.5. Łączenie shardingu i replikacji
4.6. Najważniejsze kwestie
Rozdział 5. Spójność
5.1. Spójność aktualizacji
5.2. Spójność odczytu
5.3. Rozluźnianie spójności
5.3.1. Teoria CAP
5.4. Rozluźnianie trwałości
5.5. Kwora
5.6. Dalsza lektura
5.7. Najważniejsze kwestie
Rozdział 6. Stemple wersji
6.1. Transakcje biznesowe i systemowe
6.2. Stemple wersji na wielu serwerach
6.3. Najważniejsze kwestie
Rozdział 7. Map-reduce
7.1. Podstawy map-reduce
7.2. Partycjonowanie i łączenie
7.3. Tworzenie obliczeń map-reduce
7.3.1. Przykład dwuetapowego map-reduce
7.3.2. Inkrementacyjny map-reduce
7.4. Dalsza lektura
7.5. Najważniejsze kwestie
Część II. Implementacja
Rozdział 8. Bazy klucz – wartość
8.1. Czym jest magazyn klucz – wartość?
8.2. Funkcjonalności magazynów klucz – wartość
8.2.1. Spójność
8.2.2. Transakcje
8.2.3. Możliwości zapytań
8.2.4. Struktura danych
8.2.5. Skalowanie
8.3. Pasujące przypadki użycia
8.3.1. Przechowywanie informacji o sesjach
8.3.2. Profile i preferencje użytkownika
8.3.3. Dane koszyka zakupów
8.4. Kiedy nie stosować
8.4.1. Relacje pomiędzy danymi
8.4.2. Transakcje dla wielu operacji
8.4.3. Zapytania na danych
8.4.4. Operacje na zestawach
Rozdział 9. Bazy dokumentów
9.1. Czym jest baza dokumentów?
9.2. Funkcjonalności
9.2.1. Spójność
9.2.2. Transakcje
9.2.3. Dostępność
9.2.4. Możliwości zapytań
9.2.5. Skalowanie
9.3. Pasujące przypadki użycia
9.3.1. Logowanie zdarzeń
9.3.2. Systemy zarządzania zawartością i platformy blogerskie
9.3.3. Analizy stron internetowych lub analizy w czasie rzeczywistym
9.3.4. Aplikacje e-commerce
9.4. Kiedy nie stosować
9.4.1. Złożone transakcje obejmujące różne operacje
9.4.2. Zapytania na zmiennej strukturze agregacji
Rozdział 10. Bazy rodziny kolumn
10.1. Czym jest magazyn rodziny kolumn?
10.2. Funkcjonalności
10.2.1. Spójność
10.2.2. Transakcje
10.2.3. Dostępność
10.2.4. Możliwości zapytań
10.2.5. Skalowanie
10.3. Pasujące przypadki użycia
10.3.1. Logowanie zdarzeń
10.3.2. Systemy zarządzania treścią i platformy blogowe
10.3.3. Liczniki
10.3.4. Wygasające dane
10.4. Kiedy nie stosować
Rozdział 11. Bazy grafowe
11.1. Czym jest baza grafowa?
11.2. Funkcjonalności
11.2.1. Spójność
11.2.2. Transakcje
11.2.3. Dostępność
11.2.4. Możliwości zapytań
11.2.5. Skalowanie
11.3. Pasujące przypadki użycia
11.3.1. Dane połączone
11.3.2. Wytyczanie trasy, wysyłka i usługi oparte o położenie
11.3.3. Silniki rekomendacji
11.4. Kiedy nie stosować
Rozdział 12. Zmiany schematów
12.1. Zmiany schematu
12.2. Zmiany schematu w bazach transakcyjnych
12.2.1. Zmiany w projektach budowanych od podstaw
12.2.2. Zmiany w projektach zastanych
12.3. Zmiany schematu w magazynach danych NoSQL
12.3.1. Zmiany inkrementacyjne
12.3.2. Zmiany w bazach grafowych
12.3.3. Zmiana struktury agregacji
12.4. Dalsza lektura
12.5. Najważniejsze kwestie
Rozdział 13. Poliglotyczne przechowywanie danych
13.1. Odmienne potrzeby przechowywania danych
13.2. Poliglotyczne wykorzystanie magazynu danych
13.3. Usługi a bezpośrednie przechowywanie danych
13.4. Rozszerzanie dla polepszenia funkcjonalności
13.5. Wybór odpowiedniej technologii
13.6. Problemy korporacyjne przy poliglotycznym przechowywaniu danych
13.7. Złożoność wdrożenia
13.8. Najważniejsze kwestie
Rozdział 14. Poza NoSQL
14.1. Systemy plików
14.2. Event sourcing
14.3. Obraz w pamięci
14.4. Kontrola wersji
14.5. Bazy XML
14.6. Bazy obiektowe
14.7. Najważniejsze kwestie
Rozdział 15. Wybór bazy danych
15.1. Wydajność programistów
15.2. Wydajność dostępu do danych
15.3. Trzymanie się standardów
15.4. Odwoływanie przypuszczeń
15.5. Najważniejsze kwestie
15.6. Końcowe przemyślenia
Bibliografia
Skorowidz

Citation preview

Spis treści

Przedmowa . ........................................................................................................................... 11

Część I. Zrozumienie . .............................................................................17 Rozdział 1. Dlaczego NoSQL? . .............................................................................................. 19 1.1. Wartość baz relacyjnych ................................................................................................19 1.1.1. Przechowywanie trwałych danych ......................................................................19 1.1.2. Współbieżność .......................................................................................................20 1.1.3. Integracja . ..............................................................................................................20 1.1.4. Ustandaryzowany (przeważnie) model ..............................................................20 1.2. Niezgodność impedancji ................................................................................................21 1.3. Bazy aplikacji i integracji . .............................................................................................22 1.4. Atak klastrów . .................................................................................................................23 1.5. Pojawienie się baz NoSQL . ...........................................................................................25 1.6. Najważniejsze kwestie . ..................................................................................................27 Rozdział 2. Agregacyjne modele danych . ............................................................................. 29 2.1. Agregacje . ........................................................................................................................29 2.1.1. Przykłady relacji i agregacji . ...............................................................................30 2.1.2. Konsekwencje orientacji na agregacje . ..............................................................34 2.2. Modele klucz – wartość i dokumentów . .....................................................................35 2.3. Magazyny rodziny kolumn ............................................................................................36 2.4. Podsumowanie baz zorientowanych na agregacje .....................................................38 2.5. Dalsza lektura . ................................................................................................................39 2.6. Najważniejsze kwestie . ..................................................................................................39 Rozdział 3. Więcej szczegółów na temat modelów danych . ................................................. 41 3.1. Relacje . .............................................................................................................................41 3.2. Bazy grafowe . ..................................................................................................................42 3.3. Bazy danych bez schematu . ..........................................................................................44

5

6

SPIS TREŚCI

3.4. Widoki zmaterializowane . ............................................................................................46 3.5. Modelowanie z myślą o dostępie do danych ...............................................................47 3.6. Najważniejsze kwestie . ..................................................................................................51 Rozdział 4. Modele dystrybucyjne . ....................................................................................... 53 4.1. Pojedynczy serwer . ........................................................................................................53 4.2. Współdzielenie . ..............................................................................................................54 4.3. Replikacja master-slave . ................................................................................................56 4.4. Replikacja peer-to-peer . ................................................................................................57 4.5. Łączenie shardingu i replikacji . ....................................................................................59 4.6. Najważniejsze kwestie . ..................................................................................................59 Rozdział 5. Spójność . ............................................................................................................. 61 5.1. Spójność aktualizacji . ....................................................................................................61 5.2. Spójność odczytu . ..........................................................................................................63 5.3. Rozluźnianie spójności . .................................................................................................66 5.3.1. Teoria CAP . ...........................................................................................................66 5.4. Rozluźnianie trwałości . .................................................................................................69 5.5. Kwora . ..............................................................................................................................70 5.6. Dalsza lektura . ................................................................................................................72 5.7. Najważniejsze kwestie . ..................................................................................................72 Rozdział 6. Stemple wersji . .................................................................................................... 73 6.1. Transakcje biznesowe i systemowe ..............................................................................73 6.2. Stemple wersji na wielu serwerach . .............................................................................75 6.3. Najważniejsze kwestie . ..................................................................................................76 Rozdział 7. Map-reduce . ........................................................................................................ 77 7.1. Podstawy map-reduce ....................................................................................................78 7.2. Partycjonowanie i łączenie . ..........................................................................................79 7.3. Tworzenie obliczeń map-reduce ...................................................................................81 7.3.1. Przykład dwuetapowego map-reduce ................................................................82 7.3.2. Inkrementacyjny map-reduce . ...........................................................................85 7.4. Dalsza lektura . ................................................................................................................86 7.5. Najważniejsze kwestie . ..................................................................................................86

Część II. Implementacja . ........................................................................89 Rozdział 8. Bazy klucz – wartość . .......................................................................................... 91 8.1. Czym jest magazyn klucz – wartość? . .........................................................................91 8.2. Funkcjonalności magazynów klucz – wartość ............................................................93 8.2.1. Spójność . ................................................................................................................93 8.2.2. Transakcje . ............................................................................................................94 8.2.3. Możliwości zapytań . .............................................................................................94 8.2.4. Struktura danych . .................................................................................................95 8.2.5. Skalowanie . ............................................................................................................96

SPIS TREŚCI

8.3. Pasujące przypadki użycia .............................................................................................96 8.3.1. Przechowywanie informacji o sesjach . ..............................................................96 8.3.2. Profile i preferencje użytkownika . .....................................................................97 8.3.3. Dane koszyka zakupów . ......................................................................................97 8.4. Kiedy nie stosować . ........................................................................................................97 8.4.1. Relacje pomiędzy danymi . ..................................................................................97 8.4.2. Transakcje dla wielu operacji . ............................................................................97 8.4.3. Zapytania na danych . ...........................................................................................97 8.4.4. Operacje na zestawach . ........................................................................................98 Rozdział 9. Bazy dokumentów . ............................................................................................. 99 9.1. Czym jest baza dokumentów? . .....................................................................................99 9.2. Funkcjonalności . ......................................................................................................... 100 9.2.1. Spójność . ............................................................................................................. 101 9.2.2. Transakcje . ......................................................................................................... 102 9.2.3. Dostępność . ........................................................................................................ 102 9.2.4. Możliwości zapytań ............................................................................................ 103 9.2.5. Skalowanie . ......................................................................................................... 105 9.3. Pasujące przypadki użycia . ........................................................................................ 107 9.3.1. Logowanie zdarzeń . .......................................................................................... 107 9.3.2. Systemy zarządzania zawartością i platformy blogerskie ............................. 107 9.3.3. Analizy stron internetowych lub analizy w czasie rzeczywistym ................ 107 9.3.4. Aplikacje e-commerce . ..................................................................................... 107 9.4. Kiedy nie stosować . ..................................................................................................... 107 9.4.1. Złożone transakcje obejmujące różne operacje ............................................. 107 9.4.2. Zapytania na zmiennej strukturze agregacji . ................................................. 108 Rozdział 10. Bazy rodziny kolumn . .................................................................................... 109 10.1. Czym jest magazyn rodziny kolumn? . ................................................................... 109 10.2. Funkcjonalności . ....................................................................................................... 110 10.2.1. Spójność . ........................................................................................................... 112 10.2.2. Transakcje . ....................................................................................................... 113 10.2.3. Dostępność . ...................................................................................................... 113 10.2.4. Możliwości zapytań .......................................................................................... 114 10.2.5. Skalowanie . ....................................................................................................... 116 10.3. Pasujące przypadki użycia . ...................................................................................... 116 10.3.1. Logowanie zdarzeń . ........................................................................................ 116 10.3.2. Systemy zarządzania treścią i platformy blogowe ....................................... 117 10.3.3. Liczniki . ............................................................................................................ 117 10.3.4. Wygasające dane . ............................................................................................ 117 10.4. Kiedy nie stosować . ................................................................................................... 118

7

8

SPIS TREŚCI

Rozdział 11. Bazy grafowe . .................................................................................................. 119 11.1. Czym jest baza grafowa? . ......................................................................................... 119 11.2. Funkcjonalności . ....................................................................................................... 120 11.2.1. Spójność . ........................................................................................................... 122 11.2.2. Transakcje . ....................................................................................................... 122 11.2.3. Dostępność . ...................................................................................................... 123 11.2.4. Możliwości zapytań .......................................................................................... 123 11.2.5. Skalowanie . ....................................................................................................... 126 11.3. Pasujące przypadki użycia . ...................................................................................... 127 11.3.1. Dane połączone . .............................................................................................. 127 11.3.2. Wytyczanie trasy, wysyłka i usługi oparte o położenie . ............................. 127 11.3.3. Silniki rekomendacji . ...................................................................................... 128 11.4. Kiedy nie stosować . ................................................................................................... 128 Rozdział 12. Zmiany schematów . ........................................................................................ 129 12.1. Zmiany schematu . ..................................................................................................... 129 12.2. Zmiany schematu w bazach transakcyjnych . ........................................................ 129 12.2.1. Zmiany w projektach budowanych od podstaw . ........................................ 130 12.2.2. Zmiany w projektach zastanych ..................................................................... 132 12.3. Zmiany schematu w magazynach danych NoSQL . ............................................. 133 12.3.1. Zmiany inkrementacyjne . .............................................................................. 135 12.3.2. Zmiany w bazach grafowych .......................................................................... 136 12.3.3. Zmiana struktury agregacji ............................................................................. 137 12.4. Dalsza lektura . ........................................................................................................... 137 12.5. Najważniejsze kwestie . ............................................................................................. 137 Rozdział 13. Poliglotyczne przechowywanie danych . ........................................................ 139 13.1. Odmienne potrzeby przechowywania danych ...................................................... 139 13.2. Poliglotyczne wykorzystanie magazynu danych . ................................................. 140 13.3. Usługi a bezpośrednie przechowywanie danych . ................................................. 142 13.4. Rozszerzanie dla polepszenia funkcjonalności . .................................................... 142 13.5. Wybór odpowiedniej technologii . .......................................................................... 143 13.6. Problemy korporacyjne przy poliglotycznym przechowywaniu danych ........... 144 13.7. Złożoność wdrożenia . .............................................................................................. 145 13.8. Najważniejsze kwestie . ............................................................................................. 145 Rozdział 14. Poza NoSQL . ................................................................................................... 147 14.1. Systemy plików . ......................................................................................................... 147 14.2. Event sourcing . .......................................................................................................... 148 14.3. Obraz w pamięci . ...................................................................................................... 150 14.4. Kontrola wersji . ......................................................................................................... 151 14.5. Bazy XML . .................................................................................................................. 151 14.6. Bazy obiektowe . ......................................................................................................... 152 14.7. Najważniejsze kwestie . ............................................................................................. 152

SPIS TREŚCI

Rozdział 15. Wybór bazy danych . ....................................................................................... 153 15.1. Wydajność programistów ......................................................................................... 153 15.2. Wydajność dostępu do danych ................................................................................ 155 15.3. Trzymanie się standardów . ...................................................................................... 156 15.4. Odwoływanie przypuszczeń . ................................................................................... 156 15.5. Najważniejsze kwestie . ............................................................................................. 157 15.6. Końcowe przemyślenia . ........................................................................................... 157 Bibliografia . ......................................................................................................................... 159 Skorowidz . ........................................................................................................................... 163

9

10

SPIS TREŚCI

Przedmowa

W świecie profesjonalnego oprogramowania funkcjonujemy już prawie dwadzieścia lat. Widzieliśmy wiele zmian w językach, architekturach, platformach i procesach. Przez cały ten czas jedna rzecz pozostawała jednak niezmienna — wykorzystanie baz relacyjnych do przechowywania danych. Powstawały konkurencyjne technologie, niektóre z nich odnosiły nawet pewne sukcesy w niszowych rozwiązaniach, jednak ciągle jedynym pytaniem zadawanym sobie przez architektów było, którą bazę relacyjną wybrać. Stabilność rządów baz relacyjnych posiada wiele wartościowych aspektów. Dane w organizacji mają znacznie dłuższy czas życia niż same aplikacje (przynajmniej tak ludzie mówią — widzieliśmy jednak kilka bardzo starych programów). Warto mieć stabilny magazyn danych, który jest zrozumiały i dostępny z poziomu wielu platform programistycznych. Teraz jednak pojawił się nowy rywal pod nazwą NoSQL. Narodził się z potrzeby obsługiwania większych wolumenów danych, która wymusiła przejście na model budowania platform na klastrach mniej wydajnych serwerów. Ta zmiana wzbudziła także wątpliwości odnośnie łatwości tworzenia kodu dobrze współpracującego z bazami relacyjnymi. Termin „NoSQL” jest bardzo źle zdefiniowany. Jest wykorzystywany w odniesieniu do niedawno powstałych baz nierelacyjnych, takich jak Cassandra, Mongo, Neo4J i Riak. Te bazy pozwalają na przechowywanie danych bez schematu, mogą działać w klastrach i pozwalają wymienić tradycyjną spójność na inne wartościowe właściwości. Zwolennicy NoSQL twierdzą, że są w stanie budować bardziej wydajne systemy, które będą łatwiej się skalować i których programowanie będzie łatwiejsze. Czy jest to pierwszy zwiastun zmierzchu baz relacyjnych, czy tylko kolejny pretendent do tronu? Nasza odpowiedź brzmi „żaden z nich”. Bazy relacyjne to potężne narzędzie i przewidujemy, że będzie wykorzystywane jeszcze przez wiele dekad, jednak widzimy także głębokie zmiany zwiastujące, że bazy relacyjne nie będą jedynymi bazami wykorzystywanymi w aplikacjach. Uważamy, że wkraczamy w świat poliglotycznego przechowywania danych, gdzie przedsiębiorstwa, a nawet pojedyncze aplikacje, wykorzystują do zarządzania danymi wiele technologii. W rezultacie architekci będą musieli znać te technologie i być w stanie ocenić, która z nich jest odpowiednia dla danego zadania. Gdybyśmy tak nie uważali, nie poświęcilibyśmy czasu i wysiłku koniecznego do napisania tej książki.

11

12

PRZEDMOWA

Książka ma za zadanie dać Ci dość informacji, abyś mógł odpowiedzieć na pytanie, czy zastosowanie bazy NoSQL jest warte rozważenia w Twoich przyszłych projektach. Każdy projekt jest inny i nie ma możliwości stworzenia prostego drzewa decyzyjnego pozwalającego na wybranie odpowiedniego magazynu. Próbujemy natomiast dać Ci wystarczająco dużo wiedzy, abyś mógł podjąć decyzję samodzielnie, bez konieczności przeszukiwania całej sieci. Specjalnie wybraliśmy formę niewielkiej książki, abyś mógł szybko przyswoić podstawowe informacje. Książka nie odpowie na wszystkie Twoje pytania, ale powinna zawęzić spektrum opcji, które musisz rozważyć, aby zrozumieć, jakie pytania musisz zadać.

Dlaczego bazy NoSQL są interesujące? Widzimy dwa podstawowe powody, dla których ludzie rozważają wykorzystanie baz NoSQL: ■ Produktywność tworzenia aplikacji. Wiele wysiłku przy tworzeniu aplikacji poświęcane jest na mapowanie danych pomiędzy strukturami przechowywanymi w pamięci programu a relacyjnymi bazami danych. Baza NoSQL może udostępniać model lepiej pasujący do potrzeb aplikacji, a tym samym uprościć interakcję i zmniejszyć ilość potrzebnego kodu. ■ Duże wolumeny danych. Organizacje przechwytują coraz więcej danych, które wymagają szybkiego przetwarzania. Robienie tego przy pomocy baz relacyjnych staje się kosztowne lub nawet niemożliwe. Głównym powodem jest to, że baza relacyjna jest zaprojektowana do pracy na jednej maszynie, a przeważnie bardziej ekonomiczne jest uruchomienie kilku mniejszych i tańszych maszyn. Wiele baz NoSQL jest zaprojektowanych do pracy w klastrach, są więc lepszym rozwiązaniem w przypadku dużych zbiorów danych.

Zawartość tej książki Podzieliliśmy książkę na dwie części. Pierwsza skupia się na głównych koncepcjach, które naszym zdaniem powinieneś znać, aby odpowiedzieć sobie na pytanie, czy bazy NoSQL będą miały zastosowanie w przypadku Twojego projektu i jak się od siebie różnią. W drugiej części skupiamy się bardziej na implementacji systemów z wykorzystaniem baz NoSQL. W rozdziale 1. rozpoczynamy od wyjaśnienia, dlaczego bazy NoSQL rozwijają się tak dynamicznie — konieczność przetwarzania większych wolumenów danych spowodowała przejście w dużych systemach od skalowania w górę do skalowania w bok. To tłumaczy ważną cechę modelu większości baz NoSQL — przechowywanie bogatej struktury blisko spokrewnionych danych, które traktowane są jako jednostki. W tej książce taką strukturę nazywamy agregacją. Rozdział 2. opisuje, jak agregacje objawiają się w trzech głównych modelach baz NoSQL: klucz – wartość (2.2, „Modele klucz – wartość i dokumentów”), dokumentów (2.2, „Modele klucz – wartość i dokumentów”) i rodziny kolumn (2.3, „Magazyny rodziny kolumn”). Agregacje są naturalną jednostką interakcji dla wielu aplikacji i umożliwiają zarówno poprawienie wydajności programowania, jak i działanie w klastrach. Rozdział 3. zwraca uwagę na minusy agregacji — trudności w obsłudze relacji (3.1, „Relacje”) pomiędzy encjami w róż-

ZAWARTOŚĆ TEJ KSIĄŻKI

nych agregacjach. To w naturalny sposób prowadzi do baz grafowych (3.2, „Bazy grafowe”) i modelu NoSQL, który nie pasuje do obozu baz zorientowanych na agregacje. Przyglądamy się także ogólnym charakterystykom baz NoSQL działających bez schematu (3.3, „Bazy danych bez schematu”) — bazy te charakteryzują się większą elastycznością, chociaż nie tak dużą, jak może się na początku wydawać. Po omówieniu aspektów modeli danych NoSQL przechodzimy do dystrybucji: rozdział 4. opisuje, jak bazy danych dystrybuują dane podczas pracy w klastrach. W kontekście pracy w klastrach rozróżniamy sharding (4.2, „Współdzielenie”) i replikację; replikacja przyjmuje dwie formy: master-slave (4.3, „Replikacja master-slave”) i peer-to-peer (4.4, „Replikacja peer-to-peer”). Mając omówione modele dystrybucji, możemy skierować naszą uwagę na problem spójności. Bazy NoSQL udostępniają bardziej zróżnicowany wachlarz opcji spójności niż bazy relacyjne — co jest konsekwencją zorientowania na klastry. Rozdział 5. opisuje, jak spójność zmienia się dla aktualizacji danych (5.1, „Spójność aktualizacji”) i odczytu (5.2, „Spójność odczytu”), jaka jest rola kworów (5.5, „Kwora”) i jak można zamienić część trwałości danych na inne cechy (5.4, „Rozluźnianie trwałości”). Jeżeli słyszałeś cokolwiek o NoSQL, prawie na pewno słyszałeś o teorii CAP; punkt 5.3.1, „Teoria CAP”, tłumaczy, czym ona jest i co oznacza. Podczas gdy te rozdziały koncentrują się na zasadach rządzących dystrybucją danych i utrzymaniem spójności, następne dwa skupiają się na kilku ważnych narzędziach, które umożliwiają takie działanie. Rozdział 6. opisuje stemple wersji, które mają za zadanie śledzić zmiany i wykrywać niespójności. Rozdział 7. opisuje algorytm map-reduce, który pozwala organizować równoległe przetwarzanie danych, dobrze pasujące do klastrów, a co za tym idzie do systemów NoSQL. Kiedy już uporamy się z koncepcjami, przechodzimy do problemów z implementacją, omawiając przykładowe bazy danych w każdej z czterech głównych kategorii: rozdział 8. wykorzystuje bazę Riak jako przykład baz klucz – wartość, rozdział 9. jako przykład baz dokumentów podaje bazę MongoDB, rozdział 10. na podstawie Cassandry omawia bazy rodziny kolumn, a rozdział 11. koncentruje się na Neo4J jako przykładzie baz grafowych. Musimy podkreślić, że nie jest to kompletne omówienie — jest zbyt dużo informacji, które musielibyśmy uwzględnić. Nasz wybór przykładów nie ma też na celu sugerowania konkretnych rozwiązań. Chcemy, abyś poczuł, jak zróżnicowany jest wybór baz NoSQL i jak wcześniej opisane koncepcje wykorzystywane są przez poszczególne bazy. Zobaczysz, jaki kod będziesz musiał napisać, aby się z nimi komunikować, i jaki sposób myślenia będziesz musiał obrać. Często mówi się o bazach NoSQL, że skoro nie posiadają schematu, można łatwo zmienić strukturę danych w trakcie czasu życia aplikacji. Nie zgadzamy się z tym stwierdzeniem — dane przechowywane w bazie bez schematu wciąż mają schemat w aplikacji, który wymaga uwagi podczas zmiany. W rozdziale 12. objaśniamy, jak wykonać migrację danych zarówno dla systemów ze schematem, jak i dla tych, które go nie posiadają. Te wszystkie informacje powinny Ci uświadomić, że NoSQL to nie jest jedna rzecz, ani też coś, co zastąpi bazy relacyjne. Rozdział 13. zajmuje się światem poliglotycznego przechowywania danych, w którym koegzystują różne systemy przechowywania danych, nawet w obrębie jednej aplikacji. Rozdział 14. poszerza nasze horyzonty poza tę książkę, opisując inne technologie, których nie omawialiśmy, a które również mogą stać się częścią świata poliglotycznego przechowywania danych.

13

14

PRZEDMOWA

Mając tę całą wiedzę, jesteś w punkcie, w którym możesz rozważyć, z jakiej technologii przechowywania danych chcesz skorzystać, więc ostatni rozdział („Wybór bazy danych”, rozdział 15.) zawiera kilka rad na temat podejmowania takich wyborów. Naszym zdaniem istnieją dwa główne czynniki — znalezienie produktywnego modelu programistycznego i zapewnienie wystarczająco wydajnego dostępu do danych. Ponieważ bazy NoSQL są młodą dziedziną, nie mamy niestety dobrze zdefiniowanej procedury podejmowania wyboru i będziesz musiał testować swoje opcje w kontekście swoich potrzeb. Jest to krótki przegląd zawartości książki — specjalnie ograniczyliśmy jej rozmiary. Wybraliśmy informacje, które naszym zdaniem są najważniejsze — tak, abyś Ty nie musiał. Jeżeli zamierzasz dokładnie zapoznać się z tymi technologiami, będziesz musiał wykroczyć poza informacje, które tu podajemy, ale mamy nadzieję, że ta książka będzie dobrym początkiem tej drogi. Chcemy też zaznaczyć, że jest to bardzo dynamiczna branża przemysłu informatycznego. Ważne aspekty tych baz zmieniają się co roku — nowe funkcjonalności, nowe bazy. Staraliśmy skupiać się na koncepcjach, które naszym zdaniem warto zrozumieć, nawet jeżeli sama technologia ulegnie zmianie. Jesteśmy przekonani, że większość z tego, o czym napisaliśmy, będzie jeszcze długo prawdą, ale przy tym absolutnie pewni, że nie wszystko.

Dla kogo jest ta książka W przypadku tej książki naszą grupą docelową są osoby rozważające wykorzystanie baz NoSQL — czy to na potrzeby nowego projektu, czy dlatego, że napotkali na bariery skłaniające do zmiany bazy w obecnym projekcie. Naszym celem jest dać Ci dość informacji, abyś mógł określić, czy bazy NoSQL są odpowiedzią na Twoje potrzeby, a jeżeli tak, jakie narzędzie powinieneś poznać dokładniej. Jako naszego czytelnika wyobrażamy sobie architekta lub kierownika technicznego, ale jesteśmy też zdania, że książka może być wartościowa dla osób zajmujących się zarządzaniem oprogramowaniem, którzy chcą zapoznać się z nową technologią. Uważamy też, że jeżeli jesteś programistą zainteresowanym tą nową technologią, ta książka będzie dla Ciebie dobrym punktem wyjścia. Nie wdajemy się w szczegóły programowania i uruchamiania konkretnych baz danych — pozostawiamy to bardziej wyspecjalizowanym książkom. Staraliśmy się też znacznie ograniczyć liczbę stron, aby książka była tylko zwięzłym wprowadzeniem. Jest to książka, którą powinieneś móc przeczytać podczas lotu samolotem: nie odpowie na wszystkie Twoje pytania, ale powinna dać Ci porządny zestaw pytań do zadania. Jeżeli już zagłębiłeś się w świat NoSQL, ta książka prawdopodobnie nie da Ci żadnej nowej wiedzy. Może natomiast być pomocna w przekazaniu Twojej wiedzy innym. Objaśnienie problemów związanych z bazami NoSQL jest ważne — w szczególności jeżeli zamierzasz przekonywać do ich stosowania innych.

JAKIE SĄ BAZY DANYCH

Jakie są bazy danych W tej książce obraliśmy powszechną strategię kategoryzowania baz danych ze względu na ich model danych. Poniżej znajduje się tabela z czterema modelami danych i pasującymi do nich bazami danych. Nie jest to pełna lista — zawiera tylko bardziej znane bazy, z którymi się spotkaliśmy. Kiedy pisaliśmy tę książkę, pełniejsze listy baz można było znaleźć pod adresami http://nosql-database.org i http://nosql.mypopescu.com/kb/nosql. W każdej kategorii wyróżniliśmy bazę, którą wykorzystaliśmy jako przykład w danym rozdziale. Naszym celem jest wybranie reprezentatywnego narzędzia z każdej kategorii baz. Kiedy omawiamy konkretne przykłady, większość dyskusji powinna się odnosić do całej kategorii, mimo że te produkty są unikalne i nie powinny być generalizowane. Wybierzemy jedną bazę z każdej kategorii, ale jeżeli będzie to konieczne, wspomnimy też o innych. Model danych

Przykładowe bazy danych

Klucz – wartość (2.2, „Modele klucz – wartość i dokumentów”)

BerkeleyDB LevelDB Memcached Project Voldemort Redis Riak

Dokument (2.2, „Modele klucz – wartość i dokumentów”)

CouchDB MongoDB OrientDB RavenDB Terrastore

Rodzina kolumn (2.3, „Magazyny rodziny kolumn”)

Amazon SimpleDB Cassandra HBase Hypertable

Graf (3.2, „Bazy grafowe”)

FlockDB HyperGraphDB Infinite Graph Neo4J OrientDB

Podział na podstawie modelu danych jest użyteczny, ale niepełny. Linia podziału pomiędzy różnymi modelami danych, np. pomiędzy modelem klucz – wartość i baz dokumentów, jest często bardzo wąska (2.3, „Magazyny rodziny kolumn”). Wiele baz danych nie pasuje jednoznacznie do żadnej z kategorii; np. OrientDB określa się jako bazę dokumentów i bazę grafową.

15

16

PRZEDMOWA

Podziękowania Na początku musimy podziękować naszym kolegom z ThoughtWorks, z których wielu wykorzystywało NoSQL w projektach budowanych w ciągu ostatnich kilku lat. Ich doświadczenie stało się głównym źródłem naszej motywacji do pisania tej książki i praktycznych informacji dotyczących tej technologii. Pozytywne doświadczenia, jakie do tej pory mieliśmy z bazami NoSQL, są podstawą naszego przekonania, że ta technologia jest ważna i wprowadza znaczące zmiany w sposobie przechowywania danych. Chcielibyśmy także podziękować różnym grupom, które urządzały publiczne przemówienia, publikowały artykuły i tworzyły blogi dotyczące NoSQL. Wiele odkryć z dziedziny tworzenia oprogramowania pozostaje w ukryciu, jeżeli programiści nie mówią o nich publicznie. Szczególne podziękowania należą się firmom Google i Amazon, których artykuły o Bigtable i Dynamo miały ogromny wpływ na powstanie ruchu NoSQL. Dziękujemy także firmom, które sponsorowały i wspierały ruch NoSQL. Interesującą zmianą w odniesieniu do wcześniejszych modyfikacji sposobu przechowywania danych jest to, jak NoSQL jest zakorzeniony w oprogramowaniu open source. Dziękujemy także ThoughtWorks za udostępnienie nam czasu na pisanie tej książki. Zaczęliśmy pracę w ThoughtWorks mniej więcej w tym samym czasie i jesteśmy tu już od prawie dekady. Firma ThoughtWorks jest dla nas bardzo gościnna, jest również źródłem wiedzy i praktyki oraz otwartym środowiskiem, w którym możemy swobodnie dzielić się zdobytą wiedzą — jest bardzo różna od tradycyjnych firm tworzących oprogramowanie. Bethany Anders-Beck, Ilias Bartolini, Tim Berglund, Duncan Craig, Paul Duvall, Oren Eini, Perryn Fowler, Michael Hunger, Eric Kascic, Joshua Kerievsky, Anand Krishnaswamy, Bobby Norton, Ade Oshineye, Thiyagu Palanisamy, Prasanna Pendse, Dan Pritchett, David Rice, Mike Roberts, Marko Rodriquez, Andrew Slocum, Toby Tripp, Steve Vinoski, Dean Wampler, Jim Webber i Wee Witthawaskul przeglądali wczesne wersje i dzięki swoim radom pomagali je ulepszać. Dodatkowo Pramod chciałby podziękować bibliotece Schaumburg za doskonałe usługi i ciche miejsce do pisania — moje piękne córki, Arhana i Arula, zasługują na podziękowania za zrozumienie, że tato chodził do biblioteki i nie zabierał ich ze sobą, zaś Rupali, moja ukochana żona, za olbrzymie wsparcie i pomoc w pozostaniu skupionym.

Część I

Zrozumienie

17

18

ROZDZIAŁ 1.

DLACZEGO NOSQL?

Rozdział 1

Dlaczego NoSQL?

Prawie tak długo, jak długo zajmujemy się tworzeniem oprogramowania, zwłaszcza w zastosowaniach biznesowych, relacyjne bazy były oczywistym narzędziem do przechowywania danych. Jeżeli jesteś architektem rozpoczynającym nowy projekt, najprawdopodobniej zastanawiasz się jedynie, którą bazę relacyjną wybrać (czasami, jeżeli Twoja firma posiada preferowanego producenta, nawet taki wybór nie będzie rozważany). Zdarzało się, że inne technologie, np. bazy obiektowe w latach 90., próbowały zagrozić panowaniu baz relacyjnych, ale nigdy żadne inne modele danych nie zyskały popularności. Po tak długim okresie dominacji baz relacyjnych obecne zainteresowanie bazami NoSQL jest dość nieoczekiwane. W tym rozdziale opowiemy, dlaczego bazy relacyjne zyskały popularność i dlaczego uważamy, że aktualne zainteresowanie bazami NoSQL nie jest tylko przejściową modą.

1.1. Wartość baz relacyjnych Bazy relacyjne stały się tak nieodłącznym elementem naszego oprogramowania, że łatwo nie doceniać ich wartości. Warto więc przypomnieć sobie, jakie zalety oferują.

1.1.1. Przechowywanie trwałych danych Najbardziej oczywistą zaletą baz danych jest możliwość trwałego przechowywania dużych ilości danych. Większość architektów ma wyobrażenie istnienia dwóch rodzajów pamięci: szybkiej i nietrwałej pamięci głównej oraz obszerniejszego, ale wolniejszego magazynu wspierającego. Pamięć główna ma ograniczoną pojemność, a jej zawartość jest tracona, jeżeli ustanie zasilanie lub coś złego stanie się z systemem operacyjnym. Aby zachować dane, zapisujemy je w magazynie wspierającym, często obrazowanym jako dysk (chociaż w obecnych czasach ten dysk może być pamięcią stałą). Magazyn wspierający może być zorganizowany na wiele sposobów. W przypadku wielu programów użytkowych (takich jak na przykład edytory tekstu) magazyn przyjmuje postać pliku w systemie plików systemu operacyjnego. Dla większości aplikacji biznesowych 19

20

ROZDZIAŁ 1. DLACZEGO NOSQL?

magazynem jest jednak baza danych. W porównaniu do systemu plików oferuje ona większą elastyczność w przypadku przechowywania dużych ilości danych, tak aby program mógł szybko uzyskać dostęp do fragmentu tych danych.

1.1.2. Współbieżność W aplikacjach biznesowych zwykle wielu użytkowników jednocześnie przegląda te same dane, często jednocześnie je modyfikując. Przez większą część czasu pracują na różnych częściach tych danych, jednak czasami zdarza się, że operują na tym samym ich fragmencie. Musimy zatem zadbać o koordynację tych operacji, aby zapobiec takim sytuacjom jak zarezerwowanie tego samego pokoju hotelowego przez dwie osoby. Współbieżność jest trudna do osiągnięcia, a błędy mogą się zdarzać nawet najbardziej uważnym programistom. Ponieważ w aplikacjach biznesowych wielu użytkowników i wiele systemów może pracować jednocześnie, wiele rzeczy może pójść źle. Bazy relacyjne pomagają poradzić sobie z tym problemem, kontrolując cały dostęp do danych poprzez transakcje. Nie jest to lek na wszystko (nadal musisz obsłużyć błąd transakcji w przypadku, kiedy spróbujesz zarezerwować pokój, który właśnie przestał być dostępny), ale świetnie pomaga w radzeniu sobie z problemami współbieżności. Transakcje mogą też odegrać rolę w obsłudze błędów. Dzięki transakcjom możesz wprowadzić zmianę, a jeżeli podczas jej przetwarzania wystąpi błąd, transakcja może wycofać zmiany i przywrócić poprzednie wartości.

1.1.3. Integracja Systemy biznesowe funkcjonują w bogatym ekosystemie, składającym się z wielu aplikacji współpracujących w celu wykonania zadań i często tworzonych przez różne zespoły. Taki sposób wewnętrznej współpracy między aplikacjami jest niewygodny, ponieważ wymaga zaawansowanej organizacji. Aplikacje często muszą korzystać z tych samych danych, a modyfikacje dokonane w jednej aplikacji muszą być widoczne także w innych. Często spotykanym sposobem na osiągnięcie tego celu jest integracja poprzez współdzieloną bazę danych [Hohpe i Woolf], w której wiele aplikacji przechowuje swoje dane w tej samej bazie danych. Dzięki wykorzystaniu jednej bazy wszystkie aplikacje mogą korzystać ze wspólnych danych, a system kontroli współbieżności bazy danych obsługuje wiele aplikacji w ten sam sposób, w jaki obsługuje dostęp wielu użytkowników.

1.1.4. Ustandaryzowany (przeważnie) model Bazy relacyjne zyskały popularność, ponieważ udostępniają wyżej wymienione funkcjonalności w (przeważnie) ustandaryzowany sposób. Dzięki temu programiści i specjaliści baz danych mogą nauczyć się podstawowego modelu relacyjnego i wykorzystywać go w wielu projektach. Mimo że pomiędzy systemami relacyjnymi występują różnice, podstawowy mechanizm pozostaje bez zmian: dialekty SQL różnych producentów są podobne, a transakcje działają prawie tak samo.

1.2. NIEZGODNOŚĆ IMPEDANCJI

1.2. Niezgodność impedancji Bazy transakcyjne oferują wiele zalet, jednak absolutnie nie są doskonałe. Już od początku swego istnienia powodowały wiele frustracji. Dla twórców aplikacji największym problemem było zawsze zjawisko zwane niezgodnością impedancji: różnica pomiędzy modelem relacyjnym a strukturami zawartymi w pamięci. Model relacyjny organizuje dane w strukturę tabel i wierszy lub, zgodnie z nazewnictwem, w relacje i krotki. W modelu relacyjnym krotki to zestaw par nazwa – wartość, a relacja to zestaw krotek (relacyjna definicja krotki jest nieco inna niż matematyczna i ta zawarta w wielu językach programowania, gdzie krotka jest sekwencją wartości). Wszystkie operacje w SQL pobierają i zwracają relacje, co składa się na matematycznie elegancką algebrę relacyjną. Poleganie na relacjach wprowadza pewną elegancję i prostotę, ale powoduje także ograniczenia. Przede wszystkim dane w krotce relacyjnej muszą być proste — nie mogą zawierać struktur, takich jak rekord zagnieżdżony czy lista. To ograniczenie nie występuje w przypadku struktur danych zapisanych w pamięci, gdzie dane mogą tworzyć znacznie bogatsze struktury niż tylko relacje. W rezultacie jeżeli chcesz skorzystać z bardziej skomplikowanej struktury zawartej w pamięci, musisz w celu zapisania danych na dysku przetłumaczyć ją na strukturę relacyjną. Właśnie to jest określane mianem niezgodności impedancji — dwie różne struktury wymagające tłumaczenia (rysunek 1.1).

Rysunek 1.1. Zamówienie, które w interfejsie użytkownika wygląda jak jedna integralna struktura, w bazie relacyjnej jest podzielone na wiele wierszy i tabel Niezgodność impedancji jest poważnym źródłem frustracji u twórców aplikacji, a w latach 90. ludzie wierzyli, że doprowadzi ona do zastąpienia baz relacyjnych bazami przechowującymi dane w strukturach zbliżonych do tych przechowywanych w pamięci. Dekada ta cechowała się

21

22

ROZDZIAŁ 1. DLACZEGO NOSQL?

wzrostem liczby języków obiektowych, a wraz z nimi pojawiały się także obiektowe bazy danych — wydawało się, że zarówno języki obiektowe, jak i obiektowe bazy danych zdominują tworzenie oprogramowania w nowym milenium. O ile jednak języki obiektowe zyskały ogromną popularność, o tyle obiektowe bazy danych odeszły w zapomnienie. Bazy relacyjne obroniły się dzięki swej roli w integracji systemów, obsłudze przez ustandaryzowany język SQL oraz podziałowi inżynierów na twórców oprogramowania oraz administratorów baz danych. Niezgodność impedancji stała się mniej dokuczliwa dzięki szeroko dostępnym mapującym na model obiektowy bibliotekom, takim jak Hibernate i iBATIS, implementującym dobrze znane wzorce mapujące [Fowler PoEAA], jednak problem mapowania nie zniknął. Biblioteki mapujące pozwalają wyeliminować wiele pracy, jednak mogą też same stanowić problem, jeżeli ludzie zaczną zbyt mocno ignorować część transakcyjną i spadnie szybkość wykonywania zapytań. Bazy relacyjne są po roku 2000 nadal najczęściej stosowanymi bazami, jednakże w ciągu tych lat zaczęły pojawiać się technologie zagrażające ich dominacji.

1.3. Bazy aplikacji i integracji Dokładne przyczyny, dla których bazy relacyjne zyskały większą popularność niż bazy obiektowe, nadal są czasami dla starszych programistów powodem do dyskusji przy piwku. Naszym zdaniem głównym powodem była jednakże rola bazy danych jako elementu integrującego aplikacje. W tym scenariuszu baza danych jest bazą integrującą, w której dane przechowuje wiele aplikacji, często rozwijanych przez różne zespoły. Taka architektura poprawia komunikację, ponieważ wszystkie aplikacje operują na spójnym zestawie trwałych danych. Są też wady integracji za pomocą współdzielonej bazy. Struktura zaprojektowana do integracji wielu aplikacji staje się bardziej — czasami dramatycznie bardziej — skomplikowana, niż wymagałaby tego pojedyncza aplikacja. Co więcej, jeżeli jedna aplikacja musi dokonać zmian w sposobie przechowywania danych, musi te zmiany skoordynować z innymi aplikacjami wykorzystującymi bazę. Różne aplikacje mają różne potrzeby odnośnie struktury i wydajności, więc indeks stworzony na potrzeby jednej aplikacji może mieć negatywny wpływ na szybkość wstawiania danych w innej aplikacji. Przeważnie każda aplikacja oznacza inny zespół, przez co baza nie może zakładać, że dane będą modyfikowane w sposób gwarantujący ich integralność, i w związku z tym musi przejąć odpowiedzialność także za to. Innym podejściem jest traktowanie bazy jako bazy aplikacji, do której dostęp ma tylko jedna aplikacja i o którą dba tylko jeden zespół. Dzięki temu podejściu tylko zespół zajmujący się daną aplikacją musi znać strukturę bazy, co sprawia, że znacznie łatwiej jest utrzymać i modyfikować strukturę. Ponieważ jeden zespół zajmuje się zarówno aplikacją, jak i bazą danych, odpowiedzialność za integralność danych może zostać umieszczona w kodzie aplikacji. Interfejsy aplikacji mogą zająć się współpracą między aplikacjami, co pozwala na zastosowanie lepszych protokołów interakcji oraz daje wsparcie do ich zmiany. Po roku 2000 obserwujemy zmianę w kierunku usług sieciowych — Web Services [Daigneau] — co pozwala na komunikację aplikacji za pośrednictwem protokołu HTTP. Usługi sieciowe wprowadzają nowy, szeroko stosowany, mechanizm komunikacyjny — konkurencyjny dla funkcji

1.4. ATAK KLASTRÓW

integracyjnych baz transakcyjnych (duża część pracy nad usługami sieciowymi została wykonana pod szyldem architektury zorientowanej na usługi — SOA (ang. Service Oriented Architecture) — pojęcia znanego z braku jednoznaczności). Interesującym aspektem nowego mechanizmu integracji jest zwiększenie elastyczności struktury wymienianych danych. Podczas komunikacji z bazą SQL dane muszą przyjmować formę relacji. W przypadku usług sieciowych możesz natomiast wykorzystywać bardziej rozbudowane struktury, włącznie z zagnieżdżonymi rekordami i listami. Dane te są z reguły reprezentowane przez dokumenty XML lub, od niedawna, jako obiekty JSON. Generalnie podczas komunikacji sieciowej powinniśmy redukować liczbę wymaganych połączeń, dlatego ważne jest przesyłanie jak najbardziej rozbudowanych struktur podczas jednego żądania lub odpowiedzi. Jeżeli do integracji zamierzasz wykorzystać usługi, w większości przypadków to usługi sieciowe — przekazujące tekst poprzez protokół HTTP — będą tym, czego potrzebujesz. Jeżeli jednak Twoje interakcje wymagają wysokiej wydajności, możesz potrzebować protokołu binarnego. Wykorzystuj go jedynie, jeżeli jesteś pewien, że go potrzebujesz, ponieważ protokoły tekstowe są znacznie łatwiejsze w implementacji — tak działa cały Internet. Kiedy już podjąłeś decyzję o wykorzystaniu bazy aplikacji, zyskujesz większą swobodę w doborze bazy danych. Ponieważ nie istnieje bezpośrednie powiązanie pomiędzy bazą danych a usługami, za pośrednictwem których komunikujesz się ze światem zewnętrznym, świat zewnętrzny nie musi widzieć, z jakiej bazy korzystasz — dzięki temu możesz korzystać z baz nierelacyjnych. Co więcej, istnieje wiele funkcjonalności, np. związanych z bezpieczeństwem, które mają mniejsze znaczenie w kontekście bazy aplikacji, ponieważ możesz odciąć ją od świata zewnętrznego. Mimo tej swobody wydawało się, że bazy aplikacyjne nie będą początkiem zwiększenia popularności alternatywnych baz danych. Większość zespołów decydujących się na bazy aplikacji mimo wszystko pozostała przy bazach relacyjnych. W końcu wykorzystanie bazy aplikacji ma wiele zalet, nawet pomijając swobodę wyboru silnika bazodanowego (właśnie dlatego rekomendujemy to podejście). Bazy relacyjne są dobrze znane i zwykle bardzo dobrze — a przynajmniej wystarczająco dobrze — sprawdzają się w swojej roli. Być może z upływem czasu to bazy aplikacji staną się początkiem końca dominacji baz relacyjnych — na razie jednak główny atak na bazy relacyjne nadchodzi z innej strony.

1.4. Atak klastrów Na początku nowego tysiąclecia świat technologii odczuł pęknięcie bańki dot-comów z lat 90. Mimo że wiele osób zaczęło kwestionować ekonomiczną przyszłość Internetu, po roku 2000 kilka firm internetowych zanotowało jednak znaczne wzrosty. Wzrost ten odbywał się w wielu kierunkach. Strony internetowe zaczęły bardzo szczegółowo śledzić aktywność i analizować strukturę. Pojawiły się ogromne zbiory danych: łącza, sieci społecznościowe, logi aktywności, dane mapujące. Wraz ze wzrostem danych wzrosła także liczba użytkowników — największe strony stały się miejscami regularnie odwiedzanymi przez ogromne liczby użytkowników.

23

24

ROZDZIAŁ 1. DLACZEGO NOSQL?

Nadążenie za rosnącą ilością danych i użytkowników wymaga większych zasobów. Aby obsłużyć taki wzrost, masz dwie drogi: skalowanie w górę lub skalowanie w bok (na zewnątrz). Skalowanie w górę związane jest z zastosowaniem lepszego sprzętu, większej liczby procesorów, większej ilości miejsca na dyskach i większej pamięci. Lepszy sprzęt staje się jednak bardziej kosztowny, nie wspominając już o tym, że taka rozbudowa ma swoje granice. Alternatywą jest wykorzystanie wielu słabszych maszyn w klastrze. Klaster serwerów może wykorzystywać słabszy sprzęt, dzięki czemu takie skalowanie jest tańsze. Klaster może być też bardziej niezawodny — usterki pojedynczych maszyn się zdarzają, ale cały klaster może nadal funkcjonować pomimo tych awarii. Większe organizacje skierowały swoje działania w stronę klastrów, co uwidoczniło nowy problem — bazy relacyjne nie są zaprojektowane do pracy w klastrach. Te, które wspierają pracę w klastrach, np. Oracle RAC czy Microsoft SQL Server, działają na podstawie współdzielonego dysku — oznacza to jednak, że system plików nadal jest podatny na awarie. Bazy relacyjne mogą być też uruchamiane na odrębnych serwerach z dzieleniem danych pomiędzy serwerami (patrz podrozdział 4.2). O ile takie podejście pozwala rozdzielić ruch, to jednak podział danych wymaga aplikacji przechowującej informacje, na którym serwerze znajduje się dana część danych. Tracimy także kontrolę zapytań, integralności referencyjnej, transakcji i spójności dla danych podzielonych na części. Od osób zajmujących się takimi bazami często słyszymy stwierdzenie „nienaturalne działania”. Problemy techniczne idą w parze ze zwiększającymi się kosztami licencji. Komercyjne systemy baz danych relacyjnych są z reguły wyceniane per serwer, co w przypadku klastra zwiększa koszty i prowadzi do frustrujących negocjacji z działem zakupów. To niedopasowanie pomiędzy bazami danych a klastrami stało się kolejnym powodem do rozpatrzenia alternatywnych sposobów przechowywania danych. Szczególnie dwie firmy — Google i Amazon — mają na tym polu znaczące osiągnięcia. Obie firmy posiadały duże klastry oraz zapisywały znaczne ilości danych. To dało im motywację. Obie świetnie sobie radziły na rynku i rozwijały się w oparciu o silne zaplecze technologiczne, co dało im narzędzie i sposobność. Nic dziwnego, że postanowiły zamordować swoje bazy relacyjne. W ostatnich latach obie firmy stworzyły zwięzłe, ale wpływowe artykuły na temat swoich działań: BigTable od Google i Dynamo od Amazona. Często twierdzi się, że Amazon i Google operują w skali niedostępnej dla innych organizacji, tak więc rozwiązania, z których korzystają, mogą nie być idealne dla przeciętnej firmy. To prawda, że większość projektów nie wymaga takiego skalowania, jednakże coraz więcej firm eksperymentuje z zapisywaniem i przetwarzaniem większej ilości danych, a w związku z tym napotyka na te same problemy. Kiedy więc wyciekła większa ilość informacji na temat tego, co zrobiły Google i Amazon, ludzie zaczęli próbować projektować bazy w podobny sposób — tak aby były przystosowane do pracy w klastrach. Podczas gdy wcześniejsze zagrożenia dominacji baz relacyjnych były raczej iluzoryczne, klastry stały się zagrożeniem realnym.

1.5. POJAWIENIE SIĘ BAZ NOSQL

1.5. Pojawienie się baz NoSQL To piękna ironia, że termin NoSQL po raz pierwszy pojawił się w latach 90., jako nazwa bazy relacyjnej open source [Strozzi NoSQL]. Twórcą był Carlo Strozzi, a baza przechowywała dane w formie plików ASCII, w których każda krotka reprezentowana była linią z wartościami oddzielonymi znakami tabulacji. Nazwa wzięła się stąd, że baza nie używała języka SQL do tworzenia zapytań. Danymi w bazie manipulowało się za pomocą skryptów powłoki, które można było łączyć w standardowe potoki systemu UNIX. Poza zbieżnością nazw baza Strozziego nie miała żadnego wpływu na bazy opisywane w tej książce. Wykorzystanie baz NoSQL w formie, jaką znamy teraz, zapoczątkowane zostało 11 czerwca 2009 roku w San Francisco, na spotkaniu zorganizowanym przez Johana Oskarssona — twórcy oprogramowania z Londynu. Przykłady BigTable i Dynamo zainspirowały wielu projektantów do eksperymentowania z alternatywnym przechowywaniem danych, a dyskusja o nich stała się elementem wszystkich lepszych konferencji w tamtym czasie. Johan był bardzo zainteresowany uzyskaniem informacji na temat tych nowych baz podczas pobytu w San Francisco na spotkaniu użytkowników Hadoop. Ponieważ nie miał wiele czasu, uznał, że nie uda mu się obejrzeć wszystkich projektów, i postanowił zorganizować spotkanie, na którym twórcy tych rozwiązań będą mogli zaprezentować swoje osiągnięcia wszystkim zainteresowanym. Johan szukał dla spotkania nazwy, która stanowiłaby jednocześnie dobry znacznik Twittera: krótkiej, łatwej do zapamiętania i takiej, dla której Google nie wyświetlałby zbyt wielu wyników, tak aby wyszukiwanie po nazwie pozwalało łatwo znaleźć spotkanie. Poprosił o sugestie na kanale IRC #cassandra i spośród propozycji wybrał nazwę NoSQL, którą zgłosił Eric Evans (programista w firmie Rackspace, niemający związku z Erikiem Evansem, ojcem pojęcia DDD). Chociaż nazwa była negatywna i nie do końca opisywała przedstawiane systemy, doskonale pasowała jako znacznik Twittera. Miała to być nazwa wyłącznie tego jednego spotkania i nikt nie spodziewał się, że nazwa zostanie podchwycona przez ten nowy trend [Oskarsson]. Termin „NoSQL” zyskał wielką popularność, ale nazwa ta nigdy dokładnie nie opisywała zjawiska. Oryginalne wezwanie na spotkanie [NoSQL Meetup] dotyczyło otwartych, rozproszonych, nierelacyjnych baz danych. Wykłady na spotkaniu dotyczyły baz Voldemort, Cassandra, Dynomite, HBase, Hypertable, CouchDB i MongoDB, ale termin „NoSQL” nie był ograniczony tylko do nich. Nie ma żadnej ogólnie przyjętej definicji ani organizacji, która mogłaby taką definicję stworzyć — jedyne, co możemy zrobić, to opowiedzieć o cechach baz nazywanych bazami NoSQL. Po pierwsze, nie ma żadnego konkretnego powodu, dla którego bazy NoSQL nie wykorzystują języka SQL. Niektóre z nich posiadają języki zapytań i logiczne jest, aby języki te były podobne do SQL-a — dzięki temu ich nauka jest łatwiejsza. Właśnie taki jest język CQL z Cassandry — „dokładnie jak SQL, poza miejscami, gdzie się różni”. Do tej pory jednak żadna z baz NoSQL nie implementuje niczego, co pasowałoby do bardzo elastycznej definicji języka SQL. Interesujące będzie zobaczyć, co się stanie, kiedy baza NoSQL zaimplementuje w miarę standardowy SQL; można przewidzieć jedynie to, że takie wydarzenie sprowokuje wiele dyskusji.

25

26

ROZDZIAŁ 1. DLACZEGO NOSQL?

Kolejną ważną cechą tych baz jest to, że zazwyczaj są bazami open source. Mimo że termin „NoSQL” używany jest często w odniesieniu do zamkniętego oprogramowania, pojęcie to dotyczy głównie systemów open source. Większość baz NoSQL powstała z myślą o klastrach — zwłaszcza te, które prezentowane były na spotkaniu NoSQL. Wpływa to na ich model danych, a także na ich podejście do spójności danych. Bazy transakcyjne wykorzystują transakcje ACID (patrz punkt 2.1.2) w celu zapewnienia spójności danych w całej bazie danych. To przeszkadza w działaniu w klastrze, dlatego też bazy NoSQL oferują kilka opcji odnośnie zachowania spójności i dystrybucji. Nie wszystkie bazy NoSQL są jednak zorientowane na działanie w klastrach. Bazy grafowe są jednym z typów baz NoSQL wykorzystujących model dystrybucji zbliżony do baz relacyjnych, ale oferują inny model danych, dzięki czemu są lepsze w przechowywaniu danych zawierających skomplikowane relacje. Bazy NoSQL są z reguły tworzone na podstawie wymagań aplikacji webowych z początku XXI wieku, w związku z czym tylko bazy stworzone w tym czasie określane są tym terminem — w przeciwieństwie do baz powstałych przed rokiem 2000, nie mówiąc już o bazach powstałych BC (ang. Before Codd — przed Coddem). Bazy NoSQL działają bez schematu danych, dzięki czemu możesz dowolnie dodawać pola do rekordów w bazie, bez konieczności wprowadzania zmian w strukturze. Jest to bardzo użyteczne w przypadku danych nieustandaryzowanych i pól dodatkowych, które w bazach relacyjnych były nazywane w stylu nowePole6 lub wymuszały wykorzystanie osobnych tabel, co jest niewygodne i trudne do zrozumienia. Wszystkie powyższe cechy są wspólne dla baz, które określamy mianem NoSQL. Żadna z nich nie jest definicją i najprawdopodobniej nigdy nie będzie spójnej definicji NoSQL. Ten ogólny zestaw właściwości był jednak naszym przewodnikiem w pisaniu tej książki. Nasz entuzjazm w stosunku do tego tematu spowodowany jest tym, że pojawienie się baz NoSQL otwiera nowe możliwości przechowywania danych. Co więcej, te nowe możliwości nie powinny być ograniczane definicją baz NoSQL. Mamy nadzieje, że opcje przechowywania danych inne niż transakcyjne staną się szerzej stosowane — także te powstałe przed ruchem NoSQL. Liczba tematów, które możemy omówić w tej książce, jest jednak ograniczona, więc zdecydowaliśmy się skoncentrować na tej nie-definicji. Kiedy pierwszy raz słyszysz termin „NoSQL”, od razu nasuwa się pytanie, co on oznacza — czy negację języka SQL? Większość ludzi mówiących o NoSQL twierdzi, że termin ten oznacza nie tylko SQL (ang. Not Only SQL), jednakże ta interpretacja ma kilka wad. Większość osób pisze „NoSQL”, podczas gdy Not Only SQL tworzyłoby skrót NOSQL. Ponadto nazywanie czegoś NoSQL w znaczeniu „nie tylko SQL” nie jest poprawne, ponieważ wtedy także Oracle i Postgres pasowałyby do tej definicji, udowodnilibyśmy, że białe równa się czarne, i wszyscy zostalibyśmy przejechani na przejściu dla pieszych. Aby rozwiązać ten problem, sugerujemy, abyś nie zawracał sobie głowy tym, co oznacza ten termin (jest to dobrą radą w przypadku większości akronimów). W związku z tym kiedy termin „NoSQL” używany jest w odniesieniu do bazy danych, odnosi się do niezdefiniowanego zestawu baz, przeważnie typu open source, przeważnie stworzonych w XXI wieku i przeważnie niewykorzystujących języka SQL. Interpretacja „nie tylko” także ma swoją wartość, ponieważ opisuje ekosystem, który zdaniem wielu będzie przyszłością baz danych. Naszym zdaniem jest to najbardziej istotna

1.6. NAJWAŻNIEJSZE KWESTIE

zaleta takiego sposobu myślenia — lepiej myśleć o NoSQL jako o ruchu, a nie konkretnej technologii. Nie uważamy, że bazy transakcyjne odchodzą w niebyt — nadal będą najpopularniejszym sposobem przechowywania danych. Co więcej, mimo że napisaliśmy tę książkę, nadal polecamy bazy relacyjne. Ich powszechna znajomość, stabilność, zestaw funkcjonalności oraz dostępne wsparcie są istotnymi zaletami w większości projektów. Zmiana polega na tym, że teraz bazy relacyjne postrzegamy jako jedną z opcji. Zamiast wybierać bazę relacyjną dlatego, że wszyscy tak robią, musimy zrozumieć dane, które będziemy przechowywać, oraz sposób, w jaki będziemy chcieli tymi danymi manipulować. Rezultat jest taki, że firmy wykorzystywały będą miks różnych technologii bazodanowych, w zależności od potrzeb. Aby takie podejście mogło mieć sens, uważamy, że organizacje muszą przejść od używania baz integracji do baz aplikacji. W niniejszej książce zakładamy, że bazy NoSQL będziesz wykorzystywać właśnie jako bazy aplikacji; naszym zdaniem w przypadku baz integracji NoSQL jest złym wyborem. Nie uznajemy tego za wadę, ponieważ uważamy, że nawet jeżeli nie korzystasz z baz NoSQL, enkapsulowanie baz danych w usługi jest dobrym posunięciem. W naszej praktyce korzystania z baz NoSQL skupiliśmy się na przechowywaniu dużych danych w klastrach. O ile uważamy, że takie zastosowania były głównym powodem powstania tego nurtu, to jednak nie jest to jedyny powód, dla którego zespoły rozważają wykorzystanie baz NoSQL. Równie ważnym powodem jest frustracja spowodowana niedopasowaniem impedancji. Problemy związane z dużymi danymi stworzyły okazję do nowego spojrzenia na temat przechowywania danych i niektóre zespoły zauważyły, że wykorzystanie baz NoSQL może pozytywnie wpłynąć na ich wydajność dzięki uproszczeniu modelu danych, nawet jeżeli nie będą musieli skalować poza jedną maszynę. Kiedy więc będziesz czytać dalszą część tej książki, pamiętaj, że są dwa główne powody wykorzystania baz NoSQL. Jednym powodem jest obsługa danych wymagających klastrów; drugim jest poprawa produktywności przy tworzeniu aplikacji dzięki bardziej przyjaznemu modelowi danych.

1.6. Najważniejsze kwestie ■ Bazy relacyjne są od 20 lat technologią dominującą — oferują spójność, kontrolę współbieżności oraz mechanizmy integracji. ■ Twórców aplikacji frustruje niedopasowanie impedancji pomiędzy modelem relacyjnym a strukturami danych zapisanych w pamięci. ■ Obecnie odchodzi się od integracji za pośrednictwem baz danych w stronę enkapsulacji baz w aplikacjach i integracji za pośrednictwem usług sieciowych. ■ Głównym czynnikiem wpływającym na potrzebę wprowadzenia zmian w sposobie przechowywania danych była konieczność obsługi dużych wolumenów danych w klastrach. Bazy relacyjne nie są zaprojektowane do efektywnej pracy w klastrze.

27

28

ROZDZIAŁ 1. DLACZEGO NOSQL?

■ NoSQL to niezamierzony neologizm. Nie ma jednoznacznej definicji — możemy tylko zebrać listę wspólnych cech. ■ Wspólne cechy baz NoSQL to: ■ brak modelu relacyjnego, ■ sprawne działanie w klastrze, ■ otwarty kod, ■ budowa na potrzeby aplikacji webowych XXI wieku, ■ brak schematu danych. ■ Najważniejszym rezultatem pojawienia się baz danych NoSQL jest możliwość wyboru sposobu przechowywania danych w zależności od ich specyfiki.

Rozdział 2

Agregacyjne modele danych

Model danych to model, za pośrednictwem którego przeglądamy dane i manipulujemy nimi. Model przechowywania opisuje natomiast, w jaki sposób dane są przechowywane i manipulowane wewnętrznie. W idealnym świecie powinniśmy móc całkowicie ignorować model przechowywania, jednak w praktyce musimy mieć o nim przynajmniej niewielkie pojęcie — głównie po to, aby osiągać zadawalającą wydajność. W dyskusji termin „model danych” oznacza często model konkretnych danych w aplikacji. Programista może wskazać miejsce w diagramie związków encji bazy danych i powiedzieć, że jest to ich model danych zawierający klientów, zamówienia, produkty itp. W tej książce będziemy jednak korzystać z terminu „model danych” w znaczeniu modelu, zgodnie z którym baza danych organizuje dane — bardziej formalnie można by to nazwać metamodelem. Modelem danych dominującym w ciągu ostatnich dekad jest model relacyjny, który wizualizowany jest przeważnie jako zestaw tabel, nie jako strona arkusza kalkulacyjnego. Każda tabela posiada wiersze, a każdy wiersz reprezentuje interesującą nas encję. Encję opisujemy za pomocą kolumn, a każda z kolumn zawiera pojedynczą wartość. Kolumna może odnosić się do innego wiersza w tej lub innej tabeli, dzięki czemu budowane są relacje między encjami (kiedy mówimy o tabelach i wierszach, używamy nieformalnej, ale ogólnie przyjętej terminologii; bardziej formalne nazwy to relacje i krotki). Jedną z najbardziej oczywistych zmian wprowadzanych przez bazy NoSQL jest odejście od modelu relacyjnego. Każde rozwiązanie NoSQL wykorzystuje inny model, najczęściej stosowane modele możemy podzielić na cztery kategorie: klucz – wartość, dokument, rodzina kolumn i graf. Pierwsze trzy są do siebie na tyle podobne, że możemy zgrupować je pod wspólną nazwą modeli zorientowanych na agregacje. W tym rozdziale wytłumaczymy, co w przypadku modeli danych rozumiemy pod pojęciem orientacji na agregacje.

2.1. Agregacje Model relacyjny przechowuje nasze dane podzielone na krotki (wiersze). Krotka jest ograniczoną strukturą danych — przechowuje zestaw wartości, więc nie możesz zagnieżdżać krotek wewnątrz innych krotek. Ta prostota jest podstawą modelu relacyjnego, dzięki temu możemy myśleć o wszystkich operacjach jako o modyfikacji i zwracaniu krotek. 29

30

ROZDZIAŁ 2. AGREGACYJNE MODELE DANYCH

W przypadku orientacji na agregacje podejście jest inne. Podejście to zakłada, że zazwyczaj będziesz chciał operować na danych w jednostkach bardziej skomplikowanych niż tylko zestaw krotek. Wygodnie jest myśleć w kategoriach rozbudowanego rekordu pozwalającego na przechowywanie innych rekordów czy list. Jak wkrótce zobaczysz, modele klucz – wartość, dokument i rodzina kolumn pozwalają na tworzenie takich rekordów. Nie ma jednak wspólnej nazwy dla takiego rozbudowanego rekordu; w tej książce będziemy korzystać z terminu „agregacja”. Termin „agregacje” pochodzi z modelowania Domain-Driven Design [Evans]. W tym podejściu agregacja jest kolekcją obiektów, które traktujemy jako jednostkę. Jest to przeważnie jednostka przeznaczona do manipulacji i zarządzania spójnością. Zazwyczaj chcemy aktualizować agregacje za pomocą operacji atomowych oraz komunikować się z magazynem danych, przekazując agregacje. Ta definicja bardzo dobrze pasuje do tego, w jaki sposób operują bazy danych oparte na modelu klucz – wartość, dokumentów i rodziny kolumn. Operowanie na agregacjach ułatwia bazom pracę w klastrach, ponieważ agregacja jest naturalną jednostką dla replikacji i współdzielenia. Agregacje ułatwiają też pracę programistom, ponieważ ci z reguły operują danymi zorganizowanymi w agregacje.

2.1.1. Przykłady relacji i agregacji W wyjaśnieniu, o co nam chodzi, przydatny będzie przykład. Załóżmy, że mamy zbudować stronę e-commerce; będziemy sprzedawać towary za pośrednictwem sieci bezpośrednio klientowi i przechowywać dane o użytkownikach, produktach, zamówieniach, adresach wysyłki i adresach płatników oraz dane związane z płatnościami. Możemy zamodelować nasze dane, korzystając z modelu relacyjnego oraz modelów baz NoSQL, i na ich podstawie omówić wady i zalety poszczególnych rozwiązań. W przypadku bazy relacyjnej moglibyśmy zacząć od modelu zgodnego z rysunkiem 2.1.

Rysunek 2.1. Model danych dla bazy relacyjnej (z wykorzystaniem notacji UML [Fowler UML])

2.1. AGREGACJE

Rysunek 2.2 przedstawia przykładowe dane zgodne z tym modelem.

Rysunek 2.2. Dane w relacyjnej bazie danych Ponieważ jesteśmy dobrymi wojakami baz relacyjnych, wszystko jest poprawnie znormalizowane, dzięki czemu żadne dane nie są powtarzane w wielu tabelach. Utrzymana jest też integralność referencyjna. Prawdziwy system pozwalający na składanie zamówień byłby pewnie bardziej skomplikowany, jednak takie uproszczenie to przywilej książkowych przykładów. Przyjrzyjmy się, jak wyglądałby ten model, gdybyśmy myśleli w sposób zorientowany na agregacje (rysunek 2.3). Znowu należy przedstawić dane przykładowe — tym razem w formacje JSON, ponieważ jest to format zazwyczaj wykorzystywany przez bazy NoSQL. // w klientach { "id":1, "nazwa":"Marcin", "adresPlatnika":[{"miasto":"Warszawa"}] } // w zamówieniach { "id":99, "klientId":1, "pozycjeZamowienia":[ { "produktId":27, "cena": 32.45, "produktNazwa": "NoSQL. Kompedium" }

31

32

ROZDZIAŁ 2. AGREGACYJNE MODELE DANYCH

Rysunek 2.3. Model danych oparty o agregacje ], "adresWysylki":[{"miasto":"Warszawa"}] "zamowieniePlatnosc":[ { "numerKarty":"1000-1000-1000-1000", "NIP":"abelif879rft", "adresPlatnika": {"city": "Warszawa"} } ],

} W tym modelu mamy dwie podstawowe agregacje: klienta i zamówienie. Użyliśmy markera złożenia (czarny diament), aby pokazać, jak dane wpasowują się w strukturę agregacji. Klient zawiera listę adresów płatnika, a zamówienie zawiera listę pozycji, adres wysyłki i płatności. Płatność zawiera wykorzystany adres płatnika. Pojedynczy rekord logiczny adresu pojawia się w danych trzy razy; identyfikator adresu nie został jednak wykorzystany — adres został potraktowany jako wartość i przekopiowany za każdym razem. Jest to dobre podejście, ponieważ nie chcemy, aby adres płatności i adres wysyłki ulegały zmianie. W bazie relacyjnej musielibyśmy zadbać, aby dane nie zostały zmienione, tworząc nowy rekord. W agregacjach możemy wstawić cały adres do agregacji zgodnie z naszymi potrzebami.

2.1. AGREGACJE

W żadnej z agregacji nie ma połączenia pomiędzy klientem i zamówieniem — to połączenie możemy odczytać z relacji pomiędzy agregacjami. Analogicznie połączenie z pozycji zamówienia kierowałoby do osobnej struktury dla agregacji produktu, której nie umieściliśmy w przykładzie. Nazwę produktu pokazaliśmy jako część pozycji zamówienia; taka denormalizacja jest podobna do denormalizacji w bazach relacyjnych, jednak w przypadku agregacji jest częstsza, ponieważ chcemy minimalizować liczbę różnych agregacji, do których uzyskujemy dostęp podczas interakcji z bazą. Najważniejsze tutaj jest nie to, gdzie wyznaczyliśmy granice pomiędzy poszczególnymi agregacjami, ale to, że musisz myśleć o sposobie, w jaki będziesz chciał te dane pobierać — musisz o tym myśleć już podczas tworzenia modelu danych aplikacji. Moglibyśmy zupełnie inaczej wyznaczyć granice tych agregacji — moglibyśmy na przykład umieścić w agregacji klienta wszystkie jego zamówienia (rysunek 2.4).

Rysunek 2.4. Zawarcie w agregacji klienta wszystkich związanych z nim obiektów Przykładowe dane dla powyższego modelu wyglądałyby następująco: // w klientach { "klient": { "id": 1, "nazwa": "Marcin", "adresPlatnika": [{"miasto": "Warszawa"}], "zamowienia": [ { "id":99, "klientId":1, "pozycjeZamowienia":[ { "produktId":27, "cena": 32.45, "produktNazwa": "NoSQL. Kompedium" } ],

33

34

ROZDZIAŁ 2. AGREGACYJNE MODELE DANYCH "adresWysylki":[{"miasto":"Warszawa"}] "zamowieniePlatnosc":[ { "numerKarty":"1000-1000-1000-1000", "NIP":"abelif879rft", "adresPlatnika": {"city": "Warszawa"} } ], } ] } }

Jak to zwykle bywa podczas modelowania, nie ma jednoznacznej odpowiedzi na pytanie, jak powinieneś wyznaczyć granice agregacji. Jest to uzależnione wyłącznie od sposobu, w jaki zamierzasz tymi danymi manipulować. Jeżeli zamierzasz pobierać klientów wraz z danymi dotyczącymi ich zamówień, na pewno lepsza byłaby jedna agregacja. Jeżeli natomiast zamierzasz pobierać po jednym zamówieniu naraz, powinieneś rozbić je na osobne agregacje. Oczywiście to wszystko zależy od kontekstu; niektóre zastosowania będą wymagały jednego lub drugiego rozwiązania, nawet w obrębie tego samego systemu, dlatego właśnie dużo osób nie poświęca wiele uwagi projektowaniu agregacji.

2.1.2. Konsekwencje orientacji na agregacje Mapowanie relacyjne dość dobrze odzwierciedla elementy danych i powiązania pomiędzy nimi, nie posiada jednak żadnej formy encji agregującej. W naszym przykładzie możemy powiedzieć, że zamówienie składa się z elementów zamówienia, adresu wysyłki i płatności. Takie zależności możemy w bazie relacyjnej wyrazić w postaci kluczy obcych — nie ma jednak żadnej możliwości rozróżnienia relacji reprezentujących agregacje od tych, które agregacji nie reprezentują. W rezultacie system zarządzania nie może wykorzystać agregacji do przechowywania i dystrybucji danych. Istnieją techniki modelowania, które pozwalają tworzyć struktury złożone lub agregacyjne. Problem polega jednak na tym, że techniki te nie udostępniają semantyki pozwalającej odróżnić związek agregujący od dowolnego innego; jeżeli taka semantyka istnieje, jest niespójna. Podczas pracy z bazami zorientowanymi na agregacje semantyka jest znacznie prostsza, musimy skupić się tylko na jednostce interakcji z magazynem danych. Nie jest to jednak logiczna właściwość danych; wszystko zależy od tego, jak dane będą wykorzystywane przez aplikację — sposób wykorzystania danych nie jest zazwyczaj brany pod uwagę podczas modelowania danych. Koncepcja agregacji w modelu danych baz relacyjnych nie funkcjonuje, mówimy więc, że ignorują agregacje. W świecie NoSQL bazy grafowe także ignorują agregacje. Nie jest to wadą. Zazwyczaj trudno jest dobrze wyznaczyć granice agregacji, szczególnie jeżeli te same dane wykorzystywane są w różnym kontekście. Zamówienie jest dobrą agregacją, jeżeli użytkownik tworzy lub przegląda zamówienia oraz kiedy sprzedawca realizuje zamówienia. Jeżeli jednak sprzedawca chce przeanalizować sprzedaż produktu w ciągu kilku ostatnich miesięcy, agregacja zamówienia staje się problemem. Aby dostać się do historii sprzedaży produktu, będzie trzeba zajrzeć do każdej agregacji znajdującej się w bazie.

2.2. MODELE KLUCZ – WARTOŚĆ I DOKUMENTÓW

Agregacje mogą być pomocne podczas niektórych interakcji z danymi, ale przeszkadzać przy innych. Model ignorujący agregacje pozwala na łatwe przeglądanie danych w dowolny sposób — model taki jest lepszy, jeżeli nie masz zdefiniowanej dominującej struktury manipulacji danymi. Głównym argumentem za orientacją na agregacje jest to, że taka architektura doskonale sprawdza się w przypadku klastrów, co, jak pewnie pamiętasz, było głównym powodem powstania baz NoSQL. Jeżeli baza działa w klastrze, musimy zminimalizować liczbę serwerów, które musimy odpytać w celu zebrania potrzebnych danych. Dzięki zastosowaniu agregacji dajemy bazie danych ważną informację o tym, które części danych będą manipulowane razem, a co za tym idzie, powinny być przechowywane na tym samym serwerze. Pewna ważna konsekwencja stosowania agregacji dotyczy transakcji. Bazy relacyjne pozwalają manipulować dowolną kombinacją wierszy z różnych tabel w jednej transakcji. Są to transakcje ACID (ang. Atomic, Consistent, Isolated, Durable — „atomowe, spójne, izolowane, trwałe”). ACID to dość wymyślny akronim; tak naprawdę liczy się atomowość: wiele wierszy w obrębie różnych tabel aktualizowanych jest w ramach jednej operacji. Taka operacja kończy się albo sukcesem, albo porażką wszystkich jej elementów, a równoległe operacje są od siebie izolowane, więc nie mogą wykonać częściowego zapisu. Często mówi się, że bazy NoSQL nie wspierają transakcji ACID i przez to cierpi spójność danych. Jest to bardzo duże uproszczenie. Prawdą jest, że bazy zorientowane na agregacje nie mają transakcji ACID obejmujących kilka agregacji. Wspierają jednak atomową manipulację w obrębie jednej agregacji. Oznacza to, że jeżeli chcemy manipulować wieloma agregacjami na raz, musimy zadbać o atomowość w kodzie aplikacji. W praktyce w większości przypadków będziemy w stanie utrzymać nasze atomowe potrzeby w obrębie jednej agregacji; co więcej, jest to jedna z rzeczy, które należy wziąć pod uwagę podczas projektowania agregacji. Warto też pamiętać, że bazy grafowe i inne bazy ignorujące agregacje przeważnie wspierają transakcje ACID w sposób podobny jak bazy relacyjne. Problem spójności wykracza jednak daleko poza kwestię wspierania przez bazę transakcji ACID — więcej na ten temat opowiemy w rozdziale 5.

2.2. Modele klucz – wartość i dokumentów Wcześniej wspomnieliśmy, że bazy typu klucz – wartość i dokumentów są mocno zorientowane na agregacje. Mówiąc to, mieliśmy na myśli, że bazy takie są naszym zdaniem oparte w głównej mierze na agregacjach. Oba typy baz zawierają dużą liczbę agregacji, a każda agregacja posiada klucz lub identyfikator pozwalający do niej dotrzeć. Różnica pomiędzy modelami polega na tym, że w bazie typu klucz – wartość dane są przeźroczyste dla bazy — są tylko zestawem zazwyczaj nic nieznaczących bitów. W bazie dokumentów baza jest natomiast w stanie zajrzeć w strukturę agregacji. Zaletą przeźroczystości jest to, że w bazie możemy przechowywać, co tylko chcemy. Baza może wprowadzać ograniczenia odnośnie rozmiaru, ale poza tym mamy zupełną swobodę. Baza dokumentów ogranicza to, co możemy w niej przechowywać, poprzez definicje dopuszczalnych struktur i typów. W zamian otrzymujemy większą elastyczność w dostępie do danych.

35

36

ROZDZIAŁ 2. AGREGACYJNE MODELE DANYCH

W bazie klucz – wartość dostęp do agregacji możemy uzyskać tylko za pośrednictwem jej klucza. W bazie dokumentów możemy do bazy wysyłać zapytania w oparciu o pola agregacji, możemy pobrać tylko część agregacji, a baza może tworzyć indeksy na podstawie zawartości agregacji. W praktyce granica między bazami klucz – wartość a bazami dokumentów zaciera się. Ludzie umieszczają często w bazie dokumentów pole z identyfikatorem w celu wyszukiwania na zasadzie klucz – wartość. Bazy klucz – wartość mogą pozwalać na przechowywanie struktur wykraczających poza przeźroczyste agregacje. Na przykład Riak pozwala dodawać do agregacji metadane na potrzeby indeksowania i relacji między agregacjami, a Redis pozwala rozbijać agregacje do poziomu list lub zestawów. Możesz wprowadzić wsparcie dla zapytań za pośrednictwem narzędzi takich jak Solr — np. Riak zawiera funkcjonalność wyszukiwania zbliżoną do możliwości, jakie daje Solr; pozwala przeszukiwać wszystkie agregacje zapisane w formacie JSON lub XML. Pomimo zacierania się granic główne różnice pozostają. W bazach klucz – wartość spodziewamy się, że agregacje będą pobierane przede wszystkim za pomocą klucza. W bazach dokumentów przeważnie wykonamy jakiegoś rodzaju zapytanie w oparciu o wewnętrzną strukturę dokumentu; możemy wybrać po kluczu, ale najprawdopodobniej będzie to coś innego.

2.3. Magazyny rodziny kolumn Jedną z pierwszych i bardziej wpływowych baz NoSQL jest baza BigTable stworzona przez Google [Chang i inni]. Jej nazwa nawiązywała do struktury tabelarycznej, która realizowana była za pośrednictwem luźnych kolumn i bez wykorzystania schematu. Jak wkrótce zobaczysz, myślenie o tej strukturze jako o tabeli wcale nie pomaga; jest to raczej dwuwymiarowa mapa. Jakkolwiek byś jednak o niej nie myślał, był to model, który w znacznym stopniu wpłynął na późniejsze bazy HBase i Cassandra. Te bazy, mające model danych zbliżony do bazy BigTable, nazywane są często magazynami kolumn, jednak nazwa ta istnieje już od dawna i opisuje inne zjawisko. Magazyny kolumn istniejące przed erą NoSQL, takie jak C-Store, wykorzystywały SQL i model relacyjny. To, co czyniło je innymi, to sposób fizycznego przechowywania danych. Większość baz wykorzystuje wiersz jako jednostkę przechowywania — pozwala to poprawić wydajność zapisu. Istnieje jednak wiele scenariuszy, w których dane zapisywane są rzadko, ale często występują odczyty kilku kolumn wielu wierszy. W takich sytuacjach lepiej przechowywać jako jednostkę grupę kolumn dla wszystkich wierszy, dlatego te bazy nazywane są magazynami danych. BigTable i jego potomkowie przechowują właśnie grupy kolumn (rodziny kolumn), jednak w przeciwieństwie do C-Store i jemu podobnych porzucają model relacyjny i język SQL. W tej książce będziemy tę klasę baz danych nazywać bazami rodziny kolumn. Być może najlepszym sposobem wizualizacji rodziny kolumn jest dwupoziomowa struktura agregująca. Podobnie jak w przypadku baz klucz – wartość, pierwszy klucz jest zazwyczaj nazywany identyfikatorem wiersza, pozwalającym wybrać interesującą nas agregację. Różnica polega na tym, że w bazach rodziny kolumn agregacja wiersza stworzona jest z mapy bardziej szczegółowych wartości. Te wartości drugiego poziomu nazywane są kolumnami.

2.3. MAGAZYNY RODZINY KOLUMN

Oprócz pobierania całego wiersza operacje pozwalają wybrać poszczególne kolumny, tak więc aby wybrać nazwę konkretnego klienta z rysunku 2.5, mógłbyś wykonać operację w stylu get('1234', 'nazwa').

Rysunek 2.5. Reprezentacja informacji o kliencie w strukturze rodziny kolumn Bazy rodziny kolumn organizują kolumny w rodziny. Każda kolumna musi być częścią pojedynczej rodziny i funkcjonuje jako jednostka dostępu, z założeniem, że dana rodzina kolumn będzie zazwyczaj pobierana razem. O strukturze, w jaką organizowane są dane, możesz myśleć na kilka sposobów. ■ Skupiając się na wierszu: każdy wiersz jest agregacją (np. klient o identyfikatorze 1234), z rodzinami kolumn reprezentującymi użyteczne części danych (profil, historia zamówień) wewnątrz tej agregacji. ■ Skupiając się na kolumnach: każda rodzina kolumn definiuje typ rekordu (np. profile klientów) z wierszami dla każdego z rekordów. Wiersz funkcjonuje teraz jako złączenie rekordów ze wszystkich rodzin kolumn. Ten drugi aspekt odzwierciedla kolumnową naturę baz rodziny kolumn. Ponieważ baza posiada informacje o najczęstszych grupowaniach danych, może wykorzystać te informacje przy przechowywaniu i udostępnianiu danych. Co więcej, mimo że baza dokumentów deklaruje strukturę w bazie danych, każdy dokument nadal postrzegany jest jako osobna jednostka. Rodziny kolumn wprowadzają dwuwymiarową jakość do baz rodziny kolumn. Ta terminologia została ustalona przez BigTable i HBase, ale Cassandra patrzy na rzeczy nieco inaczej. Wiersz w Cassandrze pojawia się tylko w rodzinie jednokolumnowej, rodzina

37

38

ROZDZIAŁ 2. AGREGACYJNE MODELE DANYCH

ta jednak może posiadać superkolumny — kolumny zawierające zagnieżdżone kolumny. Superkolumny w Cassandrze są najbliższym ekwiwalentem klasycznych rodzin kolumn znanych z BigTable. Myślenie o rodzinach kolumn jako o tabelach może być zwodnicze. Możesz dodawać dowolne kolumny do wierszy, a wiersze mogą mieć bardzo różne zestawy kolumn. O ile nowe kolumny mogą być dodawane do wierszy w trybie normalnego dostępu do danych, definiowanie nowej rodziny kolumn jest rzadsze i może wiązać się z koniecznością zatrzymania bazy danych. Przykład z rysunku 2.5 przedstawia jeszcze jeden aspekt baz rodziny kolumn, który może sprawiać problemy osobom przyzwyczajonym do baz relacyjnych: rodzina kolumn zamówienia. Ponieważ kolumny można dowolnie dodawać, możesz zamodelować listę rzeczy, tworząc dla każdej z nich odrębną kolumnę. To może wydawać się bardzo dziwne, jeżeli myśli się o rodzinie kolumn jako o tabeli, ale całkiem normalne, jeżeli myślisz o niej jako o agregacji. Cassandra używa terminów szeroki i chudy. Chude wiersze to takie, które mają niewiele kolumn, z kolumnami powtarzającymi się w wielu rekordach. W takim przypadku rodzina kolumn definiuje typ rekordu, każdy wiersz jest rekordem, a każda kolumna jest polem. Szeroki wiersz ma wiele kolumn (nawet tysiące), a kolumny poszczególnych wierszy różnią się. Rodzina kolumn dla szerokich wierszy modeluje listę, a każda kolumna jest elementem tej listy. Konsekwencją rodzin szerokich kolumn jest to, że rodzina kolumn może zdefiniować porządek sortowania dla jej kolumn. Dzięki temu możemy uzyskać dostęp do zamówienia lub zestawu zamówień za pośrednictwem kluczy porządkowych. Może się wydawać, że nie jest to użyteczne, jeżeli nadalibyśmy klucze zamówieniom na podstawie ich identyfikatora, jednak byłoby, gdyby klucz byłby konkatenacją daty i identyfikatora (np. 20111027-1001). Mimo że wygodnie jest rozdzielić wiersze na chude i szerokie, nie ma technicznego powodu, dla którego zarówno kolumny przechowywane jako pola, jak i te przechowywane jako listy nie mogłyby egzystować w obrębie tej samej rodziny kolumn — takie podejście zaburzyłoby jednak porządek sortowania.

2.4. Podsumowanie baz zorientowanych na agregacje Do tego miejsca omówiliśmy wystarczająco dużo, abyś miał całkiem niezłe ogólne pojęcie na temat trzech różnych rodzajów baz zorientowanych na agregacje i różnic między nimi. Ich cechą wspólną jest idea agregacji indeksowanej za pomocą klucza, z którego możesz skorzystać do jej pobrania. Ta agregacja jest podstawą działania w klastrze, ponieważ baza danych dba, aby wszystkie dane zawarte w agregacji przechowywane były na jednym serwerze. Agregacja odgrywa też rolę jednostki atomowej podczas aktualizacji danych, dzięki czemu otrzymujemy użyteczną, choć ograniczoną, kontrolę transakcyjną. Różnice pomiędzy poszczególnymi typami baz agregacyjnych zawarte są wewnątrz notacji agregacji. Model klucz – wartość traktuje agregacje jako przeźroczystą całość, co oznacza, że dane możesz pobierać jedynie za pośrednictwem klucza — nie możesz wykonać zapytania ani pobrać części agregacji.

2.5. DALSZA LEKTURA

W modelu dokumentu baza danych zna strukturę agregacji, dzięki czemu możesz wykonywać zapytania i pobierać dowolne fragmenty danych. Ponieważ jednak dokument nie ma zdefiniowanej struktury, baza danych nie jest w stanie na jej podstawie optymalizować przechowywania i pobierania części agregacji. Bazy rodziny kolumn dzielą agregacje na rodziny kolumn, co pozwala bazie traktować je jako jednostkę danych w obrębie wiersza. To wprowadza do agregacji pewną strukturę, którą baza potrafi wykorzystać w celu podniesienia elastyczności dostępu do danych.

2.5. Dalsza lektura Jeżeli chcesz uzyskać więcej informacji na temat agregacji, które są często wykorzystywane także w bazach relacyjnych, patrz [Evans]. Społeczność Domain-Driven Design jest najlepszym źródłem informacji na temat agregacji; najnowsze wiadomości pojawiają się z reguły pod adresem http://domaindrivendesign.org/.

2.6. Najważniejsze kwestie ■ Agregacja jest zestawem danych, które funkcjonują w bazie jako jednostka. ■ Bazy klucz – wartość, dokumentów i rodziny kolumn to bazy zorientowane na agregacje. ■ Dzięki agregacjom przechowywanie danych w klastrach jest łatwiejsze. ■ Bazy zorientowane na agregacje działają najlepiej, jeżeli interakcje z danymi podejmowane są w ramach jednej agregacji; bazy nieoperujące na agregacjach lepiej radzą sobie, jeżeli interakcje wykorzystują dane z wielu różnych formacji.

39

40

ROZDZIAŁ 2. AGREGACYJNE MODELE DANYCH

Rozdział 3

Więcej szczegółów na temat modelów danych

Do tej pory omówiliśmy podstawową cechę większości baz NoSQL: sposób wykorzystania agregacji i różne sposoby ich budowania w bazach zorientowanych na agregacje. Agregacje są podstawą w tego typu bazach danych, ale są też inne aspekty modelowania danych — to właśnie nimi zajmiemy się w tym rozdziale.

3.1. Relacje Agregacje grupują dane, które zazwyczaj pobierane są razem. Jest jednak wiele przypadków, kiedy dane skorelowane ze sobą pobierane są rozdzielnie. Rozważmy relację między klientem a wszystkimi jego zamówieniami. W niektórych przypadkach będziemy chcieli uzyskać dane dotyczące klienta wraz z historią jego zamówień — wtedy warto połączyć klienta i zamówienia w jedną agregację. W innych przypadkach zamówienia przetwarzane będą pojedynczo, czyli powinny być zamodelowane jako osobne agregacje. W takim przypadku zamówienia i klienci powinny być osobnymi, ale połączonymi relacjami agregacjami, tak aby podczas pracy z zamówieniami można było uzyskać dostęp do danych klienta. Najprostszy sposób to umieszczenie w danych zamówienia identyfikatora (ID) klienta. Dzięki temu jeżeli będziesz potrzebować danych z rekordu klienta, odczytujesz zamówienie, pobierasz identyfikator i w kolejnym zapytaniu do bazy pobierasz dane klienta. Takie rozwiązanie zadziała i doskonale sprawdzi się w wielu przypadkach, jednak baza danych nie będzie posiadała informacji o połączeniu między klientem a zamówieniem. To ważne, ponieważ w wielu przypadkach dobrze jest, aby baza wiedziała o tego typu przypadkach. W rezultacie wiele baz — w tym także bazy klucz – wartość — udostępnia mechanizmy do definiowania takich relacji. W magazynach dokumentów dane agregacji są widoczne dla bazy, dzięki czemu możliwe jest tworzenie indeksów i zapytań. Riak, magazyn klucz – wartość, pozwala wstawiać informacje o połączeniach w metadanych, wspierając częściowe pobieranie i możliwość poruszania się po relacjach.

41

42

ROZDZIAŁ 3. WIĘCEJ SZCZEGÓŁÓW NA TEMAT MODELÓW DANYCH

Ważnym aspektem relacji pomiędzy agregacjami jest sposób obsługi aktualizacji danych. Bazy zorientowane na agregacje traktują agregację jako jednostkę pobierania danych. W konsekwencji atomowość wspierana jest tylko wewnątrz pojedynczej agregacji. Jeżeli jednocześnie aktualizujesz wiele agregacji, musisz sam obsłużyć możliwość częściowego niepowodzenia aktualizacji. Bazy relacyjne zajmują się tym za ciebie i pozwalają modyfikować wiele rekordów w ramach jednej transakcji, gwarantując zachowanie zasad ACID podczas aktualizacji. Oznacza to, że bazy zorientowane na agregacje są mniej wygodne podczas pracy z wieloma agregacjami. Z tym problemem można sobie poradzić na kilka sposobów — omówimy je w dalszej części tego rozdziału — problem jednak pozostaje. W związku z tym jeżeli posiadasz dane połączone dużą liczbą relacji, powinieneś raczej wybrać model relacyjny zamiast bazy NoSQL. O ile jest to prawdą w przypadku baz zorientowanych na agregacje, warto pamiętać, że bazy relacyjne także nie są doskonałe w przypadku rozbudowanych relacji. W SQL możesz używać zapytań operujących na wielu tabelach, jednak sprawy komplikują się wraz z rosnącą liczbą połączeń — spada czytelność i wydajność zapytań. W tym miejscu warto przedstawić inną kategorię baz danych często wrzucaną do jednego worka z bazami NoSQL.

3.2. Bazy grafowe Bazy grafowe to dziwna ryba w stawie NoSQL. Większość baz NoSQL została stworzona z myślą o pracy w klastrach, dzięki czemu popularność zyskał model zorientowany na duże rekordy agregacji połączone prostymi relacjami. Powstanie baz grafowych zostało spowodowane innym problemem związanym z bazami relacyjnymi, a co za tym idzie model baz grafowych jest odwrotny: funkcjonują tu niewielkie rekordy z rozbudowanymi połączeniami między nimi, podobnie jak na rysunku 3.1.

Rysunek 3.1. Przykład struktury grafu

3.2. BAZY GRAFOWE

W tym kontekście graf nie jest wykresem słupkowym ani histogramem; dane w strukturze grafowej składają się z węzłów połączonych krawędziami. Na rysunku 3.1 przedstawiliśmy sieć informacji z bardzo małymi węzłami (zawierają tylko nazwę), ale z bardzo rozbudowaną siecią połączeń. Dzięki tej strukturze możemy zadawać pytania w stylu: „znajdź książki z kategorii »bazy danych« napisane przez kogoś, kogo lubi ktoś z moich znajomych”. Bazy grafowe specjalizują się w przechowywaniu tego typu informacji, ale na znacznie większą skalę, niż jesteśmy w stanie pokazać na czytelnym diagramie. Jest to idealna struktura do przechowywania danych zawierających skomplikowane powiązania, takie jak sieci społecznościowe, rekomendacje produktów czy reguły uprawnień. Fundamentalny model bazy grafowej jest bardzo prosty i składają się na niego węzły powiązane przez krawędzie (zwane też łukami). Poza tą ogólną zasadą w modelach danych istnieje duże zróżnicowanie, w szczególności różne są mechanizmy przechowywania danych udostępniane przez bazy. Prosty przykład pomoże zilustrować różnorodność systemów baz grafowych: FlockDB zawiera jedynie węzły i krawędzie, bez możliwości zapisu dodatkowych właściwości; Neo4J pozwala do węzłów i krawędzi dołączać obiekty Java jako ich właściwości (11.2, „Funkcjonalności”); Infinite Graph jako węzły i krawędzie przechowuje obiekty Java, będące podklasami typów wbudowanych. Kiedy już zbudowałeś graf węzłów i krawędzi, baza pozwala odpytywać powstałą sieć za pomocą zapytań zaprojektowanych właśnie do tego. Tutaj uwidaczniają się istotne różnice pomiędzy bazami grafowymi a bazami relacyjnymi. Mimo że bazy relacyjne mogą implementować relacje za pomocą kluczy obcych, wymagane w zapytaniach złączenia mogą być kosztowne, co oznacza, że wydajność w przypadku danych z dużą liczbą relacji może być niezadowalająca. W bazach grafowych trawersowanie po zależnościach jest tanie. Jest tak, ponieważ bazy grafowe większość pracy związanej z przemieszczaniem się po relacjach przenoszą z zapytań na wstawianie. Oczywiście rozwiązanie to sprawdza się w sytuacjach, kiedy szybkość uzyskiwania danych jest ważniejsza od szybkości ich wstawiania. Większość operacji polega na przemieszczaniu się po sieci krawędzi za pomocą zapytań w stylu: „pokaż mi wszystkie rzeczy, które lubią Anna i Barbara”. Potrzebujesz miejsca początkowego, więc niektóre węzły mogą zostać zaindeksowane za pomocą identyfikatorów. Zaczynasz więc od jakiegoś ID (np. „sprawdź osoby o imionach Anna i Barbara”), a następnie korzystasz z krawędzi. Mimo to w bazach grafowych większość operacji wciąż polega na nawigowaniu po relacjach. Nacisk na relacje powoduje, że bazy grafowe bardzo różnią się od baz zorientowanych na agregacje. Różnica w modelu danych powoduje różnice także w innych aspektach; takie bazy częściej będą działały na pojedynczym serwerze niż w klastrze. Aby spójność została zachowana, transakcje ACID muszą operować na wielu węzłach i krawędziach. Jedyna wspólna cecha z bazami zorientowanymi na agregacje to odrzucenie modelu transakcyjnego i wzrost popularności w tym samym okresie, w którym na popularności zyskały inne bazy NoSQL.

43

44

ROZDZIAŁ 3. WIĘCEJ SZCZEGÓŁÓW NA TEMAT MODELÓW DANYCH

3.3. Bazy danych bez schematu Bazy NoSQL najczęściej nie mają schematu danych. Jeżeli chcesz przechowywać dane w bazie relacyjnej, musisz najpierw zdefiniować schemat — strukturę, która mówi, jakie tabele istnieją w bazie, jakie mają kolumny i jakie typy danych mogą być przechowywane w poszczególnych kolumnach. Zanim wprowadzisz dane do bazy, musisz mieć dla nich zdefiniowany schemat. Z bazami NoSQL przechowywanie danych jest dużo prostsze. Magazyn klucz – wartość pozwala Ci wstawić dowolne dane pod wskazanym kluczem. Baza dokumentów daje właściwie takie same możliwości, ponieważ nie ma ograniczeń co do struktury przechowywanego dokumentu. Bazy rodziny kolumn pozwalają przechowywać dowolne dane w dowolnej kolumnie. Bazy grafowe pozwalają dodawać nowe krawędzie i właściwości do krawędzi i węzłów wedle potrzeb. Zwolennicy braku schematu cieszą się z tej wolności i elastyczności. Budując schemat, musisz z góry wiedzieć, jakie dane będziesz przechowywać — czasami jest to trudne. Bez ograniczeń związanych ze schematem możesz bezproblemowo przechowywać wszystko, co chcesz. Dzięki temu możesz zmieniać swój magazyn danych w miarę postępów w projekcie. Możesz z łatwością dodawać nowe rzeczy. Co więcej, jeżeli okaże się, że niektórych rzeczy już nie potrzebujesz, możesz po prostu przestać je przechowywać, bez obawy, że stracisz starsze dane — jeżeli chciałbyś usunąć kolumnę ze schematu bazy relacyjnej, musiałbyś zachować ostrożność. Oprócz łatwości wprowadzania zmian bazy bez schematu pozwalają też łatwiej przechowywać dane niestandardowe: takie, w których każdy rekord ma inną liczbę kolumn. W schemacie wszystkie wiersze muszą mieć jednakową liczbę kolumn, co staje się problemem, jeżeli masz różne rodzaje danych w poszczególnych wierszach. W takim przypadku albo będziesz mieć wiele kolumn z wartościami null (tabela rozrzedzona), albo będziesz mieć kolumny, których nazwy nic nie znaczą (np. niestandardowa kolumna 4). Brak schematu pozwala tego uniknąć; każdy wiersz musi zawierać tylko to, co potrzebuje — nie mniej i nie więcej. Brak schematu jest pociągający i na pewno pozwala uniknąć wielu problemów występujących w bazach ze schematem, wprowadza jednak także kilka problemów. Jeżeli tylko przechowujesz jakieś dane, a następnie wyświetlasz je w raporcie jako prostą listę nazwaKolumny: wartość, schemat będzie tylko przeszkadzał. Często robimy jednak z naszymi danymi znacznie więcej i robimy to za pośrednictwem programów, które muszą wiedzieć, że adres płatnika nazywa się adresPlatnika, a nie platnikAdres, i że pole liczba zawiera wartość całkowitą 5, a nie ciąg znaków. Faktem jest (choć czasami jest to fakt niewygodny), że pisząc program mający dostęp do danych, prawie zawsze korzystamy z jakiegoś domniemanego schematu. Chyba że jest to tylko coś takiego: // pseudokod foreach (Record r in records) { foreach (Field f in r.fields) { print (f.name, f.value) } }

3.3. BAZY DANYCH BEZ SCHEMATU

W przeciwnym wypadku program będzie zakładał, że istnieją pola o danych nazwach, zawierające dane o konkretnym znaczeniu; założy także, że dane mają jakiś konkretny typ. Programy to nie ludzie, nie mogą przeczytać wartości wlk i zinterpretować ją jako wielkość — chyba że dokładnie tak je zaprogramujemy. Nieważne więc, że nasza baza nie posiada schematu — i tak musi istnieć domniemany schemat. Ten domniemany schemat to zestaw założeń w kodzie manipulującym danymi, dotyczących struktury tych danych. Posiadanie w kodzie domniemanego schematu wprowadza kilka problemów. Aby zrozumieć, jakie dane zawarte są w magazynie, musisz zagłębić się w kod aplikacji. Jeżeli ten kod jest dobrze napisany, powinieneś móc znaleźć miejsce, z którego łatwo odczytasz schemat. Nie ma jednak gwarancji — wszystko zależy od tego, jak dobra jest struktura kodu. Co więcej, baza danych nie uznaje schematu — nie może wykorzystać go do określenia, jak lepiej przechowywać i pobierać dane. Nie może walidować danych, aby zapewnić, że różne aplikacje nie manipulują nimi w sposób niespójny. Są to powody, dla których bazy relacyjne i większość baz w przeszłości korzystały ze schematu. Schematy są wartościowe, a odrzucenie je przez bazy NoSQL jest zdumiewające. Baza bez schematu przenosi schemat na stronę aplikacji korzystającej z danych. Staje się to problemem, jeżeli do tej samej bazy dostęp uzyskuje kilka aplikacji tworzonych przez różne osoby. Problemy te można złagodzić na kilka różnych sposobów. Jednym z podejść jest enkapsulacja interakcji z bazą za pomocą jednej aplikacji, a następnie integracja tej aplikacji z innymi za pośrednictwem usług sieciowych. To podejście dobrze pasuje do aktualnego trendu wykorzystania usług Web Services do integracji aplikacji. Innym podejściem jest jasne wyznaczenie różnych obszarów agregacji, do których dostęp będą miały różne aplikacje. Mogą to być różne sekcje w bazie dokumentów lub różne rodziny kolumn w bazie rodziny kolumn. Mimo że fani baz NoSQL często krytykują schematy za to, że muszą być z góry definiowane, i za brak ich elastyczności, nie jest to do końca prawdą. Schematy relacyjne mogą być zmieniane w dowolnym momencie za pomocą standardowych poleceń SQL. Jeżeli jest to konieczne, możesz w locie tworzyć nowe tabele do przechowywania danych niestandardowych. Rzadko widzi się takie rozwiązania, ale tam, gdzie się z tym spotkaliśmy, działały całkiem dobrze. W większości przypadków dane nieposiadające jednego schematu są jednak dobrym powodem do rozważenia baz NoSQL. Brak schematu ma duży wpływ na zmiany zachodzące z czasem w strukturze bazy, w szczególności dla bardziej jednolitych danych. Mimo że nie jest to praktykowane tak szeroko, jak powinno być, zmiana relacyjnego schematu danych może być wykonywana w sposób kontrolowany. Podobnie musisz kontrolować sytuację podczas zmiany sposobu przechowywania danych w bazie nieposiadającej schematu, tak abyś mógł bez problemu uzyskać dostęp do nowych i starych danych. Co więcej, elastyczność zapewniana przez brak schematu występuje tylko wewnątrz agregacji. Jeżeli musisz zmienić granice agregacji, modyfikacje są tak skomplikowane jak w przypadku baz transakcyjnych. W dalszej części porozmawiamy szerzej na temat migracji baz danych (12.1, „Zmiany schematu”).

45

46

ROZDZIAŁ 3. WIĘCEJ SZCZEGÓŁÓW NA TEMAT MODELÓW DANYCH

3.4. Widoki zmaterializowane Kiedy rozmawialiśmy o modelach zorientowanych na agregacje, podkreślaliśmy ich zalety. Jeżeli chcesz uzyskać dostęp do zamówień, warto jest mieć wszystkie dane zamówienia zawarte w jednej agregacji, która może być przechowywana i pobierana jako jednostka. Orientacja na agregacje ma jednak związaną z tym wadę. Co, jeżeli menedżer produktu chce wiedzieć, ile razy dany przedmiot został sprzedany w ciągu ostatnich kilku tygodni? Wówczas orientacja na agregacje staje się Twoim wrogiem, a w celu poznania odpowiedzi zostajesz zmuszony do odczytu wszystkich rekordów w bazie. Możesz zmniejszyć skalę problemu, tworząc indeks dla produktu, jednak wciąż będziesz pracować na strukturze agregacji. Bazy relacyjne mają w takich przypadkach przewagę, ponieważ brak agregacji pozwala na zapewnianie dostępu do danych w inny sposób. Co więcej, zawierają wygodny mechanizm pozwalający na pobieranie danych w inny sposób, niż są przechowywane — widoki. Widok jest jak tabela relacyjna (jest relacją), jest jednak definiowany jako połączenie istniejących tabel. Kiedy uzyskujesz dostęp do widoku, baza przetwarza jego dane — jest to wygodny sposób enkapsulacji. Widoki pozwalają ukryć przed klientem, czy dane pochodzą z widoku, czy z danych bazowych, nie można jednak przeoczyć faktu, że przetwarzanie niektórych widoków może być kosztowne. Aby sobie z tym poradzić, wymyślone zostały widoki zmaterializowane — są to widoki przetworzone wcześniej i zapisane na dysku. Widoki zmaterializowane sprawdzają się dla danych, których odczyt jest kosztowny, ale które są w miarę stałe. Mimo że bazy NoSQL nie mają widoków, mają możliwość wcześniejszego przeliczania i przechowywania w pamięci wyników działań zapytań, a do ich opisu wykorzystują pojęcie widoku zmaterializowanego. Zapytania są istotniejszym elementem niż w przypadku baz relacyjnych, ponieważ większość aplikacji będzie musiała zmagać się z zapytaniami, które nie pasują do struktury agregacji (bazy NoSQL tworzą często widoki zmaterializowane za pośrednictwem modelu map-reduce, który omówimy w rozdziale 7.). Istnieją dwie ogólne strategie budowania widoków zmaterializowanych. Pierwsza z nich to podejście gorliwe: aktualizujesz widok zmaterializowany w momencie, w którym aktualizujesz dane z nim związane. W tym przypadku dodanie zamówienia wywołałoby aktualizację agregacji z historią zakupów dla każdego z produktów. To podejście jest dobre, jeżeli częściej odczytujesz widok, niż aktualizujesz dane z nim związane, i chcesz, aby widok był zawsze aktualny. Podejście bazy aplikacji (podrozdział 1.3) jest tutaj bardziej na miejscu, ponieważ łatwiej wtedy zadbać o to, aby każda aktualizacja danych powodowała aktualizację widoków. Jeżeli nie chcesz spowalniać każdej aktualizacji danych, możesz uruchamiać zadania wsadowe aktualizujące widoki zmaterializowane w regularnych odstępach czasu. Musisz zrozumieć swoje wymagania biznesowe, aby określić, jak aktualne muszą być Twoje widoki zmaterializowane. Możesz tworzyć widoki zmaterializowane poza bazą danych, odczytując dane, przetwarzając widok i zapisując je z powrotem w bazie. Najczęściej jednak bazy wspierają budowanie widoków wewnątrz bazy. W tym przypadku przekazujesz obliczenia, które muszą zostać wykonane, a baza wykonuje je wtedy, kiedy jest to konieczne, zgodnie z zadanymi parametrami. Jest to szczególnie użyteczne w przypadku aktualizacji gorliwych z inkrementacyjnym modelem map-reduce (7.3.2, „Inkrementacyjny map-reduce”).

3.5. MODELOWANIE Z MYŚLĄ O DOSTĘPIE DO DANYCH

Widoki zmaterializowane mogą być wykorzystywane wewnątrz tej samej agregacji. Dokument z zamówieniem może zawierać element z podsumowaniem zamówienia, dzięki czemu zapytanie o podsumowanie zamówienia nie będzie musiało przekazywać całego dokumentu. Możliwość wykorzystania różnych rodzin kolumn w widokach zmaterializowanych jest często spotykaną funkcjonalnością w bazach rodziny kolumn. Zaletą takiego podejścia jest to, że pozwala zaktualizować widok zmaterializowany wewnątrz tej samej atomowej operacji.

3.5. Modelowanie z myślą o dostępie do danych Jak już wcześniej wspominaliśmy, modelując agregacje, musimy myśleć o tym, jak dane będą odczytywane, a także o konsekwencjach, jakie może spowodować przypisanie danych do agregacji. Zacznijmy od przykładu, w którym wszystkie dane dotyczące klienta są osadzone w magazynie klucz – wartość (rysunek 3.2).

Rysunek 3.2. Osadzenie wszystkich obiektów klientów i ich zamówień W tym scenariuszu aplikacja może pobrać dane dotyczące klienta i wszystkie dane z nim związane za pomocą jednego klucza. Jeżeli konieczne jest pobranie zamówień lub produktów sprzedanych w poszczególnych zamówieniach, cały obiekt musi zostać odczytany, a następnie przetworzony po stronie aplikacji w celu wyodrębnienia interesujących nas danych. Jeżeli wymagane są referencje, moglibyśmy zmienić bazę na bazę dokumentów, a następnie wykonywać

47

48

ROZDZIAŁ 3. WIĘCEJ SZCZEGÓŁÓW NA TEMAT MODELÓW DANYCH

zapytania wewnątrz dokumentów lub nawet zmienić dane w magazynie klucz – wartość, aby podzielić obiekt na klienta (Klient) i zamówienie (Zamowienie), a referencje przechowywać wewnątrz obiektów. Mając referencje (rysunek 3.3), możemy pobierać zamówienia niezależnie od klienta, a dzięki referencji zamowienieId wewnątrz obiektu klienta możemy odnaleźć wszystkie jego zamówienia. Korzystając z agregacji w ten sposób, optymalizujemy odczyt danych, jednak musimy wstawiać zamowienieId do obiektu klienta za każdym razem, kiedy powstanie dla niego nowe zamówienie. # obiekt Klient{ "klientId": 1, "klient": { "nazwa": "Marcin", "adresPlatnika": [{"miasto": "Warszawa"}], "platnosc": [{"typ": "debetowa","numerKarty": "1000-1000-1000-1000"}], "zamowienia":[{"zamowienieId":99}] } } # obiekt Zamowienie { "klientId": 1, "zamowienieId": 99, "zamowienie":{ "dataZamowienia":"20.11.2011", "pozycjeZamowienia":[{"produktId":27, "cena": 32.45}], "zamowieniePlatnosc":[{"numerKarty":"1000-1000-1000-1000", "NIP":"abelif879rft"}], "adresWysylki":{"miasto":"Warszawa"} } }

Agregacje mogą też być wykorzystywane do zdobywania danych analitycznych; na przykład aktualizacja agregacji może uzupełniać informacje, które zamówienie posiada w sobie dany produkt. Taka denormalizacja danych daje nam szybki dostęp do danych, którymi jesteśmy zainteresowani, i jest podstawą analiz danych w czasie rzeczywistym, gdzie firmy nie muszą polegać na zadaniach działających na koniec dnia i zasilających dane analityczne; dane te są teraz aktualizowane na bieżąco, dla wielu wymagań, w momencie składania przez klienta zamówienia. { "elementid":27, "zamowienia":{99,545,897,678} } { "elementid":29, "zamowienia":{199,545,704,819} }

Ponieważ w bazach dokumentów możemy wykonywać zapytania do danych wewnątrz agregacji, możliwe jest usunięcie z obiektu klienta referencji do zamówień. Taka zmiana pozwala nam nie aktualizować obiektu klienta w momencie składania zamówienia.

3.5. MODELOWANIE Z MYŚLĄ O DOSTĘPIE DO DANYCH

Rysunek 3.3. Klient przechowywany jest odrębnie z zamówieniami # obiekt Klient { "klientId": 1, "nazwa": "Marcin", "adresPlatnika": [{"miasto": "Warszawa"}], "platnosc": [ {"typ": "debetowa", "numerKarty": "1000-1000-1000-1000"} ] } # obiekt Zamowienie { "zamowienieId": 99, "klientId": 1, "dataZamowienia":"20.11.2011", "pozycjeZamowienia":[{"produktId":27, "cena": 32.45}], "zamowieniePlatnosc":[{"numerKarty":"1000-1000-1000-1000", "NIP":"abelif879rft"}], "adresWysylki":{"miasto":"Warszawa"} }

49

50

ROZDZIAŁ 3. WIĘCEJ SZCZEGÓŁÓW NA TEMAT MODELÓW DANYCH

Ponieważ magazyny dokumentów pozwalają wykonywać zapytania na podstawie atrybutów wewnątrz dokumentu, wyszukiwania takie jak „znajdź wszystkie zamówienia zawierające produkt Refaktoryzacja Baz Danych” są możliwe, jednak decyzja o stworzeniu agregacji obiektów i zamówień, do których należą, nie jest oparta na możliwościach wykonywania zapytań do bazy, ale na optymalizacji odczytu wymaganej przez aplikację. Kiedy modelujemy dane dla baz rodziny kolumn, mamy zaletę w postaci kolejności sortowania kolumn, możemy więc wskazać często wykorzystywane kolumny, dzięki czemu będą one pobierane jako pierwsze. Korzystając z rodzin kolumn do modelowania danych, warto pamiętać, aby robić to z myślą o przyszłych zapytaniach, a nie o szybkości zapisu. Należy dążyć do jak najprostszych zapytań i denormalizować dane podczas zapisu.

Rysunek 3.4. Koncepcyjny widok danych w magazynie rodziny kolumn Jak możesz sobie wyobrazić, jest wiele sposobów na modelowanie danych; jeden z nich to przechowywanie klienta (Klient) i zamówienia (Zamowienie) w różnych rodzinach kolumn (rysunek 3.4). W tym przypadku warto zauważyć, że referencje do wszystkich zamówień

3.6. NAJWAŻNIEJSZE KWESTIE

składanych przez klienta umieszczane są wewnątrz rodziny kolumn klienta. Podobnie inne normalizacje robione są zazwyczaj z myślą o poprawieniu szybkości zapytań (odczytu). Jeżeli korzystamy z baz grafowych do zamodelowania tych samych danych, modelujemy wszystkie obiekty jako węzły, a relacje między nimi jako krawędzie; typ i kierunek krawędzi ma znaczenie. Każdy z węzłów połączony jest niezależnymi relacjami z innymi węzłami. Te relacje mają nazwy takie jak: ZAKUPIL, ZAPLACONY_ZA_POMOCA czy NALEZY_DO (rysunek 3.5); te nazwy relacji pozwalają Ci poruszać się po grafie. Załóżmy, że chcesz odnaleźć wszystkich klientów (Klient), którzy kupili (ZAKUPIL) produkt o nazwie Refaktoryzacja Baz Danych. Musisz jedynie wykonać zapytanie o węzeł Refaktoryzacja Baz Danych, a następnie poszukać wszystkich węzłów klientów z relacjami ZAKUPIL.

Rysunek 3.5. Model grafowy danych sklepu Takie poruszanie się po relacjach jest w przypadku bazy grafowej bardzo proste. Jest to szczególnie wygodne, jeżeli musisz wykorzystać dane do rekomendacji produktów lub szukać wzorców w akcjach podejmowanych przez użytkowników.

3.6. Najważniejsze kwestie ■ W bazach zorientowanych na agregacje relacje między agregacjami są trudniejsze do obsłużenia niż relacje wewnątrz agregacji.

51

52

ROZDZIAŁ 3. WIĘCEJ SZCZEGÓŁÓW NA TEMAT MODELÓW DANYCH

■ Bazy grafowe organizują dane w grafy węzłów i krawędzi; najlepiej sprawdzają się w przypadku struktur ze skomplikowanymi zależnościami między obiektami. ■ Bazy bez schematu pozwalają swobodnie dodawać pola i rekordy; z reguły użytkownicy danych oczekują jakiegoś nieformalnego schematu. ■ Bazy zorientowane na agregacje często przetwarzają widoki zmaterializowane w celu zapewnienia danych zorganizowanych inaczej niż pierwotne agregacje. Operacje te wykonywane są przeważnie za pośrednictwem mechanizmu map-reduce.

Rozdział 4

Modele dystrybucyjne

Główną cechą napędzającą zainteresowanie bazami NoSQL jest ich zdolność do działania w dużych klastrach. W miarę jak ilość danych rośnie, skalowanie w górę (kupno większego serwera, na którym działa baza danych) staje się coraz trudniejsze i droższe. Wygodniejszą opcją jest skalowanie w bok — uruchomienie bazy na klastrze serwerów. Orientacja na agregacje daje dobre podstawy do skalowania w bok, ponieważ agregacja jest idealną jednostką do dystrybucji. Zależnie od Twojego modelu dystrybucyjnego możesz wykorzystać magazyn danych, który da Ci możliwość obsługi większych ilości danych, możliwość przetworzenia większego ruchu odczytu i zapisu lub większą niezawodność w dostępie do danych. Zazwyczaj są to ważne cechy, jednak wszystko ma swoją cenę. Praca w klastrze wprowadza złożoność — nie jest to więc coś, co powinieneś rozważać, jeżeli nie ma prawdziwej konieczności. Są dwie główne ścieżki dystrybucji: replikacja i współdzielenie. W przypadku replikacji te same dane kopiowane są na wiele serwerów. Współdzielenie to umieszczenie różnych danych na różnych serwerach. Replikacja i współdzielenie to techniki ortogonalne — możesz korzystać tylko z jednej lub z obu. Replikacja przyjmuje dwie formy: master-slave lub peer-to-peer. Omówimy powyższe techniki, rozpoczynając od najprostszej, a następnie przechodząc do bardziej złożonych: najpierw pojedynczy serwer, potem replikacja masterslave, następnie współdzielenie i w końcu replikacja peer-to-peer.

4.1. Pojedynczy serwer Pierwszą i najprostszą opcją dystrybucji jest ta, którą przeważnie polecam — brak dystrybucji, czyli uruchomienie bazy danych na jednej maszynie, która obsługuje wszystkie odczyty i zapisy do magazynu danych. Polecamy tę opcję, ponieważ pozwala wyeliminować problemy istniejące przy innych formach dystrybucji. Taki model nie sprawia problemów ani osobom zajmującym się serwerem, ani programistom korzystającym z bazy. Mimo że wiele baz NoSQL zaprojektowanych jest z myślą o pracy w klastrach, sensowne jest też używanie NoSQL na pojedynczym serwerze, jeżeli model danych oferowany przez NoSQL jest bardziej przystosowany do danych zastosowań. Bazy grafowe są tutaj oczywistym przykładem — działają najlepiej na jednym serwerze. Jeżeli Twoje potrzeby skupiają się na 53

54

ROZDZIAŁ 4. MODELE DYSTRYBUCYJNE

przetwarzaniu agregacji, jednoserwerowy magazyn dokumentów lub baza klucz – wartość mogą być warte rozważenia, ponieważ ułatwią pracę programistom. Przez resztę tego rozdziału będziemy rozważać zalety i problemy związane z bardziej rozbudowanymi modelami dystrybucyjnymi. Nie myśl jednak, że ilość poświęconego miejsca oznacza, że preferujemy te opcje. Jeżeli możemy poradzić sobie bez konieczności dystrybuowania danych, zawsze wybieramy rozwiązanie z pojedynczym serwerem.

4.2. Współdzielenie Zajęty magazyn danych jest często zajęty, ponieważ różne osoby uzyskują dostęp do różnych części danych. W takich przypadkach możemy wykonać skalowanie poziome, umieszczając różne części danych na różnych serwerach — technika ta nazywana jest shardingiem (rysunek 4.1).

Rysunek 4.1. Sharding pozwala umieścić różne dane na osobnych serwerach; każdy z tych serwerów wykonuje własne odczyty i zapisy W idealnym przypadku różni użytkownicy komunikowaliby się z różnymi serwerami. Każdy użytkownik musiałby komunikować się z tylko jednym serwerem, dzięki czemu odpowiedzi na żądania byłyby bardzo szybkie. Ruch rozkładałby się na wszystkie serwery — na przykład jeżeli mamy dziesięć serwerów, każdy musiałby obsłużyć tylko 10% ruchu. Oczywiście sytuacja idealna występuje bardzo rzadko. Aby się do niej zbliżyć, musimy zadbać, aby dane, które pobierane są wspólnie, były przechowywane razem oraz aby organizacja danych była maksymalnie zoptymalizowana pod względem dostępu do danych. Pierwsze pytanie brzmi, jak zgrupować dane tak, aby użytkownik, uzyskując do nich dostęp, odwoływał się w większości do jednego serwera. Tutaj z pomocą przychodzi orientacja na agregacje. Cała idea agregacji polega na budowaniu ich w taki sposób, aby dane, które prze-

4.2. WSPÓŁDZIELENIE

ważnie obsługiwane są wspólnie, były też wspólnie przechowywane — agregacje stają się więc naturalną jednostką dystrybucji. Jeżeli chodzi o organizację danych na serwerze, jest kilka czynników, które mogą pomóc poprawić wydajność. Jeżeli wiesz, że większość dostępu do danych agregacji wychodzi z jednego fizycznego miejsca, możesz umieścić te dane blisko odbiorców. Jeżeli realizujesz zamówienia dla kogoś mieszkającego w Londynie, możesz umieścić dane w centrum danych na Wyspach Brytyjskich. Kolejnym czynnikiem jest rozłożenie obciążenia. Oznacza to, że powinieneś tak organizować agregacje, aby były rozłożone na serwerach równomiernie, dzięki czemu poszczególne serwery będą obciążone w podobnym stopniu. Ten czynnik może być zmienny w czasie, na przykład jeżeli niektóre dane używane są tylko w niektóre dni tygodnia — w tym przypadku mogą więc występować reguły zależne od zastosowania bazy. W niektórych przypadkach warto umieszczać agregacje razem, jeżeli myślisz, że mogą być pobierane w sekwencjach. Dokument o BigTable [Chang i inni] opisał przechowywanie wierszy w porządku leksykograficznym i sortowanie adresów na podstawie odwróconych domen (np. com.martinfowler). Dzięki temu dane z różnych stron mogły być odczytywane razem, co pozwoliło podnieść wydajność przetwarzania. Większość projektantów uwzględniała lub uwzględnia sharding w warstwie logiki aplikacji. Możesz umieścić wszystkich klientów o nazwiskach zaczynających się od A do D na jednym serwerze, a tych, których nazwiska zaczynają się od E do G, na innym. To komplikuje model programistyczny, ponieważ kod aplikacji musi zadbać, aby zapytania były dystrybuowane na różne serwery. Co więcej, zmiana rozłożenia danych na serwerach wiąże się ze zmianami w kodzie aplikacji oraz migracją danych. Wiele baz NoSQL udostępnia opcję auto-sharding, która pozwala przenieść odpowiedzialność za rozdzielanie danych i przekierowywanie zapytań do odpowiedniego serwera na bazę. Takie rozwiązanie jest znacznie prostsze niż sharding za pośrednictwem aplikacji. Sharding jest bardzo pomocny w poprawianiu wydajności, ponieważ zwiększa zarówno szybkość odczytu, jak i szybkość zapisu danych. Korzystając z replikacji, szczególnie przy wykorzystywaniu buforowania, można znacznie poprawić wydajność operacji odczytu, jednak replikacja nie poprawi wydajności w przypadku bazy, do której wykonywane jest dużo zapisów. Sam sharding poprawia niezawodność w bardzo niewielkim stopniu. Mimo że dane są na różnych serwerach, awaria serwera spowoduje, że dane te będą niedostępne, tak samo jak w przypadku rozwiązania z pojedynczym serwerem. Jedyna poprawa polega na tym, że w przypadku awarii serwera jej skutki odczują tylko użytkownicy tych danych; mimo wszystko jednak niedobrze jest mieć bazę danych posiadającą tylko część danych. Mając pojedynczy serwer, łatwiej jest poświęcić uwagę i pieniądze w celu zapewnienia jego działania; w klastrach pracują przeważnie bardziej zawodne maszyny i awaria serwera w klastrze jest bardziej prawdopodobna. To wszystko powoduje, że w praktyce sam sharding wpływa na osłabienie niezawodności. Mimo że sharding jest łatwiejszy do zaimplementowania w przypadku agregacji, nie jest to krok, który powinniśmy wykonywać pochopnie. Niektóre bazy danych są przeznaczone do wykorzystania shardingu od samego początku; w takim przypadku warto uruchamiać je na klastrze już w czasie prac nad programem i koniecznie od początku działania aplikacji produkcyjnej. Inne bazy pozwalają na wykorzystanie shardingu w formie rozbudowy

55

56

ROZDZIAŁ 4. MODELE DYSTRYBUCYJNE

rozwiązania jednoserwerowego; wtedy klaster powinien być zakładany dopiero wtedy, gdy wydajność pojedynczego serwera przestanie być wystarczająca. W obu przypadkach przejście z jednego serwera do klastra będzie wymagało uwagi. Znamy opowieści o zespołach, które napotkały problemy, ponieważ zbyt długo odkładały sharding, a kiedy włączyły go na środowisku produkcyjnym, baza przestała być dostępna, ponieważ wsparcie dla shardingu pochłonęło wszystkie dostępne zasoby na potrzeby przenoszenia danych na nowe serwery. Morał z tego taki, że powinieneś implementować sharding, zanim zaczniesz go potrzebować, kiedy jeszcze masz zasoby do przenoszenia danych.

4.3. Replikacja master-slave Podczas dystrybucji master-slave replikujesz dane na wiele serwerów. Jeden z serwerów jest serwerem głównym (master). Serwer główny jest autorytatywnym źródłem danych i jest z reguły odpowiedzialny za przetwarzanie aktualizacji tych danych. Pozostałe serwery to serwery podległe (slave). Proces replikacji synchronizuje serwery podrzędne z serwerem głównym (rysunek 4.2).

Rysunek 4.2. Dane są replikowane z serwera głównego do serwerów podległych. Główny serwer obsługuje zapis danych, a odczyt może być obsłużony przez serwer główny lub któryś z serwerów podległych Replikacja master-slave jest pomocna w przypadku systemów, w których dane są głównie odczytywane. Można skalować w bok w celu poprawienia wydajności odczytu danych poprzez dodanie większej liczby serwerów i zapewnienie rozprowadzenia po nich ruchu.

4.4. REPLIKACJA PEER-TO-PEER

W przypadku zapisu jesteś jednak nadal ograniczany przez możliwości głównego serwera. W konsekwencji nie jest to rozwiązanie dobre dla baz obsługujących duży ruch, chociaż rozłożenie odczytów pozwoli też nieznacznie poprawić wydajność zapisu. Drugą zaletą replikacji master-slave jest niezawodność odczytu: jeżeli główny serwer zawiedzie, serwery podrzędne nadal będą odpowiadały na żądania odczytu. Jest to oczywiście użyteczne, jeżeli większość ruchu stanowi odczyt. Awaria serwera głównego eliminuje możliwość zapisu danych, dopóki serwer główny nie zostanie przywrócony lub nie zostanie wyznaczony nowy serwer główny. Mając serwery podległe, na które replikowane są dane z serwera głównego, możemy szybko przywrócić system do działania, ponieważ serwer podległy może być szybko wyznaczony na serwer główny. Możliwość wyznaczenia serwera podległego na nowy serwer główny oznacza, że ten typ replikacji może być przydatny nawet wtedy, kiedy nie musisz skalować w bok. Cały ruch może być kierowany na serwer główny, a serwer poboczny może działać jako wciąż aktualizowana kopia zapasowa. W takim przypadku najłatwiej myśleć o systemie jako o systemie z pojedynczym serwerem z automatycznie aktualizowaną kopią zapasową. Wygoda systemu jednoserwerowego łączy się z poprawą niezawodności — jest to szczególnie wygodne, jeżeli chcesz z łatwością radzić sobie z awariami systemu. Serwery główne mogą być wyznaczane ręcznie lub automatycznie. Ręczne wyznaczanie serwera głównego oznacza zazwyczaj, że podczas konfiguracji klastra określasz jeden z serwerów jako główny. W wyznaczaniu automatycznym tworzysz klaster serwerów, a serwery same wybierają jeden z nich na serwer główny. Poza łatwiejszą konfiguracją wyznaczanie automatyczne oznacza, że klaster może automatycznie wyznaczyć nowy serwer główny w przypadku awarii poprzednika. W celu uzyskania niezawodności odczytu musisz dopilnować, aby ścieżki odczytu i zapisu w Twojej aplikacji były różne, tak abyś mógł obsłużyć awarię ścieżki zapisu i móc nadal odczytywać dane. Łączy się to z koniecznością zapisywania i odczytywania danych za pomocą różnych połączeń — jest to funkcjonalność nieczęsto wspierana przez biblioteki do interakcji z bazami. Jak w przypadku wszystkich funkcjonalności, nie możesz mieć pewności, że odczyt danych jest niezawodny, bez dobrych testów wyłączających możliwość zapisu i sprawdzających odczyt. Replikacja ma na pewno pociągające zalety, ma jednak także ciemną stronę — brak spójności. Istnieje niebezpieczeństwo, że różni klienci odczytujący różne serwery podrzędne zobaczą różne wartości, ponieważ wszystkie zmiany nie zostały rozpropagowane po wszystkich serwerach. W najgorszym przypadku może to oznaczać, że klient nie zobaczy danych, które właśnie zapisał. Nawet jeżeli korzystasz z replikacji master-slave wyłącznie do tworzenia kopii zapasowej danych, nadal możesz mieć problem, ponieważ jeżeli serwer główny ulegnie awarii, zmiany nieprzekazane do serwera podrzędnego zostaną utracone. Porozmawiamy o tym, jak radzić sobie z tym problemem, w rozdziale 5., „Spójność”.

4.4. Replikacja peer-to-peer Replikacja master-slave pomaga w skalowaniu możliwości odczytu danych, nie pomaga jednak w przypadku zapisu. Poprawia niezawodność pod względem awarii serwerów podległych, ale nie serwera głównego. W gruncie rzeczy serwer główny jest nadal słabym ogniwem

57

58

ROZDZIAŁ 4. MODELE DYSTRYBUCYJNE

systemu i miejscem podatnym na awarię. Replikacja peer-to-peer (rysunek 4.3) radzi sobie z tym problemem, eliminując serwer główny. Wszystkie repliki mają równe prawa i wszystkie mogą wykonywać zapis, a utrata którejś z nich nie ogranicza dostępu do magazynu danych.

Rysunek 4.3. W replikacji peer-to-peer wszystkie serwery obsługują odczyt i zapis danych Perspektywa wygląda pięknie. Z klastrem replikującym się w trybie peer-to-peer możesz radzić sobie z awariami serwerów bez utraty dostępu do danych. Co więcej, możesz z łatwością dodawać nowe serwery, poprawiając wydajność. Taki system ma wiele zalet — powoduje też jednak pewne komplikacje. Największym problemem jest, po raz kolejny, spójność. Jeżeli możesz zapisywać w dwóch różnych miejscach, występuje ryzyko, że dwie osoby spróbują jednocześnie zapisać te same dane: konflikt zapis – zapis. Niespójność przy odczycie prowadzi do problemów, ale niespójność taka jest chwilowa. Niespójność przy zapisie jest natomiast trwała. W dalszej części wyjaśnimy, jak radzić sobie z niespójnością danych, w tej chwili jednak omówimy z grubsza kilka opcji. Z jednej strony możemy zapewnić, że kiedy dane zostaną zapisane, repliki skoordynują się w celu zapobieżenia konfliktom. Może nam to dać tak dobrą gwarancję jak baza główna, jednak kosztem ruchu sieciowego występującego podczas koordynacji. Nie potrzebujemy, aby wszystkie repliki zgadzały się na zapis, wystarczy większość, dzięki czemu będziemy mogli przeżyć, jeżeli stracimy mniejszość serwerów. Z drugiej strony możemy zdecydować, aby zezwolić na niespójny zapis. W niektórych przypadkach możemy znaleźć sposób na scalenie niespójnych danych. W takim przypadku możemy korzystać z pełni wydajności zapewnianej przez repliki. Te dwa sposoby są po przeciwnych stronach spektrum — zamieniamy spójność na dostępność.

4.5. ŁĄCZENIE SHARDINGU I REPLIKACJI

4.5. Łączenie shardingu i replikacji Replikacja i sharding to strategie, które możemy połączyć. Jeżeli wykorzystamy zarówno replikację master-slave, jak i sharding (rysunek 4.4), będziemy mieli wiele serwerów głównych, jednak każda część danych będzie miała tylko jeden serwer główny. Zależnie od konfiguracji możesz wybrać, aby serwer był serwerem głównym dla niektórych danych, a serwerem podległym dla innych, lub możesz wyznaczyć serwery dedykowane do bycia serwerami głównymi i pobocznymi. Wykorzystanie replikacji peer-to-peer i shardingu jest częste w przypadku baz rodziny kolumn. W takim przypadku możesz mieć dziesiątki lub setki serwerów w klastrze. Dobrym punktem startu w replikacji peer-to-peer jest współczynnik powtórzenia równy 3, czyli każda część danych reprezentowana jest na trzech serwerach. Jeżeli któryś z serwerów ulegnie awarii, shardy z tego serwera będą działały na pozostałych serwerach (rysunek 4.5).

Rysunek 4.4. Wykorzystanie replikacji master-slave razem z shardingiem

4.6. Najważniejsze kwestie ■ Istnieją dwa sposoby obsługi danych rozproszonych: ■ Sharding rozdziela dane pomiędzy wiele serwerów, tak aby każdy serwer działał jako źródło dla podzbioru danych. ■ Replikacja kopiuje dane pomiędzy wieloma serwerami, tak aby każda część danych znajdowała się w wielu miejscach.

59

60

ROZDZIAŁ 4. MODELE DYSTRYBUCYJNE

Rysunek 4.5. Wykorzystanie replikacji peer-to-peer razem z shardingiem ■ Replikacja przyjmuje dwie postacie: ■ W replikacji master-slave jeden serwer odgrywa rolę serwera głównego obsługującego zapis danych i rozgłaszającego dane do serwerów podległych, które mogą obsługiwać odczyt danych. ■ W replikacji peer-to-peer zapis możliwy jest do dowolnego serwera; serwery koordynują synchronizację danych. Replikacja master-slave redukuje ryzyko wystąpienia konfliktu zapisu, ale replikacja peer-to-peer pomaga rozładować ruch związany z zapisem danych.

Rozdział 5

Spójność

Jedną z największych zmian podczas przejścia od scentralizowanej bazy relacyjnej do bazy NoSQL działającej w klastrze jest sposób myślenia o spójności. Bazy relacyjne próbują zapewniać wysoką spójność poprzez unikanie wszystkich drobnych niespójności, które niedługo omówimy. Kiedy zaczniesz zapoznawać się ze światem NoSQL, zaczną pojawiać się frazy teoria CAP czy spójność końcowa i zanim zaczniesz coś budować, musisz przemyśleć, jaka spójność będzie wymagana w Twoim systemie. Spójność przybiera różne formy i słowo to określa miriady sposobów, na które błędy mogą zakraść się do Twojego życia. Zaczniemy więc od omówienia, jaki kształt może przyjmować spójność. Następnie omówimy, dlaczego mógłbyś chcieć rozluźnić spójność (i jej większą siostrę — trwałość).

5.1. Spójność aktualizacji Rozpoczniemy od rozważenia przykładu aktualizacji numeru telefonu. Marcin i Radosław przypadkowo oglądają niezależnie od siebie stronę firmy i obaj zauważają, że numer telefonu jest nieaktualny. Obaj mają dostęp do aktualizacji, więc jednocześnie modyfikują numer telefonu. Aby przykład był bardziej interesujący, założymy, że obaj aktualizują numer trochę inaczej, ponieważ każdy z nich użył innego formatu. Taki problem nazywany jest konfliktem zapis – zapis: dwoje ludzi aktualizuje te same dane w tym samym momencie. Kiedy zapisy dotrą do serwera, serwer wykona kolejkowanie i zdecyduje, czy wykonać jeden z nich, czy oba. Załóżmy, że serwer wykorzysta porządek alfabetyczny i najpierw zrealizuje zapis Marcina. Bez kontroli współbieżności zapis Marcina zostałby wykonany, a następnie natychmiast nadpisany przez Radosława. Operacja Marcina to więc stracona aktualizacja. W tym przypadku stracona aktualizacja nie stanowi większego problemu, jednak nie zawsze tak jest. Naszym zdaniem jest to brak spójności, ponieważ aktualizacja Radosława została wykonana na podstawie stanu sprzed aktualizacji Marcina, a jednak została wykonana. Podejścia do zachowania spójności w przypadku współbieżności określane są zazwyczaj jako optymistyczne i pesymistyczne. Podejście pesymistyczne polega na zapobieganiu powstawaniu konfliktów, natomiast podejście optymistyczne pozwala na powstanie konfliktów, 61

62

ROZDZIAŁ 5. SPÓJNOŚĆ

ale wykrywa je i podejmuje próbę ich rozwiązania. W przypadku konfliktów aktualizacji najbardziej powszechnym podejściem pesymistycznym jest wprowadzenie blokad zapisu: aby wykonać zapis, musisz najpierw uzyskać blokadę, a system dba o to, aby jednocześnie możliwa była tylko jedna blokada. Marcin i Radosław spróbowaliby uzyskać blokadę, ale tylko Marcin (jako pierwszy) mógłby ją otrzymać. Radosław miałby okazję zobaczyć zmianę Marcina przed podjęciem decyzji, czy mimo wszystko chce wykonać zapis. Częstym podejściem optymistycznym jest aktualizacja warunkowa, gdzie klient wykonujący aktualizację sprawdza wartość tuż przed zapisem, aby skontrolować, czy nie uległa zmianie od ostatniego odczytu. W takim przypadku zapis Marcina zostałby wykonany, a zapis Radosława nie powiódłby się. Komunikat błędu poinformowałby Radosława, że powinien jeszcze raz sprawdzić wartość i zdecydować, czy chce spróbować zapisać wartość. Oba podejścia, które opisaliśmy, opierają się o spójne kolejkowanie aktualizacji. W przypadku pojedynczego serwera jest to oczywiste — serwer najpierw wybiera jedną aktualizację, a potem następną. Jeżeli jednak istnieje więcej niż jeden serwer, tak jak np. w przypadku replikacji peer-to-peer, dwa serwery mogą wykonać aktualizacje w różnej kolejności, a w rezultacie na każdym z nich zostanie ostatecznie zapisana inna wartość. Podczas rozmów o systemach rozproszonych ludzie mówią często o spójności sekwencyjnej — zapewnieniu, że wszystkie serwery wykonują operacje w tej samej kolejności. Istnieje jeszcze jeden optymistyczny sposób na rozwiązywanie konfliktów zapis – zapis: zapisanie obu konfliktów i oznaczenie, że istnieje konflikt. Programiści znają to podejście z systemów kontroli wersji, a w szczególności z rozproszonych systemów kontroli wersji, które z natury często zawierają konflikty. Kolejny krok znowu jest analogiczny jak w systemach kontroli wersji: musisz jakoś scalić obie aktualizacje. Możesz pokazać obie wartości użytkownikowi i zapytać, która jest poprawna — takie rozwiązanie ma miejsce, jeżeli zaktualizujesz jakiś kontakt jednocześnie w swoim telefonie i na komputerze. Komputer może też być w stanie sam wykonać scalenie; jeżeli problemem był sposób formatowania numeru telefonu, może sobie z tym poradzić i ustawić numer ze standardowym formatowaniem. Każde automatyczne scalanie konfliktów zapis – zapis jest ściśle zależne od problemu i musi być obsługiwane dla każdego rozwiązania osobno. Przeważnie, kiedy ludzie po raz pierwszy napotykają ten problem, preferują współbieżność pesymistyczną, ponieważ są zdeterminowani, aby unikać konfliktów. O ile w niektórych przypadkach jest to prawidłowe podejście, wszystko ma swoje wady. Programowanie współbieżne powoduje, że zamieniamy szybkość działania (wykonywanie szybkich odpowiedzi na żądania użytkowników) na bezpieczeństwo (unikanie błędów takich jak konflikty zapisu). Podejście pesymistyczne często bardzo obniża wydajność systemu, czasem nawet do tego stopnia, że system przestaje pełnić swoją funkcję. Ten problem jeszcze pogarszają możliwe błędy — współbieżność pesymistyczna może prowadzić do permanentnych blokad, które są trudne do uniknięcia i odnalezienia. Replikacja zwiększa ryzyko napotkania konfliktów zapis – zapis. Jeżeli różne serwery przechowują różne kopie tych samych danych, które z kolei mogą być aktualizowane osobno, konflikty będą występować — chyba że podejmiesz konkretne kroki w celu zapobieżenia im. Wykorzystanie jednego serwera jako jedynego punktu zapisu konkretnych danych znacznie ułatwia zachowanie spójności. Ze wszystkich modeli dystrybucji omówionych w poprzednim rozdziale tylko replikacja peer-to-peer wykorzystuje więcej niż jeden serwer do zapisu tych samych danych.

5.2. SPÓJNOŚĆ ODCZYTU

5.2. Spójność odczytu Magazyn danych, który utrzymuje spójność zapisu, nie gwarantuje, że klienci zawsze otrzymają spójną odpowiedź na swoje żądania. Wyobraźmy sobie, że mamy zamówienie z pozycjami zamówienia i opłatą za wysyłkę. Opłata za wysyłkę obliczana jest na podstawie pozycji zamówienia. Jeżeli dodamy pozycję zamówienia, musimy ponownie obliczyć opłatę za wysyłkę. W bazie relacyjnej opłata za wysyłkę i pozycje zamówienia będą w odrębnych tabelach. Niespójność może wystąpić, jeżeli Marcin doda pozycję do swojego zamówienia, Radosław odczyta pozycje zamówienia i opłatę za wysyłkę, a potem Marcin zaktualizuje opłatę. Ten problem to niespójny odczyt, czyli inaczej konflikt odczyt – zapis: rysunek 5.1 przedstawia sytuację, w której Radosław odczytał dane w trakcie zapisu Marcina.

Rysunek 5.1. Brak spójności logicznej w konflikcie odczyt – zapis Taka spójność to spójność logiczna: różne elementy danych stanowią sensowną całość. Aby uniknąć niespójnego logicznie konfliktu odczyt – zapis, bazy relacyjne wspierają transakcje. Jeżeli Marcin otoczy swoje dwie aktualizacje transakcją, system zagwarantuje, że Radosław odczyta obie informacje przed lub po aktualizacji. Częstym zarzutem w stosunku do baz NoSQL jest to, że nie wspierają transakcji, a co za tym idzie nie mogą być spójne. Takie twierdzenie jest w większości przypadków błędne, ponieważ pomija wiele ważnych szczegółów. Po pierwsze, brak transakcji jest prawdą tylko w stosunku do niektórych baz NoSQL, w szczególności tych zorientowanych na agregacje. W przeciwieństwie do nich bazy grafowe wspierają transakcje ACID w ten sam sposób co bazy relacyjne. Po drugie, bazy zorientowane na agregacje wspierają atomowe aktualizacje, ale tylko wewnątrz pojedynczej agregacji. Oznacza to, że wewnątrz agregacji spójność logiczna będzie utrzymana — nie jest gwarantowana tylko pomiędzy agregacjami. W naszym przykładzie

63

64

ROZDZIAŁ 5. SPÓJNOŚĆ

mógłbyś uniknąć problemu ze spójnością, gdyby zamówienie, jego pozycje i opłata za wysyłkę były częścią jednej agregacji. Oczywiście nie możemy umieścić wszystkich danych w jednej agregacji, więc każda aktualizacja obejmująca wiele agregacji będzie stanowiła potencjalne miejsce, w którym użytkownik będzie mógł wykonać niespójny odczyt. Czas, w jakim istnieje niespójność, nazywany jest oknem niespójności. System NoSQL może mieć bardzo małe okno niespójności: w dokumentacji bazy Simple DB firmy Amazon jest zapisana informacja, że okno niespójności wynosi z reguły mniej niż sekundę. Ten przykład niespójnego logicznie odczytu jest klasycznym przykładem, jaki zobaczysz w każdej książce zawierającej zagadnienia z dziedziny programowania baz danych. Kiedy jednak wprowadzisz replikację, zetkniesz się z zupełnie nowym rodzajem niespójności. Załóżmy, że w hotelu w Londynie dostępny jest ostatni pokój hotelowy. System rezerwacji pokojów działa na wielu serwerach. Marcin i Klaudia są parą rozważającą zarezerwowanie pokoju, ale dyskutują o tym przez telefon, ponieważ Marcin jest w Krakowie, a Klaudia w Berlinie. W międzyczasie Radosław, który jest w Warszawie, rezerwuje ten ostatni pokój. Jego akcja oznacza pokój jako zajęty, jednak informacja replikowana jest do Berlina szybciej niż do Krakowa. Kiedy Marcin i Klaudia uruchamiają przeglądarki, aby sprawdzić dostępność pokoju, Klaudia zobaczy, że pokój jest zajęty, a Marcin, że pokój jest wolny. Jest to kolejny przykład niespójnego odczytu — jest to jednak naruszenie innego rodzaju spójności, spójności replikacji: zapewnienia, że ten sam element danych będzie miał tę samą wartość podczas odczytu z różnych miejsc (rysunek 5.2).

Rysunek 5.2. Przykład niespójności replikacji W końcu zmiany zostaną w pełni rozpropagowane i Marcin zobaczy, że pokój jednak jest zajęty. Dane te są zatem ostatecznie spójne, co oznacza, że w pewnej chwili serwery mogą wykazywać niespójność logiczną, ale jeżeli nie ma kolejnych aktualizacji, w końcu wszystkie zostaną zaktualizowane do stanu spójności. Nieaktualne dane określane są mianem nieświeżych,

5.2. SPÓJNOŚĆ ODCZYTU

co przypomina nam, że bufor jest inną formą replikacji — działającą podobnie jak replikacja master-slave. Mimo że spójność replikacji jest niezależna od spójności logicznej, replikacja może pogorszyć niespójność logiczną poprzez wydłużenie okna niespójności. Dwie różne aktualizacje na serwerze głównym mogą być wykonane jedna za drugą, pozostawiając okno niespójności przez milisekundy. Opóźnienia w działaniu sieci mogą jednak spowodować, że okno niespójności na serwerze podrzędnym będzie znacznie dłuższe niż na serwerze głównym. Gwarancje spójności nie są czymś globalnym dla aplikacji. Z reguły możesz określić poziom gwarancji, jakiego wymagasz dla każdego żądania. Dzięki temu jeżeli nie ma potrzeby zachowania spójności, możesz korzystać ze słabej spójności, a jeżeli taka potrzeba zajdzie, możesz zażądać większej spójności. Obecność okna niespójności oznacza, że różne osoby będą mogły zobaczyć różne rzeczy w tym samym czasie. Kiedy Marcin i Klaudia będą oglądać stronę, rozmawiając przez telefon, będą zdziwieni, że widzą co innego. Przeważnie użytkownicy działają niezależnie, wtedy nie jest to problemem. Okna niespójności mogą jednak być szczególnie problematyczne, jeżeli otrzymujesz niespójność z samym sobą. Rozważmy przykład, w którym wysyłasz komentarze do wpisu na blogu. Niewiele osób będzie zawracało sobie głowę problemem okien niespójności wydłużających się nawet do kilku minut, podczas gdy ludzie wpisują swoje najnowsze myśli. Często takie systemy obsługują obciążenie poprzez rozłożenie ruchu na serwery klastra. W tym miejscu pojawia się niebezpieczeństwo: możesz wysłać wiadomość, używając jednego serwera, ale otrzymać odświeżoną stronę z innego, który jeszcze nie otrzymał Twojej wiadomości — wygląda więc na to, że Twoja wiadomość została utracona. W takich przypadkach można tolerować względnie długie okna niespójności, jednak musisz spójnie odczytywać własny zapis, co oznacza, że kiedy zrobisz aktualizację, powinieneś w odpowiedzi zobaczyć te zmiany. Jednym ze sposobów na osiągnięcie tego jest zapewnienie spójności w sesji: w ramach sesji użytkownika występuje spójny odczyt własnych zapisów. Oznacza to, że w przypadku kiedy sesja użytkownika wygaśnie lub kiedy użytkownik uzyska dostęp do systemu jednocześnie z wielu komputerów, ta spójność może zostać utracona — takie przypadki są jednak relatywnie rzadkie. Jest kilka sposobów na zapewnienie spójności sesji. Najczęstszym i często najłatwiejszym sposobem jest przyklejona sesja: sesja związana z jednym z serwerów (nazywana także pokrewieństwem sesji). Przyklejona sesja pozwala zapewnić, że dopóki będziesz zapewniać spójność czytania własnego zapisu w obrębie serwera, będziesz ją też zapewniać w ramach sesji. Minusem tego rozwiązania jest to, że ogranicza ono możliwość balansowania obciążenia serwerów. Innym podejściem do zapewnienia spójności sesji jest wykorzystanie stempli wersji (rozdział 6.) i zapewnienie, że każda interakcja z magazynem danych zawiera najnowszy stempel wersji widziany przez sesję. Serwer przed odesłaniem odpowiedzi musi zapewnić, że posiada aktualizacje oznaczone tym stemplem. Utrzymanie spójności sesji za pomocą przyklejonych sesji i replikacji master-slave może być niewygodne, jeżeli w celu poprawienia wydajności chcesz odczytywać z serwerów podległych, ale zapisywać musisz za pośrednictwem serwera głównego. Jednym z rozwiązań może być przesyłanie zmian do serwera podrzędnego, który następnie przesyła je do serwera głównego, dbając jednocześnie o spójność sesji. Innym sposobem jest tymczasowe

65

66

ROZDZIAŁ 5. SPÓJNOŚĆ

przekazywanie sesji do serwera głównego na czas zapisu i tylko tak długo, aby odpowiedzi były przesyłane z serwera głównego, dopóki serwery podległe nie zostaną zaktualizowane. Rozmawiamy o spójności replikacji w kontekście magazynu danych, jest to jednak istotna kwestia w całym projekcie aplikacji. Nawet w prostym systemie bazodanowym będzie mnóstwo miejsc, gdzie dane są prezentowane użytkownikowi, użytkownik zastanawia się, a następnie robi aktualizację. Z reguły kiepskim pomysłem jest utrzymywanie transakcji podczas całej interakcji użytkownika, ponieważ istnieje realne niebezpieczeństwo powstania konfliktów, kiedy użytkownik spróbuje zaktualizować dane; w wyniku tego powstają takie rozwiązania jak blokady offline [Fowler PoEAA].

5.3. Rozluźnianie spójności Spójność jest pożądaną rzeczą, jednak niestety czasami musimy ją poświęcić. Zawsze można zaprojektować system, który pozwoli uniknąć niespójności, przeważnie jest to jednak niemożliwe bez poświęcenia jednocześnie innych charakterystyk systemu. W rezultacie często musimy zamienić spójność na coś innego. Podczas gdy niektórzy projektanci widzą to jako katastrofę, my widzimy to jako nieuniknione rozterki występujące przy projektowaniu systemów. Co więcej, różne zastosowania mają różną tolerancję dla niespójności, a my musimy wziąć tę tolerancję pod uwagę podczas podejmowania decyzji. Poświęcanie spójności jest zwyczajną praktyką nawet w przypadku systemów jednoserwerowych. Naszym głównym narzędziem zapewniania spójności są wtedy transakcje, a transakcje mogą zapewnić silne gwarancje spójności. Systemy transakcyjne mają jednak z reguły mechanizmy pozwalające rozluźnić najwyższy poziom izolacji (kolejkowanie) w celu poprawienia wydajności. Przeważnie ludzie korzystają z poziomu czytaj-zatwierdzone, który eliminuje niektóre konflikty odczyt – zapis, ale zezwala na inne. Wiele systemów całkowicie rezygnuje z transakcji, ponieważ ich wpływ na wydajność jest bardzo duży. Mieliśmy styczność z kilkoma sposobami na rezygnację z transakcji. Na niewielką skalę widzieliśmy popularność bazy MySQL, kiedy jeszcze nie wspierała transakcji. Wiele stron internetowych doceniało wysoką wydajność MySQL i było gotowych żyć bez transakcji. Z drugiej strony spektrum niektóre bardzo duże strony, takie jak eBay [Pritchett], musiało porzucić transakcje w celu osiągnięcia zadowalającej wydajności — w szczególności jeżeli w grę wchodził sharding. Nawet bez tych ograniczeń wiele aplikacji musi współpracować z systemami zdalnymi, które nie mogą być umieszczone w ramach transakcji, więc aktualizacja bez transakcji jest dość powszechna w systemach korporacyjnych.

5.3.1. Teoria CAP Teoria CAP jest często w świecie NoSQL powodem do rozluźnienia spójności. Teoria ta została przedstawiona przez Erica Brewera w roku 2000 [Brewer], a kilka lat potem Seth Gilbert i Nancy Lynch udowodnili jej prawdziwość [Lynch i Gilbert]; teoria jest też znana jako „przypuszczenie Brewera”.

5.3. ROZLUŹNIANIE SPÓJNOŚCI

Głównym twierdzeniem teorii CAP jest to, że system może posiadać tylko dwie z trzech właściwości: spójność (ang. consistency), dostępność (ang. availability) i tolerancja na partycjonowanie (ang. partition tolerance). Oczywiście zależy to bardzo od tego, jak określisz te trzy właściwości, a różnice w ich postrzeganiu doprowadziły do wielu debat na temat tego, jakie są rzeczywiste konsekwencje teorii CAP. Spójność zdefiniowaliśmy już wcześniej. Dostępność ma szczególne znaczenie w kontekście CAP — oznacza to, że jeżeli możesz połączyć się z serwerem w klastrze, może on odczytywać i zapisywać dane. Jest to subtelna różnica w stosunku do tradycyjnego znaczenia, które omówimy dalej. Tolerancja na partycjonowanie oznacza, że klaster może poradzić sobie z przerwami w komunikacji, które dzielą klaster na wiele partycji niemogących się komunikować (sytuacja znana pod nazwą podzielony umysł — rysunek 5.3).

Rysunek 5.3. Po wystąpieniu awarii komunikacji w dwóch miejscach sieć zostaje podzielona na dwie części System z pojedynczym serwerem jest oczywiście przykładem systemu CA — system charakteryzuje się spójnością i dostępnością, jednak nie tolerancją na partycjonowanie. Jest tylko jeden serwer; jeżeli więc pracuje, jest dostępny. Działanie i utrzymywanie spójności jest wystarczające; w takim świecie funkcjonuje większość systemów relacyjnych. Jest teoretycznie możliwe, aby zbudować klaster CA. Oznaczałoby to jednak, że gdyby wystąpiło partycjonowanie, wszystkie serwery musiałyby przestać być dostępne, więc żaden klient nie mógłby komunikować się z żadnym serwerem. Zgodnie z tradycyjną definicją dostępności oznaczałoby to jej brak, w tym miejscu jednak dochodzimy do specjalnego znaczenia dostępności w kontekście CAP. CAP definiuje dostępność następująco: każde żądanie otrzymane przez działający serwer musi otrzymać odpowiedź [Lynch i Gilbert]. W związku z tym niedziałający nieodpowiadający serwer nie oznacza braku dostępności w kontekście CAP.

67

68

ROZDZIAŁ 5. SPÓJNOŚĆ

Oznacza to, że możesz zbudować klaster CA, ale musisz zadbać, aby mógł się partycjonować rzadko i kompletnie. Można to osiągnąć, przynajmniej w ramach jednego centra danych, jest to jednak zazwyczaj nieopłacalnie drogie. Pamiętaj, że aby w przypadku wystąpienia partycjonowania wyłączać wszystkie serwery w klastrze, musiałbyś także wykrywać partycjonowanie, co samo w sobie nie jest drobną funkcjonalnością. Klastry muszą więc tolerować partycjonowanie sieci. Oto sedno teorii CAP. Mimo że przeważnie interpretowana jest jako stwierdzenie, iż możesz mieć tylko dwie z trzech funkcjonalności, w praktyce teoria ta oznacza, że w systemach, które mogą podlegać partycjonowaniu, musisz zdecydować, czy zależy Ci na spójności, czy dostępności. To nie jest decyzja zero-jedynkowa; często możesz zamienić odrobinę spójności na trochę dostępności. System, jaki w rezultacie otrzymasz, nie będzie ani idealnie spójny, ani idealnie dostępny — jednak będzie zawierał rozsądne proporcje obu tych cech. Aby to zilustrować, posłużymy się przykładem. Marcin i Radosław próbują zarezerwować ostatni pokój w systemie, który korzysta z replikacji peer-to-peer z dwoma serwerami (Marcin korzysta z serwera w Krakowie, a Radosław z serwera w Warszawie). Jeżeli chcemy zapewnić spójność w momencie, kiedy Marcin próbuje zarezerwować pokój na serwerze krakowskim, ten serwer musi przed potwierdzeniem rezerwacji skontaktować się z serwerem w Warszawie. Oba serwery muszą zgodzić się na kolejkowanie ich żądań. To nam daje spójność — jeżeli jednak połączenie między nimi zostanie zerwane, żaden z systemów nie będzie pozwalał na rezerwację pokojów; poświęcimy dostępność. Sposobem na poprawienie dostępności jest wyznaczenie jednego z serwerów jako głównego dla danego hotelu i zadbanie, aby wszystkie rezerwacje obsługiwane były przez ten serwer. Gdyby głównym został serwer w Warszawie, mógłby nadal przetwarzać rezerwacje i Radosław dostałby pokój. Jeżeli skorzystamy z replikacji master-slave, klienci w Krakowie będą widzieli niespójne dane, ale nie będą mogli rezerwować, co spowoduje niespójność aktualizacji. Klienci jednak spodziewają się takiego zachowania w tej sytuacji — taki kompromis sprawdzi się w tym przypadku. To poprawia sytuację, ale ponieważ serwer główny jest w Warszawie, a połączenie między serwerami nie działa, nadal nie możemy zarezerwować pokoju na krakowskim serwerze. W terminologii CAP jest to niedotrzymanie dostępności, ponieważ Marcin może połączyć się z serwerem w Krakowie, ale serwer nie może zapisywać danych. Aby poprawić dostępność, możemy pozwolić obu systemom akceptować rezerwacje nawet, jeżeli komunikacja między nimi nie będzie możliwa. Istnieje teraz niebezpieczeństwo, że Marcin i Radosław zarezerwują ten sam pokój. W zależności od tego, jak działa hotel, może to być akceptowalna sytuacja. Niektóre hotele przyjmują nadmiarowe rezerwacje, aby zrekompensować sobie niepojawianie się niektórych gości. Inne hotele zawsze mają kilka wolnych pokoi, aby móc przenieść gościa z pokoju, w którym pojawiły się jakieś problemy, lub obsłużyć gościa o wysokim statusie. Niektóre mogą również przeprosić i anulować rezerwację, kiedy już problem zostanie wykryty — być może takie rozwiązanie będzie korzystniejsze niż tracenie rezerwacji z powodu problemów z siecią. Klasycznym przykładem zezwalania na niespójny zapis jest koszyk zakupów [Dynamo Amazonu]. W tym przypadku zawsze możesz zapisać dane w swoim koszyku, nawet jeżeli problemy z siecią spowodują, że będziesz mieć wiele koszyków. Podczas finalizowania zakupów system potrafi scalić koszyki, umieszczając elementy z wszystkich koszyków w jednym.

5.4. ROZLUŹNIANIE TRWAŁOŚCI

Prawie zawsze daje to poprawny wynik — a nawet jeżeli nie, klient zawsze ma szansę przejrzenia koszyka przed zatwierdzeniem zamówienia. Należy pamiętać, że nawet jeżeli wielu twórców oprogramowania uważa spójność danych za absolutnie konieczną, istnieje wiele sytuacji, w których można poradzić sobie z niespójnymi odpowiedziami na żądania. Te przypadki są mocno zależne od danej sytuacji i wymagają rozpatrzenia konkretnego przypadku. Nie możesz też szukać rozwiązania wyłącznie wewnątrz zespołu programistycznego — musisz porozumieć się z ekspertami w danym temacie. Jeżeli będziesz w stanie poradzić sobie z niespójnymi aktualizacjami, będziesz mieć większe możliwości poprawiania dostępności i wydajności. W przypadku koszyka zakupów oznacza to, że klienci zawsze będą mogli z niego korzystać i będzie on wydajny. Podobna logika obowiązuje w przypadku spójności odczytu. Jeżeli za pośrednictwem platformy internetowej handlujesz instrumentami finansowymi, prawdopodobnie nie będziesz tolerować nieaktualnych danych. Jeżeli jednak wysyłasz informację na stronę z wiadomościami, pewnie będziesz w stanie znieść kilkuminutowe opóźnienie w aktualności strony. W takich przypadkach musisz wiedzieć, jak duże opóźnienie możesz tolerować oraz jak długie może być okno niespójności — zazwyczaj w kategoriach średniej długości, wartości maksymalnej i rozłożenia długości. Różne dane mogą mieć różną tolerancję, a co za tym idzie mogą wymagać różnych ustawień podczas replikacji. Zwolennicy NoSQL często twierdzą, że zamiast wspierać właściwości ACID relacyjnych transakcji, bazy NoSQL wspierają właściwości BASE (Basically Available, Soft state, Eventual consistency — „podstawowo dostępne, miękkiego stanu, ostatecznie spójne”) [Brewer]. Mimo że uważamy, że powinniśmy w tej książce wspomnieć o akronimie BASE, nie uważamy go za szczególnie użyteczny i jest nawet bardziej wydumany niż akronim ACID — ani „podstawowa dostępność”, ani „miękki stan” nie zostały zdefiniowane wystarczająco dobrze. Musimy też podkreślić, że kiedy Brewer przedstawiał pojęcie BASE, określał przejście pomiędzy BASE a ACID jako płynne, a nie jako decyzję zero-jedynkową. Zdecydowaliśmy się na omówienie teorii CAP, ponieważ jest ona często używana i nadużywana podczas dyskusji o kompromisach spójności w systemach rozproszonych. Warto jednak myśleć nie o kompromisie pomiędzy spójnością a dostępnością, ale o kompromisie pomiędzy spójnością a opóźnieniami systemu. Możemy podsumować większą część tej dyskusji stwierdzeniem, że jesteśmy w stanie poprawić spójność, dodając serwery uczestniczące w interakcji, ale każdy nowy serwer zwiększa czas reakcji na interakcję. O dostępności możemy myśleć jako o granicy opóźnienia, jakie jesteśmy w stanie tolerować; kiedy opóźnienie staje się zbyt duże, poddajemy się i traktujemy dane jako niedostępne — co prawie pasuje do definicji dostępności w teorii CAP.

5.4. Rozluźnianie trwałości Do tej pory rozmawialiśmy o spójności, która jest najczęściej przywoływana podczas rozmów na temat właściwości ACID transakcji bazodanowych. Kluczem do spójności jest kolejkowanie żądań i tworzenie atomowych, izolowanych jednostek roboczych. Wiele osób wybuchłoby jednak śmiechem, gdyby usłyszało o rozluźnianiu trwałości — w końcu jaki sens ma magazyn danych, jeżeli możemy stracić nasze aktualizacje?

69

70

ROZDZIAŁ 5. SPÓJNOŚĆ

Jak się okazuje, istnieją sytuacje, w których warto zamienić trochę trwałości na wyższą wydajność. Jeżeli baza danych może pracować i zapisywać zmiany w reprezentacji głównie w pamięci i tylko okresowo przekazywać dane na dysk, może być w stanie znacznie szybciej odpowiadać na żądania. Minusem tego rozwiązania jest to, że jeżeli serwer ulegnie awarii, wszystkie zmiany od ostatniego zapisu na dysk zostaną utracone. Jednym z przykładów, gdzie takie rozwiązanie może być użyteczne, jest przechowywanie stanu sesji użytkowników. Duża strona może mieć wielu użytkowników i przechowywać tymczasowe informacje o tym, co każdy z nich robi w sesji. Na stronie jest mnóstwo aktywności, co kreuje duży ruch w przechowywanych sesjach i znacząco wpływa na wydajność strony. Najważniejsze jest to, że utrata danych sesji nie będzie problemem — będzie niewygodna, ale być może mniej niż wolniejsze działanie strony. W związku z tym te dane stają się dobrym kandydatem dla nietrwałych zapisów. Przeważnie potrzeby odnośnie trwałości danych mogą być wyszczególnione dla poszczególnych żądań, można więc wywoływać zapis na dysku po ważniejszych z nich. Kolejnym przykładem rozluźniania trwałości jest przechwytywanie danych telemetrycznych z urządzeń fizycznych. Być może będziesz wolał przechwytywać dane szybciej, ale kosztem utracenia ostatnich aktualizacji, jeżeli serwer przestanie działać. Z innym typem kompromisu trwałości mamy do czynienia podczas replikowania danych. Utrata trwałości replikacji ma miejsce, jeżeli serwer przetwarza aktualizację, ale w jej trakcie replikacja zostaje przerwana. Może to mieć miejsce na przykład podczas replikacji master-slave, kiedy serwery podrzędne automatycznie typują nowy serwer nadrzędny w przypadku awarii poprzedniego. Jeżeli serwer główny zawiedzie, wszystkie zmiany nieprzekazane do serwerów podrzędnych zostaną utracone. Jeżeli serwer główny zacznie z powrotem działać, te nieprzekazane dane będą w konflikcie z aktualizacjami, które miały miejsce po awarii. Uważamy, że jest to problem trwałości, ponieważ może Ci się wydawać, że Twoja aktualizacja przebiegła pomyślnie, bo serwer główny przyjął zmiany, podczas gdy awaria serwera spowodowała zniknięcie tych zmian. Jeżeli jesteś pewien, że serwer główny szybko zacznie powtórnie działać, jest to powód, aby nie włączać automatycznego wyznaczania nowego serwera głównego. W przeciwnym wypadku w celu poprawienia trwałości danych możesz wymóc, aby serwer główny poczekał z zakomunikowaniem użytkownikowi, że dane zostały zapisane, aż któraś z replik poinformuje go, że przyjęła zmiany. To oczywiście spowolni aktualizacje i spowoduje niedostępność klastra w przypadku awarii serwera głównego — tak więc znowu musimy z czegoś zrezygnować. Zawsze też warto przy poszczególnych żądaniach określać, jakiej trwałości wymagają.

5.5. Kwora Jeżeli rezygnujesz ze spójności lub trwałości, nigdy nie jest to kwestia „wszystko albo nic”. Im więcej serwerów bierze udział w żądaniu, tym większa jest szansa na uniknięcie niespójności. Naturalnie nasuwa się pytanie: ile serwerów muszę wykorzystać, aby uzyskać silną niezawodność?

5.5. KWORA

Wyobraź sobie, że jakieś dane zostały zreplikowane na trzy serwery. W celu zapewnienia silnej spójności nie potrzebujesz, aby wszystkie serwery potwierdziły zapis — wystarczą dwa z nich, czyli większość. W przypadku konfliktowego zapisu tylko jeden serwer może uzyskać większość. Określane jest to terminem kworum zapisu i wyrażane nierównością W > N/2, która oznacza, że liczba serwerów partycypujących w zapisie (W) musi być większa niż połowa liczby serwerów biorących udział w replikacji (N). Liczba replik jest często określana mianem współczynnika replikacji. Podobnie jak w przypadku kworum zapisu, istnieje notacja odnosząca się do kworum odczytu: ile serwerów musisz odpytać, aby mieć pewność, że masz najbardziej aktualne dane. Kworum odczytu jest trochę bardziej skomplikowane, ponieważ jest zależne od liczby serwerów, które muszą potwierdzić zapis. Rozważmy współczynnik replikacji równy 3. Jeżeli podczas zapisu wymagane jest potwierdzenie z dwóch serwerów (W = 2), to musimy skontaktować się z przynajmniej dwoma serwerami, aby mieć pewność, że otrzymane dane są aktualne. Jeżeli jednak zapis potwierdzany jest tylko przez jeden serwer (W = 1), musimy porozumieć się z wszystkimi trzema serwerami. W tym przypadku ponieważ nie mamy kworum zapisu, może wystąpić konflikt aktualizacji, jednak kontaktując się z wystarczającą liczbą serwerów, jesteśmy w stanie go wykryć. Dzięki temu możemy mieć spójny odczyt nawet wtedy, kiedy zapis nie jest do końca spójny. Tę relację między liczbą serwerów, z którymi musisz się skontaktować w celu odczytu (R), serwerami potwierdzającymi zapis (W) i współczynnikiem replikacji (N) możemy zapisać nierównością: możesz osiągnąć silną spójność odczytu, jeżeli R + W > N. Powyższe nierówności zostały stworzone z myślą o modelu dystrybucji peer-to-peer. Jeżeli masz dystrybucję master-slave, w celu uniknięcia konfliktów zapis – zapis wystarczy Ci zapis tylko na serwerze głównym i analogicznie aby uniknąć konfliktów odczyt – zapis, wystarczy odczyt tylko z serwera głównego. Przy takiej notacji łatwo pomylić liczbę serwerów w klastrze ze współczynnikiem replikacji, są one jednak często różne. Mogę mieć 100 serwerów w klastrze i mieć współczynnik replikacji równy 3, jeżeli większość dystrybucji odbywa się za pośrednictwem shardingu. W rzeczy samej większość autorytetów sugeruje, że współczynnik replikacji równy 3 daje wystarczająco dobrą nadmiarowość. Dzięki temu pojedynczy serwer może przestać działać, a kworum dla zapisu i odczytu nadal zostanie utrzymane. Jeżeli posiadasz automatyczne rozdzielanie ruchu, nie potrwa długo, zanim klaster utworzy trzecią replikę, tak więc ryzyko utraty drugiej repliki przed powstaniem nowej jest niewielkie. Liczba serwerów biorących udział w operacji może się różnić w zależności od operacji. Podczas zapisu możemy potrzebować kworum dla niektórych typów aktualizacji, a dla innych nie, zależnie od tego, jak bardzo cenimy spójność i dostępność. Podobnie odczyt, który wymaga szybkości, ale może wybaczyć niepełną aktualność danych, powinien kontaktować się z mniejszą liczbą serwerów. Często możesz potrzebować wzięcia pod uwagę obu parametrów. Jeżeli potrzebujesz szybkich i spójnych odczytów, możesz wymagać od zapisu, aby skontaktował się ze wszystkimi serwerami, dzięki temu odczyt będzie musiał skontaktować się tylko z jednym (N = 3, W = 3, R = 1). Oznaczałoby to spowolnienie zapisu, ponieważ konieczna jest komunikacja ze

71

72

ROZDZIAŁ 5. SPÓJNOŚĆ

wszystkimi trzema serwerami i nie mógłbyś tolerować utraty żadnego z nich. W niektórych sytuacjach może to być jednak konieczny kompromis. To wszystko pokazuje, że masz szeroki wachlarz opcji i możesz wybierać, którą kombinację problemów i zalet wolisz. Niektórzy ludzie piszący o NoSQL mówią o prostym kompromisie pomiędzy spójnością i dostępnością; mamy nadzieję, że teraz zdajesz sobie sprawę, że problem jest bardziej skomplikowany, a sam wybór bardziej elastyczny.

5.6. Dalsza lektura W internecie istnieje mnóstwo interesujących postów na blogach i artykułów na temat spójności w systemach rozproszonych, dla nas jednak najbardziej przydatnym źródłem informacji byli [Tanenbaum i Van Steen]. W doskonały sposób organizują podstawy systemów rozproszonych i jest to najlepsze miejsce, w jakie możesz się udać, jeżeli chcesz bardziej zgłębić temat poruszony w tym rozdziale. Kiedy kończyliśmy tę książkę, ukazał się specjalny numer „IEEE Computer” [IEEE Computer, luty 2012] na temat rosnącego znaczenia teorii CAP, który także jest dobrym źródłem informacji na ten temat.

5.7. Najważniejsze kwestie ■ Konflikty zapis – zapis mają miejsce, kiedy dwóch klientów próbuje zapisać te same dane w tym samym czasie. Konflikty odczyt – zapis występują, kiedy w trakcie zapisu innego klienta klient odczytuje niespójne dane. ■ Podejścia pesymistyczne blokują dane w celu uniknięcia konfliktów. Podejścia optymistyczne wykrywają konflikty i je naprawiają. ■ W systemach rozproszonych konflikty odczyt – zapis powstają, jeżeli niektóre serwery zostaną zaktualizowane, a inne nie. Ostatecznie spójne dane to takie, które staną się spójne, kiedy wszystkie zmiany zostaną rozpropagowane na wszystkie serwery. ■ Klient z reguły oczekuje, że zobaczy to, co zapisał, czyli tak zwanej spójności odczytu swojego zapisu. Może to być trudne do osiągnięcia, jeżeli dane zapisywane są na innym serwerze, a odczytywane z innego. ■ Aby uzyskać dobrą spójność, w operacje na danych musisz włączyć wiele serwerów, co z kolei zwiększa opóźnienia. Często musisz poświęcić spójność na korzyść wydajności. ■ Teoria CAP oznacza, że jeżeli wystąpi partycjonowanie sieci, musisz zdecydować pomiędzy dostępnością danych a spójnością. ■ Wydajność można też uzyskać kosztem trwałości danych, zwłaszcza kiedy Twoje zabezpieczenie przed utratą danych stanowi ich replikacja. ■ Nie musisz kontaktować się ze wszystkimi replikami, aby osiągnąć silną spójność podczas replikacji; musisz tylko osiągnąć kworum.

Rozdział 6

Stemple wersji

Wielu krytyków baz NoSQL skupia się na braku wsparcia dla transakcji. Transakcje są użytecznym narzędziem, które pomaga programistom wspierać spójność. Jednym z powodów, dla których użytkownicy baz NoSQL mniej przejmują się brakiem transakcji, jest to, że bazy zorientowane na agregacje wspierają atomowe aktualizacje w obrębie agregacji — a same agregacje zaprojektowane są tak, aby były naturalną jednostką dla aktualizacji. Oczywiście podczas podejmowania decyzji odnośnie rodzaju bazy danych należy brać pod uwagę potrzeby transakcyjne. Warto też pamiętać, że transakcje mają ograniczenia. Nawet w ramach systemu transakcyjnego nadal musimy borykać się z aktualizacjami, które wymagają interakcji użytkownika i nie mogą być uruchamiane w transakcji, ponieważ wiązałoby się to ze zbyt długim jej trwaniem. Z tym problemem możemy poradzić sobie przy wykorzystaniu stempli wersji, które są też pomocne w innych sytuacjach, w szczególności jeżeli odchodzimy od modelu pojedynczego serwera.

6.1. Transakcje biznesowe i systemowe Potrzeba wspierania spójności aktualizacji bez transakcji jest wspólną cechą systemów, nawet jeżeli budowane są z wykorzystaniem baz transakcyjnych. Kiedy użytkownik myśli o transakcjach, przeważnie chodzi o transakcje biznesowe. Transakcja biznesowa to na przykład przeglądanie katalogu produktów, wybranie butelki Taliskera w dobrej cenie, wypełnienie informacji o karcie kredytowej czy potwierdzenie zamówienia. Przeważnie żadne z powyższych nie będzie jednak wykonywane w ramach transakcji systemowej udostępnianej przez bazę danych, ponieważ oznaczałoby to zablokowanie tabeli na czas, kiedy użytkownik szuka swojej karty kredytowej lub zostaje odciągnięty przez kolegów na lunch. Aplikacje zazwyczaj rozpoczynają transakcję systemową po zakończeniu interakcji z użytkownikiem, dzięki czemu blokady zakładane są tylko na krótki okres czasu. Problem polega jednak na tym, że obliczenia i decyzje wykonywane były na podstawie danych, które mogły ulec zmianie. Cena Taliskera mogła ulec zmianie lub ktoś mógł zmienić adres klienta, zmieniając tym samym adres dostawy.

73

74

ROZDZIAŁ 6. STEMPLE WERSJI

Szerokie spektrum technik pozwalających radzić sobie z takimi problemami to techniki spójności offline [Fowler PoEAA], które są także użyteczne podczas prac z bazami NoSQL. Szczególnie użyteczne jest podejście optymistycznych blokad offline [Fowler PoEAA] — jest to forma warunkowych aktualizacji, w której operacja klienta ponownie odczytuje wszystkie informacje, na których bazuje transakcja, i sprawdza, czy nie uległy zmianie od czasu pierwotnego odczytu. Dobrym sposobem na osiągnięcie tego jest zawarcie w rekordach bazy jakiejś formy stempla wersji; pola, które zmienia się za każdym razem, kiedy rekord ulegnie zmianie. Kiedy odczytujesz dane, przechowujesz stempel wersji; potem gdy zapisujesz dane, sprawdzasz, czy stempel nie uległ zmianie. Być może spotkałeś się z techniką aktualizacji zasobów za pośrednictwem HTTP. Jednym ze sposobów takiej aktualizacji jest wykorzystanie e-znaczników. Kiedy pobierasz zasób, serwer przesyła odpowiedź z e-znacznikiem w nagłówku. Ten e-znacznik to łańcuch znaków wskazujący wersję zasobu. Jeżeli zasób zostanie zmieniony, możesz wykonać aktualizację warunkową z wykorzystaniem e-znacznika, który otrzymałeś wcześniej. Jeżeli zasób na serwerze uległ zmianie, e-znaczniki nie będą zgodne i serwer odmówi wykonania aktualizacji, zwracając odpowiedź z kodem 412 („Warunek wstępny nie może być spełniony”). Niektóre bazy danych udostępniają podobny mechanizm warunkowych aktualizacji, pozwalający upewnić się, że aktualizacja nie została wprowadzona na podstawie nieaktualnych danych. Takie sprawdzenie możesz wykonać samodzielnie, chociaż wtedy musisz upewnić się, że żaden inny wątek nie działa na zasobie pomiędzy odczytem a zapisem. (Operacja ta bywa nazywana porównaj-i-ustaw — z ang. compare-and-set, CAS — nazwa pochodzi od operacji CAS wykonywanych w procesorach. Różnica polega na tym, że CAS procesora porównuje wartość przed jej ustawieniem, natomiast aktualizacja warunkowa w bazach danych porównuje stempel wersji). Istnieje kilka sposobów na budowanie stempli wersji. Możesz skorzystać z licznika, który będzie inkrementowany za każdym razem, kiedy zaktualizujesz dane. Liczniki są przydatne, ponieważ pozwalają z łatwością stwierdzić, czy dana wersja jest nowsza od innej. Z drugiej strony wymagają od serwera generowania wartości licznika oraz potrzebują serwera głównego w celu zapewnienia, że wersje nie są duplikowane. Innym sposobem jest stworzenie identyfikatora GUID — dużej losowo wybranej liczby, która na pewno będzie unikalna. Do jej generowania wykorzystywane są kombinacje dat i informacji sprzętowych oraz danych z różnych innych źródeł losowości. Zaletą identyfikatorów GUID jest to, że mogą być generowane gdziekolwiek i nigdy się nie powtórzą; wadą jest ich duży rozmiar i brak możliwości porównania, który GUID jest nowszy. Trzecim podejściem jest wykonywanie haszy zawartości zasobu. Przy wystarczająco dużym rozmiarze klucza hasz zawartości może być równie unikalny co GUID i także może być generowany gdziekolwiek. Zaletą jest ich deterministyczność — każdy serwer wykona taki sam hasz dla tej samej zawartości zasobu. Niestety podobnie jak identyfikatorów GUID, haszy nie można porównywać w celu ustalenia, który hasz jest nowszy; mogą też być dość długie. Czwartym podejściem jest wykorzystanie stempla czasowego ostatniej aktualizacji. Podobnie jak licznik, taki stempel jest dość krótki i pozwala porównać kolejność, ale też nie wymaga scentralizowanego miejsca tworzenia. Wiele maszyn może generować stemple czasowe — jednak aby takie rozwiązanie działało poprawnie, ich zegary muszą być zsynchronizowane. Jeden serwer ze źle ustawionym zegarem może spowodować wiele problemów

6.2. STEMPLE WERSJI NA WIELU SERWERACH

w danych. Istnieje także ryzyko, że zbyt mało dokładny stempel czasowy może wprowadzać duplikaty; jeżeli wykonywanych jest wiele aktualizacji w ciągu milisekundy, wykorzystanie stempla z taką dokładnością będzie niewystarczające. Możesz łączyć zalety poszczególnych stempli czasowych, wykorzystując więcej niż jeden z nich. Na przykład CouchDB wykorzystuje kombinację licznika i hasza zawartości. Pozwala to zazwyczaj porównywać stemple pod względem kolejności, nawet przy wykorzystaniu replikacji peer-to-peer. Gdyby dwa serwery wykonały aktualizację w tym samym czasie, kombinacja tego samego licznika i różnych haszy zawartości pozwala z łatwością wykryć konflikt. Oprócz możliwości wykrywania konfliktów aktualizacji stemple wersji pomagają też w zapewnieniu spójności sesji (podrozdział 5.2).

6.2. Stemple wersji na wielu serwerach Podstawowy stempel wersji sprawdza się dobrze, jeżeli masz jedno autorytatywne źródło danych, czyli np. w przypadku pojedynczego serwera lub replikacji master-slave. W tym przypadku stempel wersji kontrolowany jest przez serwer główny. Serwery podrzędne otrzymują stemple od serwera głównego. System ten musi być jednak zmodyfikowany w przypadku modelu dystrybucyjnego peer-to-peer, ponieważ w takim modelu nie ma już jednego miejsca, w którym mogą być tworzone stemple. Jeżeli prosisz dwa serwery o te same dane, istnieje szansa, że dostaniesz inne odpowiedzi. Jeżeli tak się stanie, Twoja reakcja może być różna w zależności od przyczyny konfliktu. Może się okazać, że zmiany dotarły tylko na jeden z serwerów — w takim przypadku zaakceptujesz najnowsze dane (zakładając, że jesteś w stanie to określić). Możesz też natknąć się na niespójną aktualizację — w tym przypadku prosty GUID czy e-znacznik nie będą zadowalające, ponieważ nie zawierają wystarczających informacji o relacjach. Najprostszą formą stempla wersji jest licznik. Za każdym razem, kiedy serwer aktualizuje dane, licznik jest zwiększany, a wartość licznika wstawiana jest do stempla wersji. Jeżeli masz niebieski oraz zielony serwer i niebieski serwer zwraca odpowiedź ze stemplem wersji o wartości 4, a serwer zielony — o wartości 6, to wiesz, że odpowiedź serwera zielonego jest bardziej aktualna. W przypadku wielu serwerów głównych potrzebujemy czegoś bardziej wyszukanego. Jedno podejście, wykorzystywane przez rozproszone systemy kontroli wersji, polega na zapewnieniu, że wszystkie serwery posiadają historię stempli wersji. Dzięki temu będziesz wiedzieć, czy odpowiedź serwera niebieskiego jest potomkiem odpowiedzi serwera zielonego. Aby to osiągnąć, klient także musi posiadać historię stempli, lub też taką historię muszą posiadać serwery i przesyłać ją wraz z danymi. Pozwala to także na wykrycie niespójności, jeżeli otrzymalibyśmy dwa stemple, które nie posiadałyby siebie w swojej historii. Chociaż serwery kontroli wersji korzystają z takich historii wersji, bazy NoSQL nie posiadają takich mechanizmów. Prostym, lecz problematycznym podejściem jest stosowanie stempli czasowych. Podstawowym problemem jest to, że z reguły ciężko jest zapewnić synchronizację czasową wszystkich serwerów, zwłaszcza gdy aktualizacje wykonywane są bardzo często. Rozsynchronizowanie

75

76

ROZDZIAŁ 6. STEMPLE WERSJI

zegara może spowodować wiele problemów. Dodatkowo nie jesteś w stanie wykrywać konfliktów zapis – zapis, więc takie rozwiązanie sprawdzałoby się tylko w systemach z pojedynczym serwerem głównym — a przy tej architekturze zazwyczaj lepiej sprawdzi się licznik. Najbardziej powszechnym podejściem wybieranym w większości systemów NoSQL peer-to-peer jest specjalny rodzaj stempla wersji, nazywany stemplem-wektorem. W skrócie stempel-wektor jest zestawem liczników, po jednym dla każdego serwera. Stempel-wektor dla trzech serwerów (niebieskiego, zielonego i czarnego) wyglądałby mniej więcej tak: [niebieski: 43, zielony: 54, czarny: 12]. Za każdym razem, kiedy serwer wykonuje wewnętrzną aktualizację, aktualizuje swój własny licznik, więc aktualizacja na serwerze zielonym spowodowałaby zmianę wektora na: [niebieski: 43, zielony: 55, czarny: 12]. Podczas komunikacji między serwerami następuje synchronizacja ich wektorów. Istnieje kilka wariantów tej synchronizacji. W tej książce wykorzystujemy nazwę stempel-wektor, ale możesz się też spotkać z zegarami wektorów i wektorami wersji — są to wariacje stempli-wektorów różniące się sposobem synchronizacji. Korzystając z tego schematu, jesteś w stanie powiedzieć, czy jeden stempel wersji jest nowszy od drugiego, ponieważ nowszy będzie miał wszystkie liczniki większe lub równe od starszego. Czyli [niebieski: 1, zielony: 2, czarny: 5] jest nowszy niż [niebieski: 1, zielony: 1, czarny: 5], ponieważ jeden z jego liczników jest większy. Jeżeli oba stemple mają liczniki większe niż w drugim, na przykład: [niebieski: 1, zielony: 2, czarny: 5] i [niebieski: 2, zielony: 1, czarny: 5], masz konflikt zapis – zapis. W wektorze może brakować wartości — w takim przypadku brakujące wartości traktowane są jako 0. Czyli wektor [niebieski: 6, czarny: 2] jest równoznaczny z wektorem [niebieski: 6, zielony: 0, czarny: 2]. Stemple-wektory są wartościowym narzędziem do wykrywania niespójności, jednak ich nie rozwiązują. Rozwiązanie potencjalnych konfliktów będzie zależne od zastosowania. Jest to część kompromisów pomiędzy spójnością i szybkością działania. Albo musisz poradzić sobie z faktem, że partycjonowanie sieci sprawi, iż system będzie niedostępny, albo musisz wykrywać i naprawiać niespójności.

6.3. Najważniejsze kwestie ■ Stemple wersji pozwalają wykrywać konflikty spójności. Kiedy odczytujesz dane, a następnie je aktualizujesz, możesz sprawdzić stempel wersji, aby mieć pewność, że nikt nie zaktualizował danych pomiędzy Twoim odczytem a zapisem. ■ Stemple wersji można zaimplementować przy wykorzystaniu liczników, identyfikatorów GUID, haszy zawartości, stempli czasowych lub kombinacji powyższych. ■ W systemach rozproszonych wektor stempli wersji pozwala wykrywać, kiedy serwery przechowują konfliktujące aktualizacje.

Rozdział 7

Map-reduce

Wzrost popularności baz zorientowanych na agregacje jest w dużej mierze spowodowany wzrostem popularności klastrów. Praca z klastrem oznacza, że musisz podejmować zupełnie inne kompromisy niż w przypadku pojedynczego serwera. Klastry zmieniają nie tylko reguły przechowywania danych, ale także reguły przetwarzania. Jeżeli przechowujesz w klastrze znaczne ilości danych, efektywne przetwarzanie ich wymaga zmiany sposobu myślenia na temat organizacji wykonywanych zadań. W przypadku bazy scentralizowanej są dwa sposoby na uruchamianie logiki przetwarzania: albo na serwerze bazodanowym, albo na maszynie klienta. Uruchamianie jej na maszynie klienta daje większą swobodę w wyborze środowiska programistycznego, co z kolei przekłada się na łatwość tworzenia i rozbudowy programów. Minusem jest konieczność przesyłania znacznych ilości danych z serwera. Jeżeli wolumen danych jest duży, bardziej sensowne jest przetwarzanie na serwerze z poświęceniem wygody programistycznej i jednoczesnym zwiększeniem obciążenia serwera. Kiedy korzystasz z klastra, masz wiele maszyn, pomiędzy które możesz rozłożyć przetwarzanie. Musisz jednak także zredukować ilość danych przesyłanych pomiędzy serwerami, wykonując możliwie jak najwięcej pracy na serwerze, na którym znajdują się potrzebne dane. Wzorzec map-reduce (wariant wzorca Scatter-Gather [Hohpe i Woolf]) to sposób takiego organizowania przetwarzania, aby wykorzystać potencjał wielu maszyn w klastrze i jednocześnie zgrupować możliwie jak najwięcej przetwarzania i przetwarzanych danych w tych samych miejscach. Wzorzec zyskał uznanie wraz z pojawieniem się biblioteki MapReduce firmy Google [Dean i Ghemawat]. Najszerzej stosowana implementacja open source jest częścią projektu Hadoop, chociaż niektóre bazy zawierają swoje własne implementacje. Tak samo jak w przypadku większości wzorców, pomiędzy tymi implementacjami występują drobne różnice, skoncentrujemy się zatem na generalnej koncepcji. Map-reduce skupia się na funkcjach map i reduce znanych z języków funkcyjnych.

77

78

ROZDZIAŁ 7. MAP-REDUCE

7.1. Podstawy map-reduce Aby objaśnić podstawową koncepcję, zaczniemy od oklepanego przykładu — klientów i ich zamówień. Załóżmy, że jako agregację wybraliśmy zamówienia wraz z pozycjami każdego z nich. Każda pozycja zamówienia posiada identyfikator produktu, liczbę sztuk i cenę. Taka agregacja jest sensowna, ponieważ przeważnie ludzie chcą zobaczyć całe zamówienie podczas jednego dostępu. Mamy mnóstwo zamówień, więc podzieliliśmy zbiór danych na wiele maszyn. Analitycy sprzedaży chcą jednakże widzieć produkty wraz z ich sprzedażą z siedmiu ostatnich dni. Taki raport nie pasuje do naszej struktury agregacji — jest to jedna z wad korzystania z agregacji. Aby uzyskać raport sprzedaży produktu, będziesz musiał odwiedzić każdą maszynę w klastrze i przejrzeć wiele rekordów na każdej z nich. Jest to idealna sytuacja do wykorzystania map-reduce. Pierwszym etapem zadania map-reduce jest mapowanie (ang. map). Funkcja mapująca przyjmuje pojedynczą agregację i zwraca zestaw par klucz – wartość. W tym przypadku do funkcji przekazywane będzie zamówienie. Wynikiem byłyby pary klucz – wartość reprezentujące pozycje zamówienia. Każda z nich miałaby identyfikator produktu jako klucz i osadzoną mapę zawierającą liczbę i cenę jako wartość (rysunek 7.1).

Rysunek 7.1. Funkcja mapująca odczytuje rekordy w bazie i zwraca pary klucz – wartość Każde uruchomienie funkcji mapującej jest odrębne od wszystkich pozostałych. Dzięki temu bezpiecznie mogą być wykonywane równolegle, a biblioteka map-reduce może swobodnie wykonywać mapowania na każdym z serwerów i dowolnie alokować zamówienia do zadania mapującego. Dzięki temu wzorzec osiąga wysoki poziom współbieżności i pracuje z danymi na serwerach, na których się znajdują. W naszym przykładzie wyciągamy z agregacji tylko wartości, ale nic nie stoi na przeszkodzę, aby w ramach mapowania wykonywana była jakaś skomplikowana funkcja, o ile tylko operacje wykonywane byłyby w ramach tej jednej agregacji.

7.2. PARTYCJONOWANIE I ŁĄCZENIE

Operacja mapowania działa wyłącznie na jednym rekordzie: funkcja reduce pobiera wiele wyników działania mapowania o tym samym kluczu i łączy ich wartości. Funkcja map może zwrócić 1000 linii zamówień dla produktu Refaktoryzacja baz danych, a funkcja reduce zredukuje je do jednej linii, zawierającej sumę liczebności i cen. O ile działanie mapowania ograniczone jest tylko do jednej agregacji, o tyle funkcja redukująca może wykorzystać wszystkie wartości zwrócone dla danego klucza (rysunek 7.2).

Rysunek 7.2. W funkcji reduce wiele par klucz – wartość agregowanych jest do jednego rekordu Biblioteka map-reduce dba o to, aby zadania mapujące uruchamiane były na odpowiednich serwerach w celu przetworzenia wszystkich dokumentów, i o to, aby dane zostały przekazane do funkcji redukującej. Biblioteka zbiera wszystkie wartości dla jednej pary i wywołuje funkcję redukującą tylko raz, przekazując do niej klucz i kolekcję wszystkich wartości; dzięki temu łatwiej jest taką funkcję napisać. Aby więc wykonać zadanie map-reduce, musisz stworzyć dwie funkcje.

7.2. Partycjonowanie i łączenie W najprostszej formie zadanie map-reduce posiada pojedynczą funkcję redukującą. Wyniki działania wszystkich zadań mapujących działających na różnych serwerach są ze sobą łączone i przesyłane do redukcji. Takie rozwiązanie działa, ale możemy jeszcze poprawić współbieżność i zredukować transfer danych (rysunek 7.3). Pierwszą rzeczą, jaką możemy zrobić, jest zwiększenie współbieżności poprzez podzielenie na części wyniku działania funkcji mapujących. Każda funkcja redukująca działa na wynikach dla jednego klucza. Jest to ograniczeniem, ponieważ oznacza, że nie możesz zrobić niczego, co wiązałoby się z operacją na kilku kluczach, ale jest też zaletą, ponieważ dzięki temu możesz uruchomić wiele funkcji redukujących jednocześnie. Aby to wykorzystać, rezultaty funkcji mapujących są dzielone na podstawie klucza każdego z przetwarzających serwerów. Zazwyczaj wiele kluczy grupowanych jest w partycję. Biblioteka pobiera następnie wszystkie dane ze wszystkich serwerów dla jednej partycji, łączy w grupę i przesyła do serwera redukującego. Wiele serwerów redukujących może dzięki temu równolegle przetwarzać partycje, a z danych wyjściowych łączony jest wynik końcowy (ten krok nazywany jest także tasowaniem, a partycje wiadrami lub regionami).

79

80

ROZDZIAŁ 7. MAP-REDUCE

Rysunek 7.3. Partycjonowanie pozwala funkcjom redukującym działać równolegle na różnych kluczach Kolejny problem, który możemy rozwiązać, to ilość danych przesyłanych pomiędzy etapami map i reduce. Wiele z tych danych powtarza się i składa się z wielu par klucz – wartość o takim samym kluczu. Funkcja combine zmniejsza tę nadmiarowość, łącząc wszystkie dane o tym samym kluczu do jednej wartości (rysunek 7.4). Funkcja łącząca to właściwie funkcja redukująca, a w praktyce przeważnie ta sama funkcja może zostać wykorzystana do łączenia i ostatecznego redukowania. Aby to jednak było możliwe, funkcja redukująca musi spełnić jeden warunek: format danych wynikowych musi być identyczny z formatem danych wejściowych. Taką funkcję nazywamy reduktorem łączącym.

Rysunek 7.4. Funkcja combine zmniejsza nadmiarowość kluczy przed przesłaniem danych

7.3. TWORZENIE OBLICZEŃ MAP-REDUCE

Nie wszystkie funkcje redukujące mogą być łączące. Wyobraź sobie funkcję zliczającą unikalnych klientów dla danego produktu. Funkcja mapująca musiałaby zwracać produkt i klienta. Reduktor może następnie połączyć dane z mapowania i zliczyć, ile razy dany klient pojawia się dla danego produktu, zwracając produkt i liczbę (rysunek 7.5). Dane zwracane z funkcji redukującej mają jednak inny format niż dane wejściowe, nie możemy więc wykorzystać tej funkcji do łączenia. Można zastosować funkcję łączącą: taką, która wyeliminuje duplikaty w parach produkt – klient, ale nie może to być funkcja redukująca.

Rysunek 7.5. Funkcja redukująca zliczająca liczbę unikalnych klientów, którzy zamówili daną herbatę, nie jest kompatybilna Jeżeli w Twoim rozwiązaniu redukcja i łączenie wykonywane są za pomocą tej samej funkcji, biblioteka map-reduce może bezpiecznie uruchamiać ją nie tylko równolegle (aby redukować różne pozycje), ale także w seriach, aby redukować tę samą partycję w różnych miejscach i w różnej kolejności. Dodatkowo dane możesz zacząć łączyć przed zakończeniem działania funkcji mapujących. Niektóre biblioteki map-reduce wymagają, aby wszystkie funkcje redukujące były reduktorami łączącymi, dzięki czemu maksymalizowana jest elastyczność. Jeżeli w takiej bibliotece potrzebujesz stworzyć reduktor niepozwalający na łączenie, będziesz musiał rozdzielić przetwarzanie na osobne kroki map-reduce.

7.3. Tworzenie obliczeń map-reduce Podejście map-reduce to sposób myślenia o przetwarzaniu współbieżnym, w którym poświęcamy elastyczność struktury obliczeń w zamian za względnie nieskomplikowany model równoległego przetwarzania w klastrze. Ponieważ jest to kompromis, istnieją ograniczenia tego, co możesz robić podczas obliczeń. Wewnątrz zadania mapowania możesz operować tylko na jednej agregacji. Wewnątrz zadania redukującego możesz operować tylko na jednym kluczu. Oznacza to, że musisz tak organizować strukturę programów, aby mogły pracować w obrębie tych ograniczeń. Prostym ograniczeniem jest konieczność organizacji przetwarzania w operacje, które będą pasowały do notacji redukowania. Dobrym przykładem jest obliczanie średnich. Rozważmy przypadek zamówień, które do tej pory oglądaliśmy; załóżmy, że chcemy poznać średnią liczbę produktów w zamówieniach dla każdego z nich. Ważną cechą średnich jest to, że mając dwie grupy obiektów, nie mogę po prostu połączyć ich średnich. Muszę wziąć całkowitą liczbę zamówień i produktów z każdej grupy, połączyć je i dopiero na podstawie tego łączenia obliczyć średnią (rysunek 7.6).

81

82

ROZDZIAŁ 7. MAP-REDUCE

Rysunek 7.6. Podczas obliczania średnich suma i liczba mogą być łączone podczas redukcji, ale średnia musi być obliczona na podstawie połączonej sumy i liczby Taki sposób szukania obliczeń, które łatwo poddają się redukcji, wpływa także na sposób wykonywania zliczeń. Aby wykonać zliczenie, funkcja mapująca zwróci zliczane pola z wartością 1, z których następnie możemy obliczyć całkowitą sumę (rysunek 7.7).

Rysunek 7.7. Podczas zliczania każda mapa zwraca 1, co pozwala na zsumowanie liczby występowania

7.3.1. Przykład dwuetapowego map-reduce Kiedy obliczenia map-reduce stają się skomplikowane, warto rozbić je na etapy, korzystając z podejścia potoków i filtrów, gdzie wynik jednego etapu służy jako wejście kolejnego, podobnie jak w przypadku potoków systemu UNIX. Rozważmy przykład, w którym chcielibyśmy porównać sprzedaż produktów dla każdego miesiąca roku 2011 z analogicznym miesiącem roku poprzedniego. Aby to zrobić, rozbijemy obliczenia na dwa etapy. Pierwszy etap wygeneruje rekordy zawierające sumy dla pojedynczego produktu w danym miesiącu. Drugi etap wykorzysta wyniki z etapu poprzedniego i wygeneruje wynik dla pojedynczego produktu, porównując wynik z miesiąca z tego roku z wynikiem z analogicznego miesiąca poprzedniego roku (rysunek 7.8).

7.3. TWORZENIE OBLICZEŃ MAP-REDUCE

Rysunek 7.8. Obliczenia rozbite na kroki map-reduce, które zostaną rozwinięte w kolejnych trzech rysunkach W pierwszym etapie (rysunek 7.9) odczytujemy rekordy zamówień; w wyniku otrzymujemy serię par klucz – wartość dla sprzedaży produktów w danych miesiącach.

Rysunek 7.9. Tworzenie rekordów dla miesięcznej sprzedaży produktów Ten krok jest podobny jak w przypadku poprzednich przykładów. Nowością jest wykorzystanie klucza złożonego, dzięki czemu możemy redukować rekordy na podstawie wartości wielu pól. Funkcje mapujące drugiego etapu (rysunek 7.10) przetwarzają wynik z etapu poprzedniego w zależności od roku. Rekord z 2011 zasila liczbę z aktualnego roku, a rekord z roku 2010 zasila liczbę z roku poprzedniego. Dla rekordów z lat poprzednich (np. 2009) nie jest wykonywane żadne mapowanie.

83

84

ROZDZIAŁ 7. MAP-REDUCE

Rysunek 7.10. Mapowanie drugiego etapu produkuje rekordy służące do porównania dwóch różnych lat Redukcja w tym przypadku (rysunek 7.11) to łączenie rekordów poprzez sumowanie wyników z dwóch różnych lat (z uzupełnieniem brakujących wartości).

Rysunek 7.11. Krok redukcji to łączenie niekompletnych wierszy

7.3. TWORZENIE OBLICZEŃ MAP-REDUCE

Podzielenie raportu na wiele kroków upraszcza jego wykonanie. Jak w przypadku wielu przykładów transformacji, kiedy już znajdziesz odpowiednią bibliotekę pozwalającą podzielić pracę na kroki, zauważysz, że z reguły łatwiej złożyć razem wiele etapów kroków, niż próbować upchnąć całą logikę w jednym kroku. Kolejną zaletą jest to, że pośrednik wynik może też być użyteczny dla innych operacji. Takie powtórne wykorzystanie zasobów jest ważne, ponieważ oszczędza zarówno czas programistów, jak i moc obliczeniową serwerów. Rekordy pośrednie mogą być przechowane w magazynie danych, tworząc widok zmaterializowany (podrozdział 3.4, „Widoki zmaterializowane”). Wczesne fazy map-reduce są szczególnie warte zapisania, ponieważ z reguły zawierają wynik działania najbardziej pracochłonnej części dostępu do danych, więc przetworzenie ich raz i późniejsze wykorzystanie jako podstawy do dalszych zadań oszczędza wiele pracy. Podobnie jak w przypadku każdej aktywności związanej z wielokrotnym wykorzystaniem tych samych elementów, należy tworzyć je zgodnie z doświadczeniem płynącym z prawdziwych potrzeb, ponieważ robienie czegoś „na zapas” rzadko się sprawdza. Należy zatem przyjrzeć się różnym zapytaniom i wyodrębnić wspólne elementy do widoków zmaterializowanych. Map-reduce jest wzorcem, który może być zaimplementowany w dowolnym języku programowania. Ograniczenia stylu powodują jednakże, że szczególnie przydatny jest w językach zaprojektowanych właśnie do tworzenia zadań map-reduce. Apache Pig [Pig] powstał jako projekt poboczny projektu Hadoop; jest językiem mającym na celu ułatwienie tworzenia programów map-reduce. W porównaniu ze standardowymi bibliotekami Java udostępnianymi przez Hadoop, Pig znacznie ułatwia pracę. Jeżeli natomiast chciałbyś definiować programy map-reduce, korzystając ze składni podobnej do SQL-a, możesz wykorzystać hive, inny projekt poboczny projektu Hadoop. Wzorzec map-reduce warto znać, nawet pomijając kontekst baz NoSQL. Oryginalny system map-reduce firmy Google operował na rozproszonym systemie plików — takie samo podejście wykorzystuje projekt open source Hadoop. Przyzwyczajenie się do ograniczeń wzorca i sposobu projektowania etapów obliczeń może zająć trochę czasu, ale w rezultacie otrzymujemy przetwarzanie doskonale nadające się do pracy w klastrze. Podczas pracy z dużymi wolumenami danych musisz przyjąć podejście zorientowane na klastry. Bazy zorientowane na agregacje doskonale pasują do tego rodzaju przetwarzania. Uważamy, że w przeciągu kilku następnych lat znacznie więcej organizacji będzie przetwarzało ilości danych wymagające wykorzystania klastrów — a wzorzec map-reduce będzie coraz bardziej popularny.

7.3.2. Inkrementacyjny map-reduce Przykłady, które do tej pory omówiliśmy, to kompletne przetwarzania map-reduce, w których rozpoczynamy od nieprzetworzonych danych wejściowych i kończymy na oczekiwanym wyniku. Wiele zadań map-reduce trwa długo, nawet na sprzęcie pracującym w klastrach, a nowe dane cały czas napływają — oznacza to, że musimy powracać z przetwarzaniem do początku, aby dane były kompletne. Rozpoczynanie za każdym razem od początku może zajmować zbyt dużo czasu, warto więc czasami tak skonstruować przetwarzanie operacji map-reduce, aby umożliwić inkrementacyjną aktualizację, dzięki czemu wykonywane będzie tylko minimum niezbędnego przetwarzania.

85

86

ROZDZIAŁ 7. MAP-REDUCE

Etapy mapowania w łatwy sposób można wykonywać inkrementacyjnie — funkcja mapująca musi wrócić do danych tylko w przypadku ich zmiany. Ponieważ funkcje mapujące są od siebie odizolowane, inkrementacyjne aktualizacje są bardzo proste. Bardziej skomplikowany jest krok redukcji, ponieważ wykorzystuje wynik działania wielu funkcji mapujących, a zmiana w którymkolwiek mapowaniu może spowodować potrzebę ponownej redukcji. Konieczność ponownego przetwarzania jest mniej dotkliwa, jeżeli przetwarzanie jest bardziej równoległe. Jeżeli dane do redukcji podzielimy na partycje, te partycje, które nie uległy zmianie, nie będą musiały być powtórnie przetwarzane. Jeżeli nasz reduktor może podlegać łączeniom, mamy kolejną okazję do uniknięcia nadmiarowych obliczeń — oczywiście tylko wtedy, kiedy dodajemy nowe rekordy, a nie zmieniamy stare — możemy wtedy wykonać redukcję dla istniejących rekordów i dla nowo dodanych. Jeżeli wykonane zostały zmiany destrukcyjne, czyli aktualizacja lub usunięcie danych, możemy uniknąć nadmiarowych obliczeń, dzieląc operację redukowania na kroki i przeliczając tylko te kroki, dla których dane uległy zmianie — w praktyce przekłada się to na użycie sieci zależności [Fowler DSL] do organizacji przetwarzania. Biblioteka map-reduce kontroluje wiele z tych procesów, więc musisz zapoznać się ze sposobem wykonywania tych czynności w danej bibliotece.

7.4. Dalsza lektura Jeżeli zamierzasz korzystać z obliczeń map-reduce, Twoją pierwszą lekturą powinna być dokumentacja danej biblioteki. Każda z baz danych ma swoje własne podejście, słownik i zagadnienia, które musisz poznać. Poza tym musisz zdobyć więcej ogólnych informacji na temat budowania struktur zadań map-reduce i maksymalizowania wydajności i łatwości utrzymania. Nie możemy wskazać żadnych konkretnych książek, ale uważamy, że dobrym, chociaż często pomijanym źródłem informacji są książki na temat Hadoop. Mimo że Hadoop to nie baza danych, jest narzędziem, które bardzo intensywnie korzysta z map-reduce, więc umiejętność napisania wydajnego zadania w Hadoop może być przydatna także w innym kontekście (biorąc pod uwagę różnice pomiędzy Hadoop a systemem, z którego korzystasz).

7.5. Najważniejsze kwestie ■ Map-reduce jest wzorcem pozwalającym na równoległe przetwarzanie danych w klastrze. ■ Zadanie mapujące odczytuje dane z agregacji i przetwarza je na odpowiednie pary klucz – wartość. Mapowanie czyta tylko jeden rekord naraz i dzięki temu może być uruchamiane współbieżnie na serwerach przechowujących dane. ■ Zadania redukujące przyjmują wiele wartości zwróconych z mapowania dla jednego klucza i łączą je w jeden rekord. Każde zadanie redukujące operuje tylko na jednym kluczu, zadania mogą więc być uruchamiane współbieżnie.

7.5. NAJWAŻNIEJSZE KWESTIE

■ Zadania redukujące przyjmujące dane o takim samym formacie jak dane zwracane mogą być łączone w strumienie. Poprawia to współbieżność i redukuje ilość transportowanych danych. ■ Operacje map-reduce mogą być łączone w strumienie, gdzie wynik jednej redukcji stanowi dane wejściowe dla mapowania innej operacji. ■ Jeżeli wynik przetwarzania map-reduce jest często wykorzystywany, można go przechowywać jako widok zmaterializowany. ■ Widoki zmaterializowane mogą być aktualizowane za pomocą inkrementacyjnych operacji map-reduce, które przetwarzają tylko zmiany w widoku, zamiast przetwarzać wszystkie dane od początku.

87

88

ROZDZIAŁ 7. MAP-REDUCE

Część II

Implementacja

89

90

ROZDZIAŁ 1.

DLACZEGO NOSQL?

Rozdział 8

Bazy klucz – wartość

Baza klucz – wartość to prosta tabela haszy. Wykorzystywana jest głównie, jeżeli cały dostęp do danych uzyskiwany jest za pomocą klucza głównego. Wyobraź sobie tabelę w tradycyjnej bazie relacyjnej zawierającą dwie kolumny, pierwszą z identyfikatorem ID i drugą z nazwą NAZWA. Kolumna ID to klucz, a kolumna NAZWA zawiera wartość. W bazie relacyjnej kolumna NAZWA musi przechowywać łańcuch znaków. Aplikacja może przekazać klucz oraz wartość i zapisać je w bazie — jeżeli wpis z takim identyfikatorem już istnieje, wartość jest nadpisywana; w przeciwnym wypadku tworzony jest nowy wpis. Porównajmy terminologię z baz Oracle i Riak. Oracle

Riak

instancja bazy danych

klaster Riak

tabela

wiadro

wiersz

klucz – wartość

rowid

klucz

8.1. Czym jest magazyn klucz – wartość? Magazyny klucz – wartość to najprostsze, z punktu widzenia korzystania z API, bazy NoSQL. Klient może uzyskać wartość za pomocą klucza, wstawić wartość dla danego klucza lub ten klucz usunąć. Wartość to pole blob, które po prostu jest przechowywane, bez wnikania, co zawiera; zrozumienie zawartości jest zadaniem aplikacji. Ponieważ magazyny klucz – wartość zawsze wykorzystują klucz główny, przeważnie cechują się wysoką wydajnością i są łatwo skalowalne. Najpopularniejsze bazy klucz – wartość to Riak [Riak], Redis (często nazywany też serwerem Data Structure) [Redis], Memcached DB i pochodne [Memcached], Berkeley DB [Berkeley DB], HamsterDB (szczególnie dobrze przystosowana do osadzania w programach) [HamsterDB], Amazon DynamoDB (nie open source) [Amazon’s Dynamo] i Project Voldemort [Project Voldemort] (implementacja open source bazy Dynamo DB). 91

92

ROZDZIAŁ 8. BAZY KLUCZ – WARTOŚĆ

W niektórych magazynach klucz – wartość, takich jak Redis, przechowywana agregacja nie musi być obiektem — może być dowolną strukturą danych. Redis wspiera przechowywanie list, zbiorów i haszy oraz potrafi wykonywać operacje zasięgu, różnicowania i przecięcia. Dzięki temu Redis może być wykorzystany w większej gamie zastosowań niż przeciętna baza klucz – wartość. Istnieje znacznie więcej baz klucz – wartość i nad wieloma aktualnie prowadzone są prace. Aby nieco uprościć dyskusję, skupimy się głównie na bazie Riak. Riak pozwala przechowywać klucze w wiadrach, które są sposobem segmentacji kluczy — traktuj wiadra jako płaskie przestrzenie nazw dla kluczy. Jeżeli chcemy przechowywać dane sesji użytkowników, informacje o koszykach zakupów i preferencje użytkowników w bazie Riak, moglibyśmy przechowywać je wszystkie w tym samym wiadrze z pojedynczym kluczem i wartością dla wszystkich obiektów. W takim scenariuszu mielibyśmy jeden obiekt przechowujący wszystkie dane i umieszczony w jednym wiadrze (rysunek 8.1).

Rysunek 8.1. Przechowywanie wszystkich danych w jednym wiadrze Minusem przechowywania wszystkich obiektów (agregacji) w jednym wiadrze jest to, że to wiadro przechowywałoby różne rodzaje agregacji, co z kolei zwiększa szansę powstawania konfliktów kluczy. Alternatywnym podejściem jest dodanie nazwy obiektu do klucza, np. 288790b8a421_profilUzytkownika, dzięki czemu będziemy mogli dostać się do poszczególnych obiektów, kiedy będą potrzebne (rysunek 8.2). Możemy również stworzyć wiadra przechowujące poszczególne dane. W Riak takie wiadra nazywają się wiadrami domeny i pozwalają, aby sterownik klienta zajmował się serializacją i deserializacją. Bucket wiadro = client.fetchBucket(nazwaWiadra).execute(); DomainBucket wiadroProfili = DomainBucket.builder(wiadro, ProfilUzytkownika.class).build();

8.2. FUNKCJONALNOŚCI MAGAZYNÓW KLUCZ – WARTOŚĆ

Rysunek 8.2. Zmiana sposobu zapisania klucza pozwala zmienić sposób segmentacji w jednym wiadrze Wykorzystanie wiader domen, lub też różnych wiader dla różnych obiektów (takich jak ProfilUzytkownika i KoszykZakupow) segmentuje dane pomiędzy wiadra i pozwala odczytywać odpowiednie obiekty bez konieczności zmiany klucza. Magazyny klucz – wartość, takie jak Redis, pozwalają przechowywać różne struktury danych, które mogą być zbiorami, haszami, ciągami znaków i tak dalej. Taka funkcjonalność może być wykorzystana do przechowania listy rzeczy, np. stanów lub typów adresów, lub też tablicy wizyt użytkownika.

8.2. Funkcjonalności magazynów klucz – wartość Podczas korzystania z magazynów danych NoSQL należy zrozumieć różnice pomiędzy nimi a dobrze nam znanymi standardowymi bazami transakcyjnymi. Przede wszystkim musimy zdać sobie sprawę, jakich funkcjonalności brakuje i w jaki sposób musi się zmienić architektura aplikacji, aby lepiej wykorzystać właściwości magazynu klucz – wartość. Niektóre z właściwości omówionych dla wszystkich magazynów NoSQL to spójność, transakcje, możliwości zapytań, struktury danych i skalowalność.

8.2.1. Spójność Spójność utrzymywana jest wyłącznie dla operacji działających dla jednego klucza, ponieważ te operacje to pobieranie, wstawianie lub usuwanie danego klucza. Możliwe jest wykonywanie optymistycznych zapisów, ale ich implementacja jest bardzo kosztowna, ponieważ magazyn danych nie jest w stanie zdeterminować zmiany wartości. W rozproszonych magazynach klucz – wartość, takich jak Riak, implementowany jest model spójności ostatecznie spójny (patrz podrozdział 5.2). Ponieważ wartość mogła już zostać zreplikowana na inne serwery, Riak ma dwa sposoby na rozwiązywanie konfliktów aktualizacji: albo najnowszy zapis wygrywa, a najstarszy przegrywa, albo obie (wszystkie) wartości są zwracane i klient może wybrać jedną z nich. W bazie Riak te opcje mogą być ustawiane podczas tworzenia wiadra. Wiadra są sposobem na tworzenie przestrzeni nazw dla kluczy, aby zminimalizować możliwość powstawania kolizji — na przykład wszystkie klucze klientów mogą być przechowywane w jednym wiadrze. Tworząc wiadro, możemy ustawić domyślne wartości dla spójności, na przykład że zapis jest uważany za poprawny tylko wtedy, kiedy dane są spójne na wszystkich serwerach, na których są przechowywane.

93

94

ROZDZIAŁ 8. BAZY KLUCZ – WARTOŚĆ Bucket wiadro = connection .createBucket(nazwaWiadra) .withRetrier(prob(3)) .allowSiblings(pozwalajNaKonfliktujaceRekordy) .nVal(liczbaReplikDanych) .w(liczbaReplikNaKtorychZapisac) .r(liczbaReplikZKtorychOdczytywac) .execute();

Jeżeli potrzebujemy, aby dane na wszystkich serwerach były spójne, możemy ustawić wartość zmiennej liczbaReplikNaKtorychZapisac ustawianej w W na równą z nVal. Oczywiście taka zmiana obniży wydajność zapisu klastra. Aby poprawić sytuację odnośnie konfliktów odczytu i zapisu, możemy zmienić flagę allowSiblings podczas tworzenia wiadra: jeżeli jest ustawiona na false, pozwalamy, aby ostatni zapis wygrywał i nie były tworzone konfliktujące rekordy.

8.2.2. Transakcje Różne rodzaje magazynów klucz – wartość mają różne specyfikacje transakcji. Mówiąc ogólnie, nie ma gwarancji w przypadku zapisu. Większość baz klucz – wartość implementuje transakcje w zupełnie inny sposób. Riak wykorzystuje koncepcję kworum (patrz podrozdział 5.5) zaimplementowanego z wykorzystaniem wartości W — kworum zapisu — podczas zapisu za pośrednictwem API. Załóżmy, że mamy klaster Riak ze współczynnikiem replikacji równym 5, a przekazujemy wartość W równą 3. Zapis jest uważany za udany, jeżeli zostanie zapisany i zaraportowany jako udany na tylko trzech serwerach. Takie ustawienie daje bazie Riak tolerancję zapisu; w naszym przykładzie, przy N równym 5, a W równym 3, klaster w przypadku zapisu może tolerować N – W = 2 niedziałających serwerów, chociaż w przypadku odczytu i tak stracilibyśmy część danych z tych serwerów.

8.2.3. Możliwości zapytań Magazyny klucz – wartość można odpytywać po kluczu — i to właściwie wszystko. Jeżeli wymagasz wyszukiwania po jakimś atrybucie wartości, nie możesz wykorzystać bazy: Twoja aplikacja musi odczytać wartość, aby sprawdzić, czy atrybut spełnia warunki. Pobieranie za pomocą klucza ma też interesujący efekt uboczny. Co, jeżeli nie znamy klucza, w szczególności podczas wykonywania zapytań w locie w trakcie debugowania? Większość baz nie udostępnia listy wszystkich kluczy; a nawet gdyby, pobieranie listy, a następnie wykonywanie zapytania o wartość byłoby kłopotliwe. Niektóre magazyny klucz – wartość obchodzą ten problem, udostępniając możliwość przeszukiwania wartości, np. Riak Search pozwala odpytywać dane tak samo jak przy wykorzystaniu indeksów Lucene. Podczas korzystania z baz klucz – wartość należy bardzo dokładnie zaprojektować strukturę klucza. Czy klucz może być wygenerowany przy wykorzystaniu jakiegoś algorytmu? Czy klucz może być podany przez użytkownika (identyfikator użytkownika, adres e-mail itp.)? A może można go wyprowadzić ze stempla czasowego lub innych danych spoza bazy?

8.2. FUNKCJONALNOŚCI MAGAZYNÓW KLUCZ – WARTOŚĆ

Taka charakterystyka zapytań sprawia, że bazy klucz – wartość są dobrymi kandydatami do przechowywania danych sesji (z identyfikatorem sesji w roli klucza), danych koszyka zakupów, profili użytkownika i tak dalej. Właściwość expiry_secs pozwala ustawić czas życia klucza, w szczególności dla obiektów sesji, np. koszyków zakupów. Bucket wiadro = getBucket(nazwaWiadra); IRiakObject obiektRiak = wiadro.store(klucz, wartosc).execute();

Po zapisaniu w wiadrze bazy Riak przy wykorzystaniu API obiekt będzie przechowywany pod podanym kluczem. Podobnie wartość przechowywaną pod danym kluczem możemy uzyskać, wykonując żądanie API fetch. Bucket wiadro = getBucket(nazwaWiadra); IRiakObject obiektRiak = wiadro.fetch(klucz).execute(); byte[] bytes = obiektRiak.getValue(); String wartosc = new String(bytes);

Riak udostępnia interfejs oparty o HTTP, a więc wszystkie operacje mogą być wykonywane za pośrednictwem przeglądarki internetowej lub, przy wykorzystaniu curl, z linii komend. Zapiszmy te dane w Riak: { "ostatniaWizyta":1324669989288, "uzytkownik":{ "klientId":"91cfdf5bcb7c", "nazwa":"kupiec", "kodKraju":"PL", "tzOffset":0 } }

Użyj polecenia curl, aby przesłać dane i przechować je w wiadrze sesja pod kluczem a7e618d9db25 (musimy przekazać ten klucz): curl -v -X POST -d ' { "ostatniaWizyta":1324669989288, "uzytkownik":{"klientId":"91cfdf5bcb7c", "nazwa":"kupiec", "kodKraju":"PL", "tzOffset":0} }' -H "Content-Type: application/json" http://localhost:8098/buckets/sesja/keys/a7e618d9db25

Dane dla klucza a7e618d9db25 mogą być pobrane przy wykorzystaniu polecenia curl: curl -i http://localhost:8098/buckets/sesja/keys/a7e618d9db25

8.2.4. Struktura danych W bazach klucz – wartość nie ma znaczenia, co jest przechowywane jako wartość pary klucz – wartość. Może to być blob, text, JSON, XML i tak dalej. W Riak możemy wykorzystać właściwość Content-Type żądania POST do wyszczególnienia tego typu.

95

96

ROZDZIAŁ 8. BAZY KLUCZ – WARTOŚĆ

8.2.5. Skalowanie Wiele baz klucz – wartość skaluje poprzez wykorzystanie shardingu (patrz podrozdział 4.2). W shardingu wartość klucza determinuje, na którym serwerze będzie on przechowywany. Załóżmy, że dzielimy na podstawie pierwszej litery klucza; jeżeli klucz ma wartość f4b19d79587d, klucz zaczyna się od litery f i zostanie przesłany na inny serwer niż klucz ad9c7a396542. Taki sposób dzielenia na shardy pozwala podnosić wydajność poprzez dodawanie do klastra większej liczby serwerów. Sharding powoduje też pewne problemy. Jeżeli serwer przechowujący klucze na f przestanie działać, dane przechowywane na nim przestaną być dostępne, a nowe dane nie będą mogły być zapisywane. Magazyny danych takie jak Riak pozwalają kontrolować aspekty teorii CAP (patrz punkt 5.3.1): N (liczba serwerów przechowujących repliki klucz – wartość), R (liczba serwerów, do których trzeba się odwołać, zanim odczyt zostanie uznany za poprawny), W (liczba serwerów, na których dane muszą zostać zapisane, zanim zapis zostanie uznany za poprawny). Załóżmy, że mamy klaster Riak z pięcioma serwerami. Ustawienie N na 3 oznacza, że wszystkie dane są replikowane co najmniej na trzech serwerach, Ustawienie R na 2 oznacza, że dwa serwery muszą odpowiedzieć na żądanie GET, aby odpowiedź była uznana za poprawną, a ustawienie W na 2 zapewnia, że żądanie PUT musi być zapisywane na dwóch serwerach, aby było uznane za pomyślne. Te ustawienia pozwalają nam dostosować nadmiarowość danych do potrzeb odczytu i zapisu. Zależnie od naszych potrzeb możemy zmieniać wartości, aby bardziej dostępne były operacje odczytu lub zapisu. Ogólnie mówiąc, wybierz wartość W odpowiadającą Twoim wymaganiom odnośnie spójności; dla tych zmiennych można ustawić wartości domyślne podczas tworzenia wiadra.

8.3. Pasujące przypadki użycia Omówmy kilka problemów, gdzie magazyny klucz – wartość są dobrym wyborem.

8.3.1. Przechowywanie informacji o sesjach Każda sesja internetowa jest unikalna oraz otrzymuje unikalny identyfikator sesji sessionid. Aplikacje przechowujące klucze sessionid na dysku lub w bazie relacyjnej bardzo skorzystają na przeniesieniu ich do bazy klucz – wartość, ponieważ wszystkie dane dotyczące sesji mogą zostać zapisane za pomocą jednej operacji PUT lub pobrane za pomocą jednego żądania GET. Takie pojedyncze żądanie sprawia, że operacja jest bardzo szybka, ponieważ wszystkie informacje o sesji przechowywane są jako jeden obiekt. Rozwiązania takie jak Memcached są wykorzystywane przez wiele aplikacji webowych, a Riak może być zastosowany, kiedy ważna jest dostępność.

8.4. KIEDY NIE STOSOWAĆ

8.3.2. Profile i preferencje użytkownika Prawie każdy użytkownik posiada unikalny identyfikator, nazwę użytkownika lub inny atrybut oraz preferencje takie jak język, kolor, strefa czasowa, do jakiego produktu użytkownik ma dostęp i tak dalej. To wszystko można umieścić w obiekcie, tak aby pobranie preferencji użytkownika wymagało pojedynczego żądania GET. W podobny sposób mogą być przechowywane dane produktów.

8.3.3. Dane koszyka zakupów Strony e-commerce mają koszyki zakupów przypisane do użytkowników. Ponieważ chcemy, aby koszyki były dostępne cały czas, pomiędzy przeglądarkami, maszynami i sesjami, wszystkie dane odnośnie zakupów mogą być w wartości, dla której kluczem będzie identyfikator użytkownika. Klaster Riak byłby najlepszym rozwiązaniem dla tego typu zastosowań.

8.4. Kiedy nie stosować Istnieją obszary, gdzie magazyn klucz – wartość nie będzie odpowiednim narzędziem.

8.4.1. Relacje pomiędzy danymi Jeżeli potrzebujesz utrzymać relacje pomiędzy zestawami danych lub dane z różnymi zestawami kluczy muszą korelować ze sobą, magazyny klucz – wartość nie są najlepszym wyborem. Mimo to niektóre bazy klucz – wartość oferują funkcjonalność przemieszczania się po połączeniach.

8.4.2. Transakcje dla wielu operacji Jeżeli zapisujesz wiele kluczy i przy zapisie któregoś z nich wystąpi błąd, a Ty chciałbyś cofnąć zmiany wprowadzone przez pozostałe operacje, magazyny klucz – wartość będą złym rozwiązaniem.

8.4.3. Zapytania na danych Jeżeli musisz przeszukiwać klucze na podstawie informacji zapisanej w wartości pary klucz – wartość, magazyny klucz – wartość nie pozwolą Ci w łatwy sposób uzyskać zamierzonego efektu. Nie ma sposobu na sprawdzenie wartości po stronie bazy danych, z wyjątkiem niektórych produktów, takich jak Riak Search, lub systemów indeksujących takich jak Lucene [Lucene] lub Solr [Solr].

97

98

ROZDZIAŁ 8. BAZY KLUCZ – WARTOŚĆ

8.4.4. Operacje na zestawach Ponieważ operacje są ograniczone do jednego klucza naraz, nie ma możliwości wykonania operacji na wielu kluczach. Jeżeli musisz wykonać operacje na wielu kluczach, musisz obsłużyć je po stronie klienta.

Rozdział 9

Bazy dokumentów

Dokumenty są głównym elementem baz dokumentów. Baza danych przechowuje i zwraca dokumenty XML, JSON, BSON i tak dalej. Dokumenty to samoopisujące się, hierarchiczne struktury drzewiaste, które mogą składać się z map, kolekcji i wartości skalarnych. Przechowywane dokumenty są do siebie podobne, ale nie muszą być dokładnie takie same. Bazy dokumentów przechowują dokumenty w wartości magazynu klucz – wartość; bazy dokumentów to bazy klucz – wartość, w których można przeglądać wartości. Porównajmy terminologię bazy Oracle i MongoDB. Oracle

MongoDB

instancja bazy danych

instancja MongoDB

schemat

baza danych

tabela

kolekcja

wiersz

dokument

rowid

_id

złączenie

DBRef

Pole _id to pole specjalne i istnieje we wszystkich dokumentach w Mongo, podobnie jak ROWID w Oracle. W MongoDB _id może być ustawiane przez użytkownika, o ile jest unikalne.

9.1. Czym jest baza dokumentów? { "imie": "Marcin", "lubi": [ "Jazda na rowerze", "Fotografia" ], "ostatnieMiasto": "Wrocław", "ostatnieOdwiedziny": }

Powyższy dokument mógłby być wierszem w tradycyjnej bazie relacyjnej. Przyjrzyjmy się innemu dokumentowi: 99

100

ROZDZIAŁ 9. BAZY DOKUMENTÓW { "imie": "Radosław", "odwiedzoneMiasta": [ "Warszawa", "Londyn", "Pune", "Bangalur" ], "adresy": [ { "kraj": "Polska", "miasto": "KRAKÓW", "typ": "R" }, { "kraj": "Indie", "miasto": "PUNE", "typ": "R" } ], "ostatnieMiasto": "Warszawa" }

Patrząc na dokumenty, widzimy, że są podobne, ale różnią się nazwami atrybutów. Takie różnice są dozwolone w bazach dokumentów. Schemat danych może być różny pomiędzy dokumentami, a te dokumenty mogą nadal należeć do tej samej kolekcji — w przeciwieństwie do baz transakcyjnych, gdzie każdy wiersz tabeli musi mieć taki sam schemat. Lista odwiedzonych miast (odwiedzoneMiasta) reprezentowana jest przez tablicę lub listę adresów (adresy), czyli dokumentów osadzonych w głównym dokumencie. Osadzanie dokumentów potomnych jako podobiektów ułatwia dostęp i poprawia wydajność. Jeżeli spojrzysz na dokumenty, zobaczysz, że niektóre atrybuty są wspólne, np. imie lub miasto. Jednocześnie są w drugim dokumencie atrybuty, których nie ma w pierwszym, np. adresy, a dokument pierwszy posiada atrybut lubi, którego nie ma w drugim. Taka zróżnicowana reprezentacja danych różni się od tej z baz relacyjnych, gdzie każda kolumna musi być zdefiniowana, a jeżeli nie przechowuje danych, musi być oznaczona jako pusta lub wypełniona wartością null. W dokumentach nie ma pustych atrybutów; jeżeli dany atrybut nie zostanie znaleziony, zakładamy, że nie został ustawiony lub jest nie istotny dla dokumentu. Dokumenty pozwalają na tworzenie nowych atrybutów bez konieczności ich definiowania lub zmiany istniejących dokumentów. Niektóre z najbardziej popularnych baz dokumentów, z jakimi się spotkaliśmy, to MongoDB [MongoDB], CouchDB [CouchDB], Terrastore [Terrastore], OrientDB [OrientDB], RavenDB [RavenDB] i oczywiście dobrze znany i często piętnowany Lotus Notes [Notes Storage Facility], który wykorzystuje magazyn dokumentów.

9.2. Funkcjonalności Istnieje wiele wyspecjalizowanych baz dokumentów, jednak jako reprezentanta zestawu funkcjonalności wykorzystamy MongoDB. Pamiętaj, że każdy produkt posiada funkcjonalności, które mogą nie występować w pozostałych bazach magazynów. Poświęćmy trochę czasu na zrozumienie, jak działa MongoDB. Każda instancja MongoDB posiada wiele baz danych, a każda baza danych posiada wiele kolekcji. Jeżeli porównamy to z bazą relacyjną, instancja bazy relacyjnej będzie tym samym co instancja MongoDB, schematy relacyjne podobne są do baz MongoDB, a tabele to kolekcje MongoDB. Kiedy zapisujemy dokument, musimy wybrać, do jakiej bazy danych i kolekcji ma trafić — na przykład database.collection.insert(document), co reprezentowane jest z reguły poleceniem db.coll.insert(document).

9.2. FUNKCJONALNOŚCI

9.2.1. Spójność Spójność w MongoDB jest konfigurowana za pomocą zestawów replik i możliwości wyboru, czy zapis powinien być replikowany na wszystkie serwery podległe, czy tylko na określoną ich liczbę. Podczas każdego zapisu można podać liczbę serwerów, na które zapis musi zostać rozpropagowany, zanim zostanie uznany za pomyślny. Polecenie w stylu db.runCommand({ getlasterror : 1 , w : "majority" }) mówi serwerowi, jak silnej spójności wymagasz. Na przykład jeżeli posiadasz jeden serwer i w parametrze W przekażesz majority (większość), zapis zakończy się powodzeniem natychmiast, ponieważ jest jeden serwer. Jeżeli masz trzy serwery w zestawie replik i przekażesz majority, zapis będzie musiał zostać wykonany przynajmniej na dwóch z nich, zanim zostanie zaraportowany jako prawidłowy. Możesz zwiększyć wartość W w celu poprawienia spójności, ale wydajność zapisu spadnie, ponieważ zapis będzie musiał zostać wykonany na większej liczbie serwerów. Zestawy replik pozwalają też zwiększyć wydajność odczytu ze względu na możliwość odczytu z serwerów podległych dzięki parametrowi slaveOk, który ustawiany jest dla połączenia, bazy, kolekcji lub indywidualnie dla każdej operacji. Mongo mongo = new Mongo("localhost:27017"); mongo.slaveOk();

Powyżej ustawiamy parametr slaveOk dla operacji, abyśmy mogli decydować, które operacje mogą pobierać dane z serwerów podległych. DBCollection collection = getOrderCollection(); BasicDBObject query = new BasicDBObject(); query.put("imie", "Marcin"); DBCursor cursor = collection.find(query).slaveOk();

Podobnie jak w przypadku różnych operacji odczytu, jeżeli zajdzie taka konieczność, możesz zmieniać ustawienia w celu osiągnięcia silnej spójności zapisu. Domyślnie zapis jest uznawany za pomyślny, kiedy baza danych go otrzyma; możesz to zmienić tak, aby baza czekała z odpowiedzią, aż dane zostaną zapisane na dysk lub rozpropagowane na odpowiednią liczbę serwerów podległych. Zmienna ta nazywa się WriteConcern (troska o zapis): upewniasz się, że niektóre dane zapisywane są na serwer główny i serwery podległe, ustawiając dla zmiennej WriteConcern wartość REPLICAS_SAFE. Poniżej znajduje się kod, w którym zmienna WriteConcern ustawiana jest dla wszystkich zapisów w kolekcji: DBCollection zakupy = database.getCollection("zakupy"); zakupy.setWriteConcern(REPLICAS_SAFE);

Troska o zapis może też być ustawiana dla pojedynczej operacji poprzez wyszczególnienie jej podczas zapisu: WriteResult wynik = shopping.insert(zamowienie, REPLICAS_SAFE);

Musisz dokładnie przemyśleć, czy dla Twojej aplikacji odpowiednie będzie ustawienie slaveOk podczas odczytu oraz jaki poziom bezpieczeństwa chcesz zapewnić dzięki zmiennej WriteConcern.

101

102

ROZDZIAŁ 9. BAZY DOKUMENTÓW

9.2.2. Transakcje W tradycyjnym znaczeniu baz relacyjnych transakcje oznaczają, że możesz rozpocząć modyfikowanie bazy za pomocą poleceń insert, update lub delete na różnych tabelach, a potem zdecydować, czy chcesz zachować zmiany, czy nie, korzystając z poleceń commit i rollback. Taka konstrukcja jest niedostępna w bazach NoSQL — zapis albo się powiedzie, albo zostanie zakończony niepowodzeniem. Transakcje na poziomie jednego dokumentu nazywane są transakcjami atomowymi. Transakcje zawierające więcej niż jedną operację nie są możliwe, chociaż istnieją produkty, np. RavenDB, wspierające transakcje pomiędzy wieloma operacjami. Domyślnie wszystkie zapisy są raportowane jako pomyślne. Dodatkową kontrolę nad zapisem możemy sprawować poprzez parametr WriteConcern. Korzystając z WriteConcern. REPLICAS_SAFE, zapewniamy, że dane są zapisane na więcej niż jednym serwerze, zanim zapis zostanie uznany za poprawny. Różne poziomy parametru WriteConcern pozwalają regulować poziom bezpieczeństwa przy zapisie; na przykład zapisując wpisy do logu, możesz użyć najniższego poziomu bezpieczeństwa, WriteConcern.NONE. final Mongo mongo = new Mongo(mongoURI); mongo.setWriteConcern(REPLICAS_SAFE); DBCollection zakupy = mongo.getDB(bazaZamowien) .getCollection(kolekcjaZakupow); try { WriteResult result = shopping.insert(zamowienie, REPLICAS_SAFE); // Zapis został wykonany na serwerze głównym i przynajmniej jednym serwerze podległym. } catch (MongoException writeException) { // Zapis nie został wykonany na co najmniej dwóch serwerach włącznie z serwerem głównym. dealWithWriteFailure(zamowienie, writeException); }

9.2.3. Dostępność Teoria CAP (patrz punkt 5.3.1) mówi, że możemy mieć tylko dwa z trzech — spójności, dostępności i tolerancji na partycjonowanie. Bazy dokumentów starają się poprawiać dostępność poprzez replikację master-slave. Te same dane są dostępne na wielu serwerach, a klient może dostać się do danych nawet wtedy, kiedy serwer główny nie działa. Zazwyczaj kod aplikacji nie musi rozpoznawać, czy serwer główny jest dostępny, czy nie. MongoDB implementuje replikację, zapewniając wysoką dostępność przy wykorzystaniu zestawów replik. W zestawie replik funkcjonują minimum dwa serwery uczestniczące w asynchronicznej replikacji master-slave. Zestaw replik wybiera serwer główny spośród wszystkich serwerów w zestawie. Zakładając, że wszystkie serwery mają jednakowe prawo głosu, niektóre mogą otrzymać głos ze względu na ich położenie względem innych, ze względu na większą ilość pamięci RAM i tak dalej; użytkownicy mogą wpływać na głosowanie poprzez nadawanie serwerom priorytetów — priorytet to liczba pomiędzy 0 a 1000. Wszystkie żądania przesyłane są do serwera głównego, a dane są replikowane na serwery podległe. Jeżeli serwer główny przestanie działać, pozostałe serwery wybierają spośród siebie nowy serwer główny; wszystkie kolejne żądania wysyłane są do nowego serwera głównego, a serwery podległe zaczynają otrzymywać z niego dane. Kiedy serwer, który przestał działać, wraca, dołącza do zestawu jako serwer podległy i pobiera brakujące dane w celu aktualizacji.

9.2. FUNKCJONALNOŚCI

Rysunek 9.1 przedstawia przykładową konfigurację zestawu replik. Mamy dwa serwery — mongo A i mongo B, na których uruchomiona jest baza MongoDB, znajdujące się w głównym datacenter — oraz mongo C, znajdujący się w pobocznym datacenter. Jeżeli chcemy, aby pierwsze dwa serwery były wybierane na serwery główne, możemy przypisać im wyższy priorytet niż pozostałym. Do zestawu replik można dodawać nowe serwery bez konieczności wyłączania tych już funkcjonujących.

Rysunek 9.1. Konfiguracja zestawu replik z wyższym priorytetem przyznanym serwerom z tego samego datacenter Aplikacja zapisuje i odczytuje z serwera głównego. Kiedy nawiązywane jest połączenie, aplikacja musi połączyć się tylko z jednym serwerem (nie ma znaczenia, czy jest to serwer główny) z zestawu replik, a reszta serwerów jest rozpoznawana automatycznie. Kiedy główny serwer przestaje działać, sterownik rozmawia z nowo wybranym przez zestaw replik serwerem głównym. Aplikacja nie musi zarządzać usterkami w komunikacji ani kryteriami wyboru serwera. Dzięki zestawom replik możesz korzystać z wysoko dostępnego magazynu dokumentów. Zestawy replik wykorzystywane są do uzyskiwania redundancji danych, automatycznego przełączania serwerów, skalowania odczytu i możliwości prowadzenia prac konserwacyjnych bez całkowitego wyłączania bazy oraz jako zabezpieczenie przed awariami. Podobną dostępność można uzyskać z bazami CouchDB, RavenDB, Terrastore i innymi.

9.2.4. Możliwości zapytań Bazy danych dokumentów udostępniają różne możliwości zapytań. CouchDB pozwala wykonywać zapytania poprzez widoki — skomplikowane zapytania na dokumentach, które mogą być zmaterializowane (patrz podrozdział 3.4) lub dynamiczne (zupełnie jak widoki w bazach relacyjnych, które także mogą być zmaterializowane lub nie). W CouchDB, jeżeli potrzebujesz

103

104

ROZDZIAŁ 9. BAZY DOKUMENTÓW

agregację zawierającą liczbę recenzji produktu oraz średnią ocenę, możesz dodać widok zaimplementowany przy wykorzystaniu map-reduce (patrz podrozdział 7.1) zwracający takie informacje. Jeżeli taka informacja potrzebna jest wielokrotnie, nie będziesz chciał przetwarzać danych przy każdym żądaniu, możesz natomiast dodać widok zmaterializowany, który wykona obliczenia i przechowa ich wynik do późniejszego użycia. Widoki zmaterializowane są aktualizowane, gdy ktoś uzyskuje do nich dostęp i jeżeli od ostatniego ich użycia jakieś dane uległy zmianie. Jedną z zalet baz dokumentów w porównaniu z magazynami klucz – wartość jest to, że możemy odpytywać dane wewnątrz dokumentu bez konieczności pobierania go do aplikacji. Ta funkcjonalność zbliża bazy dokumentów do baz transakcyjnych. MongoDB ma język zapytań oparty na notacji JSON i posiada konstrukcje takie jak $query będące odpowiednikiem klauzuli where, $orderby na potrzeby sortowania danych czy $explain pokazujący plan wykonania zapytania. Takich konstrukcji jest znacznie więcej, a wszystkie one mogą być łączone w zapytania MongoDB. Przyjrzyjmy się konkretnym zapytaniom, które możemy wykonać w bazie MongoDB. Załóżmy, że chcemy zwrócić wszystkie dokumenty w kolekcji zamówień (wszystkie wiersze w tabeli zamówień). Zapytanie SQL wyglądałoby następująco: SELECT * FROM zamowienie

Ekwiwalentem w Mongo będzie polecenie: db.zamowienie.find()

Wybranie zamówień dla pojedynczego klientId o wartości 883c2c5b4e5b wyglądałoby tak: SELECT * FROM zamowienie WHERE klientId= "883c2c5b4e5b"

Równoznaczne zapytanie w Mongo, pobierające wszystkie zamówienia dla klienta o identyfikatorze 883c2c5b4e5b, wygląda tak: db.zamowienie.find({"klientId":"883c2c5b4e5b"})

Podobnie wybranie pól zamowienieId i dataZamowienia dla jednego klienta w bazie SQL przyjmie formę: SELECT zamowienieId,dataZamowienia FROM zamowienie WHERE klientId = "883c2c5b4e5b"

A w Mongo: db.zamowienie.find({klientId:"883c2c5b4e5b"},{zamowienieId:1,dataZamowienia:1})

Zapytania pozwalające na sumowanie, zliczanie i tak dalej także są dostępne. Ponieważ dokumenty to obiekty zagregowane, bardzo łatwo jest wykonywać zapytania o dokumenty na podstawie pól obiektów podrzędnych. Załóżmy, że chcemy znaleźć wszystkie zamówienia, dla których jedna z pozycji ma nazwę zawierającą słowo Refaktoring. SQL dla takiego zapytania wyglądałby następująco: SELECT * FROM zamowienieKlienta, pozycjaZamowienia, produkt WHERE zamowienieKlienta.zamowienieId = pozycjaZamowienia.zamowienieKlientaId

9.2. FUNKCJONALNOŚCI AND pozycjaZamowienia.produktId = produkt.produktId AND produkt.nazwa LIKE '%Refaktoring%'

W Mongo skorzystamy z polecenia: db.zamowienia.find({"pozycje.produkt.nazwa":/Refaktoring/})

Zapytanie w MongoDB jest prostsze, ponieważ obiekty są osadzone wewnątrz pojedynczego dokumentu i możesz na nich wykonywać zapytania.

9.2.5. Skalowanie Skalowanie to dodawanie serwerów lub zmienianie magazynu danych inaczej niż poprzez przenoszenie bazy danych na mocniejszy serwer. Nie rozmawiamy o zmianach w aplikacji pozwalających na obsłużenie większego obciążenia; zamiast tego interesuje nas, jakie baza danych udostępnia funkcjonalności, które pozwolą jej obsłużyć większy ruch. Skalowanie na potrzeby dużego ruchu odczytu można osiągnąć poprzez dodanie większej liczby serwerów podległych, tak aby część ruchu mogła zostać rozdzielona pomiędzy nie. W przypadku aplikacji, w której wykonywane jest bardzo dużo operacji odczytu z naszego klastra zawierającego trzy serwery, możemy poprawić możliwości obsługi odczytu klastra, dodając po prostu nowe serwery i wykonując zapytania z flagą slaveOk (rysunek 9.2). Jest to skalowanie horyzontalne na potrzeby odczytu.

Rysunek 9.2. Dodanie nowego serwera, mongo D, do istniejącego zestawu replik Kiedy nowy serwer, mongo D, zostanie uruchomiony, musimy dodać go do zestawu replik. rs.add("mongod:27017");

Nowy serwer zsynchronizuje się z pozostałymi, dołączy do zestawu replik jako serwer podrzędny i rozpocznie obsługę żądań odczytu. Zaletą takiej architektury jest to, że nie musimy restartować żadnych serwerów, a aplikacja nie odczuje braku bazy danych.

105

106

ROZDZIAŁ 9. BAZY DOKUMENTÓW

Kiedy chcemy skalować na potrzeby zapisu, możemy wykorzystać sharding (patrz podrozdział 4.2). Sharding jest podobny do partycji w bazie relacyjnej, gdzie dane dzielone są na podstawie konkretnej kolumny, np. stanu albo roku. W bazie relacyjnej partycje znajdują się z reguły na tym samym serwerze, więc klient nie musi odpytywać konkretnej partycji, może natomiast odpytywać całą tabelę; baza jest odpowiedzialna za odnalezienie odpowiedniej partycji i zwrócenie danych. W przypadku shardingu dane także dzielone są na podstawie konkretnego pola, ale są umieszczane na odrębnych serwerach Mongo. Dane są dynamicznie przesuwane pomiędzy serwerami, aby zapewnić balansowanie serwerów. Do klastra możemy dodawać dodatkowe serwery, aby zwiększyć liczbę możliwych miejsc zapisu, co z kolei pozwala na horyzontalne skalowanie na potrzeby zapisu. db.runCommand( { shardcollection : "ecommerce.klient", key : {imie : 1} } )

Dzielenie danych na podstawie imienia klienta zapewnia, że dane są równomiernie rozłożone pomiędzy serwerami w celu optymalizacji zapisu, co więcej, każdy shard może być zestawem replik, zapewniając lepszą wydajność odczytu (rysunek 9.3). Kiedy dodajemy nowy shard do istniejącego klastra, dane zostają rozłożone na cztery serwery, a nie jak dotychczas na trzy. Podczas gdy zmieniany jest sposób podziału danych, baza w dalszym ciągu działa, chociaż klaster może nieco zmniejszyć wydajność, jeżeli przenoszone będą duże ilości danych.

Rysunek 9.3. Klaster shardów MongoDB, gdzie każdy shard jest zestawem replik Klucz sharda odgrywa zasadniczą rolę. Możesz chcieć umieścić shardy MongoDB bliżej użytkowników, więc podział w oparciu o położenie geograficzne użytkownika może być dobrym pomysłem. Przy podziale danych w oparciu o położenie geograficzne użytkownika wszystkie dane dla wschodniego wybrzeża Stanów Zjednoczonych znajdą się w shardach na wschodnim wybrzeżu, a dane użytkowników z zachodniego wybrzeża na tych na zachodnim wybrzeżu.

9.3. PASUJĄCE PRZYPADKI UŻYCIA

9.3. Pasujące przypadki użycia 9.3.1. Logowanie zdarzeń Aplikacje posiadają różne potrzeby przechowywania informacji o zdarzeniach; wewnątrz korporacji istnieje wiele różnych aplikacji chcących zapisywać zdarzenia. Bazy dokumentów mogą przechowywać różne rodzaje zdarzeń i działać jako centralne miejsce przechowywania takich danych. Taka baza jest szczególnie przydatna, jeżeli logowane zdarzenia ulegają zmianie. Zdarzenia można podzielić na shardy na podstawie nazwy aplikacji, z której pochodzą, lub na podstawie typu zdarzenia, np. zamówienie_przetworzone albo klient_zalogowany.

9.3.2. Systemy zarządzania zawartością i platformy blogerskie Ponieważ bazy dokumentów nie mają predefiniowanego schematu i przeważnie rozumieją dokumenty JSON, sprawdzają się w systemach zarządzania zawartością lub aplikacjach dla stron publikujących różnego rodzaju artykuły, zarządzających kontami i komentarzami użytkowników, profilami i dokumentami sieciowymi.

9.3.3. Analizy stron internetowych lub analizy w czasie rzeczywistym Bazy dokumentów mogą przechowywać dane dla analiz w czasie rzeczywistym. Ponieważ możliwe jest aktualizowanie części dokumentu, łatwo możemy przechowywać odsłony strony czy unikalnych odwiedzających; nowe metryki mogą być dodawane bez konieczności zmiany schematu.

9.3.4. Aplikacje e-commerce Aplikacje e-commerce potrzebują często elastycznego schematu na potrzeby przechowywania produktów i zamówień oraz możliwości zmieniania modelu danych bez kosztownego refaktoringu baz danych lub konieczności migrowania danych (patrz podrozdział 12.3).

9.4. Kiedy nie stosować Istnieją obszary, gdzie magazyn dokumentów nie będzie odpowiednim narzędziem.

9.4.1. Złożone transakcje obejmujące różne operacje Jeżeli potrzebujesz operacji atomowych obejmujących więcej niż jeden dokument, bazy dokumentów nie są dla Ciebie. Istnieją jednak bazy dokumentów wspierające tego typu operacje, np. RavenDB.

107

108

ROZDZIAŁ 9. BAZY DOKUMENTÓW

9.4.2. Zapytania na zmiennej strukturze agregacji Elastyczny schemat oznacza, że baza danych nie wprowadza restrykcji odnośnie schematu. Dane zapisywane są w formie encji aplikacji. Jeżeli musisz odwoływać się do nich ad hoc, Twoje zapytania będą się zmieniać (w terminologii baz relacyjnych oznaczałoby to, że łączone tabele zmieniałyby się podczas łączenia). Ponieważ dane zapisywane są jako agregacje, jeżeli struktura agregacji ulegałaby ciągłym zmianom, musiałbyś zapisywać agregacje na niższym poziomie szczegółowości — mówiąc prościej, musiałbyś znormalizować dane. W takim scenariuszu baza dokumentów może się nie sprawdzić.

Rozdział 10

Bazy rodziny kolumn

Magazyny rodziny kolumn, takie jak Cassandra [Cassandra], HBase [Hbase], Hypertable [Hypertable] czy Amazon SimpleDB [Amazon SimpleDB], pozwalają przechowywać dane z kluczami zmapowanymi na wartości i wartości zgrupowane w rodziny kolumn, a każda rodzina kolumn to mapa danych. Baza transakcyjna

Cassandra

instancja bazy danych

klaster

baza danych

przestrzeń kluczy

tabela

rodzina kolumn

wiersz

wiersz

kolumna (jednakowe dla wszystkich wierszy)

kolumna (mogą być różne dla różnych wierszy)

10.1. Czym jest magazyn rodziny kolumn? Istnieje wiele baz rodziny kolumn. W tym rozdziale porozmawiamy o bazie Cassandra, ale w celu omówienia funkcjonalności przydatnych w szczególnych przypadkach odniesiemy się też do innych tego typu produktów. Bazy rodziny kolumn przechowują dane w rodzinach kolumn w formie wierszy, do których przypisany jest klucz (rysunek 10.1). Rodziny kolumn to grupy spokrewnionych danych, które przeważnie pobierane są razem. W przypadku encji Klient często w tym samym czasie pobieralibyśmy także Profil, ale nie Zamówienia. Cassandra to jedna z bardziej popularnych baz rodziny kolumn; inne to HBase, Hypertable i Amazon DynamoDB [Amazon DynamoDB]. Cassandra jest szybka i łatwo skalowalna z operacjami zapisu wykonywanymi na całym klastrze. Klaster nie posiada serwera głównego, więc każdy odczyt i zapis może być obsługiwany przez dowolny serwer w klastrze.

109

110

ROZDZIAŁ 10. BAZY RODZINY KOLUMN

Rysunek 10.1. Model danych bazy rodziny kolumn Cassandra

10.2. Funkcjonalności Zacznijmy od omówienia struktury danych w Cassandrze. Podstawową jednostką przechowywania jest kolumna. Kolumna składa się z pary nazwa – wartość, gdzie nazwa odgrywa rolę klucza. Każda z par klucz – wartość jest pojedynczą kolumną i zawsze przechowywana jest wraz ze stemplem czasowym. Stempel czasowy jest wykorzystywany do ustalania daty ważności danych, rozwiązywania konfliktów zapisu, radzenia sobie z nieaktualnymi danymi i innych zadań. Kiedy dane kolumny nie są wykorzystywane, przestrzeń dyskowa może zostać odzyskana w późniejszym czasie, podczas fazy przetwarzania. { name: "imie", value: "Marcin", timestamp: 12345667890 }

Kolumna posiada klucz o nazwie imie i wartości Marcin, posiada także przypisany stempel czasowy. Wiersz jest kolekcją kolumn przypisanych do klucza; kolekcja podobnych wierszy składa się na rodzinę kolumn. Kiedy kolumny w rodzinie są kolumnami prostymi, rodzina kolumn jest standardową rodziną kolumn. // rodzina kolumn { // wiersz "radoslaw-nowak" : { imie: "Radosław", nazwisko: "Nowak", ostatniaWizyta: "2012/12/12" } // wiersz "marcin-kowalski" : { imie: "Marcin", nazwisko: "Kowalski", polozenie: "Warszawa" } }

10.2. FUNKCJONALNOŚCI

Każda kolumna może być porównana z tabelą systemu relacyjnego, gdzie klucz identyfikuje wiersz, a wiersz składa się z wielu kolumn. Różnica polega na tym, że poszczególne wiersze nie muszą mieć takich samych kolumn, a kolumny mogą być swobodnie dodawane w dowolnym czasie do dowolnego wiersza bez konieczności dodawania ich do pozostałych wierszy. Posiadamy wiersze radoslaw-nowak i marcin-kowalski, a każdy z nich ma inne kolumny; oba wiersze są częścią rodziny kolumn. Kiedy kolumna składa się z mapy kolumn, otrzymujemy superkolumnę. Superkolumna składa się z nazwy i wartości, która jest mapą kolumn. Superkolumnę możesz sobie wyobrazić jako kontener na kolumny. { name: "book:978-0767905923", value: { autor: "Mitch Albom", tytul: "Wtorki z Morriem", isbn: "978-0767905923" } }

Kiedy do stworzenia rodziny kolumn wykorzystujemy superkolumny, otrzymujemy rodzinę superkolumn. // rodzina superkolumn { // wiersz name: "fakturowanie:marcin-kowalski", value: { adres: { name: "adres:domyslny", value: { imieNazwisko: "Marcin Kowalski", ulica:"Parkowa 40", kod: "10-100" } }, fakturowanie: { name: "fakturowanie:domyslne", value: { kartaKredytowa: "8888-8888-8888-8888", dataWaznosci: "12/2016" } } } // wiersz name: "fakturowanie:radoslaw-nowak", value: { adres: { name: "adres:domyslny", value: { imieNazwisko: "Radosław Nowak", ulica:"Polna 12", kod: "50-555" } }, fakturowanie: { name: "fakturowanie:domyslne", value: {

111

112

ROZDZIAŁ 10. BAZY RODZINY KOLUMN kartaKredytowa: "9999-8888-7777-4444", dataWaznosci: "01/2016" } } } }

Rodziny superkolumn pozwalają trzymać blisko siebie spokrewnione dane, jednak w tym przypadku kolumny, które przeważnie nie są wykorzystywane, są zawsze pobierane i deserializowane przez bazę, co może nie być optymalne. Cassandra grupuje standardowe rodziny i rodziny superkolumn w przestrzenie kluczy. Przestrzenie kluczy są podobne do baz danych w systemach transakcyjnych, gdzie przechowywane są wszystkie rodziny kolumn spokrewnione z aplikacją. Aby rodziny kolumn mogły być przypisywane do przestrzeni kluczy, musimy je najpierw utworzyć: create keyspace ecommerce

10.2.1. Spójność Kiedy do bazy Cassandra dociera żądanie zapisu, dane są zapisywane najpierw w logu zapisu, a następnie w strukturze w pamięci o nazwie memtable. Operacja zapisu jest uważana za udaną, kiedy dane zostaną zapisane w logu i w memtable. Dane są przechowywane w pamięci i co jakiś czas zapisywane w strukturach nazywających się SSTable. Struktury SSTable nie są zapisane do czasu ich opróżnienia; jeżeli w danych zaszły zmiany, zapisywana jest nowa SSTable. Niewykorzystywane struktury SSTable są odzyskiwane przez kompaktowanie. Przyjrzyjmy się operacji odczytu, aby zobaczyć, jaki wpływ mają na nią ustawienia spójności. Jeżeli nasze domyślne ustawienie spójności dla wszystkich operacji odczytu ma wartość ONE, w momencie otrzymania żądania odczytu Cassandra zwraca dane z pierwszej repliki, nawet jeżeli dane są nieaktualne. Jeżeli dane są nieświeże, kolejne odczyty pobiorą aktualne (najnowsze) dane; proces ten nazywa się naprawą odczytu. Niski poziom spójności jest dobry, kiedy nie przeszkadza Ci otrzymywanie danych nieaktualnych, a chciałbyś, aby odczyt miał wysoką wydajność. Podobnie kiedy zapisujesz dane, Cassandra zapisze je w logu zapisu jednego serwera i zwróci odpowiedź do klienta. Spójność o wartości ONE jest dobrym wyborem, jeżeli Twoje oczekiwania co do szybkości zapisu są bardzo wysokie i nie przeszkadza Ci, że niektóre zapisy mogą zostać utracone — co może mieć miejsce, jeżeli serwer przestanie działać, zanim zmiany zostaną zreplikowane na inne serwery. quorum = new ConfigurableConsistencyLevel(); quorum.setDefaultReadConsistencyLevel(HConsistencyLevel.QUORUM); quorum.setDefaultWriteConsistencyLevel(HConsistencyLevel.QUORUM);

Korzystając z ustawienia spójności QUORUM dla odczytu i zapisu, upewniamy się, że podczas odczytu odpowie większość serwerów i wybrana zostanie kolumna z najnowszym stemplem czasowym, a serwery nieposiadające najnowszych danych zostaną naprawione za pomocą operacji naprawy odczytu. Podczas operacji zapisu to ustawienie oznacza, że zmiany muszą zostać rozpropagowane na większość serwerów, zanim klient zostanie powiadomiony o pomyślnym zapisie.

10.2. FUNKCJONALNOŚCI

Poziom spójności ALL oznacza, że wszystkie serwery będą musiały odpowiedzieć na żądania odczytu i zapisu, przez co klaster przestanie być odporny na awarie serwerów — nawet jeżeli tylko jeden serwer przestanie działać, wszystkie operacje odczytu i zapisu będą zablokowane i raportowane jako nieudane. Do architektów systemu należy więc dopasowanie poziomu spójności do wymagań aplikacji. W ramach tej samej aplikacji różne jej elementy mogą wymagać różnego poziomu spójności; poziom spójności może być zmieniany w zależności od operacji, na przykład wyświetlanie opinii użytkowników o produktach może wymagać innego poziomu spójności niż odczyt statusu ostatniego zamówienia złożonego przez klienta. Podczas tworzenia przestrzeni kluczy możemy określić, ile replik danych chcemy przechowywać. Ta liczba odzwierciedla współczynnik replikacji danych. Jeżeli posiadasz wskaźnik replikacji równy 3, dane kopiowane są na trzy serwery. Podczas zapisu i odczytu w systemie Cassandra, jeżeli określisz wartość spójności równą 2, okaże się, ze R + W jest większe od współczynnika replikacji (2 + 2 > 3), co oznacza lepszą spójność podczas zapisu i odczytu. Możemy uruchomić polecenie naprawy serwerów i zmusić bazę, aby porównała każdy klucz, za jaki jest odpowiedzialna, z pozostałymi replikami. Ponieważ taka operacja jest kosztowna, możemy naprawić tylko jedną rodzinę kolumn lub listę rodzin kolumn. repair ecommerce repair ecommerce klientInfo

Kiedy serwer nie działa, dane, które powinny być na nim zapisane, są przekazywane do innych serwerów. Kiedy serwer wraca do pracy, zmiany wprowadzane na danych są przesyłane na właściwy serwer. Ta technika nazywa się przekazanie ze wskazaniem. Dzięki temu serwery, które przestały działać, mogą po powrocie do sprawności szybciej podjąć swoje obowiązki.

10.2.2. Transakcje Cassandra nie posiada transakcji w tradycyjnym znaczeniu — takich, które pozwalają rozpocząć wiele zapisów, a następnie zdecydować, czy zatwierdzić zmiany, czy nie. Zapis w Cassandrze jest atomowy na poziomie wiersza, co oznacza, że wstawianie lub aktualizowanie kolumn dla danego klucza wiersza będzie traktowane jako jeden zapis i zakończy się albo powodzeniem, albo porażką. Operacje najpierw zapisywane są do logów zapisu i struktur memtable i uznawane są za poprawne, kiedy zapis w tych dwóch miejscach się powiedzie. Jeżeli serwer przestanie działać, log zapisu jest wykorzystywany do przywrócenia zmian na tym serwerze, zupełnie jak w przypadku logu redo w Oracle. Do synchronizacji odczytów i zapisów możesz korzystać z zewnętrznych bibliotek transakcji, takich jak ZooKeeper [ZooKeeper]. Istnieją także biblioteki takie jak Cages [Cages], pozwalające otaczać transakcjami operacje ZooKeeper.

10.2.3. Dostępność Ponieważ w klastrze nie ma serwera głównego, Cassandra jest wysoko dostępna. Dostępność klastra można podnieść, obniżając dla żądań poziom spójności. Dostępność określana jest nierównością (R + W) > N (patrz podrozdział 5.5), gdzie W to minimalna liczba serwerów,

113

114

ROZDZIAŁ 10. BAZY RODZINY KOLUMN

na których muszą zostać zapisane dane, R to minimalna liczba serwerów, które muszą odpowiedzieć na żądanie odczytu, a N jest liczbą serwerów partycypujących w replikacji danych. Możesz dostosowywać dostępność serwera, zmieniając wartości W i R przy niezmiennej wartości N. Jeśli klaster zawiera 10 serwerów Cassandra ze współczynnikiem replikacji dla przestrzeni kluczy równym 3 (N = 3) i ustawimy R = 2 i W = 2, otrzymamy (2 + 2) > 3. W tym przypadku jeżeli jeden z serwerów przestanie działać, dostępność nie zostanie zaburzona, ponieważ dane mogą być pobierane z dwóch pozostałych serwerów. Jeżeli ustawimy W = 2 i R = 1, a dwa serwery przestaną działać, klaster nie będzie mógł zapisywać danych, ale nadal będzie poprawnie odpowiadał na odczyt. Podobnie jeżeli ustawimy R = 2 i W = 1, klaster będzie mógł wyłącznie zapisywać dane. Dzięki nierówności R + W > N możesz podejmować świadome decyzje dotyczące dostępności klastra. Powinieneś projektować przestrzenie kluczy oraz operacje zapisu i odczytu zgodnie ze swoimi potrzebami — wyższa dostępność odczytu lub wyższa dostępność zapisu.

10.2.4. Możliwości zapytań Podczas projektowania modelu danych Cassandry warto zoptymalizować kolumny i rodziny kolumn na potrzeby odczytu, ponieważ jej język zapytań nie jest zbyt bogaty; kiedy dane są wstawiane do rodzin kolumn, dane w każdym wierszu są sortowane po nazwie kolumny. Jeżeli mamy kolumnę, która jest pobierana znacznie częściej niż inne, z punktu widzenia wydajności lepiej wykorzystać tę wartość jako klucz wiersza. 10.2.4.1. Podstawowe zapytania Podstawowe zapytania, które można wykonać przy wykorzystaniu klienta Cassandry, zawierają operacje GET, SET i DEL. Zanim zaczniemy wykonywać operacje na danych, musimy wykonać polecenie przestrzeni kluczy use ecommerce;. Dzięki temu mamy pewność, że wszystkie nasze zapytania zostaną wykonane w odpowiedniej przestrzeni kluczy. CREATE COLUMN FAMILY Klient WITH comparator = UTF8Type AND key_validation_class=UTF8Type AND column_metadata = [ {column_name: miasto, validation_class: UTF8Type} {column_name: nazwa, validation_class: UTF8Type} {column_name: strona, validation_class: UTF8Type} ];

Mamy rodzinę kolumn o nazwie Klient zawierającą kolumny miasto, nazwa i strona — za pomocą klienta Cassandry możemy wstawić dane. SET Klient['mkowalski']['miasto']='Warszawa'; SET Klient['mkowalski']['nazwa']='Marcin Kowalski'; SET Klient['mkowalski']['strona']='www.marcin_kowalski.com';

Te same dane możemy wstawić, korzystając z klienta Hector [Hector] Javy. ColumnFamilyTemplate szablon = cassandra.getColumnFamilyTemplate(); ColumnFamilyUpdater updater = szablon.createUpdater(klucz);

10.2. FUNKCJONALNOŚCI for (String nazwa : values.keySet()) { updater.setString(nazwa, values.get(nazwa)); } try { szablon.update(updater); } catch (HectorException e) { handleException(e); }

Możemy z powrotem odczytać dane, korzystając z polecenia GET. Istnieje wiele sposobów na pobranie danych; możemy pobrać całą rodzinę kolumn. GET Klient ['mkowalski'];

Możemy też pobrać tylko jedną kolumnę z rodziny kolumn. GET Klient['mkowalski']['strona'];

Pobranie pojedynczej kolumny jest bardziej wydajne, ponieważ zwracane są tylko te dane, które nas interesują — co zmniejsza ilość przenoszonych danych, szczególnie jeżeli rodzina ma wiele kolumn. Aktualizowanie danych jest równoznaczne z wykorzystaniem polecenia SET dla kolumny, której trzeba nadać nową wartość. Korzystając z polecenia DEL, możemy usunąć kolumnę lub całą rodzinę kolumn. DEL Klient['mkowalski']['miasto']; DEL Klient['mkowalski'];

10.2.4.2. Zaawansowane zapytania i indeksowanie Cassandra pozwala indeksować inne kolumny niż tylko klucze. Możemy zdefiniować indeks na kolumnie miasto. UPDATE COLUMN FAMILY Klient WITH comparator = UTF8Type AND column_metadata = [{column_name: miasto, validation_class: UTF8Type, index_type: KEYS}];

Możemy teraz wykonywać zapytania na podstawie zaindeksowanej kolumny. GET Customer WHERE city = 'Warszawa';

Te indeksy implementowane są jako indeksy bit-mapped i dobrze sprawdzają się dla kolumn o niskiej kardynalności. 10.2.4.3. Język zapytań CQL (Cassandra Query Language) Cassandra posiada język zapytań wspierający polecenia podobne do poleceń SQL. Możemy skorzystać z poleceń CQL do stworzenia rodziny kolumn. CREATE COLUMNFAMILY Klient ( KEY varchar PRIMARY KEY, nazwa varchar, miasto varchar, strona varchar);

115

116

ROZDZIAŁ 10. BAZY RODZINY KOLUMN

Korzystając z CQL, wstawiamy te same dane co poprzednio. INSERT INTO Klient (KEY,nazwa,miasto,strona) VALUES ('mkowalski', 'Marcin Kowalski', 'Warszawa', 'www.marcin_kowalski.com');

Możemy odczytywać dane za pomocą polecenia SELECT. Tutaj odczytujemy wszystkie kolumny: SELECT * FROM Klient

Możemy też wybrać tylko te kolumny, które są nam potrzebne. SELECT nazwa,strona FROM Klient

Kolumny indeksowane są tworzone poleceniem CREATE INDEX, można je potem wykorzystywać w zapytaniach. SELECT nazwa,strona FROM Klient WHERE miasto='Warszawa'

CQL ma jeszcze wiele możliwości, ale nie posiada wszystkich funkcji języka SQL. CQL nie pozwala na wykonywanie złączeń i podzapytań, a klauzule WHERE są z reguły bardzo proste.

10.2.5. Skalowanie Skalowanie istniejącego klastra Cassandry polega na dodawaniu nowych serwerów. Ponieważ żaden serwer nie jest serwerem głównym, dodając do klastra nowe serwery, poprawiamy możliwości serwera co do odpowiadania na zapytania. Takie skalowanie poziome pozwala na maksymalizowanie czasu dostępności klastra, ponieważ podczas dodawania nowych serwerów klaster nadal może odpowiadać na zapytania klientów.

10.3. Pasujące przypadki użycia Omówmy kilka problemów, gdzie bazy rodziny kolumn będą dobrym rozwiązaniem.

10.3.1. Logowanie zdarzeń Bazy rodziny kolumn ze swoją zdolnością do przechowywania dowolnych struktur danych są dobrym wyborem w przypadku logowania zdarzeń, takich jak stan aplikacji lub napotkane błędy. W ramach przedsiębiorstwa wszystkie aplikacje mogą zapisywać zdarzenia w bazie Cassandra ze swoimi własnymi kolumnami i kluczem w formie nazwaaplikacji:stempel czasowy. Ponieważ możemy skalować zapis, Cassandra idealnie sprawdzi się w roli systemu logowania zdarzeń (rysunek 10.2).

10.3. PASUJĄCE PRZYPADKI UŻYCIA

Rysunek 10.2. Cassandra jako system logujący zdarzenia

10.3.2. Systemy zarządzania treścią i platformy blogowe Korzystając z rodzin kolumn, możesz przechowywać wpisy na blogu wraz ze znacznikami, kategoriami i łączami w kolumnach. Komentarze mogą być przechowywane w tym samym wierszu lub przeniesione do innej przestrzeni kluczy; podobnie użytkownicy blogów i same blogi mogą zostać umieszczone w odrębnych rodzinach kolumn.

10.3.3. Liczniki W aplikacjach webowych często na potrzeby analiz konieczne jest zliczanie i kategoryzowanie użytkowników strony. Podczas tworzenia rodziny kolumn możesz wykorzystać CounterColumnType. CREATE COLUMN FAMILY wizyta_klienta WITH default_validation_class=CounterColumnType AND key_validation_class=UTF8Type AND comparator=UTF8Type;

Kiedy rodzina kolumn zostanie stworzona, możesz mieć osobną kolumnę dla każdej strony i każdego użytkownika. INCR visit_counter['mkowalski'][startowa] BY 1; INCR visit_counter['mkowalski'][produkty] BY 1; INCR visit_counter['mkowalski'][kontakt] BY 1;

Liczniki można inkrementować, korzystając z języka CQL. UPDATE wizyta_klienta SET startowa = startowa + 1 WHERE KEY='mkowalski'

10.3.4. Wygasające dane Możesz oferować swoim użytkownikom dostęp demo lub chcieć wyświetlać reklamy przez pewien okres czasu. W tym celu możesz korzystać z wygasających kolumn: Cassandra pozwala na tworzenie kolumn, które po określonym czasie są automatycznie kasowane. Ten czas to TTL (czas życia, ang. Time To Live), podawany w sekundach. Po upływie TTL kolumna jest kasowana; kiedy kolumna przestanie istnieć, możesz anulować dostęp lub usunąć baner. SET Customer['mkowalski']['dostep_demo'] = 'dozwolony' WITH ttl=2592000;

117

118

ROZDZIAŁ 10. BAZY RODZINY KOLUMN

10.4. Kiedy nie stosować Istnieją problemy, dla których bazy rodziny kolumn nie będą dobrym rozwiązaniem, na przykład w systemach wymagających transakcji ACID dla zapisu i odczytu. Jeżeli chciałbyś, aby baza agregowała dane za pomocą zapytań (np. funkcji SUM lub AVG), musisz to zrobić po stronie klienta, korzystając z danych pobranych ze wszystkich wierszy. Cassandra nie jest dobrym wyborem dla wczesnych prototypów i testów technicznych: podczas tych wczesnych faz nie wiemy dokładnie, jak będą zmieniały się schematy zapytań, a kiedy będą się one zmieniały, będziemy musieli zmieniać projekt rodziny kolumn. Stanowi to problem dla zespołu i spowalnia proces tworzenia oprogramowania. W bazach relacyjnych koszt zmiany schematu jest wysoki, ale za to koszt zmiany zapytań jest niski. W bazach rodziny kolumn koszt zmiany zapytań może być znacznie wyższy niż koszt zmiany schematu.

Rozdział 11

Bazy grafowe

Bazy grafowe pozwalają przechowywać encje i relacje pomiędzy tymi encjami. Encje zwane są też węzłami, które z kolei posiadają właściwości. Możesz myśleć o węźle jako o instancji obiektu w aplikacji. Relacje są nazywane krawędziami i także mogą mieć właściwości. Krawędzie mają określoną kierunkowość; węzły organizowane są zgodnie z relacjami, co pozwala znajdować interesujące wzorce pomiędzy nimi. Organizacja grafu pozwala na jednokrotne sortowanie danych, a następnie ich interpretację na podstawie relacji na wiele różnych sposobów.

11.1. Czym jest baza grafowa? W przykładowym grafie na rysunku 11.1 widzimy wiele węzłów powiązanych relacjami. Węzły są encjami posiadającymi właściwości, np. nazwa. Węzeł Marcin to tak naprawdę węzeł posiadający właściwość nazwa o wartości Marcin. Widzimy także, że krawędzie posiadają typy, takie jak lubi, autor itd. Te właściwości pozwalają organizować węzły; na przykład węzły Marcin i Radosław połączone są krawędzią oznaczającą relację przyjaciel. Krawędzie mogą mieć wiele właściwości. Możemy przypisać właściwość odKiedy dla relacji przyjaciel pomiędzy węzłami Marcin i Radosław. Typy relacji mają określoną kierunkowość; relacja przyjaciel jest dwukierunkowa, ale relacja lubi już nie. Kiedy Danuta lubi NoSQL. Kompedium, nie oznacza to automatycznie, że NoSQL. Kompedium lubi Danutę. Kiedy już graf węzłów i krawędzi jest stworzony, możemy odpytywać go na wiele sposobów, na przykład, „podaj wszystkich pracowników BigCo lubiących NoSQL. Kompedium”. Zapytanie na grafie nazywane jest także trawersowaniem po grafie. Zaletą bazy grafowej jest to, że możemy zmienić wymagania trawersowania bez konieczności zmiany węzłów lub krawędzi. Jeżeli chcemy pobrać „wszystkie węzły lubiące NoSQL. Kompedium”, możemy to zrobić bez konieczności zmiany modelu danych bazy, ponieważ trawersować po grafie możemy w dowolny sposób.

119

120

ROZDZIAŁ 11. BAZY GRAFOWE

Rysunek 11.1. Przykład struktury grafu Przeważnie kiedy przechowujemy strukturę grafową w bazie relacyjnej, przechowujemy jeden typ relacji (częstym przykładem jest zapytanie „kto jest moim kierownikiem”). Dodanie nowej relacji wiąże się z reguły z wieloma zmianami schematu i przenoszeniem danych, co nie jest konieczne w przypadku baz grafowych. W bazach relacyjnych modelujemy graf wcześniej, w oparciu o to, jak chcemy trawersować; jeżeli sposób trawersowania ulegnie zmianie, będziemy musieli zmienić także dane. W bazie grafowej trawersowanie po relacjach jest bardzo szybkie. Relacje między węzłami nie są obliczane podczas wykonywania zapytania, tylko trwale zapisane. Trawersowanie zapisanych relacji jest szybsze niż ich obliczanie przy każdym zapytaniu. Węzły mogą być połączone różnego typu relacjami, dzięki czemu możesz reprezentować relacje pomiędzy encjami domeny oraz budować drugorzędne relacje dla reprezentowania kategorii, ścieżek, drzew czwórkowych, indeksowania przestrzennego lub list powiązanych. Ponieważ nie ma ograniczeń odnośnie liczby i rodzaju relacji przypisanych do węzła, wszystkie mogą być reprezentowane w tej samej bazie grafowej.

11.2. Funkcjonalności Dostępnych jest wiele baz grafowych, np. Neo4J [Neo4J], Infinite Graph [Infinite Graph], OrientDB [OrientDB] czy FlockDB [FlockDB] (która jest specjalnym przypadkiem: to baza grafowa, która wspiera tylko jednopoziomowe relacje lub listy graniczenia i której nie możesz trawersować przez więcej niż jeden poziom). Jako reprezentanta baz grafowych

11.2. FUNKCJONALNOŚCI

wybraliśmy Neo4J; na jego podstawie omówimy, jak tego typu bazy działają i jak można wykorzystać je do rozwiązywania problemów. W Neo4J stworzenie grafu polega na stworzeniu dwóch węzłów i połączeniu ich relacją. Stworzymy dwa węzły Marcin i Radoslaw: Node marcin = graphDb.createNode(); marcin.setProperty("nazwa", "Marcin"); Node radoslaw = graphDb.createNode(); radoslaw.setProperty("nazwa", "Radosław");

Dla obu węzłów dodaliśmy właściwość nazwa o wartościach Marcin i Radosław. Kiedy mamy więcej niż jeden węzeł, możemy stworzyć relację: marcin.createRelationshipTo(radoslaw, PRZYJACIEL); radoslaw.createRelationshipTo(marcin, PRZYJACIEL);

Musimy stworzyć relację pomiędzy węzłami w obu kierunkach, ponieważ kierunek relacji ma znaczenie: na przykład węzeł produkt może być lubiany przez użytkownika, ale produkt nie może lubić użytkownika. Ta kierunkowość pozwala zaprojektować bogaty model danych (rysunek 11.2). Węzły posiadają wiedzę o relacjach przychodzących (INCOMING) i wychodzących (OUTGOING), które mogą być trawersowane w obu kierunkach.

Rysunek 11.2. Relacje z właściwościami Relacje są pierwszoplanowymi obiektami w bazach grafowych; największa wartość baz grafowych pochodzi z relacji. Relacje nie tylko posiadają typ, węzeł początkowy i węzeł końcowy, ale także mogą mieć własne właściwości. Korzystając z właściwości, możemy uczynić relacje bardziej inteligentnymi; np. możemy określić, kiedy węzły zaczęły być przyjaciółmi, jakie są między nimi różnice i co ich łączy. Na podstawie właściwości relacji można wykonywać na grafie zapytania.

121

122

ROZDZIAŁ 11. BAZY GRAFOWE

Ponieważ większość możliwości baz grafowych oparta jest o relacje i ich właściwości, trzeba włożyć dużo wysiłku i uwagi w poprawne ich zaprojektowanie. Dodawanie nowego typu relacji jest proste; zmiana istniejących węzłów i ich relacji jest podobna do migracji danych (patrz punkt 12.3.2), ponieważ zmiany te będą musiały być wprowadzone dla każdego węzła i każdej relacji w istniejących danych.

11.2.1. Spójność Ponieważ bazy grafowe pracują na połączonych węzłach, większość z nich nie wspiera dystrybucji danych na wiele serwerów. Istnieje kilka rozwiązań, w których dystrybucja danych jest jednak wspierana, np. baza Infinite Graph. W obrębie jednego serwera dane są zawsze spójne, w szczególności w Neo4J, który w pełni wspiera operacje ACID. Kiedy Neo4J pracuje w klastrze, zapis na serwerze głównym jest synchronizowany na serwerach podległych, podczas gdy serwery podległe są zawsze gotowe do odczytu. Zapis na serwerach podległych jest dozwolony i jest natychmiast synchronizowany na serwerze głównym; natomiast pozostałe serwery podległe nie będą zsynchronizowane natychmiast — będą musiały zaczekać na propagację danych z serwera głównego. Bazy grafowe zapewniają spójność dzięki transakcjom. Relacje zawsze muszą być zakończone; węzeł początkowy i końcowy zawsze musi istnieć, a usuwać węzły można tylko wtedy, gdy nie ma do nich przypisanych żadnych relacji.

11.2.2. Transakcje Neo4J wspiera operacje ACID. Przed zmianą węzłów lub dodaniem relacji do istniejących węzłów musimy rozpocząć transakcję. Bez opakowania operacji w transakcję otrzymamy wyjątek NotInTransactionException. Operacje odczytu można wykonywać bez inicjowania transakcji. Transaction transakcja = database.beginTx(); try { Node wezel = database.createNode(); wezel.setProperty("nazwa", "NoSQL Kompedium"); wezel.setProperty("opublikowano", "2012"); transakcja.success(); } finally { transakcja.finish(); }

W powyższym kodzie rozpoczynamy transakcję na bazie danych, a następnie tworzymy węzeł i ustawiamy jego właściwości. Oznaczyliśmy transakcję jako udaną (success) i zakończyliśmy ją (finish). Transakcja musi zostać oznaczona jako udana, ponieważ w przeciwnym razie Neo4J uzna ją za nieudaną i po wykonaniu metody finish() cofnie zmiany. Wywołanie metody success() bez zakończenia transakcji także nie zatwierdza zmian. Ponieważ sposób zarządzania transakcjami jest inny niż w tradycyjnych bazach relacyjnych, podczas wykonywania operacji należy pamiętać o tych różnicach.

11.2. FUNKCJONALNOŚCI

11.2.3. Dostępność Neo4J od wersji 1.8 pozwala na budowanie wysokiej dostępności dzięki wykorzystaniu replikowanych serwerów podległych. Serwery podległe mogą obsługiwać także zapis: gdy dane zapisywane są na serwerze podległym, są najpierw synchronizowane z serwerem głównym, gdzie zapis potwierdzany jest przed potwierdzeniem go na serwerze podległym. Zapis zostaje potem rozpropagowany na pozostałe serwery podległe. Inne bazy grafowe, np. Infinite Graph i FlockDB, pozwalają na rozproszone przechowywanie danych. Do śledzenia ostatnich identyfikatorów transakcji na serwerze głównym i wszystkich serwerach podległych Neo4J wykorzystuje Apache ZooKeeper [ZooKeeper]. Kiedy serwer się uruchamia, komunikuje się z aplikacją ZooKeeper i dowiaduje się, który serwer jest serwerem głównym. Jeżeli serwer jest pierwszym serwerem w klastrze, staje się serwerem głównym; kiedy serwer główny przestaje działać, klaster wybiera nowy z dostępnych serwerów podległych — w ten sposób osiągana jest wysoka wydajność.

11.2.4. Możliwości zapytań Bazy danych grafowych obsługiwane są przez języki zapytań takie jak Gremlin [Gremlin]. Gremlin to specjalistyczny język pozwalający na trawersowanie po grafie. Potrafi trawersować po wszystkich bazach grafowych implementujących graf właściwości Blueprints [Blueprints]. Neo4J udostępnia także język zapytań Cypher [Cypher]. Poza tymi językami zapytań Neo4J pozwala także odpytywać właściwości węzłów, trawersować po grafie lub nawigować po relacjach między węzłami za pomocą wiązań języków programowania. Właściwości węzła mogą być indeksowane za pomocą usługi indeksowania. Podobnie indeksowane są właściwości relacji lub krawędzi; także węzeł lub krawędź mogą być odszukane na podstawie wartości. Indeksy powinny być odpytywane w celu znalezienia węzła początkowego dla trawersowania. Przyjrzyjmy się, jak odnaleźć węzeł przy pomocy indeksowania. W przypadku grafu z rysunku 11.1 możemy indeksować węzły w miarę ich dodawania do bazy danych, możemy też zaindeksować wszystkie węzły, później iterując po nich. Najpierw korzystając z usługi IndexManager: Index indexWezlow = graphDb.index().forNodes("wezly");

indeksujemy węzły na podstawie ich właściwości nazwa. Neo4J jako usługę indeksującą wykorzystuje Lucene [Lucene]. W dalszej części pokażemy, że możemy także wykorzystać funkcjonalność przeszukiwania tekstowego udostępnianą przez Lucene. Węzły do indeksu możemy dodawać podczas ich wstawiania do bazy. Transaction transakcja = graphDb.beginTx(); try { Index indexWezlow = graphDb.index().forNodes("wezly"); indexWezlow.add(marcin, "nazwa", marcin.getProperty("nazwa")); indexWezlow.add(radoslaw, "nazwa", radoslaw.getProperty("nazwa")); transakcja.success(); } finally { transaction.finish(); }

123

124

ROZDZIAŁ 11. BAZY GRAFOWE

Dodawanie węzłów do indeksu odbywa się w kontekście transakcji. Kiedy węzły zostaną zaindeksowane, możemy przeszukiwać indeks za pomocą zaindeksowanej właściwości. Jeżeli szukamy węzła o nazwie Marcin, odpytamy indeks o węzły z właściwością nazwa o wartości Marcin. Node wezel = indexWezlow.get("nazwa", "Marcin").getSingle();

Otrzymujemy węzeł o nazwie Marcin; mając węzeł, możemy pobrać jego wszystkie relacje. Node marcin = indexWezlow.get("nazwa", "Marcin").getSingle(); wszystkieRelacje = marcin.getRelationships();

Możemy pobrać relacje przychodzące (INCOMING) lub wychodzące (OUTGOING). relacjePrzychodzace = marcin.getRelationships(Direction.INCOMING);

W zapytaniach o relacje możemy podać filtry kierunkowe. W przypadku grafu z rysunku 11.1 jeżeli chcemy odnaleźć wszystkich ludzi lubiących NoSQL. Kompedium, możemy pobrać węzeł NoSQL. Kompedium, a następnie wszystkie jego relacje przychodzące. Ponieważ szukamy tylko relacji lubi, w tym miejscu do zapytania możemy również dodać typ relacji. Node nosqlKompedium = indexWezlow.get("nazwa", "NoSQL. Kompedium").getSingle(); relacje = nosqlKompedium.getRelationships(INCOMING, LUBI); for (Relationship relacja : relacje) { lubiNoSQLKompedium.add(relacja.getStartNode()); }

Wyszukiwanie węzłów i ich bezpośrednich relacji jest proste, ale jest to także możliwe do osiągnięcia w bazach relacyjnych. Bazy grafowe są najbardziej użyteczne, jeżeli chcesz trawersować po grafach na dowolnej głębokości, podając węzeł startowy. Jest to szczególnie użyteczne, jeżeli musisz znaleźć węzły powiązane z węzłem startowym na więcej niż jednym poziomie od węzła startowego. Im bardziej rośnie głębokość grafu, tym bardziej warto skorzystać z obiektu Traverser, który pozwala zdefiniować, czy szukasz relacji przychodzących, wychodzących, czy obu. Możesz także zdefiniować, czy Traverser ma wyszukiwać z góry na dół, czy na boki, korzystając z flag BREADTH_FIRST lub DEPTH_FIRST. Trawersowanie musi rozpocząć się od jakiegoś węzła — w tym przykładzie spróbujemy znaleźć wszystkie węzły połączone na dowolnej głębokości relacją PRZYJACIEL z węzłem Barbara. Node barbara = indexWezlow.get("nazwa", "Barbara").getSingle(); Traverser traverserPrzyjaciele = barbara.traverse(Order.BREADTH_FIRST, StopEvaluator.END_OF_GRAPH, ReturnableEvaluator.ALL_BUT_START_NODE, EdgeType.PRZYJACIEL, Direction.OUTGOING);

Obiekt traverserPrzyjaciele pozwala nam znaleźć wszystkie węzły powiązane relacją typu PRZYJACIEL z węzłem Barbara. Węzły mogą być na dowolnej głębokości — przyjaciel przyjaciela na dowolnym poziomie — dzięki czemu możesz przeszukiwać całą strukturę drzewiastą. Jedną z zalet baz grafowych jest możliwość znajdowania ścieżek pomiędzy dwoma węzłami — określanie, czy ścieżek jest wiele, odszukiwanie wszystkich lub tylko najkrótszej. Graf z rysunku 11.1 pokazuje, że Barbara jest połączona z Julią za pomocą dwóch odrębnych ścieżek; aby odnaleźć ścieżki i odległości między węzłami, możemy skorzystać z zapytania:

11.2. FUNKCJONALNOŚCI Node barbara = indexWezlow.get("nazwa", "Barbara").getSingle(); Node julia = indexWezlow.get("nazwa", "Julia").getSingle(); PathFinder szukacz = GraphAlgoFactory.allPaths( Traversal.expanderForTypes(PRZYJACIEL,Direction.OUTGOING), ,MAX_DEPTH); Iterable sciezki = szukacz.findAllPaths(barbara, julia);

Ta funkcjonalność jest wykorzystywana w sieciach społecznościowych do przedstawiania relacji pomiędzy węzłami. Aby znaleźć wszystkie ścieżki i odległość dla każdej ze ścieżek, najpierw pobieramy listę odrębnych ścieżek między węzłami. Długość każdej ze ścieżek to liczba skoków na grafie potrzebnych do przejścia z węzła docelowego do węzła początkowego. Często konieczne jest odnalezienie najkrótszej ścieżki pomiędzy węzłami; najkrótszą ścieżkę pomiędzy węzłami Barbara i Julia możemy wyszukać tak: PathFinder finder = GraphAlgoFactory.shortestPath( Traversal.expanderForTypes(FRIEND, Direction.OUTGOING), MAX_DEPTH); Iterable paths = finder.findAllPaths(barbara, julia);

Wiele innych algorytmów może być zastosowanych do naszego grafu, np. algorytm Dijkstry [Dijkstra’s] odnajdujący najkrótszą lub najtańszą ścieżkę pomiędzy węzłami. START wezelPoczatkowy = (specyfikacja węzła początkowego) MATCH (relacja, wzorzec dopasowania) WHERE (warunek filtrowania: na podstawie danych w węzłach i relacjach) RETURN (co zwrócić: węzły, relacje, właściwości) ORDER BY (właściwości, po których posortować wynik) SKIP (liczba węzłów od góry, które mają zostać pominięte) LIMIT (ograniczenie liczby wyników)

Neo4J udostępnia także język zapytań Cypher. Cypher wymaga węzła startowego (START) do rozpoczęcia zapytania. Węzeł startowy może być określony za pomocą identyfikatora ID, listy identyfikatorów lub poprzez przeszukanie indeksu. Cypher do dopasowania wzorców w relacjach wykorzystuje słowo kluczowe MATCH; klauzula WHERE filtruje właściwości węzła lub relacji. Słowo kluczowe RETURN pozwala określić, co zapytanie ma zwracać — węzły, relacje, czy pola węzłów i relacji. Cypher udostępnia także metody pozwalające sortować (ORDER), agregować (AGGREGATE), opuszczać (SKIP) i ograniczać (LIMIT) dane. W grafie z rysunku 11.2 znajdziemy wszystkie węzły połączone z węzłem Barbara, zarówno przychodzące, jak i wychodzące, korzystając z --. START barbara = wezel:indexWezlow(nazwa= "Barbara") MATCH (barbara)--(wezel_polaczony) RETURN wezel_polaczony

Jeżeli interesuje nas kierunkowość, możemy wykorzystać: MATCH (barbara)(wezel_polaczony)

dla relacji wychodzących. Dopasowanie może też być dla konkretnej relacji dzięki wykorzystaniu konwencji :TYP_RELACJI i może zwracać odpowiednie pola i węzły. START barbara = wezel:indexWezlow(nazwa = "Barbara") MATCH (barbara)-[:PRZYJACIEL]->(wezel_przyjaciela) RETURN wezel_przyjaciela.nazwa, wezel_przyjaciela.lokacja

125

126

ROZDZIAŁ 11. BAZY GRAFOWE

Rozpoczynamy od węzła Barbara, odnajdujemy wszystkie wychodzące relacje typu PRZYJACIEL i zwracamy nazwy przyjaciół. Zapytanie z typem relacji działa tylko dla jednego poziomu głębokości; możemy sprawić, aby działało dla większej głębokości, a następnie sprawdzić głębokość każdego z węzłów wynikowych. START barbara=wezel: indexWezlow(nazwa = "Barbara") MATCH sciezka = barbara-[:PRZYJACIEL*1..3]->wezel_koncowy RETURN barbara.nazwa, wezel_koncowy.nazwa, length(sciezka)

Można też wyszukać relacje, dla których istnieje jakaś właściwość. Możemy filtrować na podstawie właściwości relacji i sprawdzać, czy właściwość istnieje, czy nie. START barbara = wezel:indexWezlow(nazwa = "Barbara") MATCH (barbara)-[relacja]->(wezel_powiazany) WHERE type(relacja) = 'PRZYJACIEL' AND relation.dziela RETURN wezel_powiazany.nazwa, relacja.od

Cypher posiada jeszcze wiele innych funkcjonalności, które mogą być wykorzystane podczas przeszukiwania baz grafowych.

11.2.5. Skalowanie W bazach NoSQL jedną z najczęściej stosowanych technik skalowania jest sharding, gdzie dane zostają rozdzielone pomiędzy serwerami w klastrze. W przypadku baz grafowych sharding jest trudny, ponieważ bazy te nie są zorientowane na agregacje, ale na relacje. Ponieważ każdy węzeł może być spokrewniony z każdym, przechowywanie spokrewnionych węzłów na jednym serwerze jest lepsze z punktu widzenia trawersowania. Trawersowanie po grafie, którego węzły rozdzielone są pomiędzy serwerami, nie będzie wydajne. Znając to ograniczenie baz grafowych, możemy je skalować, korzystając z kilku powszechnych technik opisanych przez Jima Webbera [Webber Neo4J Scaling]. Istnieją trzy sposoby na skalowanie baz grafowych. Ponieważ współczesne serwery mają dużo pamięci RAM, możemy mieć jej tyle, aby roboczy zestaw węzłów i relacji był całkowicie przechowywany w pamięci. Ta technika jest użyteczna tylko wtedy, kiedy zestaw danych zmieści się w realistycznej ilości pamięci RAM. Możemy poprawić skalowanie odczytu bazy danych, dodając więcej serwerów podrzędnych pozwalających wyłącznie na odczyt danych, podczas gdy cały zapis wykonywany będzie na serwerze głównym. Ten wzorzec zapisu w jednym miejscu i odczytu w wielu jest szeroko stosowaną techniką w klastrach MySQL i sprawdza się dobrze, jeżeli zestaw danych jest zbyt duży, aby zmieścić go w pamięci RAM jednej maszyny, ale wystarczająco mały, aby replikować go na wiele serwerów. Serwery podrzędne mogą również przyczynić się do poprawienia dostępności. Kiedy rozmiar zestawu danych powoduje, że replikacja jest niepraktyczna, możemy wykorzystać sharding (patrz podrozdział 4.2) po stronie aplikacji, dzieląc dane na podstawie wiedzy dziedzinowej. Na przykład węzły związane z Ameryką Północną mogą być przechowywane na jednym serwerze, podczas gdy dane z Azji na innym. Sharding po stronie aplikacji musi uwzględniać fakt, że dane znajdują się na różnych serwerach (rysunek 11.3).

11.3. PASUJĄCE PRZYPADKI UŻYCIA

Rysunek 11.3. Sharding węzłów po stronie aplikacji

11.3. Pasujące przypadki użycia Omówmy kilka problemów, gdzie bazy grafowe będą dobrym rozwiązaniem.

11.3.1. Dane połączone Sieci społecznościowe są miejscem, gdzie bazy grafowe mogą zostać wdrożone i działać bardzo efektywnie. Grafy społecznościowe nie muszą dotyczyć wyłącznie przyjaźni; mogą na przykład reprezentować pracowników, ich wiedzę oraz projekty, nad którymi pracowali z innymi pracownikami. Każde zastosowanie, w którym występuje wiele powiązań, jest odpowiednie dla baz grafowych. Jeżeli pomiędzy encjami w jednej bazie posiadasz relacje z różnych dziedzin (takich jak społeczna, przestrzenna czy komercyjna), możesz uczynić te relacje bardziej wartościowymi poprzez udostępnienie możliwości trawersowania po nich.

11.3.2. Wytyczanie trasy, wysyłka i usługi oparte o położenie Każdy adres posiadający przesyłkę do dostarczenia i wszystkie adresy, do których musi zostać dostarczona przesyłka, mogą być zamodelowane jako graf węzłów. Relacje między węzłami mogą mieć właściwość oznaczającą odległość, dzięki czemu będziesz mógł określić efektywną ścieżkę dostawy. Właściwości dla odległości i położenia mogą również być zastosowane dla interesujących miejsc, dzięki czemu Twoja aplikacja będzie mogła rekomendować dobre restauracje lub miejsca, w których można się rozerwać. Możesz także

127

128

ROZDZIAŁ 11. BAZY GRAFOWE

stworzyć węzły dla punktów sprzedaży, takich jak księgarnie czy restauracje, i informować użytkowników, kiedy znajdą się blisko któregoś z takich punktów.

11.3.3. Silniki rekomendacji Kiedy węzły i relacje są tworzone w systemie, mogą zostać wykorzystane do tworzenia rekomendacji, np. „twoi znajomi kupili także ten produkt”. Można też informować turystów, że kiedy inni goście odwiedzali Barcelonę, często odwiedzali budynki zaprojektowane przez Antonio Gaudiego. Interesującym efektem ubocznym używania baz grafowych do rekomendacji jest to, że wraz ze wzrostem ilości danych liczba węzłów i relacji, które mogą być rekomendowane, szybko rośnie. Te same dane mogą również być wykorzystane do analizy — np. które produkty są często kupowane razem lub które przedmioty znajdują się na wspólnych fakturach; jeżeli te warunki nie będą spełnione, mogą być uruchamiane alerty. Jak w przypadku innych silników rekomendacji, bazy grafowe mogą być wykorzystywane do odszukiwania wzorców w relacjach pozwalających wykrywać oszustwa.

11.4. Kiedy nie stosować Istnieją problemy, dla których bazy grafowe nie będą dobrym rozwiązaniem. Kiedy chcesz zaktualizować wszystkie lub podzbiór encji — np. w rozwiązaniu analitycznym, gdzie wszystkie encje muszą zostać zaktualizowane zmienioną właściwością — bazy grafowe mogą nie być optymalne, ponieważ zmiana dla wszystkich węzłów nie jest prostą operacją. Nawet jeżeli model danych pasuje do zastosowania, niektóre bazy mogą nie być w stanie obsłużyć dużych ilości danych, w szczególności przy globalnych operacjach na grafie (angażujących cały graf).

Rozdział 12

Zmiany schematów

12.1. Zmiany schematu Najnowszym trendem w dyskusji na temat baz NoSQL jest podkreślanie ich braku schematu — jest to popularna cecha, która pozwala programistom skupić się na modelowaniu dziedziny bez konieczności myślenia o zmianach schematu. Jest to szczególnie ważne w związku ze wzrostem popularności metod zwinnych [Agile Methods], w których reagowanie na zmienne wymagania jest bardzo ważne. Dyskusje, iteracje i pętle opinii uwzględniające ekspertów w danej dziedzinie i właścicieli produktu są konieczne do dobrego zrozumienia danych; dyskusje te nie powinny być ograniczane przez złożoność schematu bazy danych. Dzięki magazynom danych NoSQL zmiany w schemacie mogą być wykonywane bezproblemowo, co z kolei podnosi wydajność programistów (patrz podrozdział 1.5). Z naszej praktyki wynika, że zbudowanie i utrzymanie aplikacji w nowym świecie baz bez schematu wymaga szczególnej uwagi podczas zmiany schematu.

12.2. Zmiany schematu w bazach transakcyjnych Tworząc rozwiązania w standardowych technologiach transakcyjnych, tworzymy obiekty, związane z nimi tabele i relacje. Rozważmy prosty model składający się z obiektów i danych Klient, Zamowienie i PozycjeZamowienia. Diagram ER został przedstawiony na rysunku 12.1.

Rysunek 12.1. Model danych systemu e-commerce 129

130

ROZDZIAŁ 12. ZMIANY SCHEMATÓW

Jeśli ten model danych jest aktualny, wszystko jest w porządku. Za każdym razem, kiedy wprowadzana jest jednak zmiana, np. dodanie właściwości preferowanySposobWysylki dla obiektu Klient, musimy zmodyfikować obiekt i bazę danych, ponieważ bez zmiany tabeli baza nie będzie zsynchronizowana z aplikacją. Za każdym razem, kiedy otrzymamy błąd ORA-00942: table or view does not exist (tabela lub widok nie istnieje) lub ORA-00904: "PREFEROWANY_SPOSOB_WYSYLKI": invalid identifier (nieprawidłowy identyfikator), wiemy, że trafiliśmy na taki właśnie błąd. Przeważnie zmiana schematu bazy danych sama w sobie jest projektem. Do wdrożenia zmian bazy przygotowywane są skrypty wykorzystujące techniki różnicowe z bazą deweloperską. Takie podejście, polegające na tworzeniu skryptów podczas wdrożenia, jest podatne na błędy i niezgodne z metodami zwinnymi.

12.2.1. Zmiany w projektach budowanych od podstaw Tworzenie skryptów podczas prac nad bazą deweloperską jest lepsze, ponieważ możemy przechowywać zmiany schematu wraz ze skryptami zmiany danych w tym samym pliku. Skrypty te powinny być nazywane z uwzględnieniem rosnących sekwencyjnie numerów odzwierciedlających wersje bazy danych; na przykład pierwsza zmiana bazy zapisana byłaby w pliku o nazwie 001_Opis_Zmiany.sql. Tworzenie skryptów w ten sposób pozwala później uruchamiać zmiany w odpowiedniej kolejności. Rysunek 12.2 przedstawia wszystkie zmiany wykonane na bazie danych do tej pory.

Rysunek 12.2. Sekwencja zmian wykonanych na bazie danych Załóżmy teraz, że musimy zmienić tabelę PozycjaZamowienia, aby przechowywała cenę po rabacie (CenaPoRabacie) i cenę bez rabatu (PelnaCena). Skrypt ze zmianą tabeli zostałby zapisany w pliku o numerze 007, zgodnie z rysunkiem 12.3.

Rysunek 12.3. Nowa zmiana, 007_CenaPoRabacie.sql, zapisana w sekwencji

12.2. ZMIANY SCHEMATU W BAZACH TRANSAKCYJNYCH

Wprowadziliśmy zmianę w bazie danych. Skrypt zawiera kod dodający nową kolumnę, zmieniający już istniejącą i zmieniający istniejące dane tak, aby nowa funkcjonalność działała poprawnie. Poniżej znajduje się skrypt z pliku 007_CenaPoRabacie.sql. ALTER TABLE pozycjazamowienia ADD cenaporabacie NUMBER(18,2) NULL; UPDATE pozycjazamowienia SET cenaporabacie = cena; ALTER TABLE pozycjazamowienia MODIFY cenaporabacie NOT NULL; ALTER TABLE pozycjazamowienia RENAME COLUMN cena TO pelnacena; --//@UNDO ALTER TABLE pozycjazamowienia RENAME pelnacena TO cena; ALTER TABLE pozycjazamowienia DROP COLUMN cenaporabacie;

Skrypt zawiera zmiany schematu bazy danych oraz niezbędne zmiany danych. W naszym przykładzie korzystamy z DBDeploy [DBDeploy] jako narzędzia do zarządzania zmianami w bazie. DBDeploy utrzymuje w bazie danych tabelę o nazwie ChangeLog, w której przechowywane są wszystkie zmiany. W tej tabeli kolumna Change_Number informuje, które zmiany zostały wykonane na bazie danych. Kolumna Change_Number, która odzwierciedla wersję bazy danych, jest wykorzystywana do odnalezienia w folderze skryptu z odpowiednim numerem i wprowadzenia zmian, które nie zostały do tej pory wprowadzone. Kiedy zapiszemy skrypt o numerze 007 i uruchomimy go na bazie danych za pomocą DBDeploy, DBDeploy sprawdzi ChangeLog i wykona wszystkie skrypty z katalogu, które nie zostały jeszcze wykonane. Rysunek 12.4 przedstawia wynik działania DBDeploy po wprowadzeniu zmian.

Rysunek 12.4. DBDeploy aktualizujące bazę danych zmianą 007 Najlepszym sposobem na integrację z resztą programistów w projekcie jest wykorzystanie systemu kontroli wersji do przechowywania skryptów zmian, dzięki czemu będziesz mógł śledzić wersję aplikacji i bazy danych w tym samym miejscu, eliminując możliwe niezgodności pomiędzy nimi. Do takich aktualizacji możesz wykorzystać wiele różnych narzędzi, w tym Liquibase [Liquibase], MyBatis Migrator [MyBatis Migrator] czy DBMaintain [DBMaintain].

131

132

ROZDZIAŁ 12. ZMIANY SCHEMATÓW

12.2.2. Zmiany w projektach zastanych Nie każdy projekt masz szansę prowadzić od początku. Jak implementować zmiany, kiedy istniejąca aplikacja znajduje się w środowisku produkcyjnym? Z naszej praktyki wiemy, że wyodrębnienie całej struktury bazy danych do skryptu wraz z kodem bazy i danymi referencyjnymi dobrze sprawdza się jako punkt startu dla projektu. Ten skrypt nie powinien zawierać danych transakcyjnych. Kiedy już punkt startu zostanie wyznaczony, możemy zarządzać zmianami w sposób opisany powyżej (rysunek 12.5).

Rysunek 12.5. Wykorzystanie skryptów startowych dla zastanej bazy danych Jednym z głównych aspektów zmian powinno być utrzymanie wstecznej kompatybilności schematu bazy. W wielu organizacjach z jednej bazy korzysta kilka aplikacji; wprowadzenie zmian w bazie dla jednej aplikacji nie powinno psuć pozostałych. Wsteczną kompatybilność możemy uzyskać poprzez zapewnienie dla zmiany fazy przejściowej, która została dokładnie opisana w książce Refactoring Databases [Ambler i Sadalage]. Podczas fazy przejściowej stary i nowy schemat utrzymywane są równolegle i są dostępne dla wszystkich aplikacji korzystających z bazy danych. Do tego niezbędny jest kod wspierający, np. wyzwalacze, widoki i kolumny wirtualne, który zadba, aby inne aplikacje miały dostęp do bazy i danych bez konieczności wprowadzania zmian w kodzie. ALTER TABLE klient ADD pelnanazwa VARCHAR2(60); UPDATE klient SET pelnanazwa = pnazwa; CREATE OR REPLACE TRIGGER SynchronizujPelnaNazweKlienta BEFORE INSERT OR UPDATE ON klient REFERENCING OLD AS OLD NEW AS NEW FOR EACH ROW BEGIN IF :NEW.pnazwa IS NULL THEN :NEW.pnazwa := :NEW.pelnanazwa; END IF; IF :NEW.pelnanazwa IS NULL THEN :NEW.pelnanazwa := :NEW.pnazwa END IF;

12.3. ZMIANY SCHEMATU W MAGAZYNACH DANYCH NOSQL END; --Usunąć wyzwalacz i kolumnę pnazwa --kiedy wszystkie aplikacje zaczną korzystać z klient.pelnanazwa

W przykładzie próbujemy zmienić nazwę kolumny klient.pnazwa na klient.pelnanazwa, ponieważ chcemy uniknąć niejednoznaczności, czy pnazwa oznacza pelnanazwa, czy może np. potocznanazwa. Bezpośrednia zmiana nazwy kolumny i kodu aplikacji, za którą jesteśmy odpowiedzialni, mogłaby zadziałać dla naszej aplikacji — ale nie zadziała dla pozostałych, które także korzystają z tej samej bazy. Korzystając z techniki fazy przejściowej, wprowadzamy nową kolumnę pelnanazwa, kopiujemy do niej dane, ale nie usuwamy starej kolumny. Tworzymy też wyzwalacz BEFORE UPDATE synchronizujący dane pomiędzy kolumnami. Kiedy aplikacje odczytują dane z tabeli, odczytują albo kolumnę pnazwa, albo pelnanazwa, ale zawsze otrzymują poprawne dane. Kiedy wszystkie aplikacje zaczną korzystać z nowego pola, stare pole i wyzwalacz będą mogły zostać usunięte. Bardzo trudno wprowadza się zmiany schematu dla dużych zestawów danych w bazach relacyjnych, w szczególności jeżeli baza ma być cały czas dostępna dla aplikacji, ponieważ zmiany tabel z dużą ilością danych powodują ich blokowanie.

12.3. Zmiany schematu w magazynach danych NoSQL Baza relacyjna musi zostać zmieniona, zanim zmieniona zostanie aplikacja. Celem baz bez schematu jest uniknięcie takiej sytuacji i poprawienie elastyczności zmiany schematu encji. Efektem szybko zmieniających się wymagań i często wprowadzanych w produkcie innowacji są częste zmiany schematu danych. Tworząc rozwiązania z bazami NoSQL, w niektórych przypadkach nie musimy myśleć o schemacie przed rozpoczęciem pracy nad projektem. Musimy przemyśleć i zaprojektować inne aspekty, takie jak typy i relacje (w przypadku baz grafowych), lub też nazwy rodzin kolumn, wierszy i kolumn oraz kolejności kolumn (w bazach rodziny kolumn), czy jak są przypisywane klucze i jaka jest struktura danych przechowywanych w wartości (w bazach klucz – wartość). Nawet jeżeli nie myślimy o tych kwestiach przed rozpoczęciem prac lub jeżeli chcemy zmienić wcześniej podjęte decyzje, możemy zrobić to z łatwością. Stwierdzenie, że bazy NoSQL są zupełnie pozbawione schematu, jest mylące; chociaż przechowują dane bez względu na schemat danych, ten schemat i tak musi być zdefiniowany w aplikacji, ponieważ dane po pobraniu ich z bazy danych muszą zostać przez aplikację sparsowane. Aplikacja musi też tworzyć dane zapisywane w bazie. Jeżeli aplikacja nie będzie w stanie sparsować danych z bazy, otrzymamy błąd niedopasowania schematu, mimo że bazą nie będzie baza relacyjna, a błąd zgłosi sama aplikacja. Nawet jeżeli korzystamy z baz NoSQL, podczas refaktoryzacji aplikacji musimy mieć na uwadze schemat danych. Zmiany schematu są szczególnie ważne, jeżeli aplikacja jest już wdrożona, a w bazie znajdują się dane produkcyjne. Dla uproszczenia załóżmy, że wykorzystujemy magazyn dokumentów, np. MongoDB [MongoDB], i posiadamy taki model danych jak poprzednio: klient, zamowienie i pozycjeZamowienia. { "_id": "4BD8AE97C47016442AF4A580",

133

134

ROZDZIAŁ 12. ZMIANY SCHEMATÓW "klientid": 99999, "nazwa": "Foo Sushi Inc", "od": "12/12/2012", "zamowienie": { "zamowienieid": "4821-UXWE-122012","orderdate": "12/12/2001", "pozycjeZamowienia": [{"produkt": "Ciasteczka z wróżbą", "cena": 19.99}] } }

Kod aplikacji zapisujący taki dokument w MongoDB wygląda tak: BasicDBObject pozycjaZamowienia = new BasicDBObject(); pozycjaZamowienia.put("produkt", nazwaProduktu); pozycjaZamowienia.put("cena", cena); pozycjeZamowienia.add(pozycjaZamowienia);

a kod odczytujący tak: BasicDBObject pozycja = (BasicDBObject) pozycjaZamowienia; String nazwaProduktu = pozycja.getString("produkt"); Double cena = pozycja.getDouble("cena");

Zamiana obiektów w celu dodania pola preferowanySposobWysylki nie wymaga zmian w bazie danych, ponieważ baza danych nie dba o to, czy różne dokumenty posiadają ten sam schemat. Dzięki temu wprowadzanie zmian w aplikacji i wdrażanie ich w środowisku produkcyjnym jest szybsze i łatwiejsze. Wdrożone muszą zostać jedynie zmiany w aplikacji — żadne zmiany po stronie bazy danych nie są wymagane. Aplikacja musi tylko być w stanie obsłużyć także obiekty nieposiadające właściwości preferowanySposobWysylki — to wszystko. Oczywiście w naszym przykładzie upraszczamy sytuację zmiany schematu. Przyjrzyjmy się zmianie, którą wprowadziliśmy wcześniej: wprowadzenie pola cenaPoRabacie i zmiana nazwy pola cena na pelnaCena. Aby wprowadzić taką zmianę, zmieniamy nazwę atrybutu cena na pelnaCena i dodajemy atrybut cenaPoRabacie. Zmieniony dokument wygląda tak: { "_id": "5BD8AE97C47016442AF4A580", "klientid": 66778, "nazwa": "India House", "od": "12/12/2012", "zamowienie": { "zamowienieid": "4821-UXWE-222012", "datazamowienia": "12/12/2001", "pozycjeZamowienia": [{"produkt": "Okrycia na krzesła", "pelnaCena": 29.99, "cenaPoRabacie":26.99}] } }

Po wprowadzeniu tych zmian można bez problemu odczytywać i zapisywać nowych klientów i nowe zamówienia, jednak dla istniejących klientów cena produktów nie będzie mogła być odczytana, ponieważ kod szuka pola pelnaCena, a w dokumentach jest tylko cena.

12.3. ZMIANY SCHEMATU W MAGAZYNACH DANYCH NOSQL

12.3.1. Zmiany inkrementacyjne Niezgodność schematu sprawia problemy wielu nowym użytkownikom baz NoSQL. Kiedy schemat jest zmieniany po stronie aplikacji, musimy zmienić wszystkie istniejące dane zgodnie z nowym schematem (w przypadku dużych magazynów może to być kosztowna operacja). Alternatywą może być zapewnienie, aby dane sprzed zmiany schematu mogły być parsowane przez nowy kod, a podczas zapisu zostały zapisane już zgodnie z nowym schematem. Technika ta, znana pod nazwą zmian inkrementacyjnych, zmienia dane podczas działania programu; niektóre dane mogą nigdy nie zostać zmienione, jeżeli nie będą używane. Z dokumentu odczytujemy pola cena i pelnaCena: BasicDBObject pozycja = (BasicDBObject)pozycjaZamowienia; String nazwaProduktu = item.getString("produkt"); Double pelnaCena = item.getDouble("cena"); if (pelnaCena == null) { pelnaCena = pozycja.getDouble("pelnaCena"); } Double cenaPoRabacie = pozycja.getDouble("cenaPoRabacie");

Podczas ponownego zapisu stary atrybut nie jest wykorzystywany: BasicDBObject pozycjaZamowienia = new BasicDBObject(); orderItem.put("produkt", nazwaProduktu); orderItem.put("pelnaCena", cena); orderItem.put("cenaPoRabacie", cenaPoRabacie); orderItems.add(pozycjaZamowienia);

Podczas korzystania z techniki zmian inkrementacyjnych może jednocześnie istnieć po stronie aplikacji wiele wersji obiektu, na który tłumaczone są stare dokumenty z bazy; podczas ponownego zapisu zapisywany jest zawsze najnowszy obiekt. Taka stopniowa migracja danych pozwala aplikacji ewoluować szybciej. Zmiany inkrementacyjne komplikują strukturę obiektów, szczególnie jeżeli wprowadzane są nowe zmiany bez usuwania starych. Okres między opublikowaniem zmian a zmianą ostatniego obiektu w bazie danych na nowy schemat nazywa się okresem przejściowym (rysunek 12.6). Powinieneś postarać się, aby okres przejścia był jak najkrótszy i ograniczony do jak najmniejszego zakresu — pozwoli Ci to utrzymać porządek w obiekcie. Technika zmian inkrementacyjnych może również być zaimplementowana z wykorzystaniem pola wersja_schematu w obiekcie, z którego aplikacja skorzysta do wybrania odpowiedniego kodu do sparsowania danych na obiekty. Podczas zapisu dane migrowane są do najnowszej wersji, a pole wersja_schematu jest aktualizowane. Posiadanie odpowiedniej warstwy translacji pomiędzy bazą a aplikacją jest ważne, ponieważ w miarę wprowadzania zmian schematu zarządzanie wieloma wersjami schematu jest ograniczone do tej warstwy i nie rozprzestrzenia się na całą aplikację. Aplikacje mobilne wprowadzają specjalne wymagania. Ponieważ nie możemy upewnić się, że działająca aplikacja jest w najnowszej wersji, aplikacja powinna być w stanie obsługiwać prawie wszystkie wersje schematu.

135

136

ROZDZIAŁ 12. ZMIANY SCHEMATÓW

Rysunek 12.6. Okres przejściowy dla zmian schematu

12.3.2. Zmiany w bazach grafowych Bazy grafowe posiadają krawędzie z typami i właściwościami. Jeżeli zmienisz typ krawędzi w kodzie aplikacji, nie będziesz mógł trawersować po grafie, co uczyni go bezwartościowym. Aby to obejść, możesz trawersować po wszystkich krawędziach, zmieniając typ każdej z nich. Taka operacja może być kosztowna i wymaga napisania kodu, który zmieni wszystkie krawędzie w bazie. Jeżeli musimy wspierać kompatybilność wstecz lub nie chcemy od razu zmieniać całego grafu, możemy stworzyć między węzłami nowe krawędzie; później, kiedy będziemy pewni zmiany, stare krawędzie będzie można usunąć. Możemy zastosować trawersowanie z wieloma typami krawędzi, wykorzystując stare i nowe krawędzie jednocześnie. Ta technika może bardzo pomóc w pracy z dużymi bazami, zwłaszcza jeżeli chcemy utrzymać wysoką dostępność. Jeżeli musimy zmienić właściwości dla wszystkich węzłów lub krawędzi, musimy pobrać wszystkie węzły i zmienić wszystkie właściwości, które muszą być zmienione. Przykładem może być dodanie pól wezelStworzonyPrzez i dataStworzeniaWezla do istniejących węzłów w celu śledzenia zmian w węzłach. for (Node wezel : database.getAllNodes()) { wezel.setProperty("wezelStworzonyPrzez", getUzytkownikSystemu()); wezel.setProperty("dataStworzeniaWezla", getStempelCzasowySystemu()); }

Być może musimy zmienić dane w węzłach. Nowe dane mogą być pochodną istniejących lub mogą być importowane z innego źródła. Zmiana może być wykonana poprzez pobranie wszystkich węzłów przy użyciu indeksu danych i zapisanie w nich odpowiednich danych.

12.4. DALSZA LEKTURA

12.3.3. Zmiana struktury agregacji Czasami musisz zmienić projekt schematu, np. rozdzielić duże obiekty na mniejsze, przechowywane niezależnie. Załóżmy, że posiadasz agregację klienta zawierającą wszystkie zamówienia i chciałbyś oddzielić klienta i każde z jego zamówień do osobnych agregacji. Będziesz musiał upewnić się, że kod poradzi sobie z obydwoma wersjami agregacji. Jeżeli nie odnajdzie starych obiektów, poszuka nowych. Kod działający w tle może odczytywać jedną agregację naraz, wprowadzać konieczne zmiany i ponownie zapisywać dane do rozdzielnych agregacji. Zaletą pracy na jednej agregacji jest to, że takie operacje nie mają wpływu na dostępność danych w aplikacji.

12.4. Dalsza lektura Jeżeli chcesz uzyskać więcej informacji na temat zmian w bazach relacyjnych, patrz [Ambler i Sadalage]. Mimo że większość tych informacji dotyczy baz relacyjnych, generalne zasady wprowadzania zmian będą również dotyczyły innych baz danych.

12.5. Najważniejsze kwestie ■ Bazy danych posiadające schemat, takie jak bazy relacyjne, mogą być zmieniane poprzez zapisanie każdej zmiany schematu oraz zmian danych w sekwencji z kontrolą wersji. ■ Bazy bez schematu wymagają uważnego wprowadzania zmian ze względu na schemat danych zawarty w aplikacjach korzystających z danych. ■ Bazy bez schematu mogą korzystać z tych samych technik wprowadzania zmian, z jakich korzystają bazy relacyjne. ■ Bazy bez schematu mogą również odczytywać dane w sposób uwzględniający zmiany w schemacie danych i wykorzystywać zmiany inkrementacyjne do aktualizacji danych.

137

138

ROZDZIAŁ 12. ZMIANY SCHEMATÓW

Rozdział 13

Poliglotyczne przechowywanie danych

Różne bazy danych są zaprojektowane w celu rozwiązywania różnych problemów. Korzystanie z jednej bazy do zaspokojenia wszystkich wymagań przeważnie skutkuje powstawaniem niewydajnych rozwiązań; przechowywanie danych transakcyjnych i danych o sesjach oraz trawersowanie po grafie klientów i produktów, jakie kupili ich znajomi, są bardzo różnymi problemami. Nawet w przestrzeni baz relacyjnych wymagania systemów OLAP i OLTP są bardzo różne — mimo to często zmuszone są wykorzystywać ten sam schemat. Rozważmy problem relacji między danymi. Bazy relacyjne są dobre w zapewnianiu, że relacja istnieje. Jeżeli chcemy odkrywać relacje lub musimy w różnych tabelach znaleźć dane należące do jednego obiektu, sprawy zaczynają się komplikować. Systemy baz danych są projektowane do sprawnego wykonywania określonych operacji, dla określonych struktur danych i określonych ilości danych — np. operacji na zestawach danych i szybkiego pobierania kluczy i ich wartości lub przechowywania rozbudowanych dokumentów czy skomplikowanych grafów.

13.1. Odmienne potrzeby przechowywania danych Wiele organizacji wykorzystuje ten sam system baz danych do przechowywania transakcji biznesowych i danych sesji oraz innych potrzeb, takich jak raportowanie, BI, hurtownie danych czy logowanie zdarzeń (rysunek 13.1). Dane sesji, koszyk zakupów czy lista zamówień nie wymagają takiej samej dostępności i spójności czy tak samo częstego tworzenia kopii zapasowych. Czy dane sesji muszą być objęte tą samą strategią tworzenia kopii zapasowych co dane dotyczące zamówień? Czy magazyn zarządzania sesjami wymaga większej dostępności niż instancja bazy odczytująca/zapisująca dane sesji?

139

140

ROZDZIAŁ 13. POLIGLOTYCZNE PRZECHOWYWANIE DANYCH

Rysunek 13.1. Wykorzystanie bazy relacyjnej do wszystkich danych aplikacji W roku 2006 Neal Ford stworzył termin programowanie poliglotyczne (ang. polyglot programming) odzwierciedlający przekonanie, że aplikacja powinna być tworzona w różnych językach, aby wykorzystać fakt, że różne języki są odpowiednie do rozwiązywania różnych problemów. Rozbudowane aplikacje łączą wiele problemów, więc wybór odpowiedniego języka dla każdego z zadań może być bardziej produktywny niż próba rozwiązania wszystkich problemów za pomocą jednego języka. Podczas pracy nad problemem biznesowym platformy e-commerce wykorzystanie wysoko dostępnego i skalowalnego silnika do przechowywania danych koszyka zakupów jest ważne, jednak ten sam silnik nie pomoże Ci w wyszukiwaniu produktów kupionych przez znajomych użytkownika — te dwa problemy są bardzo różne. Do opisania podejścia hybrydowego wykorzystujemy termin poliglotyczne przechowywanie danych.

13.2. Poliglotyczne wykorzystanie magazynu danych Przyjrzyjmy się naszej platformie e-commerce i wykorzystajmy podejście poliglotycznego przechowywania danych do omówienia, jak różne bazy danych mogą zostać wykorzystane do różnych celów (rysunek 13.2). Magazyn klucz – wartość może zostać wykorzystany do przechowywania danych koszyka zakupów przed potwierdzeniem zamówienia przez klienta i do przechowywania danych sesji, dzięki czemu baza relacyjna nie musi być wykorzystywana do przechowywania tych przejściowych danych. Magazyny klucz – wartość są dobrym wyborem, ponieważ koszyk zakupów jest zazwyczaj pobierany za pośrednictwem identyfikatora użytkownika, a kiedy zostanie potwierdzony i opłacony, będzie mógł zostać zapisany w bazie relacyjnej. Kluczem dla danych sesji będzie identyfikator sesji.

13.2. POLIGLOTYCZNE WYKORZYSTANIE MAGAZYNU DANYCH

Rysunek 13.2. Wykorzystanie baz klucz – wartość do przechowywania danych sesji i koszyka zakupów Jeżeli chcemy rekomendować produkty klientom, kiedy umieszczają produkty w swoim koszyku — na przykład „twoi znajomi kupili także te produkty” lub „twoi znajomi kupili takie akcesoria do tego produktu” — wprowadzenie do rozwiązania bazy grafowej będzie dobrym posunięciem (rysunek 13.3).

Rysunek 13.3. Przykładowa implementacja poliglotyczna Aplikacja nie musi wykorzystywać jednej bazy do wszystkich potrzeb, ponieważ różne bazy są budowane z myślą o różnym przeznaczeniu i nie każdy cel może być osiągnięty przy wykorzystaniu danej bazy. Nawet wykorzystanie w jednej aplikacji specjalistycznych baz relacyjnych do pewnych rozwiązań, takich jak hurtownie danych lub systemy analityczne, można postrzegać jako poliglotyczne przechowywanie danych.

141

142

ROZDZIAŁ 13. POLIGLOTYCZNE PRZECHOWYWANIE DANYCH

13.3. Usługi a bezpośrednie przechowywanie danych Kiedy przemieszczamy się w stronę wykorzystania wielu magazynów danych dla aplikacji, w organizacji mogą istnieć inne aplikacje, które także mogłyby skorzystać z takiego modelu. Korzystając z naszego przykładu, baza grafowa może serwować dane dla innych aplikacji, potrzebujących zrozumieć na przykład, które produkty są kupowane przez dany segment użytkowników. Zamiast bezpośredniej komunikacji pomiędzy każdą aplikacją a bazą grafową możemy obudować bazę grafową usługą, aby wszystkie relacje pomiędzy węzłami mogły być serwowane z jednego miejsca i odpytywane przez wiele aplikacji (rysunek 13.4). API udostępniane przez usługę i sprawowana przez nią władza nad danymi są bardziej wartościowe niż jedna aplikacja komunikująca się z wieloma bazami.

Rysunek 13.4. Przykładowa implementacja usługi opakowującej magazyn danych Filozofię otaczania usługami można posunąć krok dalej: można otoczyć wszystkie bazy usługami, a aplikacjom pozwolić na komunikację wyłącznie z nimi (rysunek 13.5). Dzięki temu bazy danych wewnątrz usług będą mogły być zmieniane bez konieczności modyfikowania aplikacji. Wiele produktów NoSQL, takich jak Riak [Riak] i Neo4J [Neo4J], udostępnia wbudowane API REST.

13.4. Rozszerzanie dla polepszenia funkcjonalności Często ze względu na istniejące aplikacje i ich zależności od aktualnej struktury danych nie możemy zmienić magazynu danych wykorzystywanego w danym celu na potrzeby innej funkcjonalności. Możemy natomiast dodawać funkcjonalności, na przykład przechowywanie w pamięci podręcznej lub wykorzystanie silników indeksujących, takich jak Solr [Solr], co może poprawić wydajność wyszukiwania (rysunek 13.6). Kiedy wprowadzamy takie technologie, musimy mieć pewność, że dane są synchronizowane pomiędzy magazynem danych dla aplikacji a magazynem podręcznym lub usługą indeksującą.

13.5. WYBÓR ODPOWIEDNIEJ TECHNOLOGII

Rysunek 13.5. Wykorzystanie usług zamiast bezpośredniej komunikacji z bazą

Rysunek 13.6. Wykorzystanie dodatkowego magazynu do poprawienia magazynu zastanego Przy takim rozwiązaniu jeżeli dane w bazie aplikacji ulegną zmianie, musimy także zaktualizować dane indeksowane. Proces ten może być wykonywany w czasie rzeczywistym lub pakietami, o ile aplikacji nie będą przeszkadzały nieaktualne dane w silniku indeksującym/wyszukującym. Wzorzec event sourcing (patrz podrozdział 14.2) może zostać wykorzystany do aktualizacji indeksu.

13.5. Wybór odpowiedniej technologii Istnieje szeroki wybór rozwiązań do przechowywania danych. Pierwotnie trend zmienił się z baz wyspecjalizowanych na pojedynczą bazę transakcyjną, pozwalającą na przechowywanie

143

144

ROZDZIAŁ 13. POLIGLOTYCZNE PRZECHOWYWANIE DANYCH

wszystkich modeli danych, posiadającą jednak pewne ograniczenia. Aktualnie trend odwraca się w kierunku wykorzystania baz wspierających natywną implementację rozwiązań. Jeżeli chcemy rekomendować produkty klientom na podstawie ich koszyka zakupów lub produktów, jakie kupili inni użytkownicy, możemy to zaimplementować w dowolnym systemie baz danych, obdarzając dane odpowiednimi parametrami pozwalającymi odpowiedzieć na nasze pytania. Warto jednak skorzystać z odpowiedniej technologii, aby w razie gdyby pytanie uległo zmianie, mogło być zadane w tym samym magazynie danych bez utraty istniejących danych lub konieczności zmieniania ich zgodnie z nowym formatem. Wróćmy do potrzeby wprowadzenia nowej funkcjonalności. Możemy wykorzystać bazę transakcyjną, hierarchiczne zapytanie i odpowiednio zamodelowane tabele. Kiedy konieczna będzie zmiana trawersowania, będziemy musieli przebudować bazę, zmigrować dane i rozpocząć parsowanie nowych danych. Gdybyśmy natomiast wykorzystali magazyn danych śledzący relacje między węzłami, moglibyśmy po prostu zaprogramować nowe relacje i korzystać z tego samego magazynu z minimalną liczbą zmian.

13.6. Problemy korporacyjne przy poliglotycznym przechowywaniu danych Wprowadzenie nowych technologii NoSQL zmusi osoby zarządzające danymi w organizacjach do przemyślenia sposobów ich wykorzystania. Korporacje przyzwyczajone są do wykorzystywania jednolitych środowisk transakcyjnych; niezależnie, z jakiej bazy organizacja zaczęła korzystać najpierw, istnieje duża szansa, że w ciągu kolejnych lat następne aplikacje będą budowane dookoła tej bazy. W nowym świecie poliglotyzmu zespoły zajmujące się bazami będą musiały stać się bardziej poliglotyczne — nauczyć się, jak działają nowe technologie NoSQL, jak monitorować te systemy, jak tworzyć kopie zapasowe oraz jak pobierać i zapisywać dane. Kiedy korporacja zdecyduje się na wykorzystanie bazy NoSQL, podnoszone są kwestie takie jak licencjonowanie, wsparcie, narzędzia, aktualizacje, sterowniki, audyty i bezpieczeństwo. Wiele z technologii NoSQL jest typu open source i posiada aktywną społeczność; ponadto istnieją firmy zapewniające komercyjne wsparcie. Nie istnieje bogaty ekosystem narzędzi, ale producenci oprogramowania i społeczność open source nadrabiają zaległości; wypuszczają narzędzia takie jak MongoDB Monitoring Service [Monitoring], Datastax Ops Center [OpsCenter] czy przeglądarka Rekon dla bazy Riak [Rekon]. Innym przedmiotem wątpliwości korporacji jest bezpieczeństwo danych — możliwość tworzenia użytkowników i przyznawania im uprawnień na poziomie bazy danych. Większość baz NoSQL nie posiada rozbudowanych mechanizmów bezpieczeństwa, ale to dlatego, że są zaprojektowane do innego funkcjonowania. W tradycyjnych systemach transakcyjnych dane były udostępniane przez bazę i można było dostać się do nich przy wykorzystaniu dowolnego narzędzia pozwalającego na tworzenie zapytań. W przypadku baz NoSQL także istnieją takie narzędzia, jednak dane powinny być własnością aplikacji, która z kolei powinna udostępniać usługi. Mimo wszystko istnieją jednak bazy NoSQL udostępniające mechanizmy bezpieczeństwa.

13.7. ZŁOŻONOŚĆ WDROŻENIA

Korporacje zazwyczaj posiadają hurtownie danych, systemy BI i systemy analityczne wymagające danych z wielu źródeł. Konieczne będzie upewnienie się, że narzędzia ETL lub dowolne inne mechanizmy przenoszące dane do hurtowni danych będą w stanie czytać ze źródeł NoSQL. Producenci narzędzi ETL wprowadzają funkcjonalności komunikacji z bazami NoSQL; na przykład Pentaho [Pentaho] jest w stanie pobierać dane z baz MongoDB i Cassandra. Każda korporacja w jakimś stopniu analizuje dane. Kiedy zwiększa się ilość danych, które należy przechowywać, pojawia się problem skalowania baz transakcyjnych. Ogromna liczba zapisywanych danych i konieczność skalowania z myślą o zapisie są doskonałą zachętą do użycia baz NoSQL, które pozwalają na tego typu skalowanie.

13.7. Złożoność wdrożenia Kiedy ruszymy ścieżką wdrażania poliglotyzmu w aplikacji, złożoność wdrożenia wymaga dokładnego rozpatrzenia. Aplikacja wymaga teraz wszystkich baz danych jednocześnie. Będziesz też musiał mieć te bazy w środowiskach UAT (User Acceptance Testing), QA (Quality Assurance) i deweloperskim. Ponieważ większość produktów NoSQL jest typu open source, koszt licencji nie jest problemem. Produkty te także wspierają automatyzację instalacji i konfiguracji. Aby na przykład zainstalować bazę danych, musimy tylko pobrać i rozpakować archiwum, co można zautomatyzować za pomocą poleceń curl i unzip. Bazy NoSQL mają też przeważnie rozsądne ustawienia domyślne i można je uruchamiać z minimalną ingerencją w konfigurację.

13.8. Najważniejsze kwestie ■ Poliglotyczne przechowywanie danych polega na wykorzystaniu różnych technologii przechowywania danych do rozwiązywania różnych problemów. ■ Poliglotyczne przechowywanie danych można zaimplementować w całej organizacji lub tylko dla jednej aplikacji. ■ Enkapsulowanie dostępu do danych w usługach zmniejsza wpływ wyboru sposobów przechowywania danych na inne części systemu. ■ Dodawanie kolejnych technologii przechowywania danych zwiększa złożoność programowania i operacji, więc zalety dopasowania do modelu przechowywania danych trzeba brać pod uwagę razem ze wzrostem złożoności.

145

146

ROZDZIAŁ 13. POLIGLOTYCZNE PRZECHOWYWANIE DANYCH

Rozdział 14

Poza NoSQL

Pojawienie się baz NoSQL w znacznym stopniu wpłynęło na zmiany i otwarcie świata baz danych, uważamy jednak, że rodzaje baz NoSQL, które omówiliśmy w tej książce, to tylko część obrazu środowiska poliglotycznego. Warto poświęcić trochę czasu na dyskusję o rozwiązaniach, które nie do końca pasują do pojęcia baz NoSQL.

14.1. Systemy plików Bazy danych są bardzo powszechne, ale systemy plików są właściwie wszechobecne. W ciągu ostatnich kilku dekad były szeroko stosowane do przechowywania osobistych dokumentów, jednak nie do zastosowań korporacyjnych. Nie posiadają żadnej wewnętrznej struktury, więc są jak bazy klucz – wartość z kluczem hierarchicznym. Nie posiadają też innej kontroli współbieżności niż proste blokowanie plików — co także jest zbliżone do sposobu blokowania dostępu do pojedynczej agregacji w bazach NoSQL. Systemy plików mają tę zaletę, że są proste i szeroko dostępne. Doskonale radzą sobie z dużymi encjami, takimi jak wideo czy audio. Bazy danych są często wykorzystywane do indeksowania zasobów przechowywanych w plikach. Pliki sprawdzają się też świetnie podczas sekwencyjnego dostępu, takiego jak streaming, co może być przydatne w przypadku danych, które są wyłącznie dopisywane. Najnowsza moda na środowiska pracujące w klastrach spowodowała wzrost popularności rozproszonych systemów plików. Technologie takie jak Google File System i Hadoop [Hadoop] udostępniają wsparcie dla replikacji plików. Większość dyskusji na temat map-reduce dotyczy zarządzania dużymi plikami w systemach rozproszonych i narzędzi dzielących duże pliki na segmenty przetwarzane na wielu serwerach. Często organizacje, które rozpoczynają korzystanie z baz NoSQL, to te, które wcześniej korzystały z Hadoop. Systemy plików najlepiej sprawdzają się w przypadku relatywnie małej liczby dużych plików, które mogą być przetwarzane w dużych pakietach i najlepiej przy wykorzystaniu streamingu. Duża liczba małych plików przeważnie obniża wydajność — w takim przypadku lepiej sprawdzi się magazyn danych. Pliki bez wykorzystania systemów indeksujących, takich jak Solr [Solr], nie oferują także wsparcia dla zapytań.

147

148

ROZDZIAŁ 14. POZA NOSQL

14.2. Event sourcing Event sourcing to podejście koncentrujące się na zapisie wszystkich zmian w stałym magazynie zamiast przechowywania aktualnego stanu aplikacji w niej samej. Jest to wzorzec architektoniczny, dobrze działający z większością technologii przechowywania danych, w tym z bazami relacyjnymi. Wspominamy o nim, ponieważ jest podwaliną jednego z najbardziej nietypowych sposobów myślenia o przechowywaniu danych. Rozważmy przykład systemu przechowującego informacje o położeniu statków (rysunek 14.1). System posiada prosty rejestr statków, przechowujący nazwę statku i jego aktualne położenie. Przy standardowym sposobie myślenia jeżeli dowiemy się, że statek King Roy dopłynął do Gdańska, zmieniamy wartość pola symbolizującego położenie tego statku na Gdańsk. Później dowiadujemy się, że statek odpłynął, więc zmieniamy wartość pola na na morzu i ponownie zmieniamy jego wartość, kiedy statek dopłynie do Hongkongu.

Rysunek 14.1. W typowym systemie informacja o zmianie powoduje zmianę stanu aplikacji W systemie opartym na zdarzeniach pierwszym krokiem będzie stworzenie obiektu zdarzenia przechwytującego informacje o zmianie (rysunek 14.2). Obiekt ten jest przechowywany w trwałym logu zdarzeń. Kolejnym krokiem będzie przetworzenie zdarzenia i aktualizacja stanu aplikacji. W rezultacie w systemie opartym o wzorzec event sourcing przechowujemy w logu każde zdarzenie powodujące zmianę stanu aplikacji, a stan aplikacji jest w całości pochodną tego logu. W dowolnym momencie możemy bezpiecznie odrzucić stan aplikacji i odbudować go na podstawie logu zdarzeń. Teoretycznie logi zdarzeń to wszystko, czego potrzebujesz, ponieważ zawsze możesz odtworzyć stan aplikacji, kiedy będzie potrzebny, poprzez ponowne odtworzenie zdarzeń z logu. W praktyce jednak ten proces może być zbyt wolny. W rezultacie lepiej jest przechowywać i odtwarzać migawki stanu. Migawka jest zaprojektowana tak, aby przechować obraz pamięci w sposób zoptymalizowany do szybkiego odzyskania stanu. Migawka ma na celu poprawienie wydajności, nie powinna być nigdy zastępstwem dla logu zdarzeń.

14.2. EVENT SOURCING

Rysunek 14.2. W przypadku event sourcing system przechowuje każde zdarzenie razem z pochodnym stanem aplikacji Częstotliwość tworzenia migawki jest zależna od wymagań systemu. Migawka nie musi być całkowicie aktualna, ponieważ możesz odbudować stan, ładując najnowszą migawkę, a następnie uruchamiając wszystkie zdarzenia, które miały miejsce po wykonaniu migawki. Przykładem może być tworzenie migawki co noc; gdyby system przestał działać w ciągu dnia, załadowałbyś migawkę z ostatniej nocy, a następnie uruchomił dzisiejsze zdarzenia. Jeżeli możesz to zrobić wystarczająco szybko, wszystko będzie w porządku. Aby mieć pełen zestaw zmian wprowadzanych w stanie aplikacji, musiałbyś mieć log sięgający aż do początku jej działania. W wielu przypadkach nie potrzebujesz jednak aż tak długiego logu, ponieważ możesz składać starsze zdarzenia w migawki, a z logu korzystać tylko dla zdarzeń po migawce. Wykorzystanie wzorca event sourcing posiada wiele zalet. Możesz rozgłaszać zdarzenia do wielu systemów, a każdy z nich może zbudować inny stan aplikacji w zależności od potrzeb (rysunek 14.3). W przypadku systemów, w których bardzo intensywnie wykonywany jest odczyt danych, możesz zapewnić dodatkowe serwery tylko na potrzeby odczytu, mające potencjalnie różny schemat, koncentrując jednocześnie zapis w innym systemie przetwarzania (podejście znane jako CQRS [CQRS]). Event sourcing jest także efektywną platformą do analizowania informacji historycznych, ponieważ za pomocą logu można zreplikować każdy przeszły stan. Można też łatwo zbadać alternatywne scenariusze, wprowadzając do procesora analitycznego hipotetyczne zdarzenia. Event sourcing wprowadza większą złożoność — przede wszystkim musisz mieć pewność, że wszystkie zmiany stanu są przechwytywane i przechowywane jako zdarzenia. W przypadku niektórych architektur i narzędzi może to być niewygodne. Każda współpraca z systemami zewnętrznymi wymaga wzięcia pod uwagę konieczności tworzenia zdarzeń; będziesz musiał brać też pod uwagę efekty działania systemów zewnętrznych podczas odbudowywania stanu aplikacji.

149

150

ROZDZIAŁ 14. POZA NOSQL

Rysunek 14.3. Zdarzenia mogą być rozgłaszane do wielu systemów wyświetlających

14.3. Obraz w pamięci Jedną z konsekwencji systemu opartego o wzorzec event sourcing jest to, że log zdarzeń staje się rejestrem spójności stanu — nie jest on jednak wymagany, aby zachować spójność stanu aplikacji. Otwiera to możliwość przechowywania stanu aplikacji w pamięci przy wykorzystaniu wyłącznie struktur wewnątrz pamięci. Ponieważ podczas przetwarzania zdarzeń nie są wykonywane operacje I/O na dysku, przechowywanie wszystkich danych roboczych w pamięci pozwala zwiększyć wydajność. Upraszcza to też programowanie, ponieważ mapowanie pomiędzy strukturami w pamięci i na dysku nie jest konieczne. Oczywistym ograniczeniem jest to, że wszystkie dane, do których musisz mieć dostęp, muszą być przechowywane w pamięci. Takie rozwiązanie staje się coraz bardziej opłacalne — pamiętamy dyski o pojemnościach wielokrotnie mniejszych niż aktualne rozmiary pamięci roboczych. Musisz też wiedzieć, czy jesteś w stanie wystarczająco szybko odzyskać sprawność systemu po awarii — czy to poprzez ponowne przetworzenie zdarzeń, czy poprzez uruchomienie drugiego systemu i przestawienie się na niego. Będziesz potrzebować konkretnych mechanizmów wspierających współbieżność. Jedną ścieżką jest system transakcyjny działający w pamięci, taki jak na przykład ten dostarczany wraz z językiem Clojure. Innym rozwiązaniem jest przetwarzanie całego wejścia w jednym wątku. Jednowątkowy procesor zdarzeń, jeżeli zostanie dobrze zaprojektowany, może być bardzo wydajny i dostępny [Fowler lmax]. Separacja pomiędzy danymi w pamięci a danymi zapisanymi w magazynie stałym wpływa też na sposób obsługi błędów. Powszechnym podejściem jest aktualizacja modelu i w przypadku wystąpienia błędu cofnięcie wprowadzonych zmian. W przypadku struktury w pamięci

14.4. KONTROLA WERSJI

przeważnie nie będzie dostępny mechanizm automatycznego cofania zmian; albo musisz napisać własny (rozwiązanie bardzo skomplikowane), albo zapewnić bardzo dokładną walidację przed rozpoczęciem zapisu danych.

14.4. Kontrola wersji Dla większości twórców oprogramowania ich doświadczenie z systemami typu event sourcing sprowadza się do systemu kontroli wersji. System kontroli wersji pozwala wielu osobom w zespole koordynować wprowadzane przez nich modyfikacje skomplikowanego systemu, udostępniając jednocześnie eksplorację przeszłych stanów tego systemu i jego alternatywnych wersji (branching). Kiedy myślimy o magazynie danych, często wyobrażamy sobie obraz w jednym punkcie czasu, co jest bardzo ograniczone w porównaniu do tego, co oferuje system kontroli wersji. Jest to więc zaskakujące, że narzędzia przechowywania danych nie pożyczyły kilku pomysłów z systemów kontroli wersji. W końcu wiele sytuacji wymaga historycznych danych i wsparcia dla różnych widoków świata. Systemy kontroli wersji budowane są na bazie systemów plików i w związku z tym posiadają wiele takich samych ograniczeń. Nie są zaprojektowane do pełnienia funkcji magazynu danych dla aplikacji, są więc niewygodne do użycia w takim kontekście. Warto je jednak rozważyć w zastosowaniach, w których przydatna byłaby ich zdolność do prezentowania stanu danych w dowolnym momencie czasu.

14.5. Bazy XML Gdzieś na przełomie tysiąclecia ludzie chcieli wykorzystywać XML do wszystkiego, a bazy przystosowane do przechowywania dokumentów XML były bardzo popularne. Mimo że ta popularność nie zagroziła dominacji baz transakcyjnych, bazy XML nadal są wykorzystywane. Bazy XML uważamy za bazy dokumentów, w których dokumenty przechowywane są w modelu kompatybilnym z XML i w których różne technologie XML są wykorzystywane do manipulowania dokumentami. Do sprawdzania dokumentów możesz korzystać z różnych form definicji schematu dokumentu (DTD, XML Schema, RelaxNG), tworzyć zapytania za pomocą XPath i XQuery oraz wykonywać transformacje XSLT. Bazy relacyjne wykorzystały XML i połączyły go z funkcjami relacyjnymi, przeważnie jako typ kolumny; pozwoliły także na łączenie zapytań XML z zapytaniami SQL. Oczywiście nic nie stoi na przeszkodzie, abyś wykorzystał XML jako mechanizm wprowadzający strukturę do baz klucz – wartość. XML jest aktualnie mniej modny niż JSON, ale jest równie przydatny w przechowywaniu skomplikowanych agregacji, a możliwości schematu i zapytań XML są z reguły lepsze niż te, które zapewnia JSON. Wykorzystanie bazy XML oznacza, że sama baza jest w stanie wykorzystać zalety struktury XML, a nie tylko traktować wartość jako nieznaną zawartość. Te zalety muszą jednak być brane pod uwagę razem z innymi cechami bazy danych.

151

152

ROZDZIAŁ 14. POZA NOSQL

14.6. Bazy obiektowe Kiedy języki obiektowe zaczęły zdobywać popularność, także bazy obiektowe zaczęły być obiektem zainteresowania. Problemem był poziom złożoności mapowania struktur znajdujących się w pamięci na struktury zapisane w bazie danych. Bazy obiektowe miały pomóc uniknąć tej złożoności — baza danych potrafi automatycznie zapisać struktury przechowywane w pamięci. Możesz o nich myśleć jako o stałej pamięci wirtualnej, pozwalającej Ci programować z wykorzystaniem stałych danych, ale bez troszczenia się o bazę danych. Bazy obiektowe nie zyskały popularności. Jednym z powodów jest to, że tak bliska integracja z aplikacją oznacza, że nie ma łatwego sposobu na dostęp inny niż za pośrednictwem aplikacji. Przejście od baz aplikacji do baz integracji może sprawić, że bazy obiektowe będą w przyszłości szerzej stosowane. Ważną kwestią w przypadku baz obiektowych jest sposób migracji, kiedy struktura danych ulegnie zmianie. W tym przypadku bliskie połączenie pomiędzy magazynem a strukturami przechowywanymi w pamięci może stać się problemem. Niektóre bazy obiektowe udostępniają możliwość dodawania do definicji obiektów funkcji migracyjnych.

14.7. Najważniejsze kwestie ■ NoSQL to tylko fragment wszystkich technologii przechowywania danych. Kiedy nasz komfort w korzystaniu z poliglotycznego przechowywania danych wzrośnie, powinniśmy rozważyć inne technologie przechowywania danych, niezależnie, czy mają przyklejoną metkę NoSQL.

Rozdział 15

Wybór bazy danych

Do tego miejsca omówiliśmy wiele ogólnych kwestii, których powinieneś być świadomy, aby podejmować decyzję w świecie poliglotycznego przechowywania danych. Czas teraz omówić sposób doboru odpowiedniej bazy pod względem przyszłego rozwoju. Oczywiście nie znamy Twoich wymagań, nie możemy więc podać Ci odpowiedzi; nie możemy też sprowadzić tego zagadnienia do prostego zestawu reguł. Co więcej, produkcyjne wykorzystanie baz NoSQL jest wciąż niedojrzałą dziedziną, więc nawet nasza wiedza jest niedojrzała — możliwe, że za kilka lat będziemy myśleć zupełnie inaczej. Widzimy dwa główne powody do rozpatrzenia wykorzystania bazy NoSQL: produktywność programistów i wydajność dostępu do danych. W niektórych przypadkach oba powody mogą się uzupełniać lub sobie zaprzeczać. Oba są trudne do oszacowania w początkowych stadiach projektu, co jest niewygodne, ponieważ ciężko jest zmienić decyzję dotyczącą wyboru bazy w późniejszym czasie.

15.1. Wydajność programistów Porozmawiaj z dowolnym programistą aplikacji dla przedsiębiorstw, a wyczujesz u niego frustrację spowodowaną korzystaniem z baz relacyjnych. Informacje są z reguły zbierane i wyświetlane w formie agregacji, ale muszą być przekształcane w relacje, aby mogły być przechowywane. Ta praca jest teraz lżejsza niż kiedyś; w latach 90. trzeba było uginać się pod ciężarem tworzenia warstwy mapującej obiekty na relacje. W obecnym stuleciu ukazały się biblioteki ORM, takie jak Hibernate, iBATIS i Rails Active Record, które znacznie uprościły ten proces. Problem jednak nie zniknął. ORM-y to niedokładne abstrakcje i zawsze istnieją przypadki, które wymagają więcej uwagi — w szczególności jeżeli chcemy uzyskać zadawalającą wydajność. W tej sytuacji bazy zorientowane na agregacje mogą być bardzo kuszące. Możemy zrezygnować z ORM i zapisywać agregacje zgodnie z ich postacią w aplikacji. Spotkaliśmy się z kilkoma projektami, w których zmiana bazy na bazę zorientowaną na agregacje przyniosła wymierne korzyści.

153

154

ROZDZIAŁ 15. WYBÓR BAZY DANYCH

Bazy grafowe oferują inne uproszczenie. Bazy relacyjne nie sprawdzają się dobrze w przypadku danych z dużą liczbą relacji. Bazy grafowe oferują nie tylko bardziej naturalny sposób przechowywania takich danych, ale także możliwość tworzenia zapytań przystosowanych do przeszukiwania takich struktur. Wszystkie bazy NoSQL są lepiej przystosowane do niejednolitych danych. Jeżeli okaże się, że zmagasz się ze schematem w celu zapewnienia wsparcia dla niestandardowych pól, bazy NoSQL nieposiadające schematu mogą być dobrym rozwiązaniem. To są główne powody, dla których model programistyczny baz NoSQL może pomóc zwiększyć produktywność Twojego zespołu programistycznego. Pierwszym krokiem w odniesieniu tego do Twojej sytuacji jest określenie, co będzie robiło Twoje oprogramowanie. Przejrzyj aktualne funkcjonalności i zastanów się, gdzie i jak pasowałby model danych NoSQL. Kiedy zaczniesz to robić, możesz zauważyć miejsca, w których taki model byłby użyteczny. Takie dopasowanie sugeruje, że ten model może prowadzić do łatwiejszego programowania. Pamiętaj też, że poliglotyczne przechowywanie danych polega na wykorzystywaniu wielu rozwiązań przechowywania danych. Może się okazać, że do różnych fragmentów swoich danych dopasujesz różne modele ich przechowywania. Będzie to oznaczało możliwość wykorzystania różnych baz danych do przechowywania różnych aspektów Twoich danych. Wykorzystanie wielu baz danych jest znacznie bardziej skomplikowane niż wykorzystanie jednej, jednak korzyści płynące z dobrego dopasowania modelu danych mogą się okazać znacznie większe niż niedogodności. Podczas analizy modelu danych szczególną uwagę zwróć na miejsca występowania problemów. Przekonasz się, że baza zorientowana na agregacje będzie działała dobrze w wielu przypadkach, ale nie we wszystkich. Te kilka przypadków nie jest powodem do unikania modelu — problemy z niedopasowaniem modelu mogą nie przeważać nad zaletami dobrego dopasowania — warto jednak znaleźć i przemyśleć te kilka miejsc. Przejrzenie funkcjonalności i określenie potrzeb odnośnie modelu powinno wskazać Ci jeden lub kilka alternatywnych sposobów przechowywania danych. Będzie to Twój punkt startu, a kolejny krok to wypróbowanie rozwiązań poprzez próbę stworzenia fragmentu programu. Określ początkowe funkcjonalności i zaprogramuj je, zwracając uwagę, czy praca przy wykorzystaniu wybranej technologii jest prosta i wygodna. W tym przypadku warto jest zbudować te same funkcjonalności przy wykorzystaniu kilku rozwiązań bazodanowych, aby zobaczyć, które z nich nadaje się najlepiej. Ludzie zazwyczaj podchodzą do tego niechętnie — nikt nie lubi pisać oprogramowania, które zostanie wyrzucone. Jest to jednak doskonały sposób, aby sprawdzić, czy dane rozwiązanie jest odpowiednie. Niestety nie ma możliwości zmierzenia, jak produktywne są poszczególne rozwiązania. Nie mamy możliwości poprawnego zmierzenia wyników. Nawet jeżeli stworzysz dokładnie tę samą funkcjonalność, nie możesz porównywać produktywności, ponieważ zbudowanie tej funkcjonalności po raz pierwszy uczyniło drugie podejście łatwiejszym, a budowanie ich jednocześnie w identycznych zespołach nie jest możliwe. To, co możesz zrobić, to wysłuchanie opinii ludzi, którzy pracowali nad tym rozwiązaniem. Większość programistów jest w stanie wyczuć, w którym środowisku są bardziej produktywni. Mimo że jest to osąd subiektywny, a różni członkowie zespołu mogą nie być zgodni w swoich opiniach, jest to najlepsze rozstrzygnięcie, na jakie możesz liczyć. Uważamy, że to zespół, który będzie pracował nad oprogramowaniem, powinien podjąć decyzję.

15.2. WYDAJNOŚĆ DOSTĘPU DO DANYCH

Wypróbowując bazę w celu oszacowania produktywności, ważne jest także, aby przetestować rozwiązanie w przypadkach, w których jest ono nieodpowiednie. Dzięki temu zespół zakosztuje pracy zarówno przy tych łatwiejszych elementach, jak i przy tych mniej wygodnych, co pozwoli mu wyrobić sobie opinię. To podejście ma swoje wady. Często nie będziesz w stanie w pełni docenić danej technologii bez poświęcenia jej kilku miesięcy pracy — a wykonywanie prób przez tak długi czas jest nieefektywne z finansowego punktu widzenia. Jak to jednak w życiu bywa, musimy oszacować zagadnienie w możliwie jak największym stopniu; poznać jego wady i zalety. Najważniejsze jest, aby decyzję podejmować na podstawie możliwie jak największej ilości prawdziwej pracy programistycznej. Nawet tydzień pracy z technologią da Ci o niej lepsze pojęcie niż sto prezentacji dostawców.

15.2. Wydajność dostępu do danych Problemem, który spowodował szybki rozwój baz NoSQL, był szybki dostęp do ogromnych ilości danych. Kiedy pojawiły się duże strony internetowe, ich twórcy chcieli skalować poziomo i pracować na dużych klastrach. Pierwsze bazy NoSQL zostały stworzone, aby umożliwić prace w takiej architekturze. Dla innych użytkowników podążających ich śladem także głównym obiektem zainteresowania jest szybki dostęp do danych, szczególnie w przypadku bardzo rozbudowanych baz. Jest wiele czynników determinujących lepszą wydajność bazy w porównaniu z domyślną bazą relacyjną. Baza zorientowana na agregacje w porównaniu z bazą relacyjną, w której dane podzielone są na wiele tabel, może być bardzo szybka w przypadku odczytu agregacji. Łatwiejsza replikacja i możliwość shardingu przekłada się na łatwiejsze skalowanie w poziomie. Baza grafowa może zwracać dane z dużą liczbą połączeń szybciej niż baza relacyjna z wykorzystaniem połączeń. Jeżeli rozważasz bazy NoSQL ze względu na wydajność, najważniejszą czynnością jest zmierzenie wydajności w scenariuszach, które są dla Ciebie najważniejsze. Dyskutowanie na temat wydajności bazy pomoże Ci zbudować krótką listę, jednak jedynym sposobem na poprawne jej oszacowanie jest zbudowanie jakiejś funkcjonalności, uruchomienie jej i pomiar wydajności. Podczas dokonywania oceny wydajności najtrudniejszą rzeczą jest zazwyczaj zbudowanie realistycznego zestawu testów. Nie możesz zbudować faktycznego systemu, musisz więc stworzyć reprezentatywny podzbiór. Ważne jednak, aby ten podzbiór był możliwie jak najbardziej realistyczny. Nie ma sensu testować bazy, która ma służyć jednocześnie setkom użytkowników, przy wykorzystaniu tylko jednego użytkownika. Będziesz musiał stworzyć reprezentatywne obciążenie i wolumeny danych. W szczególności jeżeli budujesz publiczną stronę, może być ciężko zbudować środowisko do testów. Dobrym rozwiązaniem może być wykorzystanie zasobów w chmurze, zarówno do wygenerowania obciążenia, jak i do budowy klastra testowego. Elastyczna wycena zasobów w chmurze sprzyja budowie krótkotrwałych środowisk testowych. Nie będziesz w stanie przetestować wszystkich sposobów, na jakie wykorzystywana będzie Twoja aplikacja, będziesz więc musiał wyodrębnić reprezentatywną próbkę. Wybierz

155

156

ROZDZIAŁ 15. WYBÓR BAZY DANYCH

scenariusze najbardziej powszechne, najbardziej zależne od wydajności i te, które wydają się nie pasować do modelu bazy danych. Te ostatnie przypadki pozwolą Ci zapoznać się z ryzykiem wystąpienia błędów poza Twoimi głównymi przypadkami użycia. Budowanie danych do testów może być trudne, szczególnie we wczesnych etapach projektu, kiedy nie jest do końca pewne, jakie ilości danych produkcyjnych będą przetwarzane. Będziesz musiał wymyślić coś, na czym oprzesz swoje założenia, upewnij się więc, że będzie to jednoznaczne i ustalone ze wszystkimi zainteresowanymi. Określenie jasnych zasad wyeliminuje ryzyko, że różne osoby inaczej rozumieją pojęcie „intensywnego odczytu”. Pozwoli Ci też na łatwiejsze radzenie sobie z problemami, gdyby późniejsze odkrycia spowodowały odejście od pierwotnych założeń. Jeżeli zasady nie będą klarowne, łatwiej od nich odejść bez świadomości, że powinieneś na nowo zaprojektować środowisko testowe po uzyskaniu nowych informacji.

15.3. Trzymanie się standardów Uważamy, że bazy NoSQL są dobrym wyborem w wielu przypadkach — inaczej nie poświęcilibyśmy wielu miesięcy na napisanie tej książki. Zdajemy sobie jednak sprawę, że w większości przypadków lepiej będzie trzymać się standardowej bazy relacyjnej. Bazy relacyjne są dobrze znane; możesz z łatwością znaleźć osoby mające doświadczenie w pracy z nimi. Są dojrzałe, więc istnieje mniejsze ryzyko trafienia na błędy w oprogramowaniu. Dla technologii relacyjnych zostało stworzonych mnóstwo narzędzi, z których możesz korzystać. Nie będziesz też musiał zmagać się z politycznymi decyzjami o wykorzystaniu czegoś nietypowego — wybór nowej technologii to narażanie się na nieprzyjemności, jeżeli w projekcie wystąpią problemy. Ogólnie skłaniamy się ku stwierdzeniu, że bazę NoSQL powinieneś wykorzystywać tylko, jeżeli będziesz potrafił wskazać jej niezaprzeczalne zalety w stosunku do bazy relacyjnej. Nie ma wstydu w wykonaniu testów produktywności i wydajności, a następnie stwierdzeniu braku zalet i konieczności pozostania przy bazach relacyjnych. Uważamy, że jest wiele miejsc, w których lepiej jest skorzystać z baz NoSQL, ale „wiele” nie oznacza „wszystkie” ani nawet „większość”.

15.4. Odwoływanie przypuszczeń Jedną z największych trudności w dawaniu rad odnośnie wykorzystania baz NoSQL jest to, że nie mamy zbyt wiele informacji. W momencie pisania tej książki dyskusje na temat swoich doświadczeniach podejmują organizacje, które także są na początku drogi korzystania z NoSQL, nie mamy więc jasnego obrazu wad i zalet. W przypadku tak niepewnej sytuacji tym bardziej warto enkapsulować wybraną bazę danych — przechowywać cały kod związany z bazą danych w odrębnej części, którą łatwo będzie wymienić, jeżeli zdecydujesz o zmianie technologii bazodanowej. Klasycznym sposobem jest wyodrębnienie w aplikacji warstwy magazynu danych — przy wykorzystaniu wzorców takich jak Data Mapper czy Repozytorium [Fowler PoEAA]. Wiąże się to z konsekwencjami, w szczególności jeżeli nie jesteś pewien, z jakiego modelu chcesz korzystać,

15.5. NAJWAŻNIEJSZE KWESTIE

np. klucz – wartość kontra bazy grafowe. Co gorsza, nie mamy doświadczenia w enkapsulowaniu warstw danych pomiędzy tymi bardzo różnymi magazynami danych. Radzimy enkapsulować, ale zwracać uwagę na koszt warstwy izolującej. Jeżeli staje się ona zbyt wielkim utrudnieniem, np. poprzez komplikowanie korzystania z pomocnych funkcji bazy danych, jest to dobry argument za bezpośrednim wykorzystaniem bazy, która takie funkcje posiada. Ta informacja może być tym, czego potrzebujesz do podjęcia decyzji o wyborze bazy i porzuceniu warstwy enkapsulacji. Jest to kolejny argument za zmianą warstwy danych na usługi enkapsulujące magazyn danych (patrz podrozdział 13.3). Oprócz zmniejszenia zależności pomiędzy różnymi usługami posiada to dodatkową zaletę — wymiana bazy danych w przypadku napotkania problemów jest znacznie łatwiejsza. Jest to dobre rozwiązanie, nawet jeżeli wszędzie korzystasz z tej samej bazy danych — jeżeli coś pójdzie źle, możesz stopniowo zmieniać usługi, skupiając się na najbardziej problematycznych kwestiach. Ta rada jest tak samo adekwatna, jeżeli zdecydujesz się pozostać przy bazie relacyjnej. Enkapsulując segmenty bazy w usługach, możesz zamieniać części magazynu danych na technologię NoSQL, kiedy technologia dojrzeje, a zalety się uwidocznią.

15.5. Najważniejsze kwestie ■ Dwa główne powody do wykorzystania bazy NoSQL to: ■ Poprawienie produktywności programistów poprzez wykorzystanie bazy bardziej pasującej do wymagań aplikacji. ■ Poprawienie wydajności dostępu do danych poprzez obsługę większych wolumenów, obniżenie czasu oczekiwania i poprawienie przepustowości. ■ Ważne jest, aby przetestować swoje przypuszczenia odnośnie produktywności programistów i wydajności dostępu do danych przed podjęciem decyzji o wykorzystaniu bazy NoSQL. ■ Enkapsulacja magazynu danych w usługach pozwala na wprowadzanie zmian, kiedy technologie i wymagania ulegają zmianie. Wyodrębnienie usług dostępu do danych pozwala również na wprowadzanie rozwiązań NoSQL w istniejących aplikacjach. ■ Większość aplikacji, w szczególności jeżeli nie są to aplikacje o znaczeniu strategicznym, powinna pozostać przy bazach relacyjnych — przynajmniej dopóki ekosystem NoSQL nie stanie się bardziej dojrzały.

15.6. Końcowe przemyślenia Mamy nadzieję, że książka była dla Ciebie inspirująca. Kiedy rozpoczęliśmy jej pisanie, byliśmy sfrustrowani brakiem na rynku pozycji dających szerszy obraz świata NoSQL. Musieliśmy więc sami wykreować ten obraz poprzez napisanie niniejszej książki, a była to dla nas bardzo przyjemna podróż. Mamy nadzieję, że Twoja podróż przez tę książkę była nie mniej przyjemna, chociaż krótsza.

157

158

ROZDZIAŁ 15. WYBÓR BAZY DANYCH

Być może rozważasz teraz wykorzystanie bazy NoSQL. Jeżeli tak, ta książka jest tylko jednym z pierwszych kroków do zrozumienia baz NoSQL. Zachęcamy, abyś pobrał kilka baz danych i zaczął z nimi pracować, ponieważ jesteśmy przekonani, że technologię można zrozumieć, tylko pracując z nią — odnajdując jej silne strony i nieuniknione kruczki, które nigdy nie trafiają do oficjalnej dokumentacji. Spodziewamy się, że większość osób, w tym także większość czytelników tej książki, jeszcze długo nie będzie wykorzystywała baz NoSQL. Jest to nowa technologia i wciąż odkrywamy, w jaki sposób i kiedy poprawnie z niej korzystać. Podobnie jednak jak ze wszystkimi aspektami świata oprogramowania, sytuacja zmienia się szybciej, niż jesteśmy w stanie to przewidzieć, warto więc mieć oko na to, co dzieje się w branży. Mamy nadzieję, że znajdziesz także inne pomocne książki i artykuły. Uważamy, że najlepsze materiały dotyczące NoSQL zostaną napisane dopiero po ukończeniu tej książki, nie możemy więc skierować Cię w żadne konkretne miejsce. Publikujemy natomiast artykuły w sieci, więc jeżeli będziesz zainteresowany naszymi aktualnymi przemyśleniami, zapraszamy do odwiedzenia www.sadalage.com i http://martinfowler.com/nosql.html.

Bibliografia

[Agile Methods] www.agilealliance.org. [Amazon’s Dynamo] www.allthingsdistributed.com/2007/10/amazons_dynamo.html. [Amazon DynamoDB] http://aws.amazon.com/dynamodb. [Amazon SimpleDB] http://aws.amazon.com/simpledb. [Ambler i Sadalage] Ambler Scott, Sadalage Pramodkumar, Refactoring Databases: Evolutionary Database Design, Addison-Wesley, 2006. [Berkeley DB] www.oracle.com/us/products/database/berkeley-db. [Blueprints] https://github.com/tinkerpop/blueprints/wiki. [Brewer] Brewer Eric, Towards Robust Distributed Systems, www.cs.berkeley.edu/~brewer/cs262b-2004/PODC-keynote.pdf. [Cages] http://code.google.com/p/cages. [Cassandra] http://cassandra.apache.org. [Chang i inni] Chang Fay, Dean Jeffrey, Ghemawat Sanjay, Hsieh Wilson C., Wallach Deborah A., Burrows Mike, Chandra Tushar, Fikes Andrew, Gruber Robert E., Bigtable: A Distributed Storage System for Structured Data, http://research.google.com/archive/ bigtable-osdi06.pdf. [CouchDB] http://couchdb.apache.org. [CQL] www.slideshare.net/jericevans/cql-sql-in-cassandra. [CQRS] http://martinfowler.com/bliki/CQRS.html. [C-Store] Stonebraker Mike, Abadi Daniel, Batkin Adam, Chen Xuedong, Cherniack Mitch, Ferreira Miguel, Lau Edmond, Lin Amerson, Madden Sam, O’Neil Elizabeth, O’Neil Pat, Rasin Alex, Tran Nga, Zdonik Stan, C-Store: A Columnoriented DBMS, http://db.csail.mit.edu/projects/cstore/vldb.pdf. 159

160

BIBLIOGRAFIA

[Cypher] http://docs.neo4j.org/chunked/1.6.1/cypher-query-lang.html. [Daigneau] Daigneau Robert. Service Design Patterns, Addison-Wesley, 2012. [DBDeploy] http://dbdeploy.com. [DBMaintain] www.dbmaintain.org. [Dean i Ghemawat] Dean Jeffrey, Ghemawat Sanjay, MapReduce: Simplified Data Processing on Large Clusters, http://static.usenix.org/event/osdi04/tech/full_papers/dean/dean.pdf. [Dijkstra’s] http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm. [Evans] Evans Eric. Domain-Driven Design, Addison-Wesley, 2004. [FlockDB] https://github.com/twitter/flockdb. [Fowler DSL] Fowler Martin, Domain-Specific Languages, Addison-Wesley, 2010. [Fowler lmax] Fowler Martin, The LMAX Architecture, http://martinfowler.com/articles/lmax.html. [Fowler PoEAA] Fowler Martin, Architektura systemów zarządzania przedsiębiorstwem. Wzorce projektowe, Helion, 2005. [Fowler UML] Fowler Martin, UML Distilled, Addison-Wesley, 2003. [Gremlin] https://github.com/tinkerpop/gremlin/wiki. [Hadoop] http://wiki.apache.org/hadoop/MapReduce. [HamsterDB] http://hamsterdb.com. [Hbase] http://hbase.apache.org. [Hector] https://github.com/hector-client/hector. [Hive] http://hive.apache.org. [Hohpe i Woolf] Hohpe Gregor, Woolf Bobby, Enterprise Integration Patterns, Addison-Wesley, 2003. [HTTP] Fielding R., Gettys J., Mogul J., Frystyk H., Masinter L., Leach P., Berners-Lee T., Hypertext Transfer Protocol — HTTP/1.1, www.w3.org/Protocols/rfc2616/rfc2616.html. [Hypertable] http://hypertable.org. [Infinite Graph] www.infinitegraph.com. [JSON] http://json.org/json-pl.html. [LevelDB] http://code.google.com/p/leveldb. [Liquibase] www.liquibase.org. [Lucene] http://lucene.apache.org.

BIBLIOGRAFIA

[Lynch i Gilbert] Lynch Nancy, Gilbert Seth, Brewer’s conjecture and the feasibility of consistent, available, partition-tolerant web services, http://lpd.epfl.ch/sgilbert/pubs/BrewersConjecture-SigAct.pdf. [Memcached] http://memcached.org. [MongoDB] www.mongodb.org. [Monitoring] http://docs.mongodb.org/manual/administration/monitoring. [MyBatis Migrator] http://mybatis.org. [Neo4J] http://neo4j.org. [NoSQL Debrief] http://blog.oskarsson.nu/post/22996140866/nosql-debrief. [NoSQL Meetup] http://nosql.eventbrite.com. [Notes Storage Facility] http://pl.wikipedia.org/wiki/Lotus_Domino_Server. [OpsCenter] www.datastax.com/products/opscenter. [OrientDB] www.orientdb.org. [Oskarsson] prywatna korespondencja. [Pentaho] www.pentaho.com. [Pig] http://pig.apache.org. [Pritchett] www.infoq.com/interviews/dan-pritchett-ebay-architecture. [Project Voldemort] http://project-voldemort.com. [RavenDB] http://ravendb.net. [Redis] http://redis.io. [Rekon] https://github.com/basho/rekon. [Riak] http://wiki.basho.com/Riak.html. [Solr] http://lucene.apache.org/solr. [Strozzi NoSQL] www.strozzi.it/cgi-bin/CSA/tw7/I/en_US/NoSQL. [Tanenbaum i Van Steen] Tanenbaum Andrew, Van Steen Maarten,. Distributed Systems, Prentice Hall, 2007. [Terrastore] http://code.google.com/p/terrastore. [Vogels] Vogels Werner, Eventually Consistent — Revisited, www.allthingsdistributed.com/2008/12/eventually_consistent.html. [Webber Neo4J Scaling] http://jimwebber.org/2011/03/strategies-for-scaling-neo4j. [ZooKeeper] http://zookeeper.apache.org.

161

162

BIBLIOGRAFIA

Skorowidz

A ACID, 35 agregacje, 12, 29–35, 48 aktualizacja warunkowa, 62 algorytm Dijkstry, 125 analiza danych w czasie rzeczywistym, 48 auto-sharding, 55

B BASE, 69 baza aplikacji, 22, 23 BigTable, 36, 55 danych typologia, 15 danych bez schematu, 44, 45 dokumentów, 35, 99–108 grafowa, 42, 43, 51, 119–128 integrująca, 22, 23 klucz – wartość, 35, 91–98 NoSQL a bazy relacyjne, 28 informacje ogólne, 11, 12, 25–28 przechowywanie danych, 44 wybór, 153–158 obiektowa, 152 relacyjna, 19–24 rodziny kolumn, 36–38, 50, 109–118 XML, 151 zorientowana na agregacje, 29–39, 42

biblioteka map-reduce, 79 BigTable, 36, 55 branching, 151 BREADTH_FIRST, 124

C CAP, 66–69 CAS, 74 Cassandra, 109–118 Cassandra Query Language, Patrz CQL chudy wiersz, 38 CQL, 115, 116 CQRS, 149 Cypher, 123, 125, 126

D dane niestandardowe, 44 nieświeże, 64 DBDeploy, 131 DEPTH_FIRST, 124 domniemany schemat, 45 dostępność klastra w Cassandrze, 113, 114 w Neo4J, 123 w teorii CAP, 67

E enkapsulacja bazy danych, 156 event sourcing, 148–150

163

164

SKOROWIDZ

F faza przejściowa przy zmianie schematu, 132, 133 FlockDB, 120 funkcja combine, 80 map, 79 reduce, 79

G graf struktura, 119, 120 Gremlin, 123 GUID, 74

H hasz zawartości zasobu, 74

I identyfikator GUID, 74 integracja poprzez współdzieloną bazę danych, 20

K klastry a bazy relacyjne, 24 klucz sessionid, 96 kolekcje w MongoDB, 100 kolumna w Cassandrze, 110, 111 wygasająca, 117 kompaktowanie, 112 koncepcja kworum, 94 konflikt odczyt – zapis, 63 zapis – zapis, 61, 62 kontrola wersji, 151 krawędzie, 43, 119 krotka, 21, 29 kwora, 70–72 kworum zapisu, 71

L log zdarzeń, 148–150

Ł łączenie a map-reduce, 79–81 łuki, Patrz krawędzie

M magazyn danych, 36 wykorzystanie poliglotyczne, 140 klucz – wartość, 91–98 rodziny kolumn, 36–38, 109–118 wspierający, 19, 20 mapowanie relacyjne, 34 map-reduce, 77–87 dwuetapowe, przykład, 82–85 inkrementacyjny, 85, 86 podstawy, 78, 79 tworzenie obliczeń, 81, 82 master, Patrz serwer główny migawka, 148, 149 model danych, 29 dla bazy relacyjnej, 30 oparty o agregacje, 32 dystrybucyjny, 53–60 ignorujący agregacje, 35 przechowywania, 29 relacyjny, 21, 29, 42 spójności ostatecznie spójny, 93 modelowanie z myślą o dostępie do danych, 47–51 MongoDB, 100–106

N naprawa odczytu, 112 Neo4J, 120–127 niezgodność\ impedancji, 21, 22 NoSQL a bazy relacyjne, 28 infomacje ogólne, 11, 12, 25–28 przechowywanie danych, 44 wybór bazy danych, 153–158

SKOROWIDZ

O obiekt Traverser, 124 obraz w pamięci, 150 odczyt niespójny, 63 okno niespójności, 64 operacja CAS, 74 porównaj-i-ustaw, 74 optymistyczna blokada offline, 74

P pamięć główna, 19 memtable, 112 parametr slaveOk, 101 WriteConcern, 102 partycjonowanie a Map-reduce, 79–81 podzielony umysł, 67 pokrewieństwo sesji, 65 pole blob, 91 polecenie db.runCommand, 101 DEL, 115 GET, 115 SET, 115 poliglotyczne przechowywanie danych, 139 porównaj-i-ustaw, 74 programowanie poliglotyczne, 140 przechowywanie danych poliglotyczne, 139 przekazanie ze wskazaniem, 113 przestrzenie kluczy, 112 przyklejona sesja, 65

Q QUORUM, 112

R Redis, 92 reduktor łączący, 80 regiony, 79 relacja, 41, 42 w bazach grafowych, 121 w modelu relacyjnym, 21

replikacja, 53, 59 master-slave, 56, 57, 102 peer-to-peer, 57, 58 Riak, 92 Riak Search, 94 rodzina kolumn, 36–38, 109 standardowa, 110 superkolumn, 111, 112 rozluźnianie spójności, 66 trwałości, 69, 70

S serwer główny, 56, 57 podległy, 56, 57 sharding, 54–56, 59, 96, 126 w MongoDB, 106 sieć zależności, 86 skalowanie, 105 baz grafowych, 126 horyzontalne, 105 w bazach klucz–wartość, 96 w Cassandrze, 116 w MongoDB, 105, 106 slave, Patrz serwer podległy spójność aktualizacji, 61, 62 logiczna, 63 odczytu, 63–65 podejście optymistyczne, 61 podejście pesymistyczne, 61 replikacji, 64 w bazach grafowych, 122 w Cassandrze, 112, 113 w MongoDB, 101 w sesji, 65 SSTable, 112 stempel czasowy ostatniej aktualizacji, 74 wektor, 76 wersji, 74, 75 na wielu serwerach, 75, 76 stracona aktualizacja, 61 struktura danych w bazach klucz–wartość, 95 superkolumna, 111 systemy plików, 147 szeroki wiersz, 38

165

166

SKOROWIDZ

T tasowanie, 79 teoria CAP, 66–69, 102 testowanie bazy danych, 155 tolerancja na partycjonowanie, 67, 68 transakcja ACID, 35 atomowa, 102 biznesowa, 73–75 systemowa, 73–75 w Cassandrze, 113 w magazynie klucz–wartość, 94 w MongoDB, 102 w Neo4J, 122 trawersowanie, 119, 120 trwałość replikacji, 70 TTL, 117 typy baz danych, 15

W wartość baz relacyjnych, 19–21 wdrożenie poliglotyzmu, 145 Webber Neo4J Scaling, 126 węzły, 43, 119 wiadra, 79, 93, 94 domeny, 92, 93

widok w bazie relacyjnej, 46 widoki zmaterializowane, 46, 47 właściwości BASE, 69 współbieżność, 20 współczynnik replikacji, 71, 113 współdzielenie, 53–56 wydajność dostępu do danych w bazach NoSQL, 155 wyjątek NotInTransactionException, 122

Z zapytania dla magazynu klucz–wartość, 94, 95 w Cassandrze, 114–116 w MongoDB, 103–105 w Neo4J, 123–126 zestawy replik w MongoDB, 101–103 zmiana struktury agregacji, 137 zmiany inkrementacyjne, 135 schematów, 129–137 schematu w bazach transakcyjnych, 129–133 schematu w magazynach danych NoSQL, 133–137 w bazach grafowych, 136 zmienna WriteConcern, 101