Menu
About me Kontakt

Multitenancy - shared infrastructure for clients. How to implement it? (film, 12m)

In the latest episode of the channel JavaSenior, Artur Laskowski analyzes the concept of multitenancy architecture. This episode addresses the key aspects of the differences between applications built for single clients versus those intended for multiple users. Laskowski explains that with a singleton approach, each client receives dedicated data and code bases, which is costly and difficult to manage. In contrast, the multitenant approach allows resource sharing among multiple clients, reducing costs and improving infrastructure efficiency.

To illustrate the distinctions, Laskowski presents two extreme cases in multitenant architecture: separate databases for each tenant and a shared database with row-level isolation. In the case of separate databases, each tenant has its own data, ensuring a high level of security, though requiring complicated management. On the other hand, while shared databases may be more efficient, they raise concerns about data security. In this way, Laskowski highlights the complexity of making decisions regarding the appropriate solution.

Another important aspect of multitenant architecture is the management of tenant data. Laskowski suggests maintaining appropriate isolation, particularly by dividing databases among tenants. Strategies he discusses include shared database schemas to eliminate the noisy neighbor problem and using globally unique identifiers. Implementing such mechanisms enhances security and eases data management. Additionally, Laskowski addresses how applications can control access to tenant data and the importance of ensuring security and performance in the architecture.

The final part of the episode focuses on creating new tenants and their growth within a multitenant system. Laskowski stresses the necessity of identifying common versus isolated data. The process of adding a new tenant hinges on communication between microservices and managing infrastructure to ensure dedicated resources for each tenant. He notes the economic aspect and tenant grouping based on their needs, which optimizes expenditures and system efficiency.

Finally, the video garnered 3394 views and 191 likes at the time of writing this article. The themes discussed by Laskowski are relevant and significant for both beginner and advanced programmers facing challenges related to architecture and data management in multitenant systems.

Toggle timeline summary

  • 00:00 Introduction of speaker and topic: multitenancy architecture.
  • 00:08 Explanation of multitenancy not focusing on single-client applications.
  • 00:11 Difference between single-client and product-based applications.
  • 00:16 Multiple clients using the same application discussed.
  • 00:42 Enterprise clients needing customizations highlight the importance of dedicated infrastructure.
  • 00:57 Discussion on selling the product in a SaaS model.
  • 01:01 Challenges for small clients in a dedicated infrastructure.
  • 01:08 Drawbacks of providing separate infrastructures for every small client.
  • 01:38 Introduction of multitenancy as a solution to manage infrastructure.
  • 01:46 Collaboration among tenants via shared resources.
  • 01:54 Analogies used to explain how different tenants share infrastructure.
  • 02:27 Strategies for data isolation in a multitenant architecture discussed.
  • 02:38 High isolation through separate databases for each tenant.
  • 03:03 Common database for all tenants, tracking tenants at the row level.
  • 03:22 Consideration of tenant identification using user roles.
  • 04:18 Scalability concerns when dealing with large record volumes per tenant.
  • 06:14 Preference for one database but different schemas for better management.
  • 07:16 Potential 'noisy neighbor' issues and strategies to mitigate them.
  • 08:11 Description of how tenant isolation can work with message brokers.
  • 10:08 Process of creating a new tenant in a multitenant setup.
  • 10:52 Conclusions emphasizing the need for adaptable multitenant architecture.
  • 11:30 Final advice on monitoring tenant behaviors and preparing for growth.

Transcription

Dziaba senior, Artur Laskowski! Multitenancy, czyli architektura wielotenantowa. Zacznijmy od tego, że w tym odcinku nie mówimy o aplikacji, która jest stworzona stricte pod potrzeby tylko jednego klienta. Tylko mówimy o aplikacji, która będzie sprzedawana jako produkt. Czyli kilku klientów będzie kupowała tą aplikację i z niej korzystało. I możemy ją sprzedawać w podejściu singletonant. Czyli każdy klient otrzyma całą infrastrukturę. Czyli każde wdrożenie to będzie baza kodu i baza danych, osobna, dedykowana dla tego klienta. Oczywiście, jeśli mamy architekturę rozproszoną, to będzie wiele baz kodu i wiele baz danych. Co do idei, chodzi o to, że infrastruktura będzie przeznaczona tylko i wyłącznie dla jednego klienta. I to podejście często jest najlepszym wyborem w momencie, kiedy mamy klientów typu enterprise. Czyli są oni duzi i potrzebują dodatkowo w naszym produkcie, naszej bazie kodu, dokonywać jakichś kustomizacji. Czyli wpadają do nas tak zwane CRy i my po prostu rozwijamy ten produkt pod potrzeby konkretnych klientów. Ale możemy spotkać się z sytuacją, gdzie my sprzedajemy nasz produkt na przykład w modelu SaaS. Tak, czyli na zasadzie subskrypcji. Więc nie będziemy mieli jakichś specjalnych kustomizacji pod klientów i będziemy mieli dużo klientów. W takim podejściu mały klient musiałby zapłacić dużo, żeby korzystać z dedykowanej infrastruktury. I dodatkowo ta infrastruktura zwyczajnie by się nudziła. Co więcej, dla każdego małego klienta stawianie osobnej infrastruktury będzie zwyczajnie nieopłacalne. Bo z reguły ten klient nie będzie wykorzystał potencjału tej infrastruktury. A dodatkowo będzie to ciężkie w utrzymaniu. Bo teraz jeśli chcemy podleć bazę kodu, bo był jakiś bug, ale podlejemy nowy feature, to musimy to zrobić w każdej infrastrukturze osobno. Więc ogólnie co do idei w takim podejściu dodatki sobie odlatują. Rozwiązaniem tego problemu może być wielotenantowość. W takim podejściu tenanci korzystają z tego samego środowiska, współdzieląc między sobą moc obliczeniową sieć czy storage. Czyli po prostu infrastrukturę. Pozwala to zmniejszyć koszta i w łatwy sposób obsługiwać nowych tenantów. To podejście można porównać do bloku, gdzie każdy tenant to po prostu mieszkanie z tego bloku. Część infrastruktury budynku jest współdzielona dla mieszkańców, jak i współdzielone są zasoby takie jak np. woda czy prąd. Oczywiście każdy mieszkaniec ma również swoją przestrzeń, do której tylko i wyłącznie on ma dostęp za pośrednictwem swojego klucza. I oczywiście to samo trzeba zapewnić w architekturze wielotenantowej. Przestrzeniami, do których powinien mieć dostęp tylko i wyłącznie konkretny tenant, są miejsca, gdzie są przechowywane dane tego tenanta. I żeby rozwiązać ten problem istnieje kilka strategii. Zastanówmy się więc, jak możemy odizolować od siebie dane w architekturze multitenancy. Zacznijmy od bazy danych. Pierwsze podejście to najwyższy poziom izolacji, czyli oddzielne bazy danych dla każdego tenanta. W takim przypadku mamy jedną bazę kodu, ale każdy tenant będzie miał swoją bazę danych. Czyli jedna aplikacja będzie musiała łączyć się do wielu baz danych. To podejście gwarantuje duże bezpieczeństwo, ponieważ dane tenantów są fizycznie oddzielone od siebie. Jednak wiele baz danych to wiele miejsc, którymi trzeba zarządzać. Jak zmieni się na przykład schemat bazodanowy, trzeba to zdobyć w każdej bazie danych. Więc tutaj będziemy mieli największą izolację, ale nie oszukujmy się, nie zawsze będziemy chcieli w to iść. Więc zobaczmy najbardziej skrajne rozwiązanie z drugiej strony. Wspólna baza danych dla wszystkich tenantów i rozpoznawanie tenanta na poziomie wierszy. W tym przypadku aplikacja będzie połączona tylko do jednej bazy danych. Konsekwencem tego jest, że każda tabela będąca w kontekście tenanta będzie musiała mieć kolumnę identyfikującą tenanta. W takim podejściu z reguły będziemy mieli jakiś mechanizm, który przed wysłaniem zapytania do bazy danych za każdym razem będzie doklejał tego uera i mówił, że operacja tyczy się konkretnego tenanta. I to podejście nie zawsze najlepiej sprzedaje się klientowi. No bo powiemy, że dane Twoje są w tej samej tabeli co innego tenanta, ale de facto my mamy automat, który dokleja nam uera, więc nikt Ci Twoich danych nie wyciągnie, mimo tego, że one leżą obok siebie. Oczywiście na koniec dnia nie zawsze musi być to uera robione z poziomu aplikacji. Niektóre silniki bazodanowe będą pozwalały nam stworzyć użytkownika na bazie danych per tenant i na tabelach mieć polityki bezpieczeństwa, które dla każdego użytkownika będą doklejały jakiś tam predykat właśnie, który będzie odfiltrowywał dane, które należą tylko i wyłącznie do tego tenanta. Czyli de facto ten użytkownik będzie mógł pobierać i korzystać z danych, które będą należały tylko do konkretnego tenanta, czyli dla niego. Ale to i tak nie zmienia faktu, że te dane fizycznie będą leżały gdzieś obok siebie. Decydując się na to podejście też warto się zastanowić o jakiej skali my mówimy. Przykładowo jeśli w tabeli mamy 10 milionów rekordów dla jednego tenanta i będziemy mieli 100 tysięcy tenantów, to trzeba zadać sobie pytanie, czy nasze narzędzia i nasza wiedza jest wystarczająca do tego, żeby obsługiwać tyle rekordów w danej tabeli. Ale jeśli tenanci nie mają problemu co do izolacji tych danych i ilość rekordów w tabeli nie jest przerażająca, to można się pokusić o takie podejście. Ale jeśli decydujemy się na takie podejście, warto stosować identyfikatory, które są globalnie unikalne, czyli na przykład UID. Ponieważ tenanci z jednej infrastruktury czasami mogą migrować do innej infrastruktury i jeśli byśmy używali identyfikatorów, które są nadawane przez jakąś sekwencję bazodonową, to podczas migracji pewnie spotkalibyśmy się z dużą liczbą konfliktów związanych z brakiem unikalności danego identyfikatora. To można powiedzieć, że mamy dwa ekstrema, jeśli chodzi o izolację danych. Zobaczmy, co jest pomiędzy. Wspólna baza danych, ale oddzielne schematy bazodonowe. W tym wypadku mamy już dane odizolowane fizycznie i odfiltrowujemy na poziomie połączenia do bazy danych, a nie na poziomie konkretnego zapytania. I oczywiście każdy schemat może być dedykowany do konkretnego użytkownika, który będzie miał swój login i hasło. No i to oczywiście dużo lepiej się przydaje. Dodatkowym plusem tego rozwiązania jest to, że mamy mniejsze tabele, ponieważ w danym schemacie będziemy mieli tabele, które będą zawierały informacje tylko i wyłącznie o konkretnym tenancie. Pamiętajmy, że w tym przypadku jesteśmy w podobnej sytuacji jak baza danych pertinent. Tutaj skryt bazonowy też będzie musiał się wykonać na każdym schemacie. Tutaj też warto podkreślić, że jeśli korzystamy z jednej bazy danych, to narażamy się na problem tzw. głośnego sąsiada. Czyli jeden tenant może bombardować naszą aplikację i wykorzystywać wszystkie połączenia do bazy danych, a drugi, mimo tego, że chce wykonać niewiele operacji, nie będzie mógł tego zrobić, ponieważ nie będzie mógł uzyskać wolnego połączenia do bazy danych. Więc pewnie poleci mu jakiś timeout. Oczywiście to może być wpisane w politykę naszego multitenancji i możliwe, że po prostu jest to w warunkach umowy. Płacisz mniej, to godzisz się na to, że infrastruktura jest spółdzielona. No oczywiście, pewnie z takim hałaśliwym sąsiadem, który by robił to notorycznie, trzeba było coś zrobić. Możemy też starać się rozwiązać ten problem przez przypisanie konkretnej ilości połączeń bazodonowych dla konkretnego tenanta. Czyli tenant to będzie jakiś tam użytkownik bazodonowej i w zależności od silnika bazodonowego będziemy przypisywać albo liczbę połączeń do tego konkretnego użytkownika, albo liczbę połączeń do roli i rolę tego konkretnego użytkownika. No w każdym razie ograniczymy limit tych połączeń do konkretnego tenanta, dzięki temu nie będzie takiej sytuacji, że jeden tenant nam wyczerpie całą pulę. Który podejście wybrać? Oczywiście wszystko zależy od kontekstu. Osobiście najbardziej lubię jedna baza danych, ale osobny schemat per tenant. W tym podejściu nie musimy łączyć się z wieloma bazami danych, a jeśli jakiś tenant nam się mocno rozrasta, to możemy w łatwy sposób go wyciągnąć i wsadzić do osobnej infrastruktury. Ale o tym sobie jeszcze powiemy. Jeśli mówimy o izolacji danych, to musimy pamiętać, że wszędzie tam, gdzie dane konkretnego tenanta są przechowywane, nawet na krótki okres czasu, czy to na przykład w keszu, czy na przykład na Message Brokerze, musimy w jakiś sposób je izolować, w taki sposób, żeby inny tenant nie mógł się do nich dostać. I tutaj znowu będziemy mieli kilka strategii, jak możemy do tego podejść. Jeśli mówimy o Message Brokerze, to w zależności od tego, jakiego narzędzia używamy, możemy na jednym topiku, bądź na jednej kolejce mieć dane, które pochodzą z wielu tenantów. No i w takim przypadku nasz konsumer na podstawie jakiegoś nagłówka, czy tam pola, będzie wiedział, że ta wiadomość należy do konkretnego tenanta i w jego kontekście ją wykona. No i oczywiście tu znowu może pojawić się problem głośnego sąsiada. Czyli jeden tenant wrzuci bardzo dużo wiadomości na konkretną kolejkę, czy topik, a drugi tenant, mimo tego, że wrzuci małą liczbę wiadomości, będzie musiał czekać, żeby skonsumowały się wiadomości tego pierwszego tenanta. Oczywiście możemy tutaj robić operacje, które będą pozwalały w jakiś sposób urównoleglić procesowanie wiadomości w zależności od tenanta, nawet jak mamy to na jednej kolejce, czy tam na jednym topiku, na przykład operując na nagłówkach, ale może okazać się, że to nie jest wystarczające. Więc możemy podejść do innego rozwiązania, czyli tworzyć kolejkę, bądź topik per tenant. W tym wypadku mamy większą izolację danych, no i nasi konsumerzy będą w sposób bardziej równoległy wykonywać operacje dla konkretnego tenanta bez szczerowania. Oczywiście wszystko zależy od ilości tych topików kolejek i od ilości tenantów, o których rozmawiamy. Przykładowo w kawce, jeśli dodamy sobie kolejnego konsumera, czyli na przykład zeskalujemy sobie naszą aplikację, dodamy kolejną instancję aplikacji, to będziemy musieli każdemu konsumerowi przypisać, z jakich partycji danego topika ma czytać wiadomości. I oczywiście im więcej topików, tym więcej będzie to trwało. Więc warto się zastanowić, czy jest to dla nas akceptowalne. Bo może nie musimy iść aż tak radykalnie i możemy tworzyć na przykład topik czy kolejkę per typ tenanta, a nie per tenant. Oczywiście wszystko trzeba osadzić w kontekście. Powiedzieliśmy sobie kilka słów, w jaki sposób możemy izolować dane konkretnych tenantów. No ale skąd aplikacja ma wiedzieć, w ramach jakiego tenanta ma wykonać dane żądanie? I oczywiście tu też mamy kilka strategii. Oczywiście w jakiś sposób musimy aplikacji przekazać informacje w ramach jakiego tenanta będzie wykonywać to żądanie. Możemy to zrobić w URL, możemy to zrobić w nagłówku, a możemy mieć to w naszym tokenie bezpieczeństwa i za pomocą jakiegoś JWT będziemy to odczytywać. Oczywiście wcześniej taki token musiał być uzyskany per tenant. Żeby nie przedłużać, zobaczmy pierwszą z tych opcji. Załóżmy, że mamy tenanta o ID KONIE. Aplikacja dostaje żądanie i w URL tego żądania jest identyfikator tenanta. Żądanie trafia do konkretnego mikroserwisu, załóżmy, że to jest kupon serwis. No i teraz nasza aplikacja dzięki temu, że ma informacje o tym tenancie, wie, że musi odwoływać się do danych znajdujących się w bazie danych dla tego konkretnego tenanta. Na przykład odizolowanych za pomocą schematu bazodanowego. Po wykonaniu operacji, po zapisaniu do bazy danych, jeśli jest wysyłany event, no to również on będzie wysyłany już, w zależności od poziomu izolacji na messagebookerze, na przykład na dedykowany topic dla tego tenanta. Jeśli inny tenant wyśle request do tej infrastruktury, oczywiście również przedstawi się jako konkretny tenant i też połączy się ze swoim schematem bazodanowym i wyśle event na swój dedykowany topic. I tak w skrócie mogłoby to działać. To jeszcze dwa słowa na szybko, jak mógł wyglądać proces stworzenia nowego tenanta. Żeby stworzyć nowego tenanta, to wiadomo, że potrzebujemy coś, co triggeruje ten proces. Może to być na przykład osobny mikroserwis dedykowany właśnie do operacji związanych z tenantami. Do takiego mikroserwisu może wpaść żądanie z prośbą o utworzenie tenanta. Jeśli to żądanie zostanie prawidłowo przeprocesowane, zostanie zapisana ta informacja w bazie danych no i zostanie wyemitowana wiadomość na messagebroker z informacją o tym, że tenant został utworzony. Następnie wszystkie mikroserwisy, które są w architekturze i będą musiały wykonywać operacje w kontekście tenanta, będą szczytywały tę wiadomość. Po szczytaniu tej wiadomości będą wykonywały operacje związane z tworzeniem nowego tenanta. Na przykład tworzenie schematu bazy do nowego, jeśli mamy takie podejście. Każdy mikroserwis wie, że ma to zrobić, ponieważ najprawdopodobniej będzie miał dostarczony kod z tymi funkcjonalnościami w postaci jakiejś biblioteki, która jest zainkludowana do tego mikroserwisu. Ta biblioteka może odpowiadać za tworzenie osobnej przestrzeni, czy to na bazie danych, czy na messagebrokerze podczas tworzenia nowego tenanta, albo po prostu odpowiadać za to, żeby właśnie wykonywać jakąś filtrację, czy to za pomocą klauzuli WHERE, która jest automatycznie dodawana podczas zapytania do bazy danych, czy to za pomocą szczytowania konkretnego nagłówka z wiadomości pobranej z messagebrokera po to, żeby wykonać operacje w kontekście konkretnego tenanta. Oczywiście każdy mikroserwis poza wykonywaniem operacji tych, które znajdują się w bibliotece, może wykonywać również swoje operacje, które są związane z jego konkretną domeną. No i na koniec standardowo krótkie podsumowanko. Pamiętajmy, że architektura multitenantowa to nie musi być tylko jedna infrastruktura. Tenant tenantowi nierówny. Dlatego starajmy się grupować tenantów dla konkretnej infrastruktury, którzy są po prostu podobni do siebie i będą mieli podobny ruch. Na koniec dnia przecież chodzi o to, żeby wszystkim się to opłacało. Dużo małych tenantów może działać na innej infrastrukturze. Ta sama infrastruktura może być dedykowana dla mniejszej ilości średnich tenantów, albo nawet dla jednego dużego tenanta. Duży tenant prawdopodobnie jest w stanie zapłacić więcej, dlatego osobna infrastruktura dla niego będzie pokrywała jego potrzeby, albo osobna infrastruktura może powstać z tego względu, że jakiś tenant zażyczył sobie funkcjonalności, których nie ma w produkcie. Oczywiście w takim podejściu na koniec dnia Excel biznesowi musiał się zgadzać. Dlatego starajmy się grupować tenantów w zależności od ich potrzeb. Takie podejście obniża prawdopodobieństwo wystąpienia problemu chałośliwego sąsiada. Duży tenant może generować więcej wiadomości w ciągu dnia niż mały tenant przez rok. Oczywiście w takim podejściu musimy cały czas monitorować ruch, ponieważ tenant, który nawet nie jest duży, może generować duży ruch, nieumiejętnie korzystając z naszego API. Albo mały tenant może zacząć się rozrastać i generować więcej ruchu i stawać się dużym tenantem, ponieważ biznes mu dobrze się wiedzie. A jeśli mu się dobrze wiedzie biznes, to pewnie będzie w stanie więcej zapłacić, czyli może np. mieć dedykowaną infrastrukturę dla siebie. No i wtedy mamy migrację danych, czyli im większą izolację tych danych mieliśmy, tym łatwiej nam wykonać taką operację. To by było na tyle, jeśli chodzi o dziś. Pamiętaj, żeby subskrybować kanał, a jeśli filmik Ci się podobał, zostaw po sobie jakąś reakcję. Jeśli na co nie pracujesz w architekturze multidenanski i chciałbyś dorzucić kilka tipów, sekcja komentarzy jest Twoja. Dzięki i mam nadzieję, do usłyszonka.