Multi-lejemål i et softwaresystem kaldes adskillelse af data i henhold til et sæt kriterier for at opfylde et sæt mål. Størrelsen/udstrækningen, arten og den endelige implementering af denne adskillelse afhænger af disse kriterier og mål. Multi-tenancy er dybest set et tilfælde af datapartitionering, men vi vil forsøge at undgå dette udtryk af indlysende årsager (udtrykket i PostgreSQL har en meget specifik betydning og er reserveret, da deklarativ tabelpartitionering blev introduceret i postgresql 10).
Kriterierne kan være:
- i henhold til id'et for en vigtig mastertabel, som symboliserer lejer-id'et, som kan repræsentere:
- en virksomhed/organisation inden for en større holdingkoncern
- en afdeling i en virksomhed/organisation
- et regionalt kontor/en filial af samme virksomhed/organisation
- i henhold til en brugers placering/IP
- i henhold til en brugers position i virksomheden/organisationen
Målene kan være:
- adskillelse af fysiske eller virtuelle ressourcer
- adskillelse af systemressourcer
- sikkerhed
- nøjagtighed og bekvemmelighed for ledelsen/brugerne på de forskellige niveauer i virksomheden/organisationen
Bemærk ved at opfylde et mål opfylder vi også alle nedenstående mål, dvs. ved at opfylde A opfylder vi også B, C og D, ved at opfylde B opfylder vi også C og D, og så videre.
Hvis vi ønsker at opfylde mål A, kan vi vælge at implementere hver lejer som en separat databaseklynge i sin egen fysiske/virtuelle server. Dette giver maksimal adskillelse af ressourcer og sikkerhed, men giver dårlige resultater, når vi skal se hele dataene under ét, dvs. det konsoliderede syn på hele systemet.
Hvis vi kun ønsker at nå mål B, kan vi implementere hver lejer som en separat postgresql-instans på den samme server. Dette ville give os kontrol over, hvor meget plads der ville blive tildelt til hver instans, og også en vis kontrol (afhængigt af OS) på CPU/mem-udnyttelse. Dette tilfælde er ikke væsentligt anderledes end A. I den moderne cloud computing-æra har afstanden mellem A og B en tendens til at blive mindre og mindre, så A vil højst sandsynligt være den foretrukne vej frem for B.
Hvis vi ønsker at opnå mål C, dvs. sikkerhed, så er det nok at have én databaseinstans og implementere hver lejer som en separat database.
Og endelig, hvis vi kun bekymrer os om "blød" adskillelse af data, eller med andre ord forskellige visninger af det samme system, kan vi opnå dette ved blot én databaseinstans og én database ved at bruge et væld af teknikker, der diskuteres nedenfor som den endelige (og hovedemnet på denne blog. Taler man om flerlejemål, set fra DBA’s perspektiv, har sag A, B og C mange ligheder. Det skyldes, at vi i alle tilfælde har forskellige databaser, og for at bygge bro over disse databaser, så skal der bruges specielle værktøjer og teknologier. Men hvis behovet for at gøre det kommer fra analyse- eller Business Intelligence-afdelingerne, er der måske slet ikke behov for brobygning, da dataene meget vel kunne replikeres til en central server dedikeret til disse opgaver, hvilket gør brobygning unødvendig. Hvis en sådan brodannelse virkelig er nødvendig, skal vi bruge værktøjer som dblink eller udenlandske tabeller. Udenlandske tabeller via Foreign Data Wrappers er i dag den foretrukne måde.
Hvis vi bruger mulighed D, så er konsolidering allerede givet som standard, så nu er den svære del det modsatte:adskillelse. Så vi kan generelt kategorisere de forskellige muligheder i to hovedkategorier:
- Blød adskillelse
- Hård adskillelse
Hård adskillelse via forskellige databaser i samme klynge
Lad os antage, at vi skal designe et system til en imaginær virksomhed, der tilbyder bil- og bådudlejning, men fordi de to er styret af forskellig lovgivning, forskellige kontroller, revisioner, skal hver virksomhed have separate regnskabsafdelinger, og derfor vil vi gerne beholde deres systemer adskilt. I dette tilfælde vælger vi at have en anden database for hvert firma:rentaldb_cars og rentaldb_boats, som vil have identiske skemaer:
# \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)
Lad os antage, at vi har følgende lejemål. I lejedb_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)
og 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 vil ledelsen gerne have et samlet syn på systemet, f.eks. en samlet måde at se lejemålene på. Vi kan løse dette via applikationen, men hvis vi ikke ønsker at opdatere applikationen eller ikke har adgang til kildekoden, så løser vi måske dette ved at oprette en central database rentaldb og ved at gøre brug af udenlandske tabeller, som følger:
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;
For at se alle lejemål og kunder i hele organisationen gør vi blot:
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)
Dette ser godt ud, isolation og sikkerhed er garanteret, konsolidering er opnået, men der er stadig problemer:
- kunder skal vedligeholdes separat, hvilket betyder, at den samme kunde kan ende med to konti
- Applikationen skal respektere begrebet en speciel kolonne (såsom tenant_db) og føje denne til hver forespørgsel, hvilket gør den udsat for fejl
- De resulterende visninger kan ikke automatisk opdateres (da de indeholder UNION)
Blød adskillelse i den samme database
Når denne tilgang er valgt, er konsolidering givet ud af boksen, og nu er den svære del adskillelse. PostgreSQL tilbyder et væld af løsninger til os for at implementere separation:
- Visninger
- Sikkerhed på rolleniveau
- Skemaer
Med visninger skal applikationen angive en indstilling, der kan forespørges på, såsom applikationsnavn, vi skjuler hovedtabellen bag en visning, og i hver forespørgsel på enhver af de underordnede (som i FK-afhængighed) tabeller, hvis nogen, i denne hovedtabel slutter sig til denne udsigt. Vi vil se dette i det følgende eksempel i en database, vi kalder rentaldb_one. Vi indlejrer lejerfirmaets identifikation i hovedtabellen:
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 Whitepaper Today PostgreSQL Management &Automation med ClusterControlFå flere oplysninger om, hvad du skal vide for at implementere, overvåge, administrere og skalere PostgreSQLDownload Whitepaper Tabellkundernes skema forbliver det samme. Lad os se det aktuelle indhold af 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 bruger det nye navn rental_one for at skjule dette bag den nye visning, som vil have samme navn på tabellen, som applikationen forventer:leje. Ansøgningen skal indstille applikationsnavnet til at angive lejeren. Så i dette eksempel vil vi have tre forekomster af applikationen, en til biler, en til både og en til topledelsen. Applikationsnavnet er indstillet som:
rentaldb_one=# set application_name to 'cars';
Vi opretter nu visningen:
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');
Bemærk:Vi beholder de samme kolonner og tabel-/visningsnavne som muligt, nøglepunktet i løsninger med flere lejere er at holde tingene ens i applikationssiden, og ændringer skal være minimale og overskuelige.
Lad os foretage nogle valg:
rentaldb_one=# sæt application_name til '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. instans af applikationen, som skal indstille applikationsnavnet til "alle", er beregnet til brug af topledelsen med henblik på hele databasen.
En mere robust løsning, sikkerhedsmæssigt, kan være baseret på RLS (row level security). Først gendanner vi navnet på tabellen, husk, at vi ikke vil forstyrre applikationen:
rentaldb_one=# alter view rental rename to rental_view;
rentaldb_one=# alter table rental_one rename TO rental;
Først opretter vi de to grupper af brugere for hver virksomhed (både, biler), som skal se deres egen delmængde af dataene:
rentaldb_one=# create role cars_employees;
rentaldb_one=# create role boats_employees;
Vi opretter nu sikkerhedspolitikker for hver gruppe:
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 at have givet de nødvendige tilskud til de to roller:
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 opretter én bruger i hver rolle
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;
Og 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=>
Det gode ved denne tilgang er, at vi ikke har brug for mange forekomster af applikationen. Al isolering udføres på databaseniveau baseret på brugerens roller. Derfor skal vi kun tildele denne bruger begge roller for at oprette en bruger i topledelsen:
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 ser på de to løsninger, ser vi, at visningsløsningen kræver ændring af det grundlæggende tabelnavn, hvilket kan være ret påtrængende, idet vi muligvis skal køre nøjagtigt det samme skema i en ikke-multitenant-løsning eller med en app, der ikke er opmærksom på applikationsnavn , mens den anden løsning binder folk til bestemte lejere. Hvad hvis den samme person arbejder f.eks. på bådelejeren om morgenen og på bilernes lejer om eftermiddagen? Vi vil se en 3. løsning baseret på skemaer, som efter min mening er den mest alsidige og ikke lider under nogen af forbeholdene ved de to løsninger beskrevet ovenfor. Det giver applikationen mulighed for at køre på en lejer-agnostisk måde, og systemingeniørerne kan tilføje lejere på farten, efterhånden som behov opstår. Vi vil beholde det samme design som før, med de samme testdata (vi fortsætter med at arbejde på lejedb_one eksempel db). Ideen her er at tilføje et lag foran hovedtabellen i form af et databaseobjekt i et separat skema som vil være tidligt nok i søgestien for den pågældende lejer. Search_path kan indstilles (ideelt set via en speciel funktion, som giver flere muligheder) i forbindelseskonfigurationen af datakilden på applikationsserverlaget (derfor uden for applikationskoden). Først opretter vi de to skemaer:
rentaldb_one=# create schema cars;
rentaldb_one=# create schema boats;
Derefter opretter vi databaseobjekterne (visningerne) i hvert skema:
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æste trin er at indstille søgestien i hver lejer som følger:
-
For bådens lejer:
set search_path TO 'boats, "$user", public';
-
Til billejeren:
set search_path TO 'cars, "$user", public';
- For den øverste forvaltningslejer lad det være som standard
Lad os teste:
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)
Relaterede ressourcer ClusterControl for PostgreSQL PostgreSQL-triggere og grundlæggende funktioner Tuning Input/Output (I/O)-operationer til PostgreSQL I stedet for at indstille søgesti kan vi skrive en mere kompleks funktion til at håndtere mere kompleks logik og kalde denne i forbindelseskonfigurationen af vores applikation eller forbindelsespooler.
I eksemplet ovenfor brugte vi den samme centrale tabel, der ligger på det offentlige skema (public.rental) og to yderligere visninger for hver lejer, idet vi brugte det heldige faktum, at disse to visninger er enkle og derfor skrivbare. I stedet for visninger kan vi bruge arv ved at oprette én underordnet tabel for hver lejer, der arver fra den offentlige tabel. Dette er et fint match til bordarv, en unik egenskab ved PostgreSQL. Den øverste tabel kan være konfigureret med regler, der ikke tillader indsættelser. I arveløsningen ville der være behov for en konvertering for at udfylde børnetabellerne og for at forhindre indsættelsesadgang til den overordnede tabel, så dette er ikke så simpelt som i tilfældet med visninger, der virker med minimal indvirkning på designet. Vi kan skrive en særlig blog om, hvordan man gør det.
Ovenstående tre tilgange kan kombineres for at give endnu flere muligheder.