Witam serdecznie. Będę miał do zaprojektowania aplikację internetową typu multitenancy z tą różnicą, że każda organizacja będzie miała swoją własną instancje bazy danych na dane oraz będzie jedna wspólna baza danych hosta. Wszystkie bazy danych organizacji będą w ramach tej samej podsieci. Wszystkie bazy organizacji będą posiadały ten sam schemat. Potrzebuję też móc sprawować kontrolę nad "subskrybentami". Z mojej strony problemem będzie chociażby paginacja z EfCore. Nie widzę teraz innej możliwości jak pobrać wszystkie dane ze wszystkich baz i dopiero wykonywać Skip()
i Take()
na serwerze. Zdaję sobie sprawę, że przy ilościach rekordów (a będzie ich sporo) to nie przejdzie. Moglibyście mnie naprowadzić?
każda organizacja będzie miała swoją własną instancje
Instancję fizyczną czy logiczną? Powiedz też jaka baza
Potrzebuję też móc sprawować kontrolę nad "subskrybentami".
Read czy też write?
Pisałem sobie coś takiego - miałem jedna scheme i w zależności od tenanta na konfiguracji DBContext podawałem ConnectionStringa.
W hedaderze leciało id tenanta i tak wybierało bazę.
Mam gdzieś na swoim GH chyba.
A co do agregowania to ciekawy temat jest z baz wielu i nie wiem jak to ugryźć. Postgres pozwala chyba na pisanie zapytań do wielu baz ze swojej instancji.
SQL Server ma linked servers. Na podlinkowanych serwerach możesz wykonywać zapytania, włącznie z insert i update, tylko z tego co widzę składnia jest nieco inna. Nie wiem, czy EF sobie z tym poradzi - jeśli nie, to możesz spróbować użyć view lub procedur składowanych (oczywiście jako ostateczność).
Nigdy nie materializuj całych tabel po stronie klienta, to bardzo krótka droga do poważnych problemów z wydajnością. Takie problemy czasami są trywialne, a czasami mogą wymagać przeprojektowania całej infrastruktury, zatem mogą być ekstremalnie kosztowne. Pamiętaj, że początkowe złe decyzje mogą doprowadzić właśnie do tego drugiego, zatem warto zainwestować każdą rozsądną ilość czasu na solidny research.
Mój plan jest mniej więcej taki:
Baza Hosta:
- Subskrybenci
- Użytkownicy
- Słowniki
- Inne wspólne
Baza Subskrybenta: - np. Budynki
- np. Pojazdy
Największym wyzwaniem dla mnie będzie przede wszystkim relacja między np. HostDb->Users->Id
a TenantDb->Buildings->CreatedUserId
. Póki co siedze w google i ludzie piszą, że to niewykonalne ;D
Kolejnym wyzwaniem będzie właśnie pobranie wszystkich np. budynków od wszystkich subskrybentów i paginacja z sortowaniem. Aby posortować budynki najpierw muszę pobrać je wszystkie od wszystkich subskrybentów.
Czy nie możesz zaimportować wszystkich baz TenantDB
co jakiś czas do jednej bazy i na niej wykonywać te query?
A dlaczego potrzebujesz mieć możliwość pobierania danych ze wszystkich baz i wyświetlenia tego w twoim "superadminie"? Czy jest to jakiś konkretny wymóg biznesowy?
Mam parę apek na multitenance i nie potrzebowałem agregować danych ze wszystkich tenantów, aby wyświetlić je na jednej tabelce. Po to mamy to wszystko odseparowane od siebie, aby w kontekście użytkownika były to osobne byty.
Michalk001 napisał(a):
A dlaczego potrzebujesz mieć możliwość pobierania danych ze wszystkich baz i wyświetlenia tego w twoim "superadminie"? Czy jest to jakiś konkretny wymóg biznesowy?
Mam parę apek na multitenance i nie potrzebowałem agregować danych ze wszystkich tenantów, aby wyświetlić je na jednej tabelce.
Na przykład mam listę produktów, które tenant chce aby były widzialne w wyszukiwarce globalnej. wtedy taki landing page na oficjalnej stronie musi zebrać wszystkie produkty od wszystkich subskrybentów, posortować je i wyświetlić n pierwszych rekordów. Normalnie trzymałbym to w jednej bazie, ale chodzi o samą odpowiedzialność za dane
Michalk001 napisał(a):
A dlaczego potrzebujesz mieć możliwość pobierania danych ze wszystkich baz i wyświetlenia tego w twoim "superadminie"?
W sumie to racja. Problem wydaje się być w innym miejscu - architektura bazy danych jest nieprawidłowa. Jeśli chodzi o odpowiedzialność za dane, to powinieneś mieć logi oraz - w bazie danych - autora danego rekordu + datę i autora ostatniej zmiany + datę (bo jak rozumiem dostęp do danych jest tylko przez API, prawda? Prawda???). Umożliwienie edycji/dodawania tylko konkretnego rodzaju rekordów w zależności od oddziału/klienta firmy jest dość proste do implementacji, natomiast oszczędzi wielu późniejszych problemów.
"Rozpraszanie" bazy danych oznacza:
- Duplikację struktury bazy danych. Jeśli będziesz musiał coś zmienić we współdzielonej strukturze, to będziesz to musiał rozpropagować po wszystkich bazach.
- Niższe SLA. Jeśli nawet jedna baza danych umarła, to staje cały system.
- Najwolniejsza baza danych może stać się wąskim gardłem całego systemu. Namierzenie, która to baza, może być nietrywialne i czasochłonne.
- Problemy, które już masz - ogarnięcie tego na poziomie EF może być niewykonalne lub może wymagać stworzenia wielu view i/lub sp, co spowoduje rozproszenie logiki biznesowej, a to wróży tylko kłopoty.
- Wyższe koszty utrzymania.
Z powyższych powodów tylko bardzo specyficzne wymagania biznesowe mogą usprawiedliwić użycie takiej "rozproszonej" bazy. Wierz mi, nie chcesz w to wchodzić.
gswidwa1 napisał(a):
Na przykład mam listę produktów, które tenant chce aby były widzialne w wyszukiwarce globalnej
Czyli rozumiem, że to jest jednak jedna organizacja, a nie niezależni klienci jeżeli ma być globalna wyszukiwarka. Od biedy można zrobić to tworząc bazę, gdzie będzie kopia tych produktów, a chcą zobaczyć szczegóły zaciągasz już z konkretnej bazy. W Projektach mam dwa podejścia. Pierwszy to schema per klient(tenant), drugie to wspólna baza i logiczne rozdzielenie na tenanty(kolumna tenantId). W podejściu drugim można bez problemu czytać ze wszystkich tenantów, w końcu dane wszystkich klientów siedzą w jednej tabeli.
gswidwa1 napisał(a):
ale chodzi o samą odpowiedzialność za dane
Co masz na myśli dokładnie? Dodajesz do tego logi, kto co i kiedy zmodyfikował. U siebie trzymam to w tabeli auditLog.
ŁF napisał(a):
- Duplikację struktury bazy danych. Jeśli będziesz musiał coś zmienić we współdzielonej strukturze, to będziesz to musiał rozpropagować po wszystkich bazach.
Od biedy można napisać mechanizm migracji, w jednym projekcie mamy to i działa. W innych projektach działam jednak na bazie dokumentowej (postgresql i kolumna data jako json w tabeli), więc problem migracji mi odpada. Apka przy starcie robi synchronizację i jak wykryje, brakujące tabele na podstawie modeli na bazie to tworzy je tabele.
Normalnie trzymałbym to w jednej bazie, ale chodzi o samą odpowiedzialność za dane
Podejrzewam, że tutaj leży problem. Nie wiem czy robisz w mikroserwisy, czy w monolit, ale tak czy inaczej nie ma żadnego problemu, żeby mieć
a) dane tenantów w osobnych bazach danych z ograniczonym dostępem
b) dane np. do wyszukiwania w jeszcze innej, osobnej bazie danych, należącej do osobnego serwisu albo modułu.
Dane do bazy danych z punktu B wpycha sobie moduł odpowiedzialny za dane z B, np. zaczytując je z systemu kolejkowego, albo zaczytując co godzinę z wszystkich baz danych punktu A, albo na początek nawet przy każdym zapisie, whatever. Masz zachowaną izolację, ale też rozdzieloną odpowiedzialność za dane. Aczkolwiek to też rodzi problemy w stylu "jak usunąć jednego klienta" - drop database nie wystarczy.
Na przykład mam listę produktów, które tenant chce aby były widzialne w wyszukiwarce globalnej. wtedy taki landing page na oficjalnej stronie musi zebrać wszystkie produkty od wszystkich subskrybentów, posortować je i wyświetlić n pierwszych rekordów.
Nie wiem jaka skala tych wyszukań będzie, ale zwykle robi się usługę typu Wyszukiwarka opartą o jakiś elastic search czy coś podobnego i GUI wyszukiwarki uderza do tej bazy, która zawiera kopię danych z baz tenantów. Oczywiście będziesz mierzył się z duplikacją danych, synchronizacją, świeżością i innymi problemami wynikającymi z trzymania tych samych danych w kilku miejscach, ale tak to już jest. Nie da się zjeść ciastka i mieć ciastka. Dlatego czasem w dużych serwisach e-commerce, sprzedażowych czy magazynowych, może się zdarzyć, że ktoś kupi/zamówi towar, którego już nie mamy na stanie, ale figuruje w bazie jako dostępny.
W przypadku dużego ruchu, robienie zapytań na rozproszonych bazach to strzał w kolano. Ale jeżeli masz 5 wyszukań na minutę to może się nawet sprawdzić.
Michalk001 napisał(a):
gswidwa1 napisał(a):
Na przykład mam listę produktów, które tenant chce aby były widzialne w wyszukiwarce globalnej
Czyli rozumiem, że to jest jednak jedna organizacja, a nie niezależni klienci jeżeli ma być globalna wyszukiwarka. Od biedy można zrobić to tworząc bazę, gdzie będzie kopia tych produktów, a chcą zobaczyć szczegóły zaciągasz już z konkretnej bazy. W Projektach mam dwa podejścia. Pierwszy to schema per klient(tenant), drugie to wspólna baza i logiczne rozdzielenie na tenanty(kolumna tenantId). W podejściu drugim można bez problemu czytać ze wszystkich tenantów, w końcu dane wszystkich klientów siedzą w jednej tabeli.
gswidwa1 napisał(a):
ale chodzi o samą odpowiedzialność za dane
Co masz na myśli dokładnie? Dodajesz do tego logi, kto co i kiedy zmodyfikował. U siebie trzymam to w tabeli auditLog.
ŁF napisał(a):
- Duplikację struktury bazy danych. Jeśli będziesz musiał coś zmienić we współdzielonej strukturze, to będziesz to musiał rozpropagować po wszystkich bazach.
Od biedy można napisać mechanizm migracji, w jednym projekcie mamy to i działa. W innych projektach działam jednak na bazie dokumentowej (postgresql i kolumna data jako json w tabeli), więc problem migracji mi odpada. Apka przy starcie robi synchronizację i jak wykryje, brakujące tabele na podstawie modeli na bazie to tworzy je tabele.
Tak. Jest to jedna organizacja w sieci WAN. Już mam jedną apkę postawioną w jednym "Oddziale" z której korzysta wiele "Oddziałów" właśnie na zasadzie TenantId
i jednej bazy danych i działa to znakomicie. Jednak Oddział A jest oburzony, że wszystkie "oddziały" korzystają z miejsca na serwerze oddziału A i jest potrzeba aby rozdzielić dane per oddział ;/ Myślałem też o webhookach, oddzielnym API do synchronizacji danych i powielaniu danych subskrybentów, użytkowników itp. na każdy Oddział, ale to nadal nie rozwiązuje problemu takiej prostej paginacji danych ze wszystkich Oddziałów naraz. Ktoś wcześniej wspomniał o widokach w SQL, ale na razie staram się pozostać w EF CORE, ponieważ SQL jest dla mnie czarną magią
markone_dev napisał(a):
Na przykład mam listę produktów, które tenant chce aby były widzialne w wyszukiwarce globalnej. wtedy taki landing page na oficjalnej stronie musi zebrać wszystkie produkty od wszystkich subskrybentów, posortować je i wyświetlić n pierwszych rekordów.
Nie wiem jaka skala tych wyszukań będzie, ale zwykle robi się usługę typu Wyszukiwarka opartą o jakiś elastic search czy coś podobnego i GUI wyszukiwarki uderza do tej bazy, która zawiera kopię danych z baz tenantów. Oczywiście będziesz mierzył się z duplikacją danych, synchronizacją, świeżością i innymi problemami wynikającymi z trzymania tych samych danych w kilku miejscach, ale tak to już jest. Nie da się zjeść ciastka i mieć ciastka. Dlatego czasem w dużych serwisach e-commerce, sprzedażowych czy magazynowych, może się zdarzyć, że ktoś kupi/zamówi towar, którego już nie mamy na stanie, ale figuruje w bazie jako dostępny.
W przypadku dużego ruchu, robienie zapytań na rozproszonych bazach to strzał w kolano. Ale jeżeli masz 5 wyszukań na minutę to może się nawet sprawdzić.
Czyli tak czy siak aby wyszukiwać globalnie muszę mieć "kopię bazy", która gdzieś będzie sobie leżeć na serwerze. Może jak tak to przedstawię to odwiodę ich od pomysłu wielu baz danych
Czemu nie zrobisz schema per user na jednej instancji bazy? Wtedy możesz sobie pisać zapytania zbierające dane że wszystkich i masz zachowaną izolacje danych klientów.
Qbelek napisał(a):
Czemu nie zrobisz schema per user na jednej instancji bazy? Wtedy możesz sobie pisać zapytania zbierające dane że wszystkich i masz zachowaną izolacje danych klientów.
Niektórzy klienci wymagają fizycznej izolacji danych.
gswidwa1 napisał(a):
Czyli tak czy siak aby wyszukiwać globalnie muszę mieć "kopię bazy", która gdzieś będzie sobie leżeć na serwerze. Może jak tak to przedstawię to odwiodę ich od pomysłu wielu baz danych
Nie do końca. Owszem możesz mieć osobną bazę i dedykowany serwis do wyszukiwania, ale możesz też to robić na rozproszonych bazach danych. Pamiętaj tylko, że w przypadku dużego ruchu takie zapytania mogą okazać się wąskim gardłem twojego systemu. To już musisz Ty zadecydować na podstawie przewidywanego ruchu i ilości danych.
Czasem searche na jednej bazie danych są problematyczne, bo ktoś skopał schemat i trzeba robić pokraczne zapytania by to ogarnąć, albo brakuje indeksów, albo serwer nie wyrabia. Teraz to pomnóż razy 5 albo dziesięć, w zależności ilu będziesz miał tenantów.
markone_dev napisał(a):
Qbelek napisał(a):
Czemu nie zrobisz schema per user na jednej instancji bazy? Wtedy możesz sobie pisać zapytania zbierające dane że wszystkich i masz zachowaną izolacje danych klientów.
Niektórzy klienci wymagają fizycznej izolacji danych.
gswidwa1 napisał(a):
Czyli tak czy siak aby wyszukiwać globalnie muszę mieć "kopię bazy", która gdzieś będzie sobie leżeć na serwerze. Może jak tak to przedstawię to odwiodę ich od pomysłu wielu baz danych
Nie do końca. Owszem możesz mieć osobną bazę i dedykowany serwis do wyszukiwania, ale możesz też to robić na rozproszonych bazach danych. Pamiętaj tylko, że w przypadku dużego ruchu takie zapytania mogą okazać się wąskim gardłem twojego systemu. To już musisz Ty zadecydować na podstawie przewidywanego ruchu i ilości danych.
Czasem searche na jednej bazie danych są problematyczne, bo ktoś skopał schemat i trzeba robić pokraczne zapytania by to ogarnąć, albo brakuje indeksów, albo serwer nie wyrabia. Teraz to pomnóż razy 5 albo dziesięć, w zależności ilu będziesz miał tenantów.
Co mi sie udało wykminić to faktycznie 3 zapytania do trzech tenantów i otrzymanie x wyników razy 3, potem na nowo przefiltrowanie, posortowanie, paginacja i mamy 10 rekordów z 3 baz. I to nawet sprawnie działa (co prawda 3 razy wolniej niż mogłoby i też ostatnia operacja zużywa zasobu serwera ale tylko na 30 rekordach). Co sądzicie?:
var filteredDb1 = await _dbContext1.Filter(input).Page(0, 10);
var filteredDb2 = await _dbContext2.Filter(input).Page(0, 10);
var filteredDb3 = await _dbContext3.Filter(input).Page(0, 10);
var filtered = await filterAndPage(0, 10, filteredDb1, filteredDb2, filteredDb3)
Ja sądzę, że przy małej skali zapytań i danych takie podejście się sprawdzi.
Co będzie za rok czy dwa to tego nie wiem, ale zakładam, że Ty lub Twój biznes to wiecie bo macie jakąś strategię biznesową i możecie podjąć decyzję, czy zaczynacie od najprostszego rozwiązania, np. takiego jakie podałeś, a jak się okaże w pewnym momencie że jest niewydajne to go odpowiednio przerobicie. Albo od razu idziecie z osobną usługą do wyszukiwania, żeby później już nie musieć tego rozgrzebywać.
gswidwa1 napisał(a):
var filteredDb1 = await _dbContext1.Filter(input).Page(0, 10); var filteredDb2 = await _dbContext2.Filter(input).Page(0, 10); var filteredDb3 = await _dbContext3.Filter(input).Page(0, 10); var filtered = await filterAndPage(0, 10, filteredDb1, filteredDb2, filteredDb3)
A teraz wyobraź sobie, co będzie na drugiej stronie. Albo ostatniej :D Będziesz musiał zmaterializować wszystkie poprzednie strony. Powodzenia.
Co się stanie, jeśli dojdzie czwarta baza?
Serio, nie tędy droga. Ktoś w firmie ubzdurał sobie, że skoro są trzy oddziały, to każdy ma mieć osobną infrastrukturę i każda wspólna aplikacja musi korzystać z tych trzech części. To bardzo komplikuje proste sprawy. W efekcie utrzymanie wszystkiego zacznie więcej kosztować, a sam koszt infrastruktury już poszedł w górę, skoro są trzy bazy, a może być jedna.
Jednak Oddział A jest oburzony, że wszystkie "oddziały" korzystają z miejsca na serwerze oddziału A i jest potrzeba aby rozdzielić dane per oddział
Skoro to dane wspólne, to powinieneś mieć na to osobny worek, jakiś global albo shared. Najwyraźniej infrastrukturę też macie źle zaprojektowaną, co teraz rodzi kolejne problemy, które później zrodzą kolejne problemy. Wspólne rzeczy do wspólnego worka, company branch specific - do company branch bags. Oddzielny tenant na każdy oddział też mi śmierdzi - u mnie w korpo jest grubo ponad 5k ludzi, kilka oddziałów rozrzuconych po całym świecie, kilka przejętych mniejszych firm razem z ich infrastrukturami, wszyscy są migrowani do jednego tenanta, jednego SSO.
Najczęściej takie głupie decyzje są podejmowane przez ludzi, którzy nie mają pełnej świadomości z jakimi konsekwencjami ich decyzje się wiążą. Jeśli są to w miarę rozsądni ludzie, to jeszcze nie ma problemu, trzeba taką decyzyjną osobę namierzyć i wszystko dokładnie wytłumaczyć (co oznacza, że najpierw implikacje trzeba zrozumieć samemu + zrobić research możliwych rozwiązań i ich plusów i minusów). Skoro jednak decyzja jest głupia, to jest szansa, że podjął ją głupi człowiek, któremu nie wytłumaczysz, że zrobił źle, bo się obrazi (vide błędy poznawcze). Wtedy problem trzeba eskalować, może przełożony tej osoby będzie rozsądniejszy. Jeśli wasze systemy informatyczne są istotną częścią działalności firmy, to może warto udać się na samą górę, do CTO, CEO albo zarządu, żeby zastanowili się nad inwestycją w sensowną architekturę infrastruktury - będzie taniej i na abonamencie, i na utrzymaniu, i na deweloperce.
Naprawdę, wierz mi, że nie chcesz iść w duplikowanie czegokolwiek (poza backupami, RAID albo klastrami ;-)). Mam wrażenie, że sam nie rozumiesz implikacji, patrząc na Twoje rozwiązanie problemu wyszukiwania na trzech bazach. Jeszcze raz, w skrócie: 1) koszty infrastruktury 2) niższe SLA 3) zduplikowana struktura = więcej roboty = wyższe koszty utrzymania 4) wąskie gardła 5) więcej błędów trudnych do naprawienia/obejścia = większe koszty, do tego słabszy UX, bo mniejsza wygoda pracy. Money, money, money.
gswidwa1 napisał(a):
(co prawda 3 razy wolniej niż mogłoby
Czemu nie wywołać tych query równolegle? Nie znam się na C#, ale to wygląda dobrze: https://stackoverflow.com/a/25010220/4638604
Co sądzicie?:
Paging będzie ciężki. Co zrobisz jak kolejność zwróconych itemóœ będzie taka dla konkretnego query: tenant1 x 1000 rows, tenant2 x 1 row, tenant3 x 1000rows
. W takim wypadku Page(1, 10)
na wszystkich bazach da błędne wyniki
Wszystko zależy ile będzie tych zapytań wykonywanych i jak będzie wyglądał rozwój tego ficzera w przyszłości. Przy większej skali trzymanie tych danych w jednym miejscu to jedyny sposóbn na utrzymanie zdrowia psychicznego