Django là một framework phổ biến mà bạn có thể chọn để phát triển ứng dụng cho công ty của mình. Nhưng nếu bạn muốn tạo một ứng dụng SaaS mà nhiều khách hàng sẽ sử dụng thì sao? Bạn nên chọn kiến trúc nào? Hãy xem nhiệm vụ này có thể được tiếp cận như thế nào.
Cách tiếp cận đơn giản nhất là tạo một phiên bản riêng cho từng khách hàng mà bạn có. Giả sử chúng ta có ứng dụng Django và cơ sở dữ liệu. Sau đó, đối với mỗi khách hàng, chúng ta cần chạy cơ sở dữ liệu và phiên bản ứng dụng của riêng nó. Điều đó có nghĩa là mỗi phiên bản ứng dụng chỉ có một đối tượng thuê.
Cách tiếp cận này rất đơn giản để triển khai: bạn chỉ cần bắt đầu một phiên bản mới của mọi dịch vụ mà bạn có. Nhưng đồng thời, nó có thể gây ra một vấn đề: mỗi khách hàng sẽ làm tăng đáng kể chi phí cơ sở hạ tầng. Nó có thể không phải là vấn đề lớn nếu bạn dự định chỉ có một vài khách hàng hoặc nếu mỗi phiên bản đều rất nhỏ.
Tuy nhiên, giả sử rằng chúng ta đang xây dựng một công ty lớn cung cấp dịch vụ nhắn tin doanh nghiệp cho 100.000 tổ chức. Hãy tưởng tượng, việc sao chép toàn bộ cơ sở hạ tầng cho mỗi khách hàng mới có thể tốn kém đến mức nào! Và khi cần cập nhật phiên bản ứng dụng, chúng ta cần triển khai nó cho từng client nên quá trình triển khai cũng sẽ bị chậm lại .
Có một cách tiếp cận khác có thể hữu ích trong trường hợp chúng tôi có nhiều khách hàng cho ứng dụng: kiến trúc nhiều người thuê. Điều đó có nghĩa là chúng tôi có nhiều khách hàng, chúng tôi gọi là người thuê nhà , nhưng tất cả họ chỉ sử dụng một phiên bản của ứng dụng.
Mặc dù kiến trúc này giải quyết được vấn đề về chi phí cao của các phiên bản chuyên dụng cho mỗi máy khách, nhưng nó lại gây ra một vấn đề mới: làm cách nào chúng tôi có thể chắc chắn rằng dữ liệu của máy khách được cách ly an toàn với các máy khách khác?
Chúng ta sẽ thảo luận về các phương pháp tiếp cận sau:
Sử dụng cơ sở dữ liệu dùng chung và lược đồ cơ sở dữ liệu dùng chung : Chúng ta có thể xác định đối tượng thuê nào sở hữu dữ liệu bằng khóa ngoại mà chúng ta cần thêm vào mỗi bảng cơ sở dữ liệu.
Sử dụng cơ sở dữ liệu dùng chung nhưng lược đồ cơ sở dữ liệu riêng biệt : Bằng cách này, chúng ta sẽ không cần duy trì nhiều phiên bản cơ sở dữ liệu nhưng sẽ có được mức độ tách biệt dữ liệu đối tượng thuê tốt.
Sử dụng cơ sở dữ liệu riêng biệt : nó trông tương tự như ví dụ về một đối tượng thuê, nhưng sẽ không giống nhau vì chúng ta sẽ vẫn sử dụng một phiên bản ứng dụng dùng chung và chọn cơ sở dữ liệu nào sẽ sử dụng bằng cách kiểm tra đối tượng thuê.
Hãy cùng tìm hiểu sâu hơn về những ý tưởng này và xem cách tích hợp chúng với ứng dụng Django.
Tùy chọn này có thể là tùy chọn đầu tiên bạn nghĩ đến: thêm Khóa ngoại vào bảng và sử dụng nó để chọn dữ liệu phù hợp cho từng đối tượng thuê. Tuy nhiên, nó có một nhược điểm rất lớn: dữ liệu của đối tượng thuê hoàn toàn không bị cô lập, do đó, một lỗi lập trình nhỏ cũng có thể đủ để làm rò rỉ dữ liệu của đối tượng thuê đến nhầm khách hàng.
Hãy lấy một ví dụ về cấu trúc cơ sở dữ liệu từ tài liệu Django :
from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Chúng tôi sẽ cần xác định hồ sơ nào thuộc sở hữu của người thuê nhà nào. Vì vậy, chúng ta cần thêm bảng Tenant
và khóa ngoại vào mỗi bảng hiện có:
class Tenant(models.Model): name = models.CharField(max_length=200) class Question(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Để đơn giản hóa mã một chút, chúng ta có thể tạo một mô hình cơ sở trừu tượng sẽ được sử dụng lại trong các mô hình khác mà chúng ta tạo.
class Tenant(models.Model): name = models.CharField(max_length=200) class BaseModel(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) class Meta: abstract = True class Question(BaseModel): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(BaseModel): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Như bạn có thể thấy, có ít nhất hai rủi ro chính ở đây: nhà phát triển có thể quên thêm trường đối tượng thuê vào mô hình mới hoặc nhà phát triển có thể quên sử dụng trường này trong khi lọc dữ liệu.
Mã nguồn của ví dụ này có thể được tìm thấy trên GitHub: https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schema .
Hãy ghi nhớ những rủi ro của lược đồ dùng chung, hãy xem xét một tùy chọn khác: cơ sở dữ liệu sẽ vẫn được chia sẻ nhưng chúng tôi sẽ tạo một lược đồ dành riêng cho từng đối tượng thuê. Để triển khai, chúng ta có thể xem thư viện phổ biến Django-tenants ( tài liệu ).
Hãy thêm django-tenants
vào dự án nhỏ của chúng ta (bạn có thể tìm thấy các bước cài đặt chính thức tại đây ).
Bước đầu tiên là cài đặt thư viện qua pip
:
pip install django-tenants
Thay đổi mô hình: mô hình Tenant
giờ đây sẽ nằm trong một ứng dụng riêng. Các mô hình Question
và Choice
sẽ không còn kết nối với đối tượng thuê nữa. Vì dữ liệu của các đối tượng thuê khác nhau sẽ nằm trong các lược đồ riêng biệt nên chúng tôi sẽ không cần liên kết các bản ghi riêng lẻ với các hàng đối tượng thuê nữa.
Tệprents/models.py
from django.db import models from django_tenants.models import TenantMixin, DomainMixin class Tenant(TenantMixin): name = models.CharField(max_length=200) # default true, schema will be automatically created and synced when it is saved auto_create_schema = True class Domain(DomainMixin): # a required table for django-tenants too ...
Tệp polls/models.py
from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Lưu ý rằng Câu hỏi và Lựa chọn không còn khóa ngoại đối với Người thuê nữa!
Một điều khác đã được thay đổi là Đối tượng thuê hiện ở trong một ứng dụng riêng biệt: nó không chỉ dùng để tách các miền mà còn quan trọng vì chúng ta sẽ cần lưu trữ bảng tenants
trong lược đồ dùng chung và các bảng polls
sẽ được tạo cho từng đối tượng thuê lược đồ.
Thực hiện các thay đổi đối với tệp settings.py
để hỗ trợ nhiều lược đồ và đối tượng thuê:
DATABASES = { 'default': { 'ENGINE': 'django_tenants.postgresql_backend', # .. } } DATABASE_ROUTERS = ( 'django_tenants.routers.TenantSyncRouter', ) MIDDLEWARE = ( 'django_tenants.middleware.main.TenantMainMiddleware', #... ) TEMPLATES = [ { #... 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', #... ], }, }, ] SHARED_APPS = ( 'django_tenants', # mandatory 'tenants', # you must list the app where your tenant model resides in 'django.contrib.contenttypes', # everything below here is optional 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.admin', ) TENANT_APPS = ( # your tenant-specific apps 'polls', ) INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] TENANT_MODEL = "tenants.Tenant" TENANT_DOMAIN_MODEL = "tenants.Domain"
Tiếp theo, hãy tạo và áp dụng các di chuyển:
python manage.py makemigrations
python manage.py migrate_schemas --shared
Kết quả là chúng ta sẽ thấy lược đồ công khai sẽ được tạo và chỉ chứa các bảng được chia sẻ.
Chúng ta sẽ cần tạo một đối tượng thuê mặc định cho lược đồ public
:
python manage.py create_tenant --domain-domain=default.com --schema_name=public --name=default_tenant
Đặt is_primary
thành True
nếu được hỏi.
Và sau đó, chúng ta có thể bắt đầu tạo những người thuê thực sự của dịch vụ:
python manage.py create_tenant --domain-domain=tenant1.com --schema_name=tenant1 --name=tenant_1 python manage.py create_tenant --domain-domain=tenant2.com --schema_name=tenant2 --name=tenant_2
Lưu ý rằng hiện có thêm 2 lược đồ trong cơ sở dữ liệu chứa các bảng polls
:
Bây giờ, bạn sẽ nhận được Câu hỏi và Lựa chọn từ các lược đồ khác nhau khi bạn gọi API trên các miền mà bạn thiết lập cho đối tượng thuê - tất cả đã hoàn tất!
Mặc dù quá trình thiết lập có vẻ phức tạp hơn và thậm chí có thể khó hơn nếu bạn di chuyển ứng dụng hiện có nhưng bản thân phương pháp này vẫn có rất nhiều ưu điểm như tính bảo mật của dữ liệu.
Mã của ví dụ có thể được tìm thấy ở đây .
Cách tiếp cận cuối cùng mà chúng ta sẽ thảo luận hôm nay là tiến xa hơn nữa và có cơ sở dữ liệu riêng cho người thuê.
Lần này, chúng ta sẽ có một vài cơ sở dữ liệu:
Chúng tôi sẽ lưu trữ dữ liệu được chia sẻ, chẳng hạn như ánh xạ của đối tượng thuê tới tên của cơ sở dữ liệu trong default_db
và tạo cơ sở dữ liệu riêng cho từng đối tượng thuê.
Sau đó, chúng ta sẽ cần đặt cấu hình cơ sở dữ liệu trong settings.py:
DATABASES = { 'default': { 'NAME': 'default_db', ... }, 'tenant_1': { 'NAME': 'tenant_1', ... }, 'tenant_2': { 'NAME': 'tenant_2', ... }, }
Và bây giờ, chúng ta sẽ có thể lấy dữ liệu cho từng đối tượng thuê bằng cách gọi using
phương thức QuerySet:
Questions.objects.using('tenant_1')…
Nhược điểm của phương pháp này là bạn sẽ cần áp dụng tất cả các lần di chuyển trên mỗi cơ sở dữ liệu bằng cách sử dụng:
python manage.py migrate --database=tenant_1
Việc tạo cơ sở dữ liệu mới cho mỗi đối tượng thuê cũng có thể kém thuận tiện hơn so với việc sử dụng django-tenants
hoặc chỉ sử dụng khóa ngoại như trong cách tiếp cận lược đồ dùng chung.
Mặt khác, khả năng cách ly dữ liệu của người thuê thực sự tốt: cơ sở dữ liệu có thể được tách biệt về mặt vật lý. Một ưu điểm khác là chúng tôi sẽ không bị giới hạn khi chỉ sử dụng Postgresql theo yêu cầu của django-tenants
, chúng tôi có thể chọn bất kỳ công cụ nào phù hợp với nhu cầu của mình.
Thông tin thêm về chủ đề đa cơ sở dữ liệu có thể được tìm thấy trong tài liệu Django.
| Người thuê nhà đơn lẻ | MT với lược đồ được chia sẻ | MT với lược đồ riêng | MT với cơ sở dữ liệu riêng biệt |
---|---|---|---|---|
Cách ly dữ liệu | ✅Cao | ❌Thấp nhất | ✅Cao | ✅Cao |
Nguy cơ rò rỉ dữ liệu vô tình | ✅Thấp | ❌Cao | ✅Thấp | ✅Thấp |
Chi phí cơ sở hạ tầng | ❌Cao hơn theo từng khách thuê | ✅Thấp hơn | ✅Thấp hơn | ✅❌ Thấp hơn so với người thuê nhà đơn lẻ |
Tốc độ triển khai | ❌Thấp hơn với từng khách thuê | ✅ | ✅❌ Quá trình di chuyển sẽ chậm hơn vì chúng cần được thực thi cho từng lược đồ | ✅❌ Quá trình di chuyển sẽ chậm hơn vì chúng cần được thực thi cho từng cơ sở dữ liệu |
Dễ để thực hiện | ✅ | ❌ Yêu cầu nhiều thay đổi nếu dịch vụ đã được triển khai dưới dạng ứng dụng một người thuê | ✅ | ✅ |
Tóm lại tất cả những điều trên, Có vẻ như không có viên đạn bạc nào cho vấn đề này, mỗi cách tiếp cận đều có ưu và nhược điểm, vì vậy, các nhà phát triển có thể quyết định xem họ có thể đánh đổi những gì.
Các cơ sở dữ liệu riêng biệt cung cấp sự cách ly tốt nhất cho dữ liệu của đối tượng thuê và dễ triển khai, tuy nhiên, bạn sẽ phải trả chi phí bảo trì cao hơn: n cơ sở dữ liệu cần cập nhật, số lượng kết nối cơ sở dữ liệu cao hơn.
Cơ sở dữ liệu dùng chung có lược đồ riêng phức tạp để triển khai và có thể gặp một số vấn đề khi di chuyển.
Một đối tượng thuê đơn lẻ là cách triển khai đơn giản nhất nhưng sẽ khiến bạn tốn kém do tiêu thụ quá nhiều tài nguyên vì bạn có toàn bộ bản sao dịch vụ của mình cho mỗi đối tượng thuê.