Multi-tenancy dalam sistem perangkat lunak disebut pemisahan data menurut seperangkat kriteria untuk memenuhi serangkaian tujuan. Besaran/perluasan, sifat dan pelaksanaan akhir dari pemisahan ini tergantung pada kriteria dan tujuan tersebut. Multi-tenancy pada dasarnya adalah kasus partisi data tetapi kami akan mencoba menghindari istilah ini karena alasan yang jelas (istilah dalam PostgreSQL memiliki arti yang sangat spesifik dan dicadangkan, karena partisi tabel deklaratif diperkenalkan di postgresql 10).
Kriterianya mungkin:
- sesuai dengan id tabel master penting, yang melambangkan id penyewa yang mungkin mewakili:
- perusahaan/organisasi dalam grup induk yang lebih besar
- departemen dalam perusahaan/organisasi
- kantor wilayah/cabang dari perusahaan/organisasi yang sama
- menurut lokasi/IP pengguna
- sesuai dengan posisi pengguna di dalam perusahaan/organisasi
Tujuannya mungkin:
- pemisahan sumber daya fisik atau virtual
- pemisahan sumber daya sistem
- keamanan
- akurasi dan kenyamanan manajemen/pengguna di berbagai tingkat perusahaan/organisasi
Perhatikan dengan memenuhi suatu tujuan kita juga memenuhi semua tujuan di bawahnya, yaitu dengan memenuhi A kita juga memenuhi B, C dan D, dengan memenuhi B kita juga memenuhi C dan D, dan seterusnya.
Jika kita ingin memenuhi tujuan A, kita dapat memilih untuk menyebarkan setiap tenant sebagai cluster database terpisah di dalam server fisik/virtualnya sendiri. Ini memberikan pemisahan maksimum antara sumber daya dan keamanan tetapi memberikan hasil yang buruk ketika kita perlu melihat seluruh data sebagai satu kesatuan, yaitu tampilan keseluruhan sistem yang terkonsolidasi.
Jika kita hanya ingin mencapai tujuan B, kita mungkin menggunakan setiap tenant sebagai instance postgresql terpisah di server yang sama. Ini akan memberi kita kendali atas berapa banyak ruang yang akan diberikan untuk setiap instans, dan juga beberapa kendali (bergantung pada OS) pada penggunaan CPU/mem. Kasus ini pada dasarnya tidak berbeda dengan A. Di era komputasi awan modern, kesenjangan antara A dan B cenderung semakin kecil, sehingga A kemungkinan besar akan menjadi cara yang lebih disukai daripada B.
Jika kita ingin mencapai tujuan C, yaitu keamanan, maka cukup memiliki satu database instance dan menerapkan setiap tenant sebagai database terpisah.
Dan akhirnya jika kita hanya peduli untuk pemisahan data "lunak", atau dengan kata lain pandangan yang berbeda dari sistem yang sama, kita dapat mencapai ini hanya dengan satu contoh database dan satu database, menggunakan sejumlah besar teknik yang dibahas di bawah ini sebagai final (dan utama) topik blog ini. Berbicara tentang multi-tenancy, dari sudut pandang DBA, kasus A, B dan C memiliki banyak kesamaan. Ini karena dalam semua kasus kita memiliki database yang berbeda dan untuk menjembatani database tersebut, maka alat dan teknologi khusus harus digunakan. Namun, jika kebutuhan untuk melakukannya berasal dari departemen analitik atau Intelijen Bisnis, maka tidak diperlukan bridging sama sekali, karena data dapat direplikasi dengan sangat baik ke beberapa server pusat yang didedikasikan untuk tugas-tugas tersebut, sehingga bridging tidak diperlukan. Jika memang diperlukan bridging seperti itu maka kita harus menggunakan tools seperti dblink atau tabel asing. Tabel asing melalui Pembungkus Data Asing saat ini merupakan cara yang disukai.
Jika kita menggunakan opsi D, bagaimanapun, maka konsolidasi sudah diberikan secara default, jadi sekarang bagian yang sulit adalah sebaliknya:pemisahan. Jadi kami biasanya mengkategorikan berbagai opsi ke dalam dua kategori utama:
- Pemisahan yang lembut
- Perpisahan yang sulit
Pemisahan Keras melalui Basis Data Berbeda dalam Cluster yang Sama
Misalkan kita harus merancang sistem untuk bisnis imajiner yang menawarkan penyewaan mobil dan perahu, tetapi karena keduanya diatur oleh undang-undang yang berbeda, kontrol yang berbeda, audit, setiap perusahaan harus mempertahankan departemen akuntansi yang terpisah dan dengan demikian kami ingin mempertahankan sistem mereka. terpisah. Dalam hal ini kami memilih untuk memiliki database yang berbeda untuk setiap perusahaan:rentaldb_cars dan rentaldb_boats, yang akan memiliki skema yang sama:
# \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)
Misalkan kita memiliki persewaan berikut. Di 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)
dan di 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)
Sekarang manajemen ingin memiliki pandangan sistem yang terkonsolidasi, mis. cara terpadu untuk melihat persewaan. Kami dapat menyelesaikan ini melalui aplikasi, tetapi jika kami tidak ingin memperbarui aplikasi atau tidak memiliki akses ke kode sumber, maka kami dapat menyelesaikannya dengan membuat database pusat rentaldb dan dengan memanfaatkan tabel asing, sebagai berikut:
'sdb>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;
Untuk melihat semua persewaan dan pelanggan di seluruh organisasi, kami cukup melakukan:
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)
Ini terlihat bagus, isolasi dan keamanan terjamin, konsolidasi tercapai, tetapi masih ada masalah:
- pelanggan harus dikelola secara terpisah, artinya pelanggan yang sama mungkin memiliki dua akun
- Aplikasi harus menghormati gagasan kolom khusus (seperti tenant_db) dan menambahkan ini ke setiap kueri, sehingga rentan terhadap kesalahan
- Tampilan yang dihasilkan tidak dapat diperbarui secara otomatis (karena mengandung UNION)
Pemisahan Lembut dalam Database yang Sama
Ketika pendekatan ini dipilih maka konsolidasi diberikan di luar kotak dan sekarang bagian yang sulit adalah pemisahan. PostgreSQL menawarkan sejumlah besar solusi kepada kami untuk menerapkan pemisahan:
- Tampilan
- Keamanan Tingkat Peran
- Skema
Dengan tampilan, aplikasi harus menyetel pengaturan yang dapat dikueri seperti nama_aplikasi, kami menyembunyikan tabel utama di belakang tampilan, dan kemudian di setiap kueri pada tabel turunan mana pun (seperti dalam ketergantungan FK), jika ada, dari tabel utama ini bergabung dengan pemandangan ini. Kita akan melihat ini dalam contoh berikut dalam database yang kita sebut rentaldb_one. Kami menyematkan identifikasi perusahaan penyewa ke dalam tabel utama:
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)
Unduh Whitepaper Hari Ini Pengelolaan &Otomatisasi PostgreSQL dengan ClusterControlPelajari tentang apa yang perlu Anda ketahui untuk menerapkan, memantau, mengelola, dan menskalakan PostgreSQLUnduh Whitepaper Skema pelanggan tabel tetap sama. Mari kita lihat isi database saat ini:
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)
Kami menggunakan nama baru rental_one untuk menyembunyikan ini di balik tampilan baru yang akan memiliki nama tabel yang sama dengan yang diharapkan aplikasi:rental.Aplikasi perlu menyetel nama aplikasi untuk menunjukkan penyewa. Jadi dalam contoh ini kita akan memiliki tiga contoh aplikasi, satu untuk mobil, satu untuk kapal dan satu untuk manajemen puncak. Nama aplikasi diatur seperti:
rentaldb_one=# set application_name to 'cars';
Kami sekarang membuat tampilan:
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');
Catatan:Kami menjaga kolom yang sama, dan nama tabel/tampilan sebanyak mungkin, poin kunci dalam solusi multi-penyewa adalah menjaga hal-hal yang sama di sisi aplikasi, dan perubahan menjadi minimal dan dapat dikelola.
Mari kita lakukan beberapa pemilihan:
rentaldb_one=# setel application_name ke '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)
Contoh aplikasi ke-3 yang harus menetapkan nama aplikasi menjadi "semua" dimaksudkan untuk digunakan oleh manajemen puncak dengan tujuan ke seluruh database.
Solusi yang lebih kuat, dari segi keamanan, mungkin didasarkan pada RLS (keamanan tingkat baris). Pertama kita restore nama tabelnya, ingat kita gak mau ganggu aplikasinya :
rentaldb_one=# alter view rental rename to rental_view;
rentaldb_one=# alter table rental_one rename TO rental;
Pertama, kami membuat dua grup pengguna untuk setiap perusahaan (perahu, mobil) yang harus melihat subset data mereka sendiri:
rentaldb_one=# create role cars_employees;
rentaldb_one=# create role boats_employees;
Kami sekarang membuat kebijakan keamanan untuk setiap grup:
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');
Setelah memberikan hibah yang diperlukan untuk dua peran:
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 ;
kami membuat satu pengguna di setiap peran
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;
Dan uji:
[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=>
Hal yang menyenangkan dengan pendekatan ini adalah kita tidak membutuhkan banyak contoh aplikasi. Semua isolasi dilakukan di tingkat basis data berdasarkan peran pengguna. Oleh karena itu, untuk membuat pengguna di manajemen puncak, yang perlu kita lakukan hanyalah memberikan kedua peran kepada pengguna ini:
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)
Melihat kedua solusi tersebut, kami melihat bahwa solusi tampilan memerlukan perubahan nama tabel dasar, yang mungkin cukup mengganggu karena kami mungkin perlu menjalankan skema yang sama persis dalam solusi non-multitenant, atau dengan aplikasi yang tidak mengetahui nama_aplikasi , sedangkan solusi kedua mengikat orang ke penyewa tertentu. Bagaimana jika orang yang sama bekerja mis. di penyewa kapal di pagi hari dan di penyewa mobil di sore hari? Kami akan melihat solusi ke-3 berdasarkan skema, yang menurut saya adalah yang paling serbaguna, dan tidak mengalami salah satu peringatan dari dua solusi yang dijelaskan di atas. Hal ini memungkinkan aplikasi untuk berjalan dengan cara agnostik penyewa, dan insinyur sistem untuk menambahkan penyewa saat bepergian sesuai kebutuhan. Kami akan mempertahankan desain yang sama seperti sebelumnya, dengan data pengujian yang sama (kami akan terus mengerjakan db contoh rentaldb_one). Idenya di sini adalah untuk menambahkan lapisan di depan tabel utama dalam bentuk objek database dalam skema terpisah yang akan cukup awal di search_path untuk penyewa tertentu. Search_path dapat diatur (idealnya melalui fungsi khusus, yang memberikan lebih banyak opsi) dalam konfigurasi koneksi sumber data di lapisan server aplikasi (oleh karena itu di luar kode aplikasi). Pertama kita buat dua skema:
rentaldb_one=# create schema cars;
rentaldb_one=# create schema boats;
Kemudian kita membuat objek database (tampilan) di setiap 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';
Langkah selanjutnya adalah mengatur jalur pencarian di setiap tenant sebagai berikut:
-
Untuk penyewa kapal:
set search_path TO 'boats, "$user", public';
-
Untuk penyewa mobil:
set search_path TO 'cars, "$user", public';
- Untuk penyewa mgmt teratas biarkan default
Mari kita uji:
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)
Resource terkait ClusterControl untuk PostgreSQL Pemicu PostgreSQL dan Dasar-dasar Fungsi Tersimpan Menyetel Operasi Input/Output (I/O) untuk PostgreSQL Alih-alih menyetel search_path kita dapat menulis fungsi yang lebih kompleks untuk menangani logika yang lebih kompleks dan memanggil ini dalam konfigurasi koneksi aplikasi atau pooler koneksi kita.
Dalam contoh di atas, kami menggunakan tabel pusat yang sama yang berada di skema publik (public.rental) dan dua tampilan tambahan untuk setiap penyewa, menggunakan fakta yang menguntungkan bahwa kedua tampilan tersebut sederhana dan oleh karena itu dapat ditulisi. Alih-alih tampilan, kami dapat menggunakan pewarisan, dengan membuat satu tabel anak untuk setiap penyewa yang mewarisi dari tabel publik. Ini sangat cocok untuk pewarisan tabel, fitur unik PostgreSQL. Tabel teratas mungkin dikonfigurasi dengan aturan untuk melarang penyisipan. Dalam solusi pewarisan, konversi akan diperlukan untuk mengisi tabel anak dan untuk mencegah akses penyisipan ke tabel induk, jadi ini tidak sesederhana dalam kasus tampilan, yang bekerja dengan dampak minimal pada desain. Kami mungkin akan menulis blog khusus tentang cara melakukannya.
Ketiga pendekatan di atas dapat digabungkan untuk memberikan lebih banyak pilihan.