Hierarchia pamięci

Typowy komputer posiada kilka komponentów mogących przechowywać dane. Różnią się one nie tylko pojemnością, czasem dostępu, ale także kosztem. Najbardziej pojemne urządzenia są najwolniejsze i vice versa. Cache
Pojemność cache'u nie przekracza 1MB, jednak czas dostępu do danych jest na poziomie szybkości procesora. Jak działa cache? Jeśli maszyna wykonuje rozkaz, szuka danych w cache'u i jeśli ich tam nie znajduje – kopiuje je z pamięci głównej. Wszelkie zmiany dokonane w cache'u muszą znaleźć odzwierciedlenie w pamięci głównej.

Pamięć główna
Każdy bajt może być uzyskany w tym samym okresie czasu. Typowy czas dostępy waha się od 10 – 100 nanosekund. Wszystko co dzieje się w komputerze – wykonywanie instrukcji, manipulacja danymi, etc. odbywa się za pośrednictwem pamięci głównej.

Pamięć wirtualna
Większość maszyn używa 32-bitowej przestrzeni adresowej – czyli mamy 2^32 adresów – 4 bilony różnych adresów. Ponieważ każdy bajt ma własny adres, to typowa pojemność pamięci wirtualnej wynosi 4 GB. Ze względu na swoje duże rozmiary przechowywana jest na dyskach – podzielonych na bloki – pamięć ma strony – tego samego rozmiaru. Jak to jest wykorzystywane w systemach baz danych? Zarządzają one swoimi danymi przez pamięć wirtualną – system operacyjny dostarcza im potrzebnych danych za pomocą mechanizmu stronicowania. „Pamięciowe systemy baz danych” – jak większość aplikacji – są w pełni użyteczne jeśli dane są małe na tyle, że nie ma potrzeby wymiany danych. Przy 32-bitowej przestrzeni adresowej aplikacje mogą trzymać na raz nie więcej niż 4 GB danych. Dlatego wielkie systemy zarządzania baz danych trzymają dane bezpośrednio na dyskach – ograniczone są tylko pojemnością użytego dysku.

Pamięć drugorzędowa
Wolniejsza, ale bardziej pojemna. Najczęściej używane są dyski magnetyczne, jednak czasem zdarzają się również optyczne i magneto-optyczne. Dyski są używane do przechowywania stron pamięci wirtualnej używanych przez aplikacje. Pliki są przenoszone pomiędzy pamięcią a dyskiem w blokach, pod kontrolą o.s. lub db s. Czytanie pliku odbywa się za pośrednictwem buforowanie, czyli czytania pliku fragmentami, blokami. Bufor znajduje się w pamięci głównej. Po skonsumowaniu zawartości przez aplikację, do bufora kopiowany jest kolejny fragment pliku. DBMS zarządza blokami dysku samodzielnie, rzadziej za pomocą o.s.

Pamięć trzeciorzędowa
W przypadku dużych baz danych, przechowujących terabajty danych zwykła pamięć drugorzędowa nie wystarcza. Z pomocą przychodzi pamięć trzeciorzędowa. Charakteryzuje się nieco wyższym czasem dostępu (czytania / pisania) niż pamięć 2-go rzędowa. Ale o ile przy pamięciach 2-go rzędowych czas dostępu jest mniej więcej taki sam dla wszystkich danych, czas dostępu pamięci 3-cio rzędowych rożni się, w zależności jak blisko jest do punktu czytania / pisania. Wyróżniamy:

Czas dostępu w pamięciach 3-cio rzędowych różni się od kilku sekund do kilku minut. Są one ok. 1000 razy pojemniejsze, co wpływa na zwiększenie czasu dostępu, również około 1000 razy.

Trwałe i ulotne przechowywanie (pamięć)

Definicje powyższych pojęć są intuicyjne: pamięci ulotne nie pamiętają co przechowują po wyłączeniu zasilania, w przeciwieństwie do pamięci trwałych. Rozważania na ten temat są bardzo ważne, ponieważ jedną z podstawowych własności DBMS jest możliwość zachowywania danych nawet w przypadku braków zasilania. Urządzenia magnetyczne, taśmy są trwałe, podobnie jak optyczne (CDROM) z tą różnicą, że CDROMy nie można powtórnie zapisywać. Generalnie pamięci 2-go i 3-cio rzędowe są trwałe. Pamięć główna jest przykładem pamięci ulotnych. DBMS, który działa na maszynie z ulotną pamięcią musi robić kopię zapasową na dysku, zanim zmiana dotrze do bazy – dlatego wszelkie zapytania, modyfikacje wymagają dużej liczby operacji I/O. Alternatywą są pamięci FLASH.

Pamięć drugorzędowa

Dyski

Używanie pamięci drugorzędowej jest jedną z podstawowych charakterystyk DBMS. A pamięci drugorzędowe prawie w całości bazują na dyskach magnetycznych.

Mechanizmy dysków
Jak jest zbudowany dysk? Składa się z talerzy złożonych z dwóch powierzchni, na których zapisane są bity. Bity organizuje się w ścieżki w postaci koncentrycznych okręgów. Następnie ścieżki organizowane są w sektory (kawałki okręgów oddzielone od siebie lukami [gaps]). Sektory są atomowymi jednostkami, jeśli chodzi o czytanie i pisanie.

Kontroler dysku
Dyski sterowane są za pośrednictwem kontrolera. Jego zadaniem jest:

Charakterystyka przechowywania dyskowego

Charakterystyka dostępu do dysku
Analizując DBMS musimy dowiedzieć się nie tylko jak dane są przechowywane, ale również w jaki sposób przebiega ich manipulacja. Ponieważ wszystkie wyliczenia mają miejsce w pamięci głównej, jedynym zadaniem dysku jest dostarczenie danych do pamięci. Dane są czytane blokami gdy:

Czas pomiędzy złożeniem zamówienia na dane, a momentem, gdy żądany blok znajdzie się w pamięci nazywa się opóźnieniem, złożonym z: Zapisywanie bloków przebiega analogicznie do odczytu: głowice ustawiają się nad właściwym cylindrem, czekamy aż odpowiedni sektor znajdzie się pod głowicą w czasie obrotu. Problem pojawia się, gdy chcemy zweryfikować zapisane dane. Musimy wtedy czekać ma dodatkowy obrót i czytać każdy zapisany sektor. Prostszym sposobem jest mechanizm sum kontrolnych, który omówię później. Modyfikowanie bloków przebiega następująco: Czas potrzebny do modyfikacji bloku jest sumą czasów: czytania, aktualizacji (który jest stosunkowo mały w porównaniu z czasem czytania/zapisu i można go pominąć), zapisywania i jednego obrotu dysku (jeśli weryfikujemy dane).

Efektywne wykorzystywanie pamięci 2-rzędowej.

Kiedy implementujemy jakieś algorytmy prawie zawsze zakładamy, że dane będą mieścić się w pamięci głównej. Nie wolno popełnić tego błędu przy programowaniu DBMS. Musimy zakładać, że pamięć nie jest w stanie pomieścić ogromu informacji, jakie będą przechowywane w bazie. Należy brać pod uwagę rozwiązania stosujące pamięci 2-go, a czasami nawet 3-cio rzędowe. Efektywność ma silny związek ze współdziałaniem pamięci głównej i 2-go rzędowej. W szczególności, lepszym algorytmem jest ten, który wymaga mniejszej ilości dostępu do dysku, nawet jeśli jako algorytm nie jest za bardzo efektywny – gdyby działał w pamięci głównej. Przy bazach danych mających wielu użytkowników i dużym ruchu na kontrolerze, operacje I/O zaczynają dominować – koszt czytania / pisania przerasta koszt, związany z manipulowaniem danymi. Dlatego też należy w używanych algorytmach minimalizować liczbę operacji I/O.

Two Phase, Multiway Merge Sort (Sortowanie zewnętrzne)
Jednym z algorytmów, które musiały się zmienić pod wpływem takiego modelu (I/O) jest klasyczny Merge Sort, którego pochodna jest bardzo popularnym algorytmem sortującym w aplikacjach bazodanowych. Składa się z dwóch faz:

Mechanizm postępowania: część rekordów, znajdującą się w pamięci sortujemy szybszym algorytmem, np. QuickSort.Faza 1 wygląda tak: Na koniec fazy 1 wszystkie rekordy relacji zostaną wczytane do pamięci i staną się elementami posortowanych podlist, zapisanych na dysku.

Faza 2 polega na łączeniu wygenerowanych list. Można łączyć je parami, tak jak w klasycznym Merge Sort. Wymaga to czytania wszystkich danych 2logn razy, przy n listach. Lepszym rozwiązaniem jest wczytanie po jednym bloku, z każdej listy do bufora, znajdującego się w pamięci głównej. Dla małych relacji (liczba podlist nie jest astronomiczna) jest to b.dobry sposób. Listę wynikową zapisujemy w buforze wyjściowym według zasady:

Spostrzeżenie
W fazie 2 bloki są czytane w nieprzewidywalnej kolejności, nie wiemy kiedy lista się wyczerpie. Każdy blok zawierający rekord jest czytany dokładnie raz. Dlatego liczba ‘czytań’ wynosi n – podobnie jak w fazie 1.

Przyśpieszanie dostępu do pamięci 2-go rzędowej

We wcześniejszych rozważaniach nie uwzględnialiśmy położenia danych na dysku. W systemach, które wykonują wiele małych zapytań fakt położenia nie ma dużego wpływu. Jednak jeśli wszystko co robi system, to sortowanie ogromnych relacji, możemy zaoszczędzić nieco czasu. Aby przyśpieszyć wykonywanie zapytań i zwiększyć przepustowość (przetwarzanie kilku zapytań w tym samym czasie) można użyć jednej z poniższych strategii:

Organizowanie danych w cylindrach

Ponieważ czas przeszukiwania to średnio połowa czasu dostępu, jest kilka aplikacji, w których całkiem dobrym rozwiązaniem jest umieszczenie danych, do których odwołujemy się razem na jednym cylindrze. Jeśli dane są większe, można użyć kilku sąsiednich cylindrów. Jeśli chcemy przeczytać wszystkie bloki kolejno w ramach jednej ścieżki lub cylindra, możemy pominąć wszystko za wyjątkiem pierwszego czasu szukania (przesunięcia nad cylinder) i pierwszego opóźnienia obrotowego (przesunięcie na początek pierwszego bloku). Zawartość cylindra możemy przeczytać w ramach czasu przeszukiwania. Takie zorganizowanie danych znacznie przyśpiesza faza 1 TPMMS, ale nie pomaga w fazie 2. Dlaczego? Ponieważ nie umiemy przewidzieć w jakiej kolejności dane będą czytane. Wiemy tylko tyle, że wybieramy najmniejszy element z pośród wszystkich pierwszych elementów list. Dlatego wynikowe bloki będą zapisywane po jednym na raz, czyli dokładnie tak samo jak wcześniej.

Użycie wielu dysków

Często możemy zwiększyć prędkość naszego systemu zastępując jeden dysk wieloma. Co za tym idzie, mamy więcej niezależnych głowic. I tak w TPMMS: załóżmy, że mamy 4 dyski. Wtedy w fazie 1 pamięć ładujemy z każdego z nich, po 1. Jeśli tylko system jest w stanie obsłużyć czytane dane, możemy czytać dane w fazie 1 cztery razy szybciej. Podobnie jest z zapisywaniem – każdą posortowaną listę możemy dystrybuować na 4 dyskach, zapisując ją na kolejnych cylindrach. Skupmy się na fazie 2. Wciąż musimy czytać bloki z początków podlist w losowy, zależny od danych sposób. Jeśli wybieranie najmniejszego elementu wymaga wczytania do pamięci pierwszych bloków każdej listy – nie możemy użyć 4 dysków. Za każdym razem gdy wyczerpie się blok, musimy poczekać, aż nowy blok zostanie wczytany na miejsce starego. Dlatego używany jest tylko jeden dysk na raz. Ale jeśli będziemy uważniej pisać kod, możemy sprawić, żeby poszukiwanie elementu najmniejszego wznowiło się jak tylko przybędzie to pamięci pierwszy element nowego bloku. Wtedy kilka list będzie mogło mieć swoje bloki załadowane do pamięci w tym samym czasie. Tak długo jak są one na osobnych dyskach, możemy czytać na raz kilka bloków. Zyskamy w ten sposób 4-krotne zwiększenie prędkości czytania w fazie 2. Pisanie w fazie 2 jest łatwiejsze do przyśpieszenia. Możemy użyć 4 buforów wyjściowych i zapisywać każdy kolejno. Gdy bufor jest pełny, jest zapisywany na kolejny dysk, na kolejne cylindry. W ten sposób możemy zapisywać dane z jednego bufora, gdy pozostałe 3 są ładowane. Jeśli jednak okazało się, że kolejne 2 bloki znajdują się na tym samym dysku, to pierwszy musi czekać ma drugiego, wszystkie operacje w pamięci są wstrzymywane do czasu, dopóki początek drugiego bloku znajdzie się w pamięci. Oczywiście nie możemy zapisywać wynikowej listy szybciej niż czytać danych z podlist. Jak zobaczyliśmy nie jest możliwym równoczesne korzystanie z 4 dysków, dlatego faza 2 uzyskuje 2-3 krotne przyśpieszenie.

Dublowanie (Mirroring)

Są sytuacje, w których dobrze jest mieć dwa lub więcej dysków zawierających te same dane. Dyski takie nazywa się mirror’ami. Jeśli jeden z dysków ulegnie uszkodzeniu, pozostanie zawsze jego kopia na innym. Dzięki mirroring’owi można też przyśpieszać dostęp do danych. Np. w fazie 2 TPMMS mogliśmy ładować maksymalnie do 4 bloków z różnych list, których poprzednie bloki zostały wykorzystane. Jednak nie mogliśmy wskazać, które 4 listy dostaną nowe bloki. W pesymistycznym przypadku mogło być tak, że pierwsze 2 listy były na jednym dysku etc. Jeśli możemy kupić i użyć 4 kopii dużego dysku, mamy gwarancję, że zawsze będziemy czytać 4 bloki za jednym razem. Jeśli mamy n kopii dysku, możemy czytać n bloków równolegle. Używanie mirror’ow nie przyśpiesza ani nie spowalnia pisania. Po prostu, zapisujemy na wszystkich dyskach posiadających kopię zapisywanego bloku. Zapisywanie przebiega równolegle, czyli czas = zapis na 1 dysk.

Planowanie algorytmem windowym (elevator alg.)

Inny sposób przyśpieszania polega na podpowiadaniu kontrolerowi kolejności obsługi żądań. Algorytm windowy polega na specyficznym poruszaniu się głowicy. Jeździ one z dołu na górę i z powrotem. Znajduje żądanie, obsługuje je i jeśli czekają jakieś inne żądania dalej od bieżącego – jedzie tam. W przeciwnym przypadku zawraca. Lepszy niż naiwny FCFS. Skraca czas dostępu do bloku.

Prefetching (podwójne buforowanie)

Ostatnim użytecznym sposobem przyśpieszania jest podwójne buforowanie. Czasem możemy przewidzieć kolejność, w jakiej dane będą żądane. Dlatego ładujemy je do pamięci zanim będą potrzebne. Znowu skupmy się na fazie 2 TPMMS. Łączymy kilka posortowanych list, ładując po jednym bloku z każdej do pamięci. Jeśli po wczytaniu elementów mamy miejsce w pamięci (liczba list nie jest wielka), moglibyśmy dołączyć do każdej listy po 2 bufory-bloki. Jeśli jeden się wyczerpie, przełączamy się na kolejny. Jednak nadal musimy czytać kolejno wszystkie elementy posortowanych list. Możemy ulepszy technikę i połączyć podwójne buforowanie z „cylindrową strategią” jeśli będziemy: Wróćmy do fazy 2 TPMMS. To co robimy, to dla każdej listy tworzymy 2 bufory wielkości cylindra. Jeden bufor wypełniamy, gdy drugi jest używany. Używając takich buforów ograniczamy się jedynie do szukania raz na cylinder.

Disk Failures

Sytuacje, w których dyski mogą zawieść:

Intermittent failures
Zwykle sektory na dyskach mają pewne zbędne bity – sumy kontrolne. Dzięki nim wiemy, czy to co czytaliśmy z sektora jest poprawne czy nie. Podobnie jest z pisaniem. Zwykłe funkcja czytająca dane zwraca parę (w,s), gdzie w – dane, s – status. Przy błędach tej klasy, możemy trzymać status „bad” kilka razy, ale w końcu dostaniemy „good”. Jednak czasami możemy zostać oszukani. Prawdopodobieństwo jest jednak niewielkie. Przy zapisywaniu możemy próbować czytać sektor po zapisie i weryfikować, czy proces przebiegł pomyślnie.

Sumy kontrolne
Każdy sektor ma kilka dodatkowych bitów, zwanych sumami kontrolnymi – zbiór bitów zależnych od danych przechowywanych w tym sektorze. Jeśli podczas czytania odkrywamy, że suma kontrolna się nie zgadza, zgłaszamy status „bad”. W p.p. zgłaszamy „good”. Mogą oczywiście wystąpić przekłamania, ale używając dostatecznie długiej sumy (liczba bitów) redukujemy ten problem.
Prosta forma sum kontrolnych bazuje na parzystości. Jeśli jest nieparzysta liczba 1-nek, mówimy że bit parzystości to 1, 0 w p.p. Gdy zapisujemy sektor można liczyć bit parzystości i dołączać na koniec sekwencji bitów. Zatem, każdy sektor będzie miał bit parzystości 0.

Stable Storage
Sumy kontrolne pomagają nam wykryć błąd – ale nie umożliwiają korekty. Dlatego jest stabilne przechowywanie, które polega na parowaniu sektorów. Każda para reprezentuje zawartość jednego sektora X. Para składa się z lewej i prawej kopii: XL i XR. Zakładamy, że kopie są uzupełnione o bity parzystości, uniemożliwiające przekłamania. Zakładamy, że jeśli funkcja czytająca zwraca (w,good) dla XL i XR to w jest prawdziwą zawartością sektora.
Pisanie odbywa się w następujący sposób:

Stabilne czytanie przebiega w następujący sposób:

Error Handling Capabilities of Stable Storage

Recovery from Disk Crashes

Trochę o najgroźniejszych wypadkach, czyli awariach głowic, kiedy to dane są trwale zniszczone. Jeśli dane nie mają kopii zapasowej na innym nośniku nic nie da się zrobić. Jednak jest kilka schematów, pokazujących jak bronić się przed tego typu sytuacjami. Mowa tu o RAID, Redundant Array of Independent Disks. Każdy schemat składa się z dysków przechowujących dane (data disks) oraz przechowujących informacje całkowicie zależne od zawartości tych pierwszych. Drugie nazywane są dyskami nadmiarowymi (redundant disks). W przypadku awarii, bez względu na rodzaj dysku, dane mogą być w pełni odzyskane.

RAID level 1

Najprostszym sposobem jest stworzenie obrazu każdego dysku. Problem nastąpi tylko wtedy, gdy w czasie awarii 1-go dysku, nastąpi awaria 2-go.

Bloki parzystości (Parity blocks) RAID level 4

Przy mirroringu musimy mieć tyle samo nadmiarowych dysków, co z danymi. RAID l4 używa tylko jednego nadmiarowego, bez względu na liczbę dysków z danymi. Zakładamy, że dyski są identyczne i numerujemy bloki od 1 do n. Nadmiarowy dysk w i-tym bloku ma sumy kontrolne dla i-tych bloków wszystkich dysków. Podobnie, j-ty bit i-tego sektora.... Wykorzystuje się dodawanie modulo 2.

Przykład

disk1 11110000
disk2 10101010
disk3 00111000
disk4 01100010 - redundant disk

Czytanie może odbywać się zwyczajnie. Załóżmy, że mamy dyski 1,2,3 i 4 – nadmiarowy. Czytamy z 1 blok. W tym samym czasie możemy przeczytać dane z tego samego dysku wykorzystując pozostałe, czyli 2 + 3 + 4 modulo 2 po każdym bicie.
Zapisywanie – na dysk z danymi + modyfikacja odpowiedniego bloku na dysku nadmiarowym. Można to zrobić za pomocą 4 operacji I/O:

Przykład

Zmieniamy zawartość disk2 z 10101010 na 11001100. Bierzemy sumę modulo 2 starej i nowej wartości i dostajemy: 01100110. Czyli na dysku nadmiarowym mamy zmienić w odpowiednim bloku 2,3,6,7 bit.

Zobaczmy co stanie się, gdy jeden z dysków ulegnie awarii. Jeśli będzie to dysk nadmiarowy, to zamieniamy na nowy dysk i obliczamy sumy. Jeśli padnie dysk z danymi, zmieniamy na nowy i obliczamy jego dane na podstawie pozostałych. Jak? Przyświeca zasada: Każdy bit jest sumą modulo 2 odpowiednich bitów ze wszystkich dysków.

RAID level 5

RAID 4 działa dobrze, dopóki nie wystąpią dwie, jednoczesne awarie dysków. Ponieważ proces odzyskiwanie danych, z dysków nadmiarowych i z danymi, jest identyczny czemu nie traktować wszystkich dysków na równi. Każdy dysk może być nadmiarowe ze względu na pewne bloki. Jeśli mamy n+1 dysków, ponumerowanych od 0 do n, to możemy traktować i-ty cylinder dysku j-tego jako nadmiarowy, jeśli j jest resztą z dzielenia i mod (n+1).

Przykład

Mamy 4 dyski, ponumerowane od 0 do 3,

Jak poradzić sobie z wieloma, jednoczesnymi awariami dysków? Odp: RAID level 6

Jeśli będziemy mieli dostatecznie dużo nadmiarowych dysków, możemy poradzić sobie z każdą awarią, nieważne jakiego rodzaju dysku – tak głosi RAID l6. Strategia to jest oparta na kodzie Humming’a – najprostszym, korygującym błędu kodzie.
Załóżmy, że mamy system złożony z 7 dysków, ponumerowanych od 1..7, w tym 1..4 – data disks, 5..7 – redundant disks. Budujemy macierz 3x7, w której:
  Data disks Redundant
Disk nr1 2 3 4 5 6 7
1 1 1 0 1 0 0
1 1 0 1 0 1 0
1 0 1 1 0 0 1
Co oznaczają te 1 i 0?
Dyski z 1 w rzędzie traktowane są jako zestawy dysków dla RAID l4. Czyli dysk nr 5 jest nadmiarowy dla dysków 1,2,3 itd. Czytanie przebiega w zwykły sposób. Nadmiarowe dyski są ignorowane. Zapisywanie polega na policzeniu sumy modulo 2 starej i nowej wartości zapisywanego bloku. Następnie wynik jest dodawany modulo 2 do tych nadmiarowych dysków, które mają 1 w wierszu, w którym zapisywanym dysk ma także 1. Np. zapisując dysk 2, używamy dysków 5,6.

Odzyskiwanie
Załóżmy, że mamy awarię dwóch dysków: a i b. Ponieważ wszystkie kolumny macierzy są różne, można znaleźć taki wiersz r w którym wartości dla a i b są różne. Załóżmy, że a w wierszu r ma 0, a b 1. Możemy policzyć dane z b dodając do siebie modulo 2 wszystkie inne dyski, które mają 1 w wierszu r. Teraz odtwarzamy zawartość dysku a. Szukamy wiersza, w którym w kolumnie a jest 1. Zawartość dysku a to suma modulo 2 wszystkich innych dysków mających 1 w tym wierszu.