Mandantenfähigkeit in einem Softwaresystem wird als Trennung von Daten nach einer Reihe von Kriterien bezeichnet, um eine Reihe von Zielen zu erfüllen. Das Ausmaß/der Umfang, die Art und die endgültige Umsetzung dieser Trennung hängen von diesen Kriterien und Zielen ab. Multi-Tenancy ist im Grunde ein Fall von Datenpartitionierung, aber wir werden versuchen, diesen Begriff aus offensichtlichen Gründen zu vermeiden (der Begriff in PostgreSQL hat eine sehr spezifische Bedeutung und ist reserviert, da die deklarative Tabellenpartitionierung in postgresql 10 eingeführt wurde).
Die Kriterien könnten sein:
- nach der ID einer wichtigen Haupttabelle, die die Mandanten-ID symbolisiert, die Folgendes darstellen könnte:
- ein Unternehmen/eine Organisation innerhalb einer größeren Holdinggruppe
- eine Abteilung innerhalb eines Unternehmens/einer Organisation
- eine regionale Niederlassung/Zweigstelle derselben Firma/Organisation
- nach Standort/IP eines Nutzers
- entsprechend der Position eines Benutzers innerhalb des Unternehmens/der Organisation
Die Ziele könnten sein:
- Trennung physischer oder virtueller Ressourcen
- Trennung von Systemressourcen
- Sicherheit
- Genauigkeit und Komfort der Verwaltung/Benutzer auf den verschiedenen Ebenen des Unternehmens/der Organisation
Beachten Sie, dass wir durch das Erfüllen eines Ziels auch alle darunter liegenden Ziele erfüllen, d. h. durch das Erfüllen von A erfüllen wir auch B, C und D, durch das Erfüllen von B erfüllen wir auch C und D und so weiter.
Wenn wir Ziel A erfüllen möchten, können wir uns dafür entscheiden, jeden Mandanten als separaten Datenbankcluster innerhalb seines eigenen physischen/virtuellen Servers bereitzustellen. Dies bietet eine maximale Trennung von Ressourcen und Sicherheit, führt jedoch zu schlechten Ergebnissen, wenn wir die gesamten Daten als Einheit sehen müssen, d. h. die konsolidierte Ansicht des gesamten Systems.
Wenn wir nur Ziel B erreichen wollen, könnten wir jeden Mandanten als separate Postgresql-Instanz auf demselben Server bereitstellen. Dies würde uns die Kontrolle darüber geben, wie viel Speicherplatz jeder Instanz zugewiesen wird, und auch eine gewisse Kontrolle (je nach Betriebssystem) über die CPU-/Arbeitsspeicherauslastung. Dieser Fall unterscheidet sich nicht wesentlich von A. In der modernen Cloud-Computing-Ära wird der Abstand zwischen A und B tendenziell immer kleiner, sodass A höchstwahrscheinlich der bevorzugte Weg gegenüber B sein wird.
Wenn wir Ziel C, also Sicherheit, erreichen wollen, reicht es aus, eine Datenbankinstanz zu haben und jeden Mandanten als separate Datenbank bereitzustellen.
Und schließlich, wenn wir uns nur um eine „weiche“ Trennung von Daten kümmern, oder mit anderen Worten, unterschiedliche Ansichten desselben Systems, können wir dies mit nur einer Datenbankinstanz und einer Datenbank erreichen, indem wir eine Fülle von Techniken verwenden, die unten als letzte (und Hauptthema dieses Blogs. Apropos Mandantenfähigkeit:Aus Sicht des DBA weisen die Fälle A, B und C viele Ähnlichkeiten auf. Dies liegt daran, dass wir in allen Fällen unterschiedliche Datenbanken haben und um diese Datenbanken zu überbrücken, müssen spezielle Tools und Technologien verwendet werden. Wenn dies jedoch von den Analyse- oder Business-Intelligence-Abteilungen erforderlich ist, ist möglicherweise überhaupt kein Bridging erforderlich, da die Daten sehr gut auf einen zentralen Server repliziert werden könnten, der diesen Aufgaben gewidmet ist, wodurch Bridging unnötig wird. Wenn tatsächlich eine solche Überbrückung benötigt wird, müssen wir Tools wie dblink oder Foreign Tables verwenden. Fremde Tabellen über Foreign Data Wrapper sind heutzutage der bevorzugte Weg.
Wenn wir jedoch Option D verwenden, dann ist Konsolidierung bereits standardmäßig gegeben, also ist jetzt der schwierige Teil das Gegenteil:Trennung. Daher können wir die verschiedenen Optionen im Allgemeinen in zwei Hauptkategorien einteilen:
- Weiche Trennung
- Harte Trennung
Harte Trennung über verschiedene Datenbanken im selben Cluster
Nehmen wir an, wir müssen ein System für ein imaginäres Unternehmen entwerfen, das Auto- und Bootsvermietungen anbietet, aber da diese beiden unterschiedlichen Gesetzen, Kontrollen und Prüfungen unterliegen, muss jedes Unternehmen separate Buchhaltungsabteilungen unterhalten, und daher möchten wir ihre Systeme behalten getrennt. In diesem Fall wählen wir für jedes Unternehmen eine andere Datenbank:rentaldb_cars und rentaldb_boats, die identische Schemata haben:
# \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)
Nehmen wir an, wir haben die folgenden Mieten. 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)
und 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)
Nun möchte das Management eine konsolidierte Sicht auf das System haben, z.B. eine einheitliche Möglichkeit, die Mietobjekte anzuzeigen. Wir können dies über die Anwendung lösen, aber wenn wir die Anwendung nicht aktualisieren möchten oder keinen Zugriff auf den Quellcode haben, können wir dies möglicherweise lösen, indem wir eine zentrale Datenbank rentaldb erstellen und durch Verwendung von Fremdtabellen wie folgt:
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;
Um alle Vermietungen und Kunden in der gesamten Organisation anzuzeigen, machen wir einfach:
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)
Das sieht gut aus, Isolation und Sicherheit sind gewährleistet, Konsolidierung ist erreicht, aber es gibt immer noch Probleme:
- Kunden müssen separat verwaltet werden, was bedeutet, dass derselbe Kunde möglicherweise zwei Konten hat
- Die Anwendung muss das Konzept einer speziellen Spalte (z. B. tenant_db) berücksichtigen und diese an jede Abfrage anhängen, wodurch sie fehleranfällig wird
- Die resultierenden Ansichten sind nicht automatisch aktualisierbar (da sie UNION enthalten)
Weiche Trennung in derselben Datenbank
Wenn dieser Ansatz gewählt wird, dann ist die Konsolidierung sofort gegeben und jetzt ist der schwierige Teil die Trennung. PostgreSQL bietet uns eine Fülle von Lösungen, um die Trennung zu implementieren:
- Aufrufe
- Sicherheit auf Rollenebene
- Schemata
Bei Ansichten muss die Anwendung eine abfragbare Einstellung wie Anwendungsname festlegen, die Haupttabelle hinter einer Ansicht verstecken und dann in jeder Abfrage auf eine der untergeordneten Tabellen (wie in der FK-Abhängigkeit) dieser Haupttabelle, falls vorhanden, mitverknüpfen diese Aussicht. Wir werden dies im folgenden Beispiel in einer Datenbank sehen, die wir rentaldb_one nennen. Wir betten die Firmenidentifikation des Mieters in die Haupttabelle ein:
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)
Laden Sie noch heute das Whitepaper PostgreSQL-Verwaltung und -Automatisierung mit ClusterControl herunterErfahren Sie, was Sie wissen müssen, um PostgreSQL bereitzustellen, zu überwachen, zu verwalten und zu skalierenLaden Sie das Whitepaper herunter Das Schema der Tischkunden bleibt gleich. Sehen wir uns den aktuellen Inhalt der Datenbank an:
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)
Wir verwenden den neuen Namen rental_one, um dies hinter der neuen Ansicht zu verbergen, die denselben Namen wie die Tabelle haben wird, die die Anwendung erwartet:rental. Die Anwendung muss den Anwendungsnamen so festlegen, dass er den Mieter bezeichnet. In diesem Beispiel haben wir also drei Instanzen der Anwendung, eine für Autos, eine für Boote und eine für das Top-Management. Der Anwendungsname wird wie folgt festgelegt:
rentaldb_one=# set application_name to 'cars';
Wir erstellen nun die Ansicht:
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');
Hinweis:Wir behalten die gleichen Spalten und Tabellen-/Ansichtsnamen so weit wie möglich bei. Der Schlüsselpunkt bei Multi-Tenant-Lösungen besteht darin, die Dinge auf der Anwendungsseite gleich zu halten und die Änderungen minimal und überschaubar zu halten.
Lassen Sie uns einige Auswahlen treffen:
rentaldb_one=# Anwendungsname auf 'Autos' setzen;
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)
Die 3. Instanz der Anwendung, die den Anwendungsnamen auf „all“ setzen muss, ist für die Verwendung durch das Top-Management im Hinblick auf die gesamte Datenbank vorgesehen.
Eine robustere Lösung in Bezug auf die Sicherheit kann auf RLS (Sicherheit auf Zeilenebene) basieren. Zuerst stellen wir den Namen der Tabelle wieder her, denken Sie daran, dass wir die Anwendung nicht stören wollen:
rentaldb_one=# alter view rental rename to rental_view;
rentaldb_one=# alter table rental_one rename TO rental;
Zuerst erstellen wir die beiden Benutzergruppen für jedes Unternehmen (Boote, Autos), die ihre eigene Teilmenge der Daten sehen müssen:
rentaldb_one=# create role cars_employees;
rentaldb_one=# create role boats_employees;
Wir erstellen jetzt Sicherheitsrichtlinien für jede 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');
Nachdem Sie den beiden Rollen die erforderlichen Berechtigungen erteilt haben:
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 ;
Wir erstellen einen Benutzer in jeder 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;
Und teste:
[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=>
Das Schöne an diesem Ansatz ist, dass wir nicht viele Instanzen der Anwendung benötigen. Die gesamte Isolierung erfolgt auf Datenbankebene basierend auf den Rollen des Benutzers. Um also einen Benutzer im Top-Management anzulegen, müssen wir diesem Benutzer nur beide Rollen zuweisen:
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)
Wenn wir uns diese beiden Lösungen ansehen, sehen wir, dass die Ansichtslösung eine Änderung des grundlegenden Tabellennamens erfordert, was ziemlich aufdringlich sein kann, da wir möglicherweise genau dasselbe Schema in einer nicht mandantenfähigen Lösung oder mit einer App ausführen müssen, die
rentaldb_one=# create schema cars;
rentaldb_one=# create schema boats;
Dann erstellen wir die Datenbankobjekte (Views) in jedem 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';
Der nächste Schritt besteht darin, den Suchpfad in jedem Mandanten wie folgt festzulegen:
-
Für den Bootsmieter:
set search_path TO 'boats, "$user", public';
-
Für den Automieter:
set search_path TO 'cars, "$user", public';
- Belassen Sie für den obersten Verwaltungsmandanten die Standardeinstellung
Testen wir:
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)
Zugehörige Ressourcen ClusterControl for PostgreSQL PostgreSQL-Trigger und Grundlagen zu gespeicherten Funktionen Optimieren von Eingabe-/Ausgabe-(E/A)-Vorgängen für PostgreSQL Anstelle von set search_path können wir eine komplexere Funktion schreiben, um eine komplexere Logik zu handhaben, und diese in der Verbindungskonfiguration unserer Anwendung oder unseres Verbindungspoolers aufrufen.
Im obigen Beispiel haben wir dieselbe zentrale Tabelle verwendet, die sich auf dem öffentlichen Schema (public.rental) und zwei zusätzlichen Ansichten für jeden Mandanten befindet, wobei wir uns glücklicherweise zunutze gemacht haben, dass diese beiden Ansichten einfach und daher beschreibbar sind. Anstelle von Ansichten können wir Vererbung verwenden, indem wir eine untergeordnete Tabelle für jeden Mandanten erstellen, der von der öffentlichen Tabelle erbt. Dies ist eine gute Übereinstimmung für die Tabellenvererbung, eine einzigartige Funktion von PostgreSQL. Die obere Tabelle kann mit Regeln konfiguriert werden, um Einfügungen zu verbieten. In der Vererbungslösung wäre eine Konvertierung erforderlich, um die untergeordneten Tabellen zu füllen und den Einfügezugriff auf die übergeordnete Tabelle zu verhindern, daher ist dies nicht so einfach wie im Fall mit Ansichten, was mit minimalen Auswirkungen auf das Design funktioniert. Wir könnten einen speziellen Blog darüber schreiben, wie man das macht.
Die oben genannten drei Ansätze können kombiniert werden, um noch mehr Optionen zu bieten.