Wielodostępność w systemie oprogramowania nazywana jest separacją danych zgodnie z zestawem kryteriów w celu spełnienia zestawu celów. Skala/zakres, charakter i ostateczna realizacja tego rozdziału zależy od tych kryteriów i celów. Wielodostępność to w zasadzie przypadek partycjonowania danych, ale postaramy się uniknąć tego terminu z oczywistych powodów (termin w PostgreSQL ma bardzo specyficzne znaczenie i jest zarezerwowany, ponieważ deklaratywne partycjonowanie tabel zostało wprowadzone w postgresql 10).
Kryteriami mogą być:
- zgodnie z identyfikatorem ważnej tabeli głównej, która symbolizuje identyfikator najemcy, który może reprezentować:
- firma/organizacja w ramach większej grupy holdingowej
- dział w firmie/organizacji
- biuro regionalne/oddział tej samej firmy/organizacji
- zgodnie z lokalizacją/adresem IP użytkownika
- zgodnie z pozycją użytkownika w firmie/organizacji
Cele mogą być następujące:
- oddzielenie zasobów fizycznych lub wirtualnych
- oddzielenie zasobów systemowych
- bezpieczeństwo
- dokładność i wygoda zarządzania/użytkowników na różnych poziomach firmy/organizacji
Zauważ, że spełniając cel, spełniamy również wszystkie poniższe cele, tj. spełniając A, spełniamy również B, C i D, spełniając B, spełniamy również C i D itd.
Jeśli chcemy osiągnąć cel A, możemy zdecydować się na wdrożenie każdego dzierżawcy jako oddzielnego klastra bazy danych na własnym serwerze fizycznym/wirtualnym. Daje to maksymalne rozdzielenie zasobów i bezpieczeństwa, ale daje słabe wyniki, gdy musimy widzieć całe dane jako jeden, tj. skonsolidowany widok całego systemu.
Jeśli chcemy osiągnąć tylko cel B, możemy wdrożyć każdego dzierżawcę jako oddzielną instancję postgresql na tym samym serwerze. Dałoby nam to kontrolę nad tym, ile miejsca zostanie przydzielone do każdej instancji, a także pewną kontrolę (w zależności od systemu operacyjnego) nad wykorzystaniem procesora/pamięci. Ten przypadek nie różni się zasadniczo od A. We współczesnej erze przetwarzania w chmurze przepaść między A i B ma tendencję do zmniejszania się, tak że A będzie najprawdopodobniej preferowaną drogą w stosunku do B.
Jeśli chcemy osiągnąć cel C, czyli bezpieczeństwo, wystarczy mieć jedną instancję bazy danych i wdrożyć każdego najemcę jako oddzielną bazę danych.
I wreszcie, jeśli zależy nam tylko na „miękkim” rozdzieleniu danych, czyli różnych widokach tego samego systemu, możemy to osiągnąć tylko dzięki jednej instancji bazy danych i jednej bazie danych, wykorzystując mnóstwo technik omówionych poniżej jako ostateczny (i głównym) tematem tego bloga. Mówiąc o wielu najemcach, z perspektywy DBA przypadki A, B i C wykazują wiele podobieństw. Dzieje się tak, ponieważ we wszystkich przypadkach mamy różne bazy danych i aby połączyć te bazy danych, należy użyć specjalnych narzędzi i technologii. Jeśli jednak taka potrzeba pochodzi z działów analityki lub Business Intelligence, wówczas mostkowanie może w ogóle nie być potrzebne, ponieważ dane mogą być bardzo dobrze zreplikowane na jakiś centralny serwer przeznaczony do tych zadań, dzięki czemu mostkowanie nie będzie konieczne. Jeśli rzeczywiście takie pomostowanie jest potrzebne, musimy użyć narzędzi takich jak dblink lub tabele obce. Obecnie preferowanym sposobem są tabele obce za pośrednictwem zewnętrznych opakowań danych.
Jeśli jednak użyjemy opcji D, to konsolidacja jest już podana domyślnie, więc teraz trudna część jest odwrotna:separacja. Możemy więc ogólnie podzielić różne opcje na dwie główne kategorie:
- Miękka separacja
- Trudna separacja
Trudna separacja za pomocą różnych baz danych w tym samym klastrze
Załóżmy, że musimy zaprojektować system dla wyimaginowanej firmy oferującej wynajem samochodów i łodzi, ale ponieważ te dwa rządzą się różnymi przepisami, różnymi kontrolami, audytami, każda firma musi mieć osobne działy księgowe i dlatego chcielibyśmy zachować ich systemy rozdzielony. W tym przypadku wybieramy inną bazę danych dla każdej firmy:rentaldb_cars i rentaldb_boats, które będą miały identyczne schematy:
# \d customers
Table "public.customers"
Column | Type | Collation | Nullable | Default
-------------+---------------+-----------+----------+---------------------------------------
id | integer | | not null | nextval('customers_id_seq'::regclass)
cust_name | text | | not null |
birth_date | date | | |
sex | character(10) | | |
nationality | text | | |
Indexes:
"customers_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "rental" CONSTRAINT "rental_customerid_fkey" FOREIGN KEY (customerid) REFERENCES customers(id)
# \d rental
Table "public.rental"
Column | Type | Collation | Nullable | Default
------------+---------+-----------+----------+---------------------------------
id | integer | | not null | nextval('rental_id_seq'::regclass)
customerid | integer | | not null |
vehicleno | text | | |
datestart | date | | not null |
dateend | date | | |
Indexes:
"rental_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"rental_customerid_fkey" FOREIGN KEY (customerid) REFERENCES customers(id)
Załóżmy, że mamy następujące wypożyczenia. W rentaldb_cars:
rentaldb_cars=# select cust.cust_name,rent.vehicleno,rent.datestart FROM rental rent JOIN customers cust on (rent.customerid=cust.id);
cust_name | vehicleno | datestart
-----------------+-----------+------------
Valentino Rossi | INI 8888 | 2018-08-10
(1 row)
oraz w rentaldb_boats:
rentaldb_boats=# select cust.cust_name,rent.vehicleno,rent.datestart FROM rental rent JOIN customers cust on (rent.customerid=cust.id);
cust_name | vehicleno | datestart
----------------+-----------+------------
Petter Solberg | INI 9999 | 2018-08-10
(1 row)
Teraz kierownictwo chciałoby mieć skonsolidowany widok systemu, m.in. ujednolicony sposób przeglądania wypożyczeń. Możemy rozwiązać ten problem za pomocą aplikacji, ale jeśli nie chcemy aktualizować aplikacji lub nie mamy dostępu do kodu źródłowego, możemy rozwiązać ten problem, tworząc centralną bazę danych rentaldb oraz korzystając z tabel obcych w następujący sposób:
CREATE EXTENSION IF NOT EXISTS postgres_fdw WITH SCHEMA public;
CREATE SERVER rentaldb_boats_srv FOREIGN DATA WRAPPER postgres_fdw OPTIONS (
dbname 'rentaldb_boats'
);
CREATE USER MAPPING FOR postgres SERVER rentaldb_boats_srv;
CREATE SERVER rentaldb_cars_srv FOREIGN DATA WRAPPER postgres_fdw OPTIONS (
dbname 'rentaldb_cars'
);
CREATE USER MAPPING FOR postgres SERVER rentaldb_cars_srv;
CREATE FOREIGN TABLE public.customers_boats (
id integer NOT NULL,
cust_name text NOT NULL
)
SERVER rentaldb_boats_srv
OPTIONS (
table_name 'customers'
);
CREATE FOREIGN TABLE public.customers_cars (
id integer NOT NULL,
cust_name text NOT NULL
)
SERVER rentaldb_cars_srv
OPTIONS (
table_name 'customers'
);
CREATE VIEW public.customers AS
SELECT 'cars'::character varying(50) AS tenant_db,
customers_cars.id,
customers_cars.cust_name
FROM public.customers_cars
UNION
SELECT 'boats'::character varying AS tenant_db,
customers_boats.id,
customers_boats.cust_name
FROM public.customers_boats;
CREATE FOREIGN TABLE public.rental_boats (
id integer NOT NULL,
customerid integer NOT NULL,
vehicleno text NOT NULL,
datestart date NOT NULL
)
SERVER rentaldb_boats_srv
OPTIONS (
table_name 'rental'
);
CREATE FOREIGN TABLE public.rental_cars (
id integer NOT NULL,
customerid integer NOT NULL,
vehicleno text NOT NULL,
datestart date NOT NULL
)
SERVER rentaldb_cars_srv
OPTIONS (
table_name 'rental'
);
CREATE VIEW public.rental AS
SELECT 'cars'::character varying(50) AS tenant_db,
rental_cars.id,
rental_cars.customerid,
rental_cars.vehicleno,
rental_cars.datestart
FROM public.rental_cars
UNION
SELECT 'boats'::character varying AS tenant_db,
rental_boats.id,
rental_boats.customerid,
rental_boats.vehicleno,
rental_boats.datestart
FROM public.rental_boats;
Aby wyświetlić wszystkie wypożyczenia i klientów w całej organizacji, po prostu wykonujemy:
rentaldb=# select cust.cust_name, rent.* FROM rental rent JOIN customers cust ON (rent.tenant_db=cust.tenant_db AND rent.customerid=cust.id);
cust_name | tenant_db | id | customerid | vehicleno | datestart
-----------------+-----------+----+------------+-----------+------------
Petter Solberg | boats | 1 | 1 | INI 9999 | 2018-08-10
Valentino Rossi | cars | 1 | 2 | INI 8888 | 2018-08-10
(2 rows)
Wygląda to dobrze, izolacja i bezpieczeństwo są gwarantowane, osiągnięto konsolidację, ale nadal występują problemy:
- klienci muszą być oddzielnie obsługiwani, co oznacza, że ten sam klient może mieć dwa konta
- Aplikacja musi respektować pojęcie specjalnej kolumny (takiej jak tenant_db) i dołączać ją do każdego zapytania, co czyni ją podatną na błędy
- Wynikowe widoki nie są automatycznie aktualizowane (ponieważ zawierają UNION)
Miękka separacja w tej samej bazie danych
Po wybraniu tego podejścia, konsolidacja jest podawana po wyjęciu z pudełka, a teraz trudną częścią jest separacja. PostgreSQL oferuje nam mnóstwo rozwiązań w celu wdrożenia separacji:
- Widoki
- Bezpieczeństwo na poziomie roli
- Schematy
W przypadku widoków aplikacja musi ustawić możliwe do zapytania ustawienie, takie jak nazwa_aplikacji, ukrywamy główną tabelę za widokiem, a następnie w każdym zapytaniu w którejkolwiek z tabel podrzędnych (jak w zależności FK), jeśli takie istnieją, tej tabeli głównej łączymy się z ten widok. Zobaczymy to w poniższym przykładzie w bazie danych, którą nazywamy rentaldb_one. W głównej tabeli osadzamy identyfikację firmy najemcy:
rentaldb_one=# \d rental_one
Table "public.rental_one"
Column | Type | Collation | Nullable | Default
------------+-----------------------+-----------+----------+------------------------------------
company | character varying(50) | | not null |
id | integer | | not null | nextval('rental_id_seq'::regclass)
customerid | integer | | not null |
vehicleno | text | | |
datestart | date | | not null |
dateend | date | | |
Indexes:
"rental_pkey" PRIMARY KEY, btree (id)
Check constraints:
"rental_company_check" CHECK (company::text = ANY (ARRAY['cars'::character varying, 'boats'::character varying]::text[]))
Foreign-key constraints:
"rental_customerid_fkey" FOREIGN KEY (customerid) REFERENCES customers(id)
Pobierz oficjalny dokument już dziś Zarządzanie i automatyzacja PostgreSQL za pomocą ClusterControlDowiedz się, co musisz wiedzieć, aby wdrażać, monitorować, zarządzać i skalować PostgreSQLPobierz oficjalny dokument Schemat klientów tabeli pozostaje bez zmian. Zobaczmy aktualną zawartość bazy danych:
rentaldb_one=# select * from customers;
id | cust_name | birth_date | sex | nationality
----+-----------------+------------+-----+-------------
2 | Valentino Rossi | 1979-02-16 | |
1 | Petter Solberg | 1974-11-18 | |
(2 rows)
rentaldb_one=# select * from rental_one ;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(2 rows)
Używamy nowej nazwy rental_one, aby ukryć to za nowym widokiem, który będzie miał taką samą nazwę tabeli, jakiej oczekuje aplikacja:rental.Aplikacja będzie musiała ustawić nazwę aplikacji, aby wskazać najemcę. W tym przykładzie będziemy mieli trzy instancje aplikacji, jedną dla samochodów, jedną dla łodzi i jedną dla najwyższego kierownictwa. Nazwa aplikacji jest ustawiona w następujący sposób:
rentaldb_one=# set application_name to 'cars';
Teraz tworzymy widok:
create or replace view rental as select company as "tenant_db",id,customerid,vehicleno,datestart,dateend from rental_one where (company = current_setting('application_name') OR current_setting('application_name')='all');
Uwaga:zachowujemy te same kolumny i nazwy tabel/widoków, jak to tylko możliwe. Kluczową kwestią w rozwiązaniach dla wielu dzierżawców jest zachowanie takich samych elementów po stronie aplikacji, a zmiany są minimalne i łatwe do zarządzania.
Zróbmy kilka wyborów:
rentaldb_one=# ustaw nazwę aplikacji na „samochody”;
rentaldb_one=# set application_name to 'cars';
SET
rentaldb_one=# select * from rental;
tenant_db | id | customerid | vehicleno | datestart | dateend
-----------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
(1 row)
rentaldb_one=# set application_name to 'boats';
SET
rentaldb_one=# select * from rental;
tenant_db | id | customerid | vehicleno | datestart | dateend
-----------+----+------------+-----------+------------+---------
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(1 row)
rentaldb_one=# set application_name to 'all';
SET
rentaldb_one=# select * from rental;
tenant_db | id | customerid | vehicleno | datestart | dateend
-----------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(2 rows)
Trzecia instancja aplikacji, która musi ustawić nazwę aplikacji na „all”, jest przeznaczona do użytku przez najwyższe kierownictwo z widokiem na całą bazę danych.
Bardziej niezawodne rozwiązanie pod względem bezpieczeństwa może być oparte na RLS (zabezpieczenia na poziomie wiersza). Najpierw przywracamy nazwę tabeli, pamiętaj, że nie chcemy zakłócać działania aplikacji:
rentaldb_one=# alter view rental rename to rental_view;
rentaldb_one=# alter table rental_one rename TO rental;
Najpierw tworzymy dwie grupy użytkowników dla każdej firmy (łodzie, samochody), które muszą widzieć własny podzbiór danych:
rentaldb_one=# create role cars_employees;
rentaldb_one=# create role boats_employees;
Tworzymy teraz zasady bezpieczeństwa dla każdej grupy:
rentaldb_one=# create policy boats_plcy ON rental to boats_employees USING(company='boats');
rentaldb_one=# create policy cars_plcy ON rental to cars_employees USING(company='cars');
Po przyznaniu wymaganych grantów dwóm rolom:
rentaldb_one=# grant ALL on SCHEMA public to boats_employees ;
rentaldb_one=# grant ALL on SCHEMA public to cars_employees ;
rentaldb_one=# grant ALL on ALL tables in schema public TO cars_employees ;
rentaldb_one=# grant ALL on ALL tables in schema public TO boats_employees ;
tworzymy jednego użytkownika w każdej roli
rentaldb_one=# create user boats_user password 'boats_user' IN ROLE boats_employees;
rentaldb_one=# create user cars_user password 'cars_user' IN ROLE cars_employees;
I przetestuj:
[email protected]:~> psql -U cars_user rentaldb_one
Password for user cars_user:
psql (10.5)
Type "help" for help.
rentaldb_one=> select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
(1 row)
rentaldb_one=> \q
[email protected]:~> psql -U boats_user rentaldb_one
Password for user boats_user:
psql (10.5)
Type "help" for help.
rentaldb_one=> select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(1 row)
rentaldb_one=>
Miłą rzeczą przy takim podejściu jest to, że nie potrzebujemy wielu instancji aplikacji. Cała izolacja odbywa się na poziomie bazy danych w oparciu o role użytkownika. Dlatego, aby utworzyć użytkownika w zarządzie najwyższego szczebla, musimy tylko przyznać temu użytkownikowi obie role:
rentaldb_one=# create user all_user password 'all_user' IN ROLE boats_employees, cars_employees;
[email protected]:~> psql -U all_user rentaldb_one
Password for user all_user:
psql (10.5)
Type "help" for help.
rentaldb_one=> select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(2 rows)
Patrząc na te dwa rozwiązania, widzimy, że rozwiązanie widoku wymaga zmiany podstawowej nazwy tabeli, co może być dość uciążliwe, ponieważ może być konieczne uruchomienie dokładnie tego samego schematu w rozwiązaniu bez wielu dzierżawców lub w aplikacji, która nie jest świadoma nazwa_aplikacji , natomiast drugie rozwiązanie wiąże ludzi z konkretnymi lokatorami. Co jeśli ta sama osoba pracuje m.in. rano na łodziach, a po południu na samochodach? Zobaczymy trzecie rozwiązanie oparte na schematach, które moim zdaniem jest najbardziej wszechstronne i nie ma żadnych zastrzeżeń z dwóch opisanych powyżej rozwiązań. Pozwala to na działanie aplikacji w sposób niezależny od dzierżawców, a inżynierowie systemowi mogą dodawać dzierżawców w miarę potrzeb. Zachowamy ten sam projekt, co poprzednio, z tymi samymi danymi testowymi (będziemy dalej pracować nad przykładową bazą danych rentaldb_one). Pomysł polega na tym, aby dodać warstwę przed główną tabelą w postaci obiektu bazy danych w osobnym schemacie który będzie wystarczająco wcześnie w ścieżce_wyszukiwania dla tego konkretnego najemcy. Ścieżkę poszukiwań można ustawić (najlepiej za pomocą specjalnej funkcji, która daje więcej możliwości) w konfiguracji połączenia źródła danych w warstwie serwera aplikacji (a więc poza kodem aplikacji). Najpierw tworzymy dwa schematy:
rentaldb_one=# create schema cars;
rentaldb_one=# create schema boats;
Następnie tworzymy obiekty bazy danych (widoki) w każdym schemacie:
CREATE OR REPLACE VIEW boats.rental AS
SELECT rental.company,
rental.id,
rental.customerid,
rental.vehicleno,
rental.datestart,
rental.dateend
FROM public.rental
WHERE rental.company::text = 'boats';
CREATE OR REPLACE VIEW cars.rental AS
SELECT rental.company,
rental.id,
rental.customerid,
rental.vehicleno,
rental.datestart,
rental.dateend
FROM public.rental
WHERE rental.company::text = 'cars';
Następnym krokiem jest ustawienie ścieżki wyszukiwania dla każdego najemcy w następujący sposób:
-
Dla najemcy łodzi:
set search_path TO 'boats, "$user", public';
-
Dla najemcy samochodów:
set search_path TO 'cars, "$user", public';
- Dla najwyższego najemcy zarządzania, pozostaw to domyślnie
Przetestujmy:
rentaldb_one=# select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(2 rows)
rentaldb_one=# set search_path TO 'boats, "$user", public';
SET
rentaldb_one=# select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
boats | 2 | 1 | INI 9999 | 2018-08-10 |
(1 row)
rentaldb_one=# set search_path TO 'cars, "$user", public';
SET
rentaldb_one=# select * from rental;
company | id | customerid | vehicleno | datestart | dateend
---------+----+------------+-----------+------------+---------
cars | 1 | 2 | INI 8888 | 2018-08-10 |
(1 row)
Zasoby pokrewne ClusterControl dla PostgreSQL Podstawy wyzwalaczy PostgreSQL i zapisanych funkcji Dostrajanie operacji wejścia/wyjścia (I/O) dla PostgreSQL Zamiast ustawiać search_path możemy napisać bardziej złożoną funkcję do obsługi bardziej złożonej logiki i wywołać ją w konfiguracji połączenia naszej aplikacji lub puli połączeń.
W powyższym przykładzie użyliśmy tej samej tabeli centralnej znajdującej się na schemacie publicznym (public.rental) i dwóch dodatkowych widoków dla każdego dzierżawcy, wykorzystując szczęśliwy fakt, że te dwa widoki są proste i dlatego można je zapisywać. Zamiast widoków możemy użyć dziedziczenia, tworząc jedną tabelę podrzędną dla każdego dzierżawcy dziedziczącego z tabeli publicznej. Jest to doskonałe dopasowanie do dziedziczenia tabel, unikalnej funkcji PostgreSQL. Górna tabela może być skonfigurowana z regułami uniemożliwiającymi wstawianie. W rozwiązaniu dziedziczenia potrzebna byłaby konwersja, aby wypełnić tabele podrzędne i uniemożliwić dostęp do wstawiania do tabeli nadrzędnej, więc nie jest to tak proste, jak w przypadku widoków, które działają z minimalnym wpływem na projekt. Możemy napisać specjalny blog, jak to zrobić.
Powyższe trzy podejścia można połączyć, aby dać jeszcze więcej opcji.