ソフトウェアシステムのマルチテナンシーは、一連の目的を満たすための一連の基準に従ったデータの分離と呼ばれます。この分離の規模/拡張、性質、および最終的な実装は、これらの基準と目的によって異なります。マルチテナンシーは基本的にデータパーティショニングの場合ですが、明らかな理由でこの用語を避けようとします(PostgreSQLの用語は非常に特殊な意味を持ち、宣言型テーブルパーティショニングがpostgresql 10で導入されたため、予約されています)。
基準は次のとおりです。
- 重要なマスターテーブルのIDによると、これは次のことを表す可能性のあるテナントIDを表します。
- より大きな保有グループ内の会社/組織
- 会社/組織内の部門
- 同じ会社/組織の地域事務所/支店
- ユーザーの場所/IPによる
- 企業/組織内のユーザーの位置に応じて
目的は次のとおりです。
- 物理リソースまたは仮想リソースの分離
- システムリソースの分離
- セキュリティ
- 会社/組織のさまざまなレベルでの管理者/ユーザーの正確さと利便性
目標を達成することで、以下のすべての目標も達成することに注意してください。つまり、Aを達成することで、B、C、Dも達成し、Bを達成することで、CとDも達成します。
目標Aを達成したい場合は、各テナントを独自の物理/仮想サーバー内の個別のデータベースクラスターとして展開することを選択できます。これにより、リソースとセキュリティを最大限に分離できますが、データ全体を1つとして表示する必要がある場合、つまりシステム全体の統合ビューを表示する必要がある場合は、結果が悪くなります。
目標Bのみを達成したい場合は、各テナントを同じサーバー内の個別のpostgresqlインスタンスとしてデプロイできます。これにより、各インスタンスに割り当てられるスペースの量を制御したり、CPU /メモリ使用率を(OSに応じて)制御したりできます。このケースは本質的にAと違いはありません。現代のクラウドコンピューティングの時代では、AとBの間のギャップはますます小さくなる傾向があるため、おそらくAがBよりも好まれる方法です。
Objective C、つまりセキュリティを実現したい場合は、1つのデータベースインスタンスを用意し、各テナントを個別のデータベースとしてデプロイするだけで十分です。
そして最後に、データの「ソフトな」分離、つまり同じシステムの異なるビューのみを気にする場合は、以下で説明する多数の手法を最終的なものとして使用して、1つのデータベースインスタンスと1つのデータベースでこれを実現できます。メジャー)このブログのトピック。マルチテナンシーについて言えば、DBAの観点からすると、ケースA、B、Cには多くの類似点があります。これは、すべての場合に異なるデータベースがあり、それらのデータベースをブリッジするために、特別なツールとテクノロジーを使用する必要があるためです。ただし、分析部門またはビジネスインテリジェンス部門からそうする必要がある場合は、データをこれらのタスク専用の中央サーバーに非常によく複製できるため、ブリッジングはまったく必要ありません。ブリッジングは不要です。実際にそのようなブリッジングが必要な場合は、dblinkや外部テーブルなどのツールを使用する必要があります。最近では、ForeignDataWrappersを介した外部テーブルが推奨される方法です。
ただし、オプションDを使用する場合、統合はデフォルトですでに提供されているため、難しい部分は反対です。分離です。したがって、通常、さまざまなオプションを2つの主要なカテゴリに分類できます。
- ソフトセパレーション
- ハードセパレーション
同じクラスター内の異なるデータベースを介したハード分離
車やボートのレンタルを提供する架空のビジネスのシステムを設計する必要があると仮定しますが、これら2つは異なる法律、異なる管理、監査によって管理されているため、各企業は別々の経理部門を維持する必要があります。したがって、システムを維持したいと考えています。分離。この場合、会社ごとに異なるデータベースを使用することを選択します。rentaldb_carsとrentaldb_boatsは、同一のスキーマを持ちます:
# \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)
次の賃貸物件があるとしましょう。 Rentalsdb_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)
および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)
ここで、経営陣はシステムの統合されたビューを持ちたいと考えています。レンタルを表示するための統一された方法。これはアプリケーションを介して解決する場合がありますが、アプリケーションを更新したくない場合、またはソースコードにアクセスできない場合は、中央データベースを作成することで解決できます Rentalsdb 次のように、外部テーブルを利用します。
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;
組織全体のすべてのレンタルと顧客を表示するには、次のようにします。
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)
これは良さそうです。分離とセキュリティが保証され、統合が実現されますが、それでも問題があります:
- 顧客は個別に管理する必要があります。つまり、同じ顧客が2つのアカウントを持つことになります
- アプリケーションは、特別な列(tenant_dbなど)の概念を尊重し、これをすべてのクエリに追加して、エラーが発生しやすくする必要があります
- 結果のビューは自動的に更新できません(UNIONが含まれているため)
同じデータベースでのソフト分離
このアプローチを選択すると、すぐに統合が可能になり、難しい部分は分離になります。 PostgreSQLは、分離を実装するために多数のソリューションを提供しています。
- ビュー
- 役割レベルのセキュリティ
- スキーマ
ビューの場合、アプリケーションはapplication_nameなどのクエリ可能な設定を設定する必要があります。メインテーブルをビューの背後に非表示にしてから、(FK依存関係のように)子テーブルのすべてのクエリで、このメインテーブルのテーブルが結合します。このビュー。これは、rentaldb_oneと呼ばれるデータベースの次の例で確認できます。テナント会社のIDをメインテーブルに埋め込みます:
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)
今日のホワイトペーパーをダウンロードするClusterControlを使用したPostgreSQLの管理と自動化PostgreSQLの導入、監視、管理、スケーリングを行うために知っておくべきことについて学ぶホワイトペーパーをダウンロードする テーブルの顧客のスキーマは同じままです。データベースの現在の内容を見てみましょう:
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)
これを新しいビューの背後に隠すために、新しい名前rental_oneを使用します。このビューは、アプリケーションが期待するテーブルと同じ名前であるレンタルです。アプリケーションは、テナントを示すためにアプリケーション名を設定する必要があります。したがって、この例では、アプリケーションの3つのインスタンスがあります。1つは自動車用、1つはボート用、もう1つは経営幹部用です。アプリケーション名は次のように設定されます:
rentaldb_one=# set application_name to 'cars';
ビューを作成します:
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');
注:同じ列とテーブル/ビュー名を可能な限り維持します。マルチテナントソリューションの重要なポイントは、アプリケーション側で同じものを維持し、変更を最小限に抑えて管理できるようにすることです。
いくつか選択してみましょう:
Rentalsdb_one =#application_nameを「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)
アプリケーション名を「all」に設定する必要があるアプリケーションの3番目のインスタンスは、データベース全体を視野に入れて経営トップが使用することを目的としています。
セキュリティ面でのより堅牢なソリューションは、RLS(行レベルのセキュリティ)に基づく場合があります。まず、テーブルの名前を復元します。アプリケーションの邪魔をしたくないことを忘れないでください:
rentaldb_one=# alter view rental rename to rental_view;
rentaldb_one=# alter table rental_one rename TO rental;
まず、会社ごとに2つのユーザーグループ(ボート、車)を作成します。これらのユーザーは、データの独自のサブセットを確認する必要があります。
rentaldb_one=# create role cars_employees;
rentaldb_one=# create role boats_employees;
グループごとにセキュリティポリシーを作成します:
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');
2つの役割に必要な助成金を与えた後:
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 ;
各役割に1人のユーザーを作成します
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;
そしてテスト:
[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=>
このアプローチの良いところは、アプリケーションのインスタンスをあまり必要としないことです。すべての分離は、ユーザーの役割に基づいてデータベースレベルで行われます。したがって、トップマネジメントにユーザーを作成するには、このユーザーに両方の役割を付与するだけです。
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)
これらの2つのソリューションを見ると、ビューソリューションでは基本テーブル名を変更する必要があることがわかります。これは、非マルチテナントソリューションで、または<を認識しないアプリでまったく同じスキーマを実行する必要があるという点でかなり煩わしい場合があります。 em> application_name 、2番目のソリューションは、人々を特定のテナントにバインドします。同じ人が働いている場合はどうなりますか?午前中はボートのテナントで、午後は車のテナントで?スキーマに基づく3番目のソリューションが表示されます。これは、私の意見では最も用途が広く、上記の2つのソリューションの警告のいずれにも影響されません。これにより、アプリケーションをテナントに依存しない方法で実行でき、システムエンジニアは必要に応じて外出先でテナントを追加できます。以前と同じ設計を維持し、同じテストデータを使用します(rentaldb_oneサンプルデータベースで作業を続けます)。ここでの考え方は、メインテーブルの前に別のスキーマのデータベースオブジェクトの形式でレイヤーを追加することです。 search_pathでは十分早いです その特定のテナントのために。 search_pathは、アプリケーションサーバー層(したがって、アプリケーションコードの外部)でのデータソースの接続構成で(理想的には、より多くのオプションを提供する特別な関数を介して)設定できます。まず、2つのスキーマを作成します。
rentaldb_one=# create schema cars;
rentaldb_one=# create schema boats;
次に、各スキーマにデータベースオブジェクト(ビュー)を作成します。
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';
次のステップは、各テナントの検索パスを次のように設定することです。
-
ボートテナントの場合:
set search_path TO 'boats, "$user", public';
-
車のテナントの場合:
set search_path TO 'cars, "$user", public';
- 上位の管理テナントの場合は、デフォルトのままにします
テストしましょう:
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)
関連リソースPostgreSQL用ClusterControlPostgreSQLトリガーとストアド関数の基本PostgreSQL用の入出力(I / O)操作の調整 search_pathを設定する代わりに、より複雑なロジックを処理するためのより複雑な関数を記述し、アプリケーションまたは接続プールの接続構成でこれを呼び出すことができます。
上記の例では、パブリックスキーマ(public.rental)にある同じ中央テーブルとテナントごとに2つの追加ビューを使用しました。これらの2つのビューは単純であり、したがって書き込み可能であるという幸運な事実を使用しています。ビューの代わりに、パブリックテーブルから継承するテナントごとに1つの子テーブルを作成することにより、継承を使用できます。これは、PostgreSQLのユニークな機能であるテーブル継承にぴったりです。一番上のテーブルは、挿入を禁止するルールで構成されている場合があります。継承ソリューションでは、子テーブルにデータを入力し、親テーブルへの挿入アクセスを防ぐために変換が必要になるため、これはビューの場合ほど単純ではなく、設計への影響を最小限に抑えて機能します。その方法について特別なブログを書くかもしれません。
上記の3つのアプローチを組み合わせて、さらに多くのオプションを提供することができます。