Vícenásobné pronajímání v softwarovém systému se nazývá oddělení dat podle souboru kritérií za účelem splnění souboru cílů. Rozsah/rozsah, povaha a konečné provedení tohoto oddělení závisí na těchto kritériích a cílech. Multi-tenancy je v podstatě případ dělení dat, ale pokusíme se tomuto termínu vyhnout ze zřejmých důvodů (výraz v PostgreSQL má velmi specifický význam a je vyhrazen, protože deklarativní dělení tabulek bylo zavedeno v postgresql 10).
Kritéria mohou být:
- podle id důležité hlavní tabulky, která symbolizuje id tenanta, které může představovat:
- společnost/organizace v rámci větší holdingové skupiny
- oddělení v rámci společnosti/organizace
- regionální kancelář/pobočka stejné společnosti/organizace
- podle polohy/IP adresy uživatele
- podle pozice uživatele ve společnosti/organizaci
Cíle mohou být:
- oddělení fyzických nebo virtuálních zdrojů
- oddělení systémových prostředků
- zabezpečení
- přesnost a pohodlí vedení/uživatelů na různých úrovních společnosti/organizace
Všimněte si, že splněním cíle plníme i všechny níže uvedené cíle, tj. splněním A plníme i B, C a D, splněním B plníme i C a D atd.
Pokud chceme splnit cíl A, můžeme se rozhodnout nasadit každého tenanta jako samostatný databázový cluster v rámci jeho vlastního fyzického/virtuálního serveru. To poskytuje maximální oddělení zdrojů a zabezpečení, ale poskytuje špatné výsledky, když potřebujeme vidět celá data jako jeden, tj. konsolidovaný pohled na celý systém.
Pokud chceme dosáhnout pouze cíle B, můžeme nasadit každého tenanta jako samostatnou postgresql instanci na stejném serveru. To by nám dalo kontrolu nad tím, kolik prostoru bude přiděleno každé instanci, a také určitou kontrolu (v závislosti na OS) nad využitím CPU/paměti. Tento případ se v podstatě neliší od A. V moderní éře cloud computingu má mezera mezi A a B tendenci se zmenšovat a zmenšovat, takže A bude s největší pravděpodobností preferovanou cestou před B.
Pokud chceme dosáhnout cíle C, tedy bezpečnosti, pak stačí mít jednu instanci databáze a každého tenanta nasadit jako samostatnou databázi.
A konečně, pokud se budeme starat pouze o „měkké“ oddělení dat, nebo jinými slovy o různé pohledy na stejný systém, můžeme toho dosáhnout pouze jednou databázovou instancí a jednou databází, za použití nepřeberného množství technik diskutovaných níže jako konečné (a hlavní) téma tohoto blogu. Když mluvíme o multi-nájmu, z pohledu DBA mají případy A, B a C mnoho podobností. Je to proto, že ve všech případech máme různé databáze a abychom tyto databáze překlenuli, je třeba použít speciální nástroje a technologie. Pokud však potřeba tak učinit přichází z oddělení analytiky nebo Business Intelligence, pak nemusí být přemostění vůbec potřeba, protože data by bylo možné velmi dobře replikovat na nějaký centrální server vyhrazený pro tyto úkoly, takže přemosťování není nutné. Pokud je skutečně takové přemostění potřeba, musíme použít nástroje jako dblink nebo cizí tabulky. Zahraniční tabulky prostřednictvím Foreign Data Wrappers jsou dnes preferovaným způsobem.
Pokud však použijeme možnost D, pak je konsolidace již standardně dána, takže nyní je ta těžší část opačná:oddělení. Můžeme tedy obecně rozdělit různé možnosti do dvou hlavních kategorií:
- Měkké oddělení
- Tvrdé oddělení
Tvrdá separace prostřednictvím různých databází ve stejném clusteru
Předpokládejme, že musíme navrhnout systém pro imaginární podnik nabízející půjčovny aut a lodí, ale protože tyto dva se řídí jinou legislativou, jinými kontrolami, audity, každá společnost musí mít samostatné účetní oddělení, a proto bychom chtěli jejich systémy zachovat oddělené. V tomto případě zvolíme pro každou společnost jinou databázi:rentaldb_cars a rentaldb_boats, které budou mít stejná schémata:
# \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)
Předpokládejme, že máme následující půjčovny. V 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)
a v 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)
Nyní by vedení chtělo mít ucelený pohled na systém, např. jednotný způsob zobrazení pronájmů. Můžeme to vyřešit přes aplikaci, ale pokud nechceme aplikaci aktualizovat nebo nemáme přístup ke zdrojovému kódu, můžeme to vyřešit vytvořením centrální databáze rentaldb a pomocí cizích tabulek následovně:
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;
Pro zobrazení všech pronájmů a zákazníků v celé organizaci jednoduše uděláme:
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)
Vypadá to dobře, izolace a bezpečnost jsou zaručeny, konsolidace je dosažena, ale stále existují problémy:
- zákazníci musí být spravováni samostatně, což znamená, že stejný zákazník může skončit se dvěma účty
- Aplikace musí respektovat pojem speciálního sloupce (např. tenant_db) a připojit jej ke každému dotazu, aby byla náchylná k chybám
- Výsledná zobrazení nelze automaticky aktualizovat (protože obsahují UNION)
Měkká separace ve stejné databázi
Když je zvolen tento přístup, konsolidace je poskytována po vybalení z krabice a nyní je nejtěžší separace. PostgreSQL nám nabízí nepřeberné množství řešení pro implementaci separace:
- Zobrazení
- Zabezpečení na úrovni role
- Schémata
U pohledů musí aplikace nastavit dotazovatelné nastavení, jako je název_aplikace, skryjeme hlavní tabulku za pohled a pak se v každém dotazu na kteroukoli z podřízených tabulek (jako v závislosti na FK), pokud existují, této hlavní tabulky spojí s tento pohled. To uvidíme v následujícím příkladu v databázi, kterou nazýváme rentaldb_one. Identifikaci společnosti nájemce vložíme do hlavní tabulky:
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)
Stáhněte si Whitepaper Today Správa a automatizace PostgreSQL s ClusterControlZjistěte, co potřebujete vědět k nasazení, monitorování, správě a škálování PostgreSQLStáhněte si Whitepaper Schéma tabulky zákazníků zůstává stejné. Podívejme se na aktuální obsah databáze:
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)
Používáme nový název rental_one, abychom to skryli za nový pohled, který bude mít stejný název tabulky, jaký aplikace očekává:rental. Aplikace bude muset nastavit název aplikace tak, aby označoval tenanta. V tomto příkladu tedy budeme mít tři instance aplikace, jednu pro auta, jednu pro lodě a jednu pro vrcholový management. Název aplikace je nastaven takto:
rentaldb_one=# set application_name to 'cars';
Nyní vytvoříme pohled:
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');
Poznámka:Zachováváme stejné sloupce a názvy tabulek/pohledů, jak je to jen možné, klíčovým bodem v řešeních pro více nájemců je zachovat stejné věci na straně aplikace a změny, aby byly minimální a zvládnutelné.
Udělejme pár výběrů:
rentdb_one=# nastavit název_aplikace na 'auta';
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)
3. instance aplikace, která musí nastavit název aplikace na „vše“, je určena pro použití vrcholovým managementem s výhledem na celou databázi.
Robustnější řešení z hlediska zabezpečení může být založeno na RLS (zabezpečení na úrovni řádků). Nejprve obnovíme název tabulky, nezapomeňte, že nechceme rušit aplikaci:
rentaldb_one=# alter view rental rename to rental_view;
rentaldb_one=# alter table rental_one rename TO rental;
Nejprve vytvoříme dvě skupiny uživatelů pro každou společnost (lodě, auta), které musí vidět svou vlastní podmnožinu dat:
rentaldb_one=# create role cars_employees;
rentaldb_one=# create role boats_employees;
Nyní vytváříme zásady zabezpečení pro každou skupinu:
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 udělení požadovaných povolení dvěma rolím:
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 ;
v každé roli vytvoříme jednoho uživatele
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;
A test:
[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=>
Pěkné na tomto přístupu je, že nepotřebujeme mnoho instancí aplikace. Veškerá izolace se provádí na úrovni databáze na základě rolí uživatele. Abychom tedy vytvořili uživatele v top managementu, stačí tomuto uživateli přidělit obě 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)
Při pohledu na tato dvě řešení vidíme, že řešení zobrazení vyžaduje změnu základního názvu tabulky, což může být docela rušivé v tom, že možná budeme muset spustit přesně stejné schéma v řešení bez více nájemců nebo s aplikací, která si není vědoma název_aplikace , zatímco druhé řešení váže lidi na konkrétní nájemníky. Co když stejná osoba pracuje např. na nájemce lodí ráno a na nájemce aut odpoledne? Uvidíme 3. řešení založené na schématech, které je podle mého názoru nejuniverzálnější a netrpí žádnou z výhrad dvou výše popsaných řešení. Umožňuje aplikaci běžet způsobem bez ohledu na tenanty a systémovým inženýrům přidávat nájemce na cestách podle potřeby. Zachováme stejný design jako dříve, se stejnými testovacími daty (budeme dále pracovat na příkladu db rentdb_one). Cílem je přidat vrstvu před hlavní tabulku ve formě databázového objektu v samostatném schématu která bude dostatečně brzy na vyhledávací_cestě pro konkrétního nájemce. Search_path lze nastavit (ideálně přes speciální funkci, která dává více možností) v konfiguraci připojení zdroje dat na vrstvě aplikačního serveru (tedy mimo kód aplikace). Nejprve vytvoříme dvě schémata:
rentaldb_one=# create schema cars;
rentaldb_one=# create schema boats;
Poté vytvoříme databázové objekty (pohledy) v každém schématu:
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';
Dalším krokem je nastavení vyhledávací cesty v každém tenantovi následovně:
-
Pro nájemce lodí:
set search_path TO 'boats, "$user", public';
-
Pro nájemce vozů:
set search_path TO 'cars, "$user", public';
- U nejvyššího nájemce správy ponechejte toto nastavení ve výchozím nastavení
Pojďme otestovat:
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)
Související zdroje ClusterControl pro PostgreSQL Spouštěče PostgreSQL a Základy uložených funkcí Vyladění operací vstupu/výstupu (I/O) pro PostgreSQL Namísto set search_path můžeme napsat složitější funkci pro zpracování složitější logiky a zavolat ji v konfiguraci připojení naší aplikace nebo pooleru připojení.
Ve výše uvedeném příkladu jsme použili stejnou centrální tabulku umístěnou na veřejném schématu (public.rental) a dvě další pohledy pro každého tenanta, přičemž jsme využili šťastnou skutečnost, že tyto dva pohledy jsou jednoduché a tudíž zapisovatelné. Místo pohledů můžeme použít dědičnost vytvořením jedné podřízené tabulky pro každého tenanta, který dědí z veřejné tabulky. Toto je dobrá shoda pro dědičnost tabulek, unikátní vlastnost PostgreSQL. Horní tabulka může být nakonfigurována s pravidly pro zakázání vkládání. V řešení dědičnosti by byla potřeba konverze k naplnění podřízených tabulek a zabránění přístupu vkládání k nadřazené tabulce, takže to není tak jednoduché jako v případě pohledů, které fungují s minimálním dopadem na návrh. Můžeme napsat speciální blog o tom, jak to udělat.
Výše uvedené tři přístupy lze zkombinovat a poskytnout tak ještě více možností.