Zarządzanie ilością pamięci roboczej w systemie bazodanowym

0

Mamy taki dość typowy problem:

Jak zarządzać pamięcią w systemie bazy danych, aby:

  • Wykonać jak najwięcej zapytań równocześnie, maksymalizując możliwości sprzętu (który wiemy, że obecnie idzie masowo w równoległość - wiele rdzeni, wiele dysków itp)
  • Nie wyjechać poza dostępną pamięć w żadnym momencie (nie ubić serwera przez OOM).

Założenia:

  • Zapytania mogą wyciągać lub zmieniać dowolną ilość danych.
  • Klient może przysłać dowolną ilość zapytań w jednostce czasu i może zlecać je równolegle (mamy bardzo szybkiego klienta, a wąskim gardłem jest baza); próba wykonania wszystkich na raz jednak może skończyć się bardzo źle (OOM).
  • Nie da się w niezawodny sposób zawczasu przewidzieć ile pamięci będzie niezbędne na realizację zapytania do końca. Zapytanie może się wydawać niewinne (np. SELECT... LIMIT 1), a tu nagle dostajemy z dysku pojedynczą komórkę, w którą ktoś wepchnął 100 MB XML :D
  • Całkowita pamięć robocza jest oczywiście ograniczona i jej rozmiar jest znany. Wyczerpanie tej pamięci nie destabilizuje systemu (system ma rezerwę na inne rzeczy), ale nie można tego limitu przekroczyć.

Kilka pomysłów:
1a. Wykonywać wszystko jak leci jak najszybciej się da, bez limitów. Jak klient zlecił 10000 równoległych SELECT * FROM bez WHERE, to robimy, a co tam. Jak się skończy limit pamięci roboczej na zapytania, alokacja kończy się błędem i pechowe zapytanie zostaje zabite i zrolowane, a klient dostaje błąd "server overloaded". Klient (sterownik) wykrywa takie sytuacje i próbuje powtórnie, po jakimś czasie.

1b. (niepoprawne, ale może rokuje) Wykonywać wszystko jak leci, a jak się skończy pamięć to... wstrzymać wykonywanie zapytania, które zażądało za dużej ilości pamięci i kontynuować pozostałe (może zaraz coś się zwolni). Jednak tu się pojawia wcale niełatwy problem jak uniknąć permanentnego zakleszczenia systemu. Bo co jak będziemy mieć 100 zapytań "w trakcie", każde na 50% wykonania i skończy się pamięć, a każde będzie potrzebowało jeszcze trochę?

  1. Podzielić dostępną pamięć M na N równych slotów (M i N konfigurowalne). Każde zapytanie dostaje swój slot. Jak się zmieści, to sukces. Jak się nie zmieści, to klient dostaje błędem "query working memory exceeded". Na wejściu do systemu kolejka o ograniczonym rozmiarze dla zapytań oczekujących na wolny slot. Długość kolejki raportowana do klienta na bieżąco, pozwala spowolnić strumień zapytań. Jeśli klient ignoruje grzeczne informacje o długość kolejki i wysyła za szybko dajemy mu w łeb wyjątkiem "server overloaded".

  2. Jak w punkcie 2, ale dzielimy pamięć M na dwie pule M1 i M2 każda odpowiednio po N1 i N2 slotów. N1 >> N2. Jeśli zapytanie w trakcie wykonania okaże się za duże, to czeka na większy slot z drugiej puli. Tych dużych slotów jest odpowiednio mniej więc musi być przed nimi kolejna kolejka. Zaleta: jak klient wysyła małe zapytania, to ma niezłą równoległość, ale okazyjnie może wysłać większe zadanie i nie dostanie od razu "query working memory exceeded". Małe zapytania będą wykonywane szybciej niż duże. Można uogólnić do dowolnej liczby pul.

  3. Zrobić twardy limit, że nie wrzucisz do jednej komórki 100 MB XMLa. Nie, bo nie, i już. Twarde limity na wszystko. Zaprojektować system tak, aby każde zapytanie wymagało nie więcej niż X bajtów pamięci roboczej, niezależnie od ilości wysyłanych / zwracanych wierszy. Może działać nieźle w połączeniu z pomysłem 2.

Różne systemy baz danych mają różne podejścia, ale jakbyście projektowali zupełnie nowy system, to jaką strategię byście wybrali?
A może jeszcze jakieś inne pomysły?

BTW: To nie musi być baza danych. Problem jest bardzo ogólny i pasuje do każdego równoległego systemu, który dostaje jakieś zadania na wejściu, ale nie wie dokładnie jakich są rozmiarów dopiero dopóki ich nie zrobi.

0

Zamiast:

Klient może przysłać dowolną ilość zapytań
Powinno być
Klient może chcieć przysłać dowolną ilość zapytań

IMHO pojedynczy klient nie może:

  • zająć całego serwera = przyblokować innych klientów = zarzucić serwer tylko swoimi zapytaniami
  • zawłaszczyć całej pamięci

Na DB2 taki "awanturujący się" klient z tego co widziałem po kilkunastu sekundach "awansuje" do "powolnej kolejki" - jego zapytanie / zapytania zaczynają się wykonywać na niższym biegu - wolniej. Dzięki temu nie dostaje HTTP 500 ale nie zakłóca też pracy innym.

https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html

0

Nie da się w niezawodny sposób zawczasu przewidzieć ile pamięci będzie niezbędne na realizację zapytania do końca

Na pewno da się to przybliżyć tym lepiej, im bardziej określone typy danych mamy zdefiniowane (wraz z długością). Takie szacowanie można przeprowadzić na poziomie budowania planu zapytania. Jeżeli zakładamy jednak, że wspieramy pola o zmiennej długości to ich rozmiaru nie poznamy aż do czasu "runtime" zapytania. Pytanie - po co mielibyśmy ładować takie pole w całości do pamięci? Jeśli potraktujemy takie pola jako streamy danych, to możemy je wysyłać do klienta za pomocą określonego bufora, lub nawet prosto z dysku do socketa (http://man7.org/linux/man-pages/man2/sendfile.2.html).

0

@damianem Wszystko się da, tylko jest kwestia stopnia skomplikowania. Np. streamowanie pojedynczych komórek znacznie komplikuje architekturę bazy i jeśli nikt tego nie przewidział wcześniej, to dodanie takiego czegoś post-factum może spowodować przepisanie 1/3 bazy.

0

Jeśli dobrze zrozumiałem, to problem dość ogólny, w którym mamy ograniczoną ilość pamięci i piszemy własnego managera pamięci (w ogólności zasobów). Taki "mały" OS.
Nie wiem czy Ci to pomoże, ale może będzie to jakiś "food for brain".

Używając konceptów, które znane są na poziomie OS czy innych silników pewnie skończysz z rozwiązaniami, które znane są z OS i innych silników bazodanowych :)

Z typowym mechanizmów, które mamy do dyspozycji do realizacji takiego zadania:
a) pożyczyć pamięci od kogoś kto ją ma, ale nie używa ("pulaX się nudzi")
b) wyrzucić na chwilę z "pamięci procesu" mało rokujące bloki i kontynuować przetwarzanie (własny plik wymiany)
c) skolejkować zadanie
d) wywłaszczyć intensywne zadanie ("kill process", "context switch")

Tu mamy użycie pamięci związane z danymi i ich przetwarzaniem, dlatego skupiłbym się na zbieraniu informacji o:
a) rozkładzie danych
b) workload
c) wykorzystaniu pamięci

OS nie wie ile pamięci "za chwilę będzie potrzebował proces X", a baza mogłaby mieć przybliżoną informację.

  • Histogram danych w "tabeli/kolekcji" X wygląda tak i tak.
  • Proces o etykietce 'batch' w godzinach 8-12 użył 2gb RAM, średnio zużywa 0,5gb RAM.
  • PulaX -(można mieć wiele pul pamięci ze slotami: Pula_4kb, Pula_8kb, Pula_16kb, itd. ) itd.

Obecne silniki potrafią zebrać dane n/t rozkładu danych i wykorzystać w planie budowy zapytania, ale mam wątpliwości czy potrafią wykorzystać dane o rozkładzie do zarządzania zasobami.

W danych historycznych czuję potencjał ;-) W teorii dokładniejsze informacje pozwalają na lepsze planowanie. Przy projektowaniu nowej bazy skupiłbym się na:

  • zbieraniu danych statystycznych (może jakaś ciekawa samo organizująca się struktura danych - histogramy w czasie rzeczywistym)
  • zarządzaniu kolejkami, pulami w oparciu o zebrane dane
0

Według mnie (mocno zależne od use case):

  1. Pozwolić klientowi zdecydować, czy woli opcję "leje ile wlezie", ryzykując, że się przeleje, ostrzegając, że wpłynie to na availability systemu jeśli zasoby się skończą
  2. Opcje 2,3,4 do ustawienia jako alternatywa dla 1

Jednemu będzie zależeć by wpychać ile wlezie, bo ma mało zapytań, drugi będzie wolał trochę zwolnić kosztem tego, że system będzie cały czas dostępny. To wszystko zależy od potrzeb. Jak mowa o równoległym systemie to można to konfigurować na końcówce lub np. na jakimś load balancerze, który już sam może w jakiś sposób decydować o rozproszeniu ruchu zanim spadnie to właściwy node.

1 użytkowników online, w tym zalogowanych: 0, gości: 1