PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Options de mutualisation pour PostgreSQL

La multilocation dans un système logiciel est appelée la séparation des données selon un ensemble de critères afin de satisfaire un ensemble d'objectifs. L'ampleur/l'étendue, la nature et la mise en œuvre finale de cette séparation dépendent de ces critères et objectifs. La multilocation est essentiellement un cas de partitionnement de données, mais nous essaierons d'éviter ce terme pour des raisons évidentes (le terme dans PostgreSQL a une signification très spécifique et est réservé, car le partitionnement déclaratif de table a été introduit dans postgresql 10).

Les critères peuvent être :

  1. selon l'identifiant d'une table maître importante, qui symbolise l'identifiant du locataire qui peut représenter :
    1. une entreprise/organisation au sein d'un groupe holding plus large
    2. un service au sein d'une entreprise/organisation
    3. un bureau régional/succursale de la même entreprise/organisation
  2. selon l'emplacement/l'IP d'un utilisateur
  3. selon la position d'un utilisateur au sein de l'entreprise/organisation

Les objectifs pourraient être :

  1. séparation des ressources physiques ou virtuelles
  2. séparation des ressources système
  3. sécurité
  4. précision et commodité de gestion/utilisateurs aux différents niveaux de l'entreprise/organisation

Notez qu'en remplissant un objectif, nous remplissons également tous les objectifs ci-dessous, c'est-à-dire qu'en remplissant A, nous remplissons également B, C et D, en remplissant B, nous remplissons également C et D, et ainsi de suite.

Si nous voulons atteindre l'objectif A, nous pouvons choisir de déployer chaque locataire en tant que cluster de bases de données distinct au sein de son propre serveur physique/virtuel. Cela donne une séparation maximale des ressources et de la sécurité, mais donne de mauvais résultats lorsque nous devons voir toutes les données comme une seule, c'est-à-dire la vue consolidée de l'ensemble du système.

Si nous voulons uniquement atteindre l'objectif B, nous pouvons déployer chaque locataire en tant qu'instance postgresql distincte sur le même serveur. Cela nous donnerait un contrôle sur la quantité d'espace qui serait attribuée à chaque instance, ainsi qu'un certain contrôle (selon le système d'exploitation) sur l'utilisation du processeur/de la mémoire. Ce cas n'est pas essentiellement différent de A. À l'ère du cloud computing moderne, l'écart entre A et B a tendance à devenir de plus en plus petit, de sorte que A sera très probablement la voie préférée à B.

Si nous voulons atteindre l'objectif C, c'est-à-dire la sécurité, il suffit d'avoir une instance de base de données et de déployer chaque locataire en tant que base de données distincte.

Et enfin, si nous ne nous soucions que de la séparation "douce" des données, ou en d'autres termes de différentes vues du même système, nous pouvons y parvenir avec une seule instance de base de données et une seule base de données, en utilisant une pléthore de techniques discutées ci-dessous comme finale (et majeur) sujet de ce blog. En parlant de multi-location, du point de vue du DBA, les cas A, B et C présentent de nombreuses similitudes. En effet, dans tous les cas, nous avons des bases de données différentes et afin de relier ces bases de données, des outils et des technologies spéciaux doivent être utilisés. Cependant, si le besoin de le faire vient des départements d'analyse ou de Business Intelligence, aucun pontage n'est peut-être nécessaire, car les données pourraient très bien être répliquées sur un serveur central dédié à ces tâches, ce qui rendrait le pontage inutile. Si en effet un tel pontage est nécessaire alors nous devons utiliser des outils comme dblink ou des tables étrangères. Les tables étrangères via des wrappers de données étrangères sont aujourd'hui la méthode préférée.

Si nous utilisons l'option D, cependant, la consolidation est déjà donnée par défaut, donc maintenant la partie la plus difficile est à l'opposé :la séparation. Ainsi, nous pouvons généralement classer les différentes options en deux catégories principales :

  • Séparation douce
  • Séparation dure

Séparation dure via différentes bases de données dans le même cluster

Supposons que nous devions concevoir un système pour une entreprise imaginaire proposant des locations de voitures et de bateaux, mais parce que ces deux-là sont régis par des législations différentes, des contrôles et des audits différents, chaque entreprise doit maintenir des services comptables distincts et nous aimerions donc conserver leurs systèmes séparé. Dans ce cas nous choisissons d'avoir une base de données différente pour chaque compagnie :rentdb_cars et rentdb_boats, qui auront des schémas identiques :

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

Supposons que nous ayons les locations suivantes. Dans 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)

et en locationdb_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)

Maintenant, la direction souhaite avoir une vue consolidée du système, par ex. une manière unifiée de visualiser les locations. Nous pouvons résoudre ce problème via l'application, mais si nous ne voulons pas mettre à jour l'application ou si nous n'avons pas accès au code source, nous pouvons résoudre ce problème en créant une base de données centrale rentaldb et en utilisant des tables étrangères, comme suit :

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;

Afin de visualiser toutes les locations et les clients de toute l'organisation, nous faisons simplement :

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)

Cela semble bien, l'isolement et la sécurité sont garantis, la consolidation est réalisée, mais il y a toujours des problèmes :

  • les clients doivent être gérés séparément, ce qui signifie qu'un même client peut se retrouver avec deux comptes
  • L'application doit respecter la notion de colonne spéciale (telle que tenant_db) et l'ajouter à chaque requête, ce qui la rend sujette aux erreurs
  • Les vues résultantes ne sont pas automatiquement mises à jour (puisqu'elles contiennent UNION)

Séparation logicielle dans la même base de données

Lorsque cette approche est choisie, la consolidation est prête à l'emploi et maintenant la partie la plus difficile est la séparation. PostgreSQL nous offre une pléthore de solutions pour implémenter la séparation :

  • Vues
  • Sécurité au niveau du rôle
  • Schémas

Avec les vues, l'application doit définir un paramètre interrogeable tel que nom_application, nous cachons la table principale derrière une vue, puis dans chaque requête sur l'une des tables enfants (comme dans la dépendance FK), le cas échéant, de cette table principale jointe avec cette vue. Nous verrons cela dans l'exemple suivant dans une base de données que nous appelons Rentaldb_one. Nous intégrons l'identification de l'entreprise locataire dans le tableau principal :

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)
Téléchargez le livre blanc aujourd'hui PostgreSQL Management &Automation with ClusterControlDécouvrez ce que vous devez savoir pour déployer, surveiller, gérer et faire évoluer PostgreSQLTélécharger le livre blanc

Le schéma des clients de la table reste le même. Voyons le contenu actuel de la base de données :

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)

Nous utilisons le nouveau nom de location_one afin de le cacher derrière la nouvelle vue qui aura le même nom de table que l'application attend :location. L'application devra définir le nom de l'application pour désigner le locataire. Ainsi, dans cet exemple, nous aurons trois instances de l'application, une pour les voitures, une pour les bateaux et une pour la haute direction. Le nom de l'application est défini comme :

rentaldb_one=# set application_name to 'cars';

Nous créons maintenant la vue :

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

Remarque :Nous gardons les mêmes colonnes et les mêmes noms de table/vue que possible, le point clé dans les solutions multi-locataires est de garder les mêmes choses du côté de l'application, et les changements doivent être minimes et gérables.

Faisons quelques sélections :

Rentaldb_one=# définir application_name sur 'voitures' ;

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)

La 3ème instance de l'application qui doit définir le nom de l'application sur "tout" est destinée à être utilisée par la direction générale en vue de l'ensemble de la base de données.

Une solution plus robuste, du point de vue de la sécurité, peut être basée sur RLS (row level security). On restaure d'abord le nom de la table, rappelez-vous que nous ne voulons pas perturber l'application :

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

Nous créons d'abord les deux groupes d'utilisateurs pour chaque entreprise (bateaux, voitures) qui doivent voir leur propre sous-ensemble de données :

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

Nous créons maintenant des politiques de sécurité pour chaque groupe :

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

Après avoir accordé les subventions requises aux deux rôles :

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 ;

nous créons un utilisateur dans chaque rôle

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;

Et testez :

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

La bonne chose avec cette approche est que nous n'avons pas besoin de beaucoup d'instances de l'application. Toute l'isolation est effectuée au niveau de la base de données en fonction des rôles de l'utilisateur. Par conséquent, pour créer un utilisateur dans le top management, il suffit d'attribuer les deux rôles à cet utilisateur :

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)

En regardant ces deux solutions, nous voyons que la solution de vue nécessite de changer le nom de la table de base, ce qui peut être assez intrusif dans la mesure où nous devrons peut-être exécuter exactement le même schéma dans une solution non mutualisée ou avec une application qui n'est pas consciente de nom_application , tandis que la deuxième solution lie les gens à des locataires spécifiques. Que se passe-t-il si la même personne travaille par ex. sur les bateaux locataires le matin et sur les voitures locataires l'après-midi ? Nous verrons une 3ème solution basée sur des schémas, qui à mon avis est la plus polyvalente, et ne souffre d'aucune des réserves des deux solutions décrites ci-dessus. Il permet à l'application de s'exécuter de manière indépendante du locataire et aux ingénieurs système d'ajouter des locataires à tout moment en fonction des besoins. Nous garderons le même design qu'avant, avec les mêmes données de test (nous continuerons à travailler sur l'exemple de base de données Rentaldb_one). L'idée ici est d'ajouter une couche devant la table principale sous la forme d'un objet de base de données dans un schéma séparé qui sera assez tôt dans le search_path pour ce locataire spécifique. Le search_path peut être défini (idéalement via une fonction spéciale, qui donne plus d'options) dans la configuration de connexion de la source de données au niveau du serveur d'application (donc en dehors du code de l'application). Nous créons d'abord les deux schémas :

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

Ensuite, nous créons les objets de la base de données (vues) dans chaque schéma :

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

L'étape suivante consiste à définir le chemin de recherche dans chaque locataire comme suit :

  • Pour le locataire des bateaux :

    set search_path TO 'boats, "$user", public';
  • Pour le locataire des voitures :

    set search_path TO 'cars, "$user", public';
  • Pour le locataire de gestion supérieur, laissez-le par défaut

Testons :

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)
Ressources associées ClusterControl pour PostgreSQL Déclencheurs PostgreSQL et principes de base des fonctions stockées Réglage des opérations d'entrée/sortie (E/S) pour PostgreSQL

Au lieu de set search_path, nous pouvons écrire une fonction plus complexe pour gérer une logique plus complexe et l'appeler dans la configuration de connexion de notre application ou pooler de connexion.

Dans l'exemple ci-dessus, nous avons utilisé la même table centrale résidant sur le schéma public (public.rental) et deux vues supplémentaires pour chaque locataire, en utilisant le fait heureux que ces deux vues sont simples et donc inscriptibles. Au lieu de vues, nous pouvons utiliser l'héritage, en créant une table enfant pour chaque locataire héritant de la table publique. C'est une bonne correspondance pour l'héritage de table, une fonctionnalité unique de PostgreSQL. La table supérieure peut être configurée avec des règles pour interdire les insertions. Dans la solution d'héritage, une conversion serait nécessaire pour remplir les tables enfants et pour empêcher l'accès par insertion à la table parent, ce n'est donc pas aussi simple que dans le cas des vues, qui fonctionnent avec un impact minimal sur la conception. Nous pourrions écrire un blog spécial sur la façon de procéder.

Les trois approches ci-dessus peuvent être combinées pour offrir encore plus d'options.