
Saya tidak akan memberikan definisi multi-tenancy, mereka sudah menulis beberapa kali tentang ini di sini dan di sini . Dan lebih baik langsung ke topik artikel dan mulai dengan pertanyaan-pertanyaan berikut:
Mengapa aplikasi ini tidak langsung multitenant?
Kebetulan aplikasi pada awalnya dikembangkan untuk instalasi hanya di sisi klien. Anda dapat memanggil aplikasi seperti kotak atau perangkat lunak sebagai produk . Seorang klien membeli sebuah kotak dan menyebarkan aplikasi di server mereka (ada banyak contoh aplikasi semacam itu).
Namun seiring berjalannya waktu, perusahaan pengembang mungkin berpikir bahwa akan menyenangkan untuk menempatkan aplikasi di cloud untuk disewa (perangkat lunak sebagai layanan). Metode penyebaran ini memiliki keuntungan untuk klien dan perusahaan pengembang. Pelanggan dapat dengan cepat mendapatkan sistem kerja dan tidak khawatir tentang penyebaran dan administrasi. Saat menyewa aplikasi, Anda tidak perlu investasi besar satu kali.
Dan perusahaan pengembang akan menerima pelanggan baru, serta tugas-tugas baru: menyebarkan aplikasi di cloud, mengelola, memperbarui ke versi baru, memigrasikan data selama memperbarui, cadangan data, kecepatan dan kesalahan pemantauan, memperbaiki masalah jika terjadi.
Mengapa aplikasi di cloud harus multi-tenant?
Untuk menempatkan aplikasi di cloud, tidak perlu membuatnya multi-tenant. Tetapi kemudian akan ada masalah berikut: untuk setiap klien, Anda harus menggunakan stand khusus di cloud dengan aplikasi sewaan, dan ini sudah mahal, baik dalam hal konsumsi sumber daya stand cloud dan dalam hal administrasi. Lebih menguntungkan untuk mengimplementasikan multi-tenancy dalam aplikasi sehingga satu instance dapat melayani beberapa klien (organisasi).
Jika aplikasi menarik 1000 pengguna yang bekerja secara bersamaan, itu menguntungkan untuk mengelompokkan klien (organisasi) sehingga secara total mereka memberikan beban yang diinginkan dari 1000 pengguna per instance aplikasi. Dan kemudian akan ada konsumsi sumber daya cloud yang paling optimal.
Asumsikan bahwa aplikasi tersebut disewa oleh organisasi untuk 20 pengguna (karyawan organisasi). Maka Anda perlu mengelompokkan 50 organisasi ini untuk mencapai beban yang tepat. Penting untuk mengisolasi organisasi dari satu sama lain. Suatu organisasi menyewakan suatu aplikasi, membiarkan karyawannya pergi ke sana, hanya menyimpan datanya, dan tidak melihat bahwa organisasi lain juga dilayani oleh aplikasi yang sama.
Menerapkan multi-tenancy tidak berarti bahwa aplikasi tidak lagi dapat digunakan secara lokal di server organisasi. Anda dapat mendukung dua metode penerapan sekaligus:
- aplikasi multi-tenant di cloud;
- aplikasi penyewa tunggal di server klien.
Aplikasi kami memiliki cara yang serupa: dari non-penyewa hingga multi-penyewa. Dan dalam artikel ini saya akan membagikan beberapa pendekatan dalam mengembangkan multi-tenancy.
Bagaimana menerapkan multi-tenancy dalam aplikasi yang dirancang sebagai non-tenant?
Kami akan segera membatasi topik, kami hanya akan mempertimbangkan pengembangan, kami tidak akan menyentuh masalah pengujian, rilis versi, penyebaran, dan administrasi. Di semua bidang ini, kemunculan multi-tenancy juga harus diperhitungkan, tetapi untuk saat ini kita hanya akan berbicara tentang pembangunan.
Untuk memahami apa itu aplikasi yang bukan tenanty dan menjadi multi-tenant, saya akan menjelaskan tujuannya, daftar layanan dan teknologi yang digunakan.
Ini adalah sistem ECM (DirectumRX), yang terdiri dari 10 layanan (5 layanan monolitik dan 5 layanan mikro). Semua layanan ini dapat ditempatkan di satu server yang kuat, atau di beberapa server.
Layanan- Layanan web - untuk melayani klien web (browser).
- Layanan WCF - untuk melayani klien desktop (aplikasi WPF).
- Layanan untuk aplikasi seluler.
- Layanan untuk melakukan proses latar belakang.
- Layanan untuk perencanaan proses latar belakang.
- Layanan Eksekusi Skema Alur Kerja
- Layanan Eksekusi Blok Workflow
- Layanan penyimpanan dokumen (data biner).
- Layanan untuk mengonversi dokumen menjadi html (pratinjau di browser).
- Layanan untuk menyimpan hasil konversi dalam html
Tumpukan teknologi yang digunakan:
.NET + SQLServer / Postgres + NHibernate + IIS + RabbitMQ + Redis
Jadi, apa yang membuat layanan menjadi multi-tenant? Untuk melakukan ini, Anda perlu memperbaiki mekanisme berikut dalam layanan, yaitu, menambah pengetahuan tentang penyewa ke:
- penyimpanan data;
- ORM;
- caching data;
- pemrosesan permintaan;
- memproses pesan antrian;
- konfigurasi;
- penebangan;
- melakukan tugas-tugas latar belakang;
- interaksi dengan layanan microser;
- interaksi dengan broker pesan.
Dalam hal aplikasi kami, ini adalah tempat utama yang membutuhkan perbaikan. Mari kita pertimbangkan secara terpisah.
Memilih metode penyimpanan data
Saat Anda membaca artikel tentang multi-tenancy, hal pertama yang harus mereka pilah adalah bagaimana mengatur penyimpanan data. Memang, intinya penting.
Untuk sistem ECM kami, penyimpanan utama adalah basis data relasional, yang memiliki sekitar 100 tabel. Bagaimana cara mengatur penyimpanan data banyak organisasi sehingga organisasi A sama sekali tidak melihat data organisasi B?
Beberapa skema diketahui (banyak yang telah ditulis tentang skema ini):
- buat database Anda sendiri untuk setiap organisasi (untuk setiap penyewa);
- menggunakan satu database untuk semua organisasi, tetapi untuk setiap organisasi membuat skema sendiri dalam database;
- gunakan satu database untuk semua organisasi, tetapi tambahkan kolom "kunci penyewa / organisasi" di setiap tabel.
Pilihan skema tidak disengaja. Dalam kasus kami, cukup mempertimbangkan kasus-kasus administrasi sistem untuk memahami opsi yang disukai. Kasus adalah sebagai berikut:
- tambah penyewa (organisasi baru menyewa sistem);
- menghapus penyewa (organisasi menolak untuk menyewa);
- memindahkan penyewa ke tegakan cloud lainnya (mendistribusikan kembali beban di antara tegakan cloud saat salah satu tegakan berhenti untuk mengatasi beban).
Pertimbangkan kasus transfer penyewa. Tugas utama transfer adalah untuk mentransfer data organisasi ke stand lain. Transfer tidak sulit dilakukan jika penyewa memiliki basis datanya sendiri, tetapi akan merepotkan jika Anda mencampur data berbagai organisasi dalam 100 tabel. Cobalah untuk mengekstrak hanya data yang diperlukan dari tabel, mentransfernya ke database lain, di mana sudah ada data dari penyewa lain, dan agar pengidentifikasi mereka tidak berpotongan.
Kasus selanjutnya adalah penambahan penyewa baru. Kasusnya juga tidak sederhana. Menambahkan penyewa adalah kebutuhan untuk mengisi direktori sistem, pengguna, hak, sehingga Anda dapat masuk ke sistem sama sekali. Tugas ini paling baik diselesaikan dengan mengkloning database referensi, yang sudah memiliki semua yang Anda butuhkan.
Kasus penghapusan penyewa sangat mudah diselesaikan dengan menonaktifkan basis data penyewa.
Karena alasan ini, kami memilih skema: satu penyewa - satu basis data .
ORM
Kami memilih metode penyimpanan data, pertanyaan berikutnya: bagaimana cara mengajar ORM untuk bekerja dengan skema yang dipilih?
Kami menggunakan Nhibernate. Diperlukan bahwa Nhibernate bekerja dengan beberapa database dan secara berkala beralih ke yang benar, misalnya, tergantung pada permintaan http. Jika kami memproses permintaan organisasi A, maka basis data A digunakan, dan jika permintaan berasal dari organisasi B, maka basis data B.
NHibernate memiliki kesempatan seperti itu. Anda perlu mengganti implementasi NHibernate.Connection.DriverConnectionProvider . Setiap kali NHibernate ingin membuka koneksi database, ia memanggil DriverConnectionProvider untuk mendapatkan string koneksi. Di sini kita akan menggantinya dengan yang diperlukan:
public class MyDriverConnectionProvider : DriverConnectionProvider { protected override string ConnectionString => TenantRegistry.Instance.CurrentTenant.ConnectionString; }
Apa itu TenantRegistry.Instance.CurrentTenant akan saya ceritakan nanti.
Caching data
Layanan sering kali menyimpan data untuk meminimalkan permintaan basis data atau tidak menghitung hal yang sama berulang kali. Masalahnya adalah bahwa cache harus dipecah oleh penyewa jika data penyewa di-cache. Tidak dapat diterima bahwa cache data dari satu organisasi digunakan saat memproses permintaan dari organisasi lain. Solusi paling sederhana adalah dengan menambahkan pengidentifikasi penyewa ke kunci setiap cache:
var tenantCacheKey = cacheKey + TenantRegistry.Instance.CurrentTenant.Id;
Masalah ini harus diingat ketika membuat setiap cache. Ada banyak cache di layanan kami. Agar tidak lupa memperhitungkan pengidentifikasi penyewa di masing-masing, lebih baik menyatukan pekerjaan dengan cache. Misalnya, buat mekanisme caching umum yang akan melakukan cache keluar dari kotak dalam konteks penyewa.
Penebangan
Cepat atau lambat, ada sesuatu yang salah dalam sistem, Anda harus membuka file log dan mulai mempelajarinya. Pertanyaan pertama adalah: atas nama pengguna mana dan organisasi mana tindakan ini dilakukan?
Akan lebih mudah jika di setiap baris log ada pengidentifikasi penyewa dan nama pengguna penyewa. Informasi ini menjadi seperlunya, misalnya, waktu pesan:
2019-05-24 17:05:27.985 <message> [User2 :Tenant1] 2019-05-24 17:05:28.126 <message> [User3 :Tenant2] 2019-05-24 17:05:28.173 <message> [User4 :Tenant3]
Pengembang tidak boleh berpikir tentang penyewa mana untuk menulis ke log, itu harus otomatis, disembunyikan "di bawah kap" dari sistem logging.
Kami menggunakan NLog, jadi saya akan memberikan contohnya. Cara termudah untuk mengamankan pengidentifikasi penyewa adalah dengan membuat NLog.LayoutRenderers.LayoutRenderer , yang akan memungkinkan Anda untuk mendapatkan pengidentifikasi penyewa untuk setiap entri log:
[LayoutRenderer("tenant")] public class TenantLayoutRenderer : LayoutRenderer { protected override void Append(StringBuilder builder, LogEventInfo logEvent) { builder.Append(TenantRegistry.Instance.CurrentTenant.Id); } }
Dan kemudian gunakan LayoutRenderer ini di templat log:
<target layout="${odate} ${message} [${user} :${tenant}]"/>
Eksekusi kode
Dalam contoh di atas, saya sering menggunakan kode berikut:
TenantRegistry.Instance.CurrentTenant
Sudah waktunya untuk mengatakan apa artinya itu. Tetapi pertama-tama Anda perlu memahami pendekatan yang kami ikuti dalam layanan:
Setiap eksekusi kode (memproses permintaan http, memproses pesan antrian, melakukan tugas latar belakang di utas terpisah) harus dikaitkan dengan beberapa penyewa.
Ini berarti bahwa di setiap tempat dalam eksekusi kode orang dapat bertanya: "Untuk penyewa mana thread ini bekerja?" atau dengan cara lain, "Apa penyewa saat ini?"
TenantRegistry.Instance.CurrentTenant adalah penyewa saat ini untuk aliran saat ini. Streaming dan penyewa dapat ditautkan dalam aplikasi kami. Mereka terhubung sementara, misalnya, saat memproses permintaan http atau saat memproses pesan dari antrian. Salah satu cara untuk mengikat penyewa ke aliran dilakukan seperti ini:
Penyewa yang diikat ke utas dapat diperoleh di mana saja dalam kode, dengan menghubungi TenantRegistry - ini adalah singleton, titik akses untuk bekerja dengan penyewa. Oleh karena itu, Nhibernate dan NLog dapat mengakses singleton ini (pada titik ekstensi) untuk mengetahui string koneksi atau pengidentifikasi penyewa.
Tugas Latar Belakang
Layanan seringkali memiliki tugas latar belakang yang perlu dilakukan pada timer. Tugas latar belakang dapat mengakses database organisasi, dan kemudian tugas latar belakang harus dilakukan untuk setiap penyewa. Untuk melakukan ini, Anda tidak perlu memulai timer atau utas terpisah untuk setiap penyewa. Dimungkinkan untuk melakukan tugas dalam penyewa yang berbeda dalam satu utas / penghitung waktu. Untuk melakukan ini, di pengatur waktu, kami memilah penyewa, menghubungkan setiap penyewa dengan aliran dan melakukan tugas latar belakang:
Dua penyewa tidak dapat dilampirkan ke aliran pada saat yang sama, jika kita lampirkan satu, yang lain terlepas dari aliran. Kami secara aktif menggunakan pendekatan ini agar tidak menghasilkan utas / penghitung waktu untuk tugas latar belakang.
Bagaimana cara menghubungkan permintaan http dengan penyewa
Untuk memproses permintaan http klien, Anda perlu tahu dari organisasi mana ia datang. Jika pengguna sudah diautentikasi, maka pengenal penyewa dapat disimpan dalam cookie otentikasi (jika bekerja dengan aplikasi dilakukan melalui browser) atau dalam token JWT. Tetapi bagaimana jika pengguna belum diautentikasi? Misalnya, pengguna anonim telah membuka situs web aplikasi dan ingin mengautentikasi. Untuk melakukan ini, ia mengirim permintaan dengan login dan kata sandi. Dalam database organisasi mana yang mencari pengguna ini?
Juga, permintaan anonim akan diterima untuk mendapatkan halaman login ke aplikasi, dan mungkin berbeda untuk organisasi yang berbeda, misalnya, bahasa lokalisasi.
Untuk mengatasi masalah korelasi antara permintaan http-anonim dan organisasi (penyewa), kami menggunakan subdomain untuk organisasi. Nama subdomain dibentuk oleh nama organisasi. Pengguna harus menggunakan subdomain untuk bekerja dengan sistem:
https://company1.service.com https://company2.service.com
Layanan web multi-penyewa yang sama tersedia di alamat ini. Tapi sekarang layanan memahami dari mana organisasi permintaan http anonim akan datang, dengan fokus pada nama domain.
Pengikatan nama domain dan penyewa dilakukan dalam file konfigurasi layanan web:
<tenant name="company1" db="database1" host="company1.service.com" /> <tenant name="company2" db="database2" host="company2.service.com" />
Tentang mengkonfigurasi layanan akan dijelaskan di bawah ini.
Layanan microser. Penyimpanan data
Ketika saya mengatakan bahwa sistem ECM membutuhkan 100 tabel, saya berbicara tentang layanan monolitik. Tetapi kebetulan bahwa suatu layanan microser memerlukan penyimpanan relasional, di mana 2-3 tabel diperlukan untuk menyimpan datanya. Idealnya, setiap microservice memiliki penyimpanan sendiri, yang hanya dapat diakses olehnya. Dan layanan mikro memutuskan bagaimana menyimpan data dalam konteks penyewa.
Tapi kami pergi ke arah lain: kami memutuskan untuk menyimpan semua data organisasi dalam satu database. Jika suatu layanan mikro memerlukan penyimpanan relasional, maka ia menggunakan basis data organisasi yang ada sehingga data tidak tersebar di berbagai penyimpanan, tetapi dikumpulkan dalam satu basis data. Layanan monolitik menggunakan database yang sama.
Layanan microser hanya bekerja dengan tabel mereka di database, dan jangan mencoba untuk bekerja dengan tabel monolith atau layanan microser lainnya. Ada pro dan kontra untuk pendekatan ini.
Pro:
- data organisasi di satu tempat;
- mudah membuat cadangan dan memulihkan data organisasi;
- Dalam cadangan, data semua layanan konsisten.
Cons:
- satu database untuk semua layanan adalah leher sempit ketika penskalaan (persyaratan untuk sumber daya DBMS meningkat);
- microservices memiliki akses fisik ke tabel masing-masing, tetapi jangan gunakan fitur ini.
Layanan microser. Pengetahuan tentang penyewa tidak selalu dibutuhkan.
Layanan Microsoft mungkin tidak tahu bahwa ia berfungsi di lingkungan multi-penyewa. Pertimbangkan salah satu layanan kami, yang terlibat dalam mengonversi dokumen menjadi html.
Apa layanan ini:
- Mengambil pesan dari antrian RabbitMQ untuk mengonversi dokumen.
- mengambil pengidentifikasi dokumen dan penyewa penyewa dari pesan
- Unduh dokumen dari layanan penyimpanan dokumen.
- untuk ini menghasilkan permintaan di mana ia mengirimkan pengidentifikasi dokumen dan pengidentifikasi penyewa
- Mengonversi dokumen menjadi html.
- Memberikan html ke layanan untuk menyimpan hasil konversi.
Layanan tidak menyimpan dokumen dan tidak menyimpan hasil konversi. Dia memiliki pengetahuan tidak langsung tentang penyewa: pengidentifikasi penyewa melewati layanan dalam perjalanan.
Layanan microser. Subdomain tidak diperlukan
Saya menulis di atas bahwa subdomain membantu menyelesaikan masalah permintaan http anonim:
https://company1.service.com https://company2.service.com
Tetapi tidak semua layanan berfungsi dengan permintaan anonim, sebagian besar memerlukan otentikasi yang sudah terlewati. Oleh karena itu, layanan microser yang berfungsi melalui http sering tidak peduli dari hostName apa permintaan berasal, mereka menerima semua informasi tentang penyewa dari token JWT atau cookie otentikasi yang datang dengan setiap permintaan.
Konfigurasi
Layanan perlu dikonfigurasi agar mereka tahu tentang penyewa. Yaitu:
- tentukan string untuk menghubungkan ke basis data penyewa;
- ikat nama domain ke penyewa;
- tentukan bahasa default dan zona waktu penyewa.
Penyewa dapat memiliki banyak pengaturan. Untuk layanan kami, kami menetapkan pengaturan penyewa dalam file konfigurasi xml. Ini bukan web.config dan bukan app.config. Ini adalah file xml terpisah, yang perubahannya harus dapat ditangkap tanpa me-reboot layanan sehingga menambahkan penyewa baru tidak me-restart seluruh sistem.
Daftar pengaturannya kira-kira seperti ini:
<block name="TENANTS"> <tenant name="Jupiter" db="DirectumRX_Jupiter" login="admin" password="password" hyperlinkUriScheme="jupiter" hyperlinkFileExtension=".jupiter" hyperlinkServer="http://jupiter-rx.directum.ru/Sungero" helpAddress="http://jupiter-rx.directum.ru/Sungero/help" devHelpAddress="http://jupiter-rx.directum.ru/Sungero/dev_help" language="Ru-ru" isAttributesSignatureAbsenceAllowed="false" endorsingSignatureLocksSignedProperties="false" administratorEmail ="admin@jupiter-company.ru" feedbackEmail="support@jupiter-company.ru" isSendFeedbackAllowed="true" serviceUserPassword="password" utcOffset="5" collaborativeEditingEnabled="false" collaborativeEditingForced="false" /> <tenant name="Mars" db="DirectumRX_Mars" login="admin" password="password" hyperlinkUriScheme="mars" hyperlinkFileExtension=".mars" hyperlinkServer="http://mars-rx.directum.ru/Sungero" helpAddress="http://mars-rx.directum.ru/Sungero/help" devHelpAddress="http://mars-rx.directum.ru/Sungero/dev_help" language="Ru-ru" isAttributesSignatureAbsenceAllowed="false" endorsingSignatureLocksSignedProperties="false" administratorEmail ="root@mars-ooo.ru" feedbackEmail="support@mars-ooo.ru" isSendFeedbackAllowed="true" serviceUserPassword="password" utcOffset="-1" collaborativeEditingEnabled="false" collaborativeEditingForced="false" /> </block>
Ketika organisasi baru menyewakan layanan, ia perlu menambahkan penyewa baru ke file konfigurasi untuknya. Dan diharapkan bahwa organisasi lain tidak merasakan ini. Idealnya, tidak boleh ada restart layanan.
Pada kami, tidak semua layanan dapat mengambil konfigurasi tanpa memulai ulang, tetapi layanan yang paling penting (monolith) dapat melakukan ini.
Ringkasan
Ketika sebuah aplikasi menjadi multi-tenant, tampaknya kompleksitas pengembangan telah meningkat secara dramatis. Tetapi kemudian Anda terbiasa dengan multitenantness, dan memperlakukan dukungannya sebagai persyaratan normal.
Perlu juga diingat bahwa multi-tenancy tidak hanya pengembangan, tetapi juga pengujian, administrasi, penyebaran, pemutakhiran, pencadangan, migrasi data. Tapi lebih baik tentang mereka lain kali.