sql >> Database >  >> RDS >> PostgreSQL

Multitenancy-opties voor PostgreSQL

Multi-tenancy in een softwaresysteem wordt de scheiding van gegevens volgens een reeks criteria genoemd om aan een reeks doelstellingen te voldoen. De omvang/uitbreiding, de aard en de uiteindelijke uitvoering van deze scheiding is afhankelijk van die criteria en doelstellingen. Multi-tenancy is in feite een geval van gegevenspartitionering, maar we zullen proberen deze term om de voor de hand liggende redenen te vermijden (de term in PostgreSQL heeft een zeer specifieke betekenis en is gereserveerd, aangezien declaratieve tabelpartitionering werd geïntroduceerd in postgresql 10).

De criteria kunnen zijn:

  1. volgens de id van een belangrijke hoofdtabel, die de tenant-id symboliseert die zou kunnen staan ​​voor:
    1. een bedrijf/organisatie binnen een grotere holdinggroep
    2. een afdeling binnen een bedrijf/organisatie
    3. een regionaal kantoor/filiaal van hetzelfde bedrijf/dezelfde organisatie
  2. volgens de locatie/IP van een gebruiker
  3. volgens de positie van een gebruiker binnen het bedrijf/de organisatie

De doelstellingen kunnen zijn:

  1. scheiding van fysieke of virtuele bronnen
  2. scheiding van systeembronnen
  3. beveiliging
  4. nauwkeurigheid en gemak van management/gebruikers op de verschillende niveaus van het bedrijf/organisatie

Merk op dat door het vervullen van een doelstelling, we ook alle onderstaande doelstellingen vervullen, d.w.z. door A te vervullen, vervullen we ook B, C en D, door B te vervullen, voldoen we ook aan C en D, enzovoort.

Als we doel A willen bereiken, kunnen we ervoor kiezen om elke tenant in te zetten als een afzonderlijk databasecluster binnen zijn eigen fysieke/virtuele server. Dit geeft maximale scheiding van middelen en beveiliging, maar geeft slechte resultaten wanneer we de hele gegevens als één geheel moeten zien, d.w.z. het geconsolideerde beeld van het hele systeem.

Als we alleen doelstelling B willen bereiken, kunnen we elke tenant als een afzonderlijke postgresql-instantie op dezelfde server implementeren. Dit zou ons controle geven over hoeveel ruimte er aan elke instantie zou worden toegewezen, en ook enige controle (afhankelijk van het besturingssysteem) over het CPU/mem-gebruik. Dit geval is niet wezenlijk anders dan A. In het moderne cloud computing-tijdperk wordt de kloof tussen A en B steeds kleiner, zodat A hoogstwaarschijnlijk de voorkeur heeft boven B.

Als we doel C willen bereiken, namelijk beveiliging, dan is het voldoende om één database-instantie te hebben en elke tenant als een afzonderlijke database te implementeren.

En tot slot, als we alleen zorgen voor "zachte" scheiding van gegevens, of met andere woorden verschillende weergaven van hetzelfde systeem, kunnen we dit bereiken door slechts één database-instantie en één database, met behulp van een overvloed aan technieken die hieronder worden besproken als de laatste (en hoofdonderwerp van deze blog. Over multi-tenancy gesproken, vanuit het perspectief van de DBA vertonen casussen A, B en C veel overeenkomsten. Dit komt omdat we in alle gevallen verschillende databases hebben en om die databases te overbruggen, moeten speciale tools en technologieën worden gebruikt. Als de noodzaak om dit te doen echter afkomstig is van de analyse- of Business Intelligence-afdelingen, is er misschien helemaal geen overbrugging nodig, omdat de gegevens heel goed kunnen worden gerepliceerd naar een centrale server die voor die taken is bestemd, waardoor overbrugging niet nodig is. Als zo'n overbrugging inderdaad nodig is, moeten we tools gebruiken zoals dblink of buitenlandse tabellen. Buitenlandse tabellen via Foreign Data Wrappers heeft tegenwoordig de voorkeur.

Als we echter optie D gebruiken, dan is consolidatie standaard al gegeven, dus nu is het moeilijkste deel het tegenovergestelde:scheiding. We kunnen de verschillende opties dus in het algemeen in twee hoofdcategorieën indelen:

  • Zachte scheiding
  • Harde scheiding

Harde scheiding via verschillende databases in dezelfde cluster

Laten we aannemen dat we een systeem moeten ontwerpen voor een denkbeeldig bedrijf dat auto- en bootverhuur aanbiedt, maar omdat die twee worden beheerst door verschillende wetgevingen, verschillende controles, audits, moet elk bedrijf afzonderlijke boekhoudafdelingen hebben en daarom willen we hun systemen behouden gescheiden. In dit geval kiezen we ervoor om voor elk bedrijf een andere database te hebben:rentaldb_cars en rentaldb_boats, die identieke schema's zullen hebben:

# \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)

Stel dat we de volgende verhuur hebben. In 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)

en in 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)

Nu wil het management een geconsolideerd beeld van het systeem hebben, b.v. een uniforme manier om de huurwoningen te bekijken. We kunnen dit via de applicatie oplossen, maar als we de applicatie niet willen updaten of geen toegang hebben tot de broncode, dan kunnen we dit oplossen door een centrale database rentaldb aan te maken. en door gebruik te maken van buitenlandse tabellen, als volgt:

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;

Om alle verhuur en de klanten in de hele organisatie te zien, doen we gewoon:

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)

Dit ziet er goed uit, isolatie en veiligheid zijn gegarandeerd, consolidatie wordt bereikt, maar er zijn nog steeds problemen:

  • klanten moeten afzonderlijk worden onderhouden, wat betekent dat dezelfde klant mogelijk twee accounts heeft
  • De toepassing moet de notie van een speciale kolom (zoals tenant_db) respecteren en deze aan elke query toevoegen, waardoor deze vatbaar is voor fouten
  • De resulterende weergaven kunnen niet automatisch worden bijgewerkt (aangezien ze UNION bevatten)

Zachte scheiding in dezelfde database

Wanneer voor deze benadering wordt gekozen, wordt consolidatie out-of-the-box gegeven en nu is het moeilijkste deel de scheiding. PostgreSQL biedt ons een overvloed aan oplossingen om scheiding te implementeren:

  • Beelden
  • Beveiliging op rolniveau
  • Schema's

Met views moet de applicatie een query-instelling instellen zoals application_name, we verbergen de hoofdtabel achter een view, en dan in elke query op een van de onderliggende (zoals in FK-afhankelijkheid) tabellen, indien aanwezig, van deze hoofdtabel join met dit beeld. We zullen dit in het volgende voorbeeld zien in een database die we rentaldb_one noemen. We integreren de identificatie van het huurdersbedrijf in de hoofdtabel:

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)
Download de whitepaper vandaag PostgreSQL-beheer en -automatisering met ClusterControlLees wat u moet weten om PostgreSQL te implementeren, bewaken, beheren en schalenDownload de whitepaper

Het schema van de tafelklanten blijft hetzelfde. Laten we eens kijken naar de huidige inhoud van de database:

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)

We gebruiken de nieuwe naam rental_one om dit te verbergen achter de nieuwe weergave die dezelfde naam zal hebben als de tabel die de toepassing verwacht:verhuur. De toepassing zal de toepassingsnaam moeten instellen om de huurder aan te duiden. In dit voorbeeld hebben we dus drie instanties van de toepassing, één voor auto's, één voor boten en één voor het topmanagement. De naam van de applicatie is ingesteld als:

rentaldb_one=# set application_name to 'cars';

We maken nu de weergave:

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');

Opmerking:we houden zoveel mogelijk dezelfde kolommen en namen van tabellen/weergaven, het belangrijkste punt bij multi-tenant oplossingen is om de zaken hetzelfde te houden aan de applicatiekant, en veranderingen om minimaal en beheersbaar te zijn.

Laten we wat selecties doen:

rentaldb_one=# zet applicatienaam op 'auto's';

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)

Het 3e exemplaar van de applicatie die de applicatienaam op "all" moet zetten, is bedoeld voor gebruik door het topmanagement met het oog op de hele database.

Een robuustere oplossing, qua beveiliging, kan gebaseerd zijn op RLS (beveiliging op rijniveau). Eerst herstellen we de naam van de tafel, onthoud dat we de applicatie niet willen storen:

rentaldb_one=# alter view rental rename to rental_view;
rentaldb_one=# alter table rental_one rename TO rental;

Eerst maken we de twee groepen gebruikers voor elk bedrijf (boten, auto's) die hun eigen subset van de gegevens moeten zien:

rentaldb_one=# create role cars_employees;
rentaldb_one=# create role boats_employees;

We maken nu beveiligingsbeleid voor elke groep:

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');

Na het toekennen van de benodigde subsidies aan de twee rollen:

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 ;

we maken één gebruiker in elke rol

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;

En 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=>

Het leuke van deze aanpak is dat we niet veel exemplaren van de applicatie nodig hebben. Alle isolatie wordt gedaan op databaseniveau op basis van de gebruikersrollen. Om een ​​gebruiker in het topmanagement aan te maken, hoeven we deze gebruiker dus alleen maar beide rollen toe te kennen:

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)

Als we naar die twee oplossingen kijken, zien we dat voor de view-oplossing de naam van de basistabel moet worden gewijzigd, wat behoorlijk opdringerig kan zijn omdat we mogelijk precies hetzelfde schema moeten uitvoeren in een niet-multitenant-oplossing, of met een app die zich niet bewust is van toepassingsnaam , terwijl de tweede oplossing mensen aan specifieke huurders bindt. Wat als dezelfde persoon werkt b.v. 's ochtends op de huurder van de boten en 's middags op de huurder van de auto? We zullen een derde oplossing zien op basis van schema's, die naar mijn mening de meest veelzijdige is en geen van de beperkingen van de twee hierboven beschreven oplossingen heeft. Het stelt de applicatie in staat om op een huurderonafhankelijke manier te werken en de systeemingenieurs kunnen onderweg huurders toevoegen als dat nodig is. We zullen hetzelfde ontwerp behouden als voorheen, met dezelfde testgegevens (we zullen blijven werken aan de rentaldb_one voorbeeld db). Het idee hier is om een ​​laag voor de hoofdtabel toe te voegen in de vorm van een database-object in een apart schema wat vroeg genoeg zal zijn in het search_path voor die specifieke huurder. Het zoekpad kan worden ingesteld (idealiter via een speciale functie, die meer opties geeft) in de verbindingsconfiguratie van de gegevensbron op de applicatieserverlaag (dus buiten de applicatiecode). Eerst maken we de twee schema's:

rentaldb_one=# create schema cars;
rentaldb_one=# create schema boats;

Vervolgens maken we de database-objecten (views) in elk schema:

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';

De volgende stap is om het zoekpad in elke tenant als volgt in te stellen:

  • Voor de botenhuurder:

    set search_path TO 'boats, "$user", public';
  • Voor de autohuurder:

    set search_path TO 'cars, "$user", public';
  • Voor de hoogste beheerder van de huurder laat het op standaard

Laten we testen:

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)
Gerelateerde bronnen ClusterControl voor PostgreSQL PostgreSQL-triggers en Stored Function Basics Tuning Input/Output (I/O)-bewerkingen voor PostgreSQL

In plaats van zoekpad in te stellen, kunnen we een complexere functie schrijven om complexere logica af te handelen en dit aan te roepen in de verbindingsconfiguratie van onze applicatie of verbindingspooler.

In het bovenstaande voorbeeld hebben we dezelfde centrale tabel gebruikt die zich op het openbare schema (public.rental) bevindt en twee extra views voor elke huurder, met het gelukkige feit dat die twee views eenvoudig en daarom beschrijfbaar zijn. In plaats van views kunnen we overerving gebruiken, door één onderliggende tabel te maken voor elke huurder die overneemt van de openbare tabel. Dit is een prima match voor tabelovererving, een uniek kenmerk van PostgreSQL. De bovenste tabel kan zijn geconfigureerd met regels om invoegingen niet toe te staan. In de overervingsoplossing zou een conversie nodig zijn om de onderliggende tabellen te vullen en om invoegtoegang tot de bovenliggende tabel te voorkomen, dus dit is niet zo eenvoudig als in het geval met views, wat werkt met minimale impact op het ontwerp. We zouden een speciale blog kunnen schrijven over hoe je dat kunt doen.

De bovenstaande drie benaderingen kunnen worden gecombineerd om nog meer opties te geven.


  1. Hoe dubbele rijen in SQL te elimineren

  2. Gebruikersaccountbeheer, rollen, machtigingen, authenticatie PHP en MySQL - Deel 3

  3. Tabelkolomnamen ophalen in MySQL?

  4. Meest efficiënte manier om rijen in de MySQL-database in te voegen