Używanie repozytorium git jako zaplecza bazy danych

119

Robię projekt, który zajmuje się ustrukturyzowaną bazą danych dokumentów. Mam drzewo kategorii (~ 1000 kategorii, do ~ 50 kategorii na każdym poziomie), każda kategoria zawiera kilka tysięcy (do, powiedzmy, ~ 10000) ustrukturyzowanych dokumentów. Każdy dokument to kilka kilobajtów danych w jakiejś uporządkowanej formie (wolałbym YAML, ale równie dobrze może to być JSON lub XML).

Użytkownicy tego systemu wykonują kilka rodzajów operacji:

  • pobranie tych dokumentów za pomocą dowodu osobistego
  • wyszukiwanie dokumentów według niektórych strukturalnych atrybutów w nich
  • edycja dokumentów (tj. dodawanie / usuwanie / zmiana nazwy / łączenie); każda operacja edycji powinna być zarejestrowana jako transakcja z pewnym komentarzem
  • przeglądanie historii zarejestrowanych zmian dla konkretnego dokumentu (w tym przeglądanie kto, kiedy i dlaczego zmienił dokument, pobieranie wcześniejszej wersji - i prawdopodobnie powrót do tej na żądanie)

Oczywiście tradycyjnym rozwiązaniem byłoby użycie jakiejś bazy danych dokumentów (takiej jak CouchDB lub Mongo) dla tego problemu - jednak ta kontrola wersji (historia) skusiła mnie do szalonego pomysłu - dlaczego nie miałbym używać gitrepozytorium jako zaplecze bazy danych dla tej aplikacji?

Na pierwszy rzut oka można to rozwiązać w następujący sposób:

  • kategoria = katalog, dokument = plik
  • pobieranie dokumentu przez ID => zmiana katalogów + odczyt pliku w kopii roboczej
  • edycja dokumentów z komentarzami do edycji => dokonywanie zatwierdzeń przez różnych użytkowników + przechowywanie wiadomości o zatwierdzeniach
  • historia => normalny dziennik git i pobieranie starszych transakcji
  • search => to trochę trudniejsza część, myślę, że wymagałoby to okresowego eksportu kategorii do relacyjnej bazy danych z indeksowaniem kolumn, które pozwolimy przeszukiwać według

Czy są jakieś inne typowe pułapki w tym rozwiązaniu? Czy ktoś próbował już zaimplementować taki backend (np. Dla jakichkolwiek popularnych frameworków - RoR, node.js, Django, CakePHP)? Czy to rozwiązanie ma jakikolwiek możliwy wpływ na wydajność lub niezawodność - tj. Czy zostało udowodnione, że git będzie znacznie wolniejszy niż tradycyjne rozwiązania bazodanowe, czy też wystąpią jakiekolwiek pułapki związane ze skalowalnością / niezawodnością? Zakładam, że klaster takich serwerów, które wypychają / ściągają swoje repozytorium, powinien być dość solidny i niezawodny.

Zasadniczo powiedz mi, czy to rozwiązanie zadziała i dlaczego zadziała lub nie?

GreyCat
źródło

Odpowiedzi:

58

Odpowiedź na moje pytanie nie jest najlepszą rzeczą do zrobienia, ale ponieważ ostatecznie porzuciłem ten pomysł, chciałbym podzielić się uzasadnieniem, które zadziałało w moim przypadku. Chciałbym podkreślić, że to uzasadnienie może nie mieć zastosowania we wszystkich przypadkach, więc decyzja należy do architekta.

Ogólnie rzecz biorąc, pierwszą główną kwestią, której pomijam, jest to, że mam do czynienia z systemem wielu użytkowników, który działa równolegle, jednocześnie, używając mojego serwera z cienkim klientem (tj. Tylko przeglądarką internetową). W ten sposób muszę zachować stan dla nich wszystkich. Istnieje kilka podejść do tego, ale wszystkie z nich są albo zbyt trudne pod względem zasobów, albo zbyt skomplikowane, aby je zaimplementować (a tym samym w pewnym sensie zabijają pierwotny cel, jakim jest odciążenie wszystkich trudnych rzeczy implementacyjnych do gita):

  • Podejście „tępe”: 1 użytkownik = 1 stan = 1 pełna kopia robocza repozytorium, które serwer utrzymuje dla użytkownika. Nawet jeśli mówimy o dość małej bazie danych dokumentów (na przykład 100s MiBs) z ~ 100K użytkowników, utrzymanie pełnego klonu repozytorium dla wszystkich sprawia, że ​​użycie dysku przebiega przez dach (tj. 100K użytkowników razy 100MiB ~ 10 TiB) . Co gorsza, klonowanie repozytorium 100 MB za każdym razem zajmuje kilka sekund, nawet jeśli odbywa się to w dość skuteczny sposób (tj. Nie jest używane przez git i rozpakowywanie-przepakowywanie), co jest nie do przyjęcia, IMO. A co gorsza - każda edycja, którą zastosujemy do głównego drzewa, powinna zostać ściągnięta do repozytorium każdego użytkownika, którym jest (1) zasób zasobów, (2) może prowadzić do nierozwiązanych konfliktów edycji w ogólnym przypadku.

    Zasadniczo może być tak zły, jak O (liczba edycji × dane × liczba użytkowników) pod względem wykorzystania dysku, a takie użycie dysku automatycznie oznacza dość wysokie użycie procesora.

  • Podejście „Tylko aktywni użytkownicy”: zachowaj kopię roboczą tylko dla aktywnych użytkowników. W ten sposób generalnie nie przechowujesz pełnego repozytorium klonu na użytkownika, ale:

    • Gdy użytkownik loguje się, klonujesz repozytorium. Zajmuje to kilka sekund i ~ 100 MB miejsca na dysku na aktywnego użytkownika.
    • Ponieważ użytkownik kontynuuje pracę w serwisie, pracuje z daną kopią roboczą.
    • Gdy użytkownik się wylogowuje, jego klon repozytorium jest kopiowany z powrotem do repozytorium głównego jako gałąź, w ten sposób przechowując tylko jego „niezastosowane zmiany”, jeśli takie istnieją, co jest dość wydajne pod względem przestrzeni.

    Zatem wykorzystanie dysku w tym przypadku osiąga wartość O (liczba edycji × dane × liczba aktywnych użytkowników), które jest zwykle ~ 100..1000 razy mniejsze niż liczba wszystkich użytkowników, ale powoduje to, że logowanie / wylogowanie jest bardziej skomplikowane i wolniejsze , ponieważ obejmuje klonowanie gałęzi na użytkownika przy każdym logowaniu i wycofywanie tych zmian przy wylogowaniu lub wygaśnięciu sesji (co powinno być wykonane transakcyjnie => dodaje kolejną warstwę złożoności). W liczbach bezwzględnych zmniejsza to 10 TiB wykorzystania dysku do 10-100 GiBs w moim przypadku, co może być akceptowalne, ale znowu mówimy teraz o dość małej bazie danych 100 MiB.

  • Podejście „rzadkie kasy”: robienie „rzadkich zakupów” zamiast pełnoprawnego klonowania repozytorium na jednego aktywnego użytkownika nie pomaga zbytnio. Może to zaoszczędzić ~ 10x wykorzystania miejsca na dysku, ale kosztem znacznie większego obciążenia procesora / dysku w przypadku operacji związanych z historią, co w pewnym sensie zabija cel.

  • Podejście „pula pracowników”: zamiast robić pełne klony za każdym razem dla osoby aktywnej, możemy przechowywać pulę klonów „pracujących”, gotowych do użycia. W ten sposób za każdym razem, gdy użytkownik się loguje, zajmuje jednego „pracownika”, wyciągając tam swoją gałąź z głównego repozytorium, a gdy się wylogowuje, uwalnia „pracownika”, który wykonuje sprytny twardy reset, aby ponownie stać się główny klon repozytorium, gotowy do użycia przez innego logującego się użytkownika. Nie pomaga zbytnio w korzystaniu z dysku (nadal jest dość wysoki - tylko pełny klon na aktywnego użytkownika), ale przynajmniej przyspiesza logowanie / wylogowywanie, ponieważ kosztuje jeszcze bardziej złożona.

To powiedziawszy, zwróć uwagę, że celowo obliczyłem liczby dość małej bazy danych i bazy użytkowników: 100 000 użytkowników, 1 000 aktywnych użytkowników, całkowita baza danych 100 MiB + historia zmian, 10 MiB kopii roboczej. Jeśli spojrzysz na bardziej znaczące projekty crowdsourcingowe, są tam znacznie wyższe liczby:

│              │ Users │ Active users │ DB+edits │ DB only │
├──────────────┼───────┼──────────────┼──────────┼─────────┤
│ MusicBrainz  │  1.2M │     1K/week  │   30 GiB │  20 GiB │
│ en.wikipedia │ 21.5M │   133K/month │    3 TiB │  44 GiB │
│ OSM          │  1.7M │    21K/month │  726 GiB │ 480 GiB │

Oczywiście przy tak dużej ilości danych / działań takie podejście byłoby całkowicie nie do przyjęcia.

Generalnie by zadziałało, gdyby można było używać przeglądarki internetowej jako „grubego” klienta, tj. Wydawać operacje git i przechowywać prawie cały checkout po stronie klienta, a nie po stronie serwera.

Są też inne punkty, które przegapiłem, ale nie są one takie złe w porównaniu z pierwszym:

  • Sam wzorzec posiadania „grubego” stanu edycji użytkownika jest kontrowersyjny pod względem normalnych ORMów, takich jak ActiveRecord, Hibernate, DataMapper, Tower itp.
  • O ile szukałem, nie ma żadnego darmowego kodu źródłowego do wykonywania tego podejścia do gita z popularnych frameworków.
  • Jest co najmniej jedna usługa, która w jakiś sposób udaje się to zrobić wydajnie - to oczywiście github - ale niestety ich baza kodu jest zamkniętym źródłem i mocno podejrzewam, że nie używają normalnych serwerów git / technik repozytorium w środku, tj. W zasadzie zaimplementowali alternatywny git „big data”.

Tak, dolna linia : to jest możliwe, ale dla większości obecnych usecases nie będzie nigdzie w pobliżu rozwiązania optymalnego. Prawdopodobnie lepszą alternatywą byłoby wprowadzenie własnej implementacji historii edycji dokumentów do SQL lub próba użycia jakiejkolwiek istniejącej bazy danych dokumentów.

GreyCat
źródło
16
Pewnie trochę spóźniłem się na imprezę, ale miałem podobne wymagania i właściwie poszedłem na git-route. Po pewnym przejściu do wewnętrznych elementów gita znalazłem sposób, aby to zadziałało. Chodzi o to, aby pracować z gołym repozytorium. Jest kilka wad, ale wydaje mi się, że jest to wykonalne. Napisałem wszystko w poście, który możesz chcieć sprawdzić (jeśli cokolwiek, ze względu na zainteresowanie): kenneth-truyers.net/2016/10/13/git-nosql-database
Kenneth
Innym powodem, dla którego tego nie robię, są możliwości zapytań. Przechowywanie dokumentów często indeksuje dokumenty, co ułatwia ich wyszukiwanie. Nie będzie to proste w przypadku git.
FrankyHollywood
12

Rzeczywiście ciekawe podejście. Powiedziałbym, że jeśli potrzebujesz przechowywać dane, korzystaj z bazy danych, a nie z repozytorium kodu źródłowego, które jest przeznaczone do bardzo konkretnego zadania. Jeśli mógłbyś używać Gita od razu po wyjęciu z pudełka, to jest w porządku, ale prawdopodobnie musisz zbudować na nim warstwę repozytorium dokumentów. Możesz więc zbudować go również na tradycyjnej bazie danych, prawda? A jeśli interesuje Cię wbudowana kontrola wersji, dlaczego nie skorzystać po prostu z jednego z narzędzi do repozytorium dokumentów typu open source ? Jest w czym wybierać.

Cóż, jeśli mimo wszystko zdecydujesz się na backend Git, to w zasadzie działałoby to dla twoich wymagań, gdybyś zaimplementował go zgodnie z opisem. Ale:

1) Wspomniałeś o „klastrze serwerów, które wzajemnie się przepychają / ciągną” - myślałem o tym przez chwilę i nadal nie jestem pewien. Nie możesz push / pull kilku repozytoriów jako operacji atomowej. Zastanawiam się, czy podczas pracy równoległej może zaistnieć jakiś bałagan przy scalaniu.

2) Może tego nie potrzebujesz, ale oczywistą funkcją repozytorium dokumentów, którego nie wymieniłeś, jest kontrola dostępu. Możesz prawdopodobnie ograniczyć dostęp do niektórych ścieżek (= kategorii) za pośrednictwem podmodułów, ale prawdopodobnie nie będziesz w stanie łatwo przyznać dostępu na poziomie dokumentu.

Kombajn zbożowy
źródło
11

moje 2 pensy warte. Trochę tęskniłem, ale ...... miałem podobne wymaganie w jednym z moich projektów inkubacyjnych. Podobnie jak twoje, moje kluczowe wymagania to baza danych dokumentów (w moim przypadku xml) z wersjonowaniem dokumentów. Było to dla systemu wielu użytkowników z wieloma przypadkami użycia współpracy. Wolałem korzystać z dostępnych rozwiązań open source, które obsługują większość kluczowych wymagań.

Aby przejść do sedna sprawy, nie mogłem znaleźć żadnego produktu, który zapewniałby oba, w sposób wystarczająco skalowalny (liczba użytkowników, wolumeny użytkowania, zasoby pamięci masowej i obliczeniowej). Byłem nastawiony na git dla wszystkich obiecujących możliwości, (prawdopodobne) rozwiązania, które można by z tego wymyślić. W miarę jak bawiłem się bardziej opcją git, przejście z perspektywy pojedynczego użytkownika do perspektywy wielu (miliardów) użytkowników stało się oczywistym wyzwaniem. Niestety, nie udało mi się przeprowadzić szczegółowej analizy wydajności, tak jak ty. (.. leniwy / zakończ wcześnie .... dla wersji 2, mantra) Moc dla ciebie !. W każdym razie, od tego czasu mój tendencyjny pomysł przekształcił się w następną (wciąż stronniczą) alternatywę: połączenie narzędzi, które są najlepsze w swoich oddzielnych sferach, bazach danych i kontroli wersji.

Podczas gdy wciąż trwają prace (... i trochę zaniedbane), wersja przekształcona jest po prostu taka.

  • na frontendu: (dla użytkownika) użyj bazy danych do przechowywania 1-go poziomu (połączenie z aplikacjami użytkownika)
  • na zapleczu użyj systemu kontroli wersji (VCS) (takiego jak git), aby przeprowadzić wersjonowanie obiektów danych w bazie danych

Zasadniczo sprowadzałoby się to do dodania wtyczki kontroli wersji do bazy danych, z pewnym klejem integracyjnym, który być może będziesz musiał opracować, ale może być znacznie łatwiejszy.

Jak to (powinno) działać, polega na tym, że główna wymiana danych w interfejsie wielu użytkowników odbywa się za pośrednictwem bazy danych. DBMS poradzi sobie ze wszystkimi zabawnymi i złożonymi problemami, takimi jak wielu użytkowników, współbieżność, operacje atomowe itp. Na zapleczu VCS wykonywałby kontrolę wersji na jednym zestawie obiektów danych (bez współbieżności lub problemów z wieloma użytkownikami). Dla każdej efektywnej transakcji w bazie danych kontrola wersji jest wykonywana tylko na rekordach danych, które zostałyby faktycznie zmienione.

Jeśli chodzi o klej do łączenia, będzie on miał postać prostej funkcji współdziałania między bazą danych a VCS. Jeśli chodzi o projekt, prostym podejściem byłby interfejs sterowany zdarzeniami, z aktualizacjami danych z bazy danych wyzwalającymi procedury kontroli wersji (wskazówka: zakładając Mysql, użycie wyzwalaczy i sys_exec () bla bla ...). Pod względem złożoności implementacji będzie to zakres od prostego i efektywnego (np. skrypty) do złożonego i wspaniałego (jakiś programowany interfejs złącza). Wszystko zależy od tego, jak szalony chcesz z tym iść i ile potu z kapitału jesteś w stanie wydać. Myślę, że proste skrypty powinny wystarczyć. Aby uzyskać dostęp do wyników końcowych, różnych wersji danych, prostą alternatywą jest wypełnienie klonu bazy danych (bardziej klonu struktury bazy danych) danymi, do których odwołuje się znacznik wersji / identyfikator / hash w VCS. znowu ten bit będzie prostym zadaniem zapytania / tłumaczenia / mapowania interfejsu.

Nadal istnieje kilka wyzwań i niewiadomych, z którymi trzeba się zmierzyć, ale przypuszczam, że wpływ i znaczenie większości z nich będą w dużej mierze zależeć od wymagań aplikacji i przypadków użycia. Niektóre mogą po prostu nie być problemem. Niektóre z problemów obejmują dopasowanie wydajności między 2 kluczowymi modułami, bazą danych i VCS, dla aplikacji z aktywnością aktualizacji danych o wysokiej częstotliwości, skalowanie zasobów (pamięci i mocy obliczeniowej) w czasie po stronie git jako dane i użytkownicy wzrost: stały, wykładniczy lub ostatecznie plateau

Z powyższego koktajlu, oto, co obecnie warzę

  • używanie Git dla VCS (początkowo uważany za stary dobry CVS ze względu na użycie tylko zestawów zmian lub delt między 2 wersjami)
  • przy użyciu mysql (ze względu na wysoce ustrukturyzowany charakter moich danych, xml ze ścisłymi schematami xml)
  • bawić się z MongoDB (aby wypróbować bazę danych NoSQl, która jest ściśle dopasowana do natywnej struktury bazy danych używanej w git)

Kilka zabawnych faktów - git faktycznie robi jasne rzeczy w celu optymalizacji pamięci, takie jak kompresja i przechowywanie tylko różnic między wersjami obiektów - TAK, git przechowuje tylko zestawy zmian lub delty między wersjami obiektów danych, gdzie ma to zastosowanie (wie kiedy i jak) . Odniesienie: pliki packfiles, głęboko w wnętrznościach Gita - Przegląd obiektowej pamięci masowej gita (system plików z adresowaniem treści) pokazuje uderzające podobieństwa (z punktu widzenia koncepcji) z bazami danych noSQL, takimi jak mongoDB. Ponownie, kosztem dużego wysiłku, może zapewnić bardziej interesujące możliwości integracji 2 i poprawienia wydajności

Jeśli dotarłeś tak daleko, pozwól mi, czy powyższe może mieć zastosowanie w twoim przypadku i zakładając, że tak będzie, jak to wyrównałoby się z niektórymi aspektami w twojej ostatniej kompleksowej analizie wydajności

młodego chisango
źródło
4

I wdrożone bibliotekę Ruby na górze libgit2, która sprawia, że to dość łatwe do wdrożenia i zbadać. Istnieją pewne oczywiste ograniczenia, ale jest to również dość wyzwalający system, ponieważ masz pełny zestaw narzędzi git.

Dokumentacja zawiera kilka pomysłów dotyczących wydajności, kompromisów itp.

ioquatix
źródło
2

Jak wspomniałeś, sprawa dla wielu użytkowników jest nieco trudniejsza w obsłudze. Jednym z możliwych rozwiązań byłoby użycie plików indeksu Git specyficznych dla użytkownika, w wyniku czego

  • brak potrzeby tworzenia oddzielnych kopii roboczych (użycie dysku jest ograniczone do zmienionych plików)
  • brak czasochłonnych prac przygotowawczych (na sesję użytkownika)

Sztuczka polega na połączeniu GIT_INDEX_FILEzmiennej środowiskowej Git z narzędziami do ręcznego tworzenia zatwierdzeń Git:

Poniżej przedstawiono zarys rozwiązania (faktyczne skróty SHA1 pominięte w poleceniach):

# Initialize the index
# N.B. Use the commit hash since refs might changed during the session.
$ GIT_INDEX_FILE=user_index_file git reset --hard <starting_commit_hash>

#
# Change data and save it to `changed_file`
#

# Save changed data to the Git object database. Returns a SHA1 hash to the blob.
$ cat changed_file | git hash-object -t blob -w --stdin
da39a3ee5e6b4b0d3255bfef95601890afd80709

# Add the changed file (using the object hash) to the user-specific index
# N.B. When adding new files, --add is required
$ GIT_INDEX_FILE=user_index_file git update-index --cacheinfo 100644 <changed_data_hash> path/to/the/changed_file

# Write the index to the object db. Returns a SHA1 hash to the tree object
$ GIT_INDEX_FILE=user_index_file git write-tree
8ea32f8432d9d4fa9f9b2b602ec7ee6c90aa2d53

# Create a commit from the tree. Returns a SHA1 hash to the commit object
# N.B. Parent commit should the same commit as in the first phase.
$ echo "User X updated their data" | git commit-tree <new_tree_hash> -p <starting_commit_hash>
3f8c225835e64314f5da40e6a568ff894886b952

# Create a ref to the new commit
git update-ref refs/heads/users/user_x_change_y <new_commit_hash>

W zależności od danych możesz użyć zadania crona do scalenia nowych referencji, masterale rozwiązanie konfliktu jest prawdopodobnie najtrudniejszą częścią tutaj.

Mile widziane są pomysły na ułatwienie.

7mp
źródło
Generalnie jest to podejście, które prowadzi donikąd, chyba że chcesz mieć pełną koncepcję transakcji i interfejsu użytkownika do ręcznego rozwiązywania konfliktów. Ogólnym pomysłem na konflikty jest nakłonienie użytkownika do rozwiązania problemu bezpośrednio po zatwierdzeniu (tj. „Przepraszam, ktoś inny edytował ten dokument, który edytowałeś -> zobacz jego zmiany i swoje zmiany i połącz je”). Kiedy pozwolisz dwóm użytkownikom na pomyślne zatwierdzenie, a następnie dowiesz się w asynchronicznej pracy cron, że sprawy poszły na marne, generalnie nie ma nikogo, kto mógłby rozwiązać te problemy.
GreyCat