sql >> Databasteknik >  >> RDS >> PostgreSQL

Multitenancy-alternativ för PostgreSQL

Multi-tenancy i ett mjukvarusystem kallas separation av data enligt en uppsättning kriterier för att tillfredsställa en uppsättning mål. Omfattningen/utsträckningen, karaktären och det slutliga genomförandet av denna separation beror på dessa kriterier och mål. Multi-tenancy är i grunden ett fall av datapartitionering men vi kommer att försöka undvika denna term av uppenbara skäl (termen i PostgreSQL har en mycket specifik betydelse och är reserverad, eftersom deklarativ tabellpartitionering introducerades i postgresql 10).

Kriterierna kan vara:

  1. enligt ID:t för en viktig huvudtabell, som symboliserar hyresgäst-ID:t som kan representera:
    1. ett företag/organisation inom en större holdinggrupp
    2. en avdelning inom ett företag/organisation
    3. ett regionalt kontor/filial till samma företag/organisation
  2. enligt en användares plats/IP
  3. enligt en användares position inom företaget/organisationen

Målen kan vara:

  1. separering av fysiska eller virtuella resurser
  2. separation av systemresurser
  3. säkerhet
  4. noggrannhet och bekvämlighet för ledningen/användarna på olika nivåer i företaget/organisationen

Observera att genom att uppfylla ett mål uppfyller vi också alla mål nedan, dvs. genom att uppfylla A uppfyller vi också B, C och D, genom att uppfylla B uppfyller vi också C och D, och så vidare.

Om vi ​​vill uppfylla mål A kan vi välja att distribuera varje hyresgäst som ett separat databaskluster inom sin egen fysiska/virtuella server. Detta ger maximal separering av resurser och säkerhet men ger dåliga resultat när vi behöver se hela data som en, det vill säga den konsoliderade synen av hela systemet.

Om vi ​​bara vill uppnå mål B kan vi distribuera varje klient som en separat postgresql-instans på samma server. Detta skulle ge oss kontroll över hur mycket utrymme som skulle tilldelas varje instans, och även viss kontroll (beroende på OS) på CPU/mem-användning. Det här fallet är inte väsentligt annorlunda än A. I den moderna cloud computing-eran tenderar gapet mellan A och B att bli mindre och mindre, så att A med största sannolikhet kommer att föredras framför B.

Om vi ​​vill uppnå mål C, det vill säga säkerhet, så räcker det med en databasinstans och distribuera varje hyresgäst som en separat databas.

Och slutligen om vi bara bryr oss om "mjuk" separering av data, eller med andra ord olika vyer av samma system, kan vi uppnå detta med bara en databasinstans och en databas, med hjälp av en uppsjö av tekniker som diskuteras nedan som den sista (och huvudämnet på den här bloggen. När man pratar om flerhyresrätt, ur DBA:s perspektiv, har fall A, B och C många likheter. Det beror på att vi i alla fall har olika databaser och för att överbrygga dessa databaser måste speciella verktyg och teknologier användas. Men om behovet av att göra det kommer från analys- eller Business Intelligence-avdelningarna kanske ingen överbryggning behövs alls, eftersom data mycket väl kan replikeras till någon central server som är dedikerad till dessa uppgifter, vilket gör att överbryggning blir onödig. Om en sådan överbryggning verkligen behövs måste vi använda verktyg som dblink eller främmande tabeller. Utländska tabeller via Foreign Data Wrappers är numera det föredragna sättet.

Om vi ​​däremot använder alternativ D, är konsolidering redan given som standard, så nu är den svåra delen motsatsen:separation. Så vi kan generellt kategorisera de olika alternativen i två huvudkategorier:

  • Mjuk separation
  • Hård separation

Hård separering via olika databaser i samma kluster

Låt oss anta att vi måste designa ett system för ett tänkt företag som erbjuder bil- och båtuthyrning, men eftersom dessa två styrs av olika lagstiftning, olika kontroller, revisioner, måste varje företag ha separata redovisningsavdelningar och därför skulle vi vilja behålla deras system separerat. I det här fallet väljer vi att ha en annan databas för varje företag:rentaldb_cars och rentaldb_boats, som kommer att ha identiska scheman:

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

Låt oss anta att vi har följande hyror. I 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)

och i 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 vill ledningen ha en samlad syn på systemet, t.ex. ett enhetligt sätt att se hyrorna. Vi kan lösa detta via applikationen, men om vi inte vill uppdatera applikationen eller inte har tillgång till källkoden, kan vi lösa detta genom att skapa en central databas rentaldb och genom att använda utländska tabeller, enligt följande:

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;

För att se alla uthyrningar och kunderna i hela organisationen gör vi helt enkelt:

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)

Det här ser bra ut, isolering och säkerhet garanteras, konsolidering uppnås, men det finns fortfarande problem:

  • kunder måste underhållas separat, vilket innebär att samma kund kan få två konton
  • Applikationen måste respektera föreställningen om en speciell kolumn (som tenant_db) och lägga till denna till varje fråga, vilket gör den benägen att göra fel
  • De resulterande vyerna kan inte uppdateras automatiskt (eftersom de innehåller UNION)

Mjuk separering i samma databas

När detta tillvägagångssätt väljs så ges konsolidering ur lådan och nu är den svåra delen separation. PostgreSQL erbjuder en uppsjö av lösningar till oss för att implementera separation:

  • Visningar
  • Säkerhet på rollnivå
  • Schema

Med vyer måste applikationen ställa in en frågebar inställning som applikationsnamn, vi gömmer huvudtabellen bakom en vy och sedan förenas i varje fråga på någon av de underordnade (som i FK-beroende) tabeller, om några, i denna huvudtabell med den här vyn. Vi kommer att se detta i följande exempel i en databas som vi kallar rentaldb_one. Vi bäddar in hyresgästföretagets identifiering i huvudtabellen:

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)
Ladda ner Whitepaper Today PostgreSQL Management &Automation med ClusterControlLäs om vad du behöver veta för att distribuera, övervaka, hantera och skala PostgreSQLDladda Whitepaper

Tabellkundernas schema förblir detsamma. Låt oss se det aktuella innehållet i databasen:

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)

Vi använder det nya namnet rental_one för att dölja detta bakom den nya vyn som kommer att ha samma namn på tabellen som applikationen förväntar sig:rental. Applikationen måste ställa in applikationsnamnet för att beteckna hyresgästen. Så i det här exemplet kommer vi att ha tre instanser av applikationen, en för bilar, en för båtar och en för högsta ledningen. Programnamnet är inställt som:

rentaldb_one=# set application_name to 'cars';

Vi skapar nu vyn:

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

Obs:Vi behåller samma kolumner och tabell-/vynamn som möjligt, nyckelpunkten i lösningar för flera klienter är att hålla sakerna oförändrade på applikationssidan och att ändringar ska vara minimala och hanterbara.

Låt oss göra några val:

rentaldb_one=# ställ in application_name till 'cars';

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)

Den 3:e instansen av applikationen som måste sätta applikationsnamnet till "alla" är avsedd att användas av högsta ledningen med sikte på hela databasen.

En mer robust lösning, säkerhetsmässigt, kan baseras på RLS (row level security). Först återställer vi namnet på tabellen, kom ihåg att vi inte vill störa applikationen:

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

Först skapar vi de två användargrupperna för varje företag (båtar, bilar) som måste se sin egen delmängd av data:

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

Vi skapar nu säkerhetspolicyer för varje grupp:

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

Efter att ha gett de erforderliga bidragen till de två rollerna:

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 ;

vi skapar en användare i varje roll

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;

Och testa:

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

Det fina med detta tillvägagångssätt är att vi inte behöver många instanser av applikationen. All isolering görs på databasnivå baserat på användarens roller. Allt vi behöver göra för att skapa en användare i högsta ledningen är därför att ge denna användare båda rollerna:

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)

När vi tittar på de två lösningarna ser vi att vylösningen kräver att det grundläggande tabellnamnet ändras, vilket kan vara ganska påträngande eftersom vi kan behöva köra exakt samma schema i en lösning som inte är multitenant, eller med en app som inte är medveten om applikationsnamn , medan den andra lösningen binder människor till specifika hyresgäster. Tänk om samma person jobbar t.ex. på båtens hyresgäst på morgonen och på bilhyresgästen på eftermiddagen? Vi kommer att se en tredje lösning baserad på scheman, som enligt min mening är den mest mångsidiga och inte lider av några av förbehållen för de två lösningarna som beskrivs ovan. Det låter applikationen köras på ett hyresgäst-agnostiskt sätt, och systemingenjörerna kan lägga till hyresgäster på språng när behov uppstår. Vi kommer att behålla samma design som tidigare, med samma testdata (vi kommer att fortsätta arbeta med rentaldb_one-exemplet db). Tanken här är att lägga till ett lager framför huvudtabellen i form av ett databasobjekt i ett separat schema som kommer att vara tillräckligt tidigt i sökvägen för den specifika hyresgästen. Sökvägen kan ställas in (helst via en speciell funktion, som ger fler alternativ) i anslutningskonfigurationen för datakällan på applikationsserverskiktet (därför utanför applikationskoden). Först skapar vi de två schemana:

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

Sedan skapar vi databasobjekten (vyerna) i varje 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';

Nästa steg är att ställa in sökvägen för varje hyresgäst enligt följande:

  • För båtens hyresgäst:

    set search_path TO 'boats, "$user", public';
  • För bilhyresgästen:

    set search_path TO 'cars, "$user", public';
  • Lämna det som standard för den högsta administratören

Låt oss testa:

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)
Relaterade resurser ClusterControl for PostgreSQL PostgreSQL-utlösare och grunder för lagrade funktioner Tuning Input/Output (I/O)-operationer för PostgreSQL

Istället för att ställa in sökväg kan vi skriva en mer komplex funktion för att hantera mer komplex logik och anropa denna i anslutningskonfigurationen för vår applikation eller anslutningspooler.

I exemplet ovan använde vi samma centrala tabell som finns på det offentliga schemat (public.rental) och två ytterligare vyer för varje hyresgäst, med det lyckliga faktumet att dessa två vyer är enkla och därför skrivbara. Istället för vyer kan vi använda arv genom att skapa en underordnad tabell för varje hyresgäst som ärver från den offentliga tabellen. Detta är en bra match för bordsarv, en unik egenskap hos PostgreSQL. Den översta tabellen kan vara konfigurerad med regler för att inte tillåta infogningar. I nedärvningslösningen skulle en konvertering behövas för att fylla i barntabellerna och för att förhindra infogningsåtkomst till den överordnade tabellen, så detta är inte så enkelt som i fallet med vyer, vilket fungerar med minimal påverkan på designen. Vi kanske skriver en speciell blogg om hur man gör det.

Ovanstående tre tillvägagångssätt kan kombineras för att ge ännu fler alternativ.


  1. välj * från tabell vs välj colA, colB, etc. från tabell intressant beteende i SQL Server 2005

  2. Hur skickar man ett argument till ett PL/SQL-block i en sql-fil som heter med START i sqlplus?

  3. Hur du gör din MySQL- eller MariaDB-databas mycket tillgänglig på AWS och Google Cloud

  4. psycopg2 läcker minne efter stor fråga