有多少人曾为应用程序数据库的扩展和优化而头疼?如果你还没有阅读过《如何用 Django 搞定 SQL 的 “脏活累活”》,强烈建议先看看那篇文章。简单来说,SQL 是专为 SQL 数据库优化的,Python 则不是,而 Django 可以充当一个中介,帮助我们构建更有效的应用程序,同时在配合使用这两种语言时减少摩擦,降低复杂性和代码量。
然而,尽管 Django 承担了创建数据库应用的重任,我们依然需要承担数据库的日常管理和监控工作。好在只要使用诸如 Linode Managed Databases 这样的托管数据库服务,很多管理任务都可以委托给云提供商,但我们可能依然会在应用扩展的过程中遇到新问题,例如:
- 数据库迁移。将现有数据库切换为新的所需状态,并以可控的方式更改数据库 Schema。
- 多数据库部署。为了优化性能,开发者可以设计应用程序中的不同功能使用独立的数据库。例如,一个主要的读 / 写数据库和一个为通用查询提供服务的可读副本数据库。
如果其中一个数据库使用了 SQL,我们可以按照《如何用 Django 搞定 SQL 的 “脏活累活”》一文的介绍,大幅简化海量数据的处理工作。
本文将介绍数据库管理工作中的两个重要概念,并通过循序渐进的指南告诉大家该如何构建面向生产环境的 Django 应用程序。
延伸阅读,了解Akamai cloud-computing
出海云服务,选择Akamai Linode!
数据库迁移
在刚上手时,为数据库中不同列的内容确定适合的数据类型,这可能是个有点棘手的任务,如果有关数据的需求会随着时间推移不可避免产生变化,感觉就更棘手了。如果希望标题字段只能包含 80 个字符该怎么办?如果需要添加一个时间戳字段以便准确追踪每个内容是在什么时候加入数据库的又该怎么办?
在创建好一个表之后再更改其内容,可能会产生一些非常混乱的后果,原因主要在于:
- 该如何处理已经存在的值?
- 如果原有的行缺少新增列 / 字段对应的数据该怎么办?
- 如果删除了某个列 / 字段该怎么办?数据会发生什么变化?
- 如果添加了一个之前不存在的关系(例如外键)又该如何处理?
好在对于 Django 开发者来说,可以使用 makemigrations 和 migrate。一起看看它们是如何生效的。
以如下的 Django 数据模型为例:
class BlogArticle(models.Model):
user = models.ForeignKey(User, default=1, on_delete=models.SET_DEFAULT)
title = models.CharField(max_length=120)
slug = models.SlugField(blank=True, null=True)
content = models.TextField(blank=True, null=True)
publish_timestamp = models.DateTimeField(
auto_now_add=False,
auto_now=False,
blank=True,
null=True,
)
先来添加一个字段:
updated_by = models.ForeignKey(
User, related_name="editor", null=True, blank=True, on_delete=models.SET_NULL
)
该字段可供我们追踪修改了模型的最后一个用户。
接着更新一下这个模型:
class BlogArticle(models.Model):
user = models.ForeignKey(User, default=1, on_delete=models.SET_DEFAULT)
title = models.CharField(max_length=120)
slug = models.SlugField(blank=True, null=True)
content = models.TextField(blank=True, null=True)
publish_timestamp = models.DateTimeField(
auto_now_add=False,
auto_now=False,
blank=True,
null=True,
)
# our new field
updated_by = models.ForeignKey(
User, related_name="editor", null=True, blank=True, on_delete=models.SET_NULL
)
当我们保存了 BlogArticle 类所声明的文件(models.py)后,如何让数据库知道发生了这个改动?有两种方法:
- python manage.py makemigrations
- python manage.py migrate
一起看看这两个命令的作用吧。
1.python manage.py makemigrations
python manage.py makemigrations 可以在我们的 Django 项目中寻找对 models.py 文件进行的所有改动。如果找到改动,将创建一个全新的 Python 文件,其中包含 SQL 数据库需要进行的提议变更(Proposed change)。这种提议变更看起来类似这样:
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('articles', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='article',
name='updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='editor', to=settings.AUTH_USER_MODEL),
),
]
这当然也是另一个 Python 文件。开发者可以通过该文件了解数据库中应该发生的事情。为了维持一致并利用 Django ORM 的内置功能,该文件会使用 Python 编写(而非 SQL)。
但为什么这个文件中记录了应该发生的事情?主要原因在于:
- 如果我们希望在实际发生前检查应该发生的事情,可以直接通过该文件了解。
- Makemigrations 命令并不会通过检查数据库来判断这些变化是否可以真正发生。
- 为了使用这些要求,数据库可能已经发生了改变(取决于谁在管理数据库等一系列因素)。
- 如果希望在更改生产数据库前进行测试,那么此时就是最佳时机。
假设这些变更是有效的(只要我们可以正确判断),随后就可以通过下列方法提交这些变更了。
2.python manage.py migrate
python manage.py migrate 将尝试着为我们更改数据库(所有字段、列、表、外键等),Django 会代替我们做这些工作,以确保数据库能按照我们希望的方式更新。
需要注意的是,Django 可能会因为一些原因而无法执行这些变更。对于新接触 Django 的开发者,最主要的原因往往是添加或删除了字段 / 列,但未能正确进行迁移。
如果操作正确无误,python manage.py migrate 可以保证产生稳定的系统,并且各方面均与我们的 Python 代码和 SQL 表保持匹配,从而确保可以正确发挥出 Django 和 SQL 数据库提供的强大功能。
这种做法如何为我们带来更高灵活性?
Python 有着广泛的应用,但 SQL 就未必了。顾名思义,结构化查询语言(Structured Query Language)存在一些固有的局限。谁能只使用 SQL 就创建出精彩的动画电影呢?
这样说的意图在于,Python 的简洁性有助于开发者发挥出 SQL 的真正威力,甚至可能是在不知不觉中发挥出来的。
为何使用托管数据库和 Django 是合理的做法
在创建包括 Django 在内的 Web 应用程序时,我们需要决定一些事情,例如:
上述这些问题的答案可能会随着项目复杂性的提高而改变,但所有答案都始于同一个地方:决定到底是使用自管理的还是第三方托管的存储解决方案。
自管理数据库:
- 优势:控制能力和更低的成本
- (巨大的)不足:一切都由我们自行负责
托管服务往往从一开始就需要付出更多成本,而自行管理意味着我们可以使用自己喜欢,并且以某种方式(或在某种程度上)针对我们的需求进行了优化的 Linux 发行版。例如我们可以用自己团队创建并修改的 MySQL 分支版本。自行运行的服务往往更省钱,但需要付出更多的时间和精力来维护。
第三方托管数据库服务:
没错,这类服务往往成本更高一些,但可以大幅减少我们需要投入的维护时间。这种方式以及托管式的数据存储解决方案已经成为很多 Web 应用程序的首选。本例中,我们已经在使用 Django 来管理数据库事务,SQLAlchemy 也具备类似优势,因为它可以配合 FastAPI、Flask 等框架一起使用。如果你已经将 SQL 的相关开发工作通过 Python 包来实现,那么为何不把 SQL 服务器的运行也照此处理?
考虑到 Python ORM(例如 Django ORM 和 SQLAlchemy)的效果,建议尽可能使用托管数据库和 / 或托管的数据存储服务,从而可以获得下列这些收益:
- 缩短开发时间
- 缩短管理时间
- 缩短恢复时间
- 减少服务中断
- 降低部署和开发工作的复杂度
- 减少(从其他服务)迁移数据库的复杂度
- 减少 SQL 开发者的重复 / 无效 / 低效活动
- 降低 DevOps/Ops 复杂度
- 提高 non-SQL 开发者的效率
- 加快部署和开发速度
- 提高可靠性(托管服务通常有服务级别协议作为保证)
- 增强安全性
- 增强可维护性
- 增加备份和冗余
- 成本略微增加
实现上述好处的前提是:在 Linode 平台上使用托管的 MySQL 数据库集群,并使用 Linode 的 Object Storage(存储 CSS、JavaScript、图像、视频等文件)。实际上,使用这些服务还有助于我们将更多精力专注于通过 Django、 FastAPI、Flask、Node.js 等技术构建卓越的 Web 应用程序。换句话说,我们可以将工作的重心放在构建用户实际需要的工具和软件上,毕竟对用户而言,这些才是真正的价值所在。
MySQL、PostgreSQL、Redis 和 Django
很长一段时间以来,配合 Django 使用最广泛的数据库都是 PostgreSQL,这在很大程度上可能因为只能在 Postgres 中使用 JSONField。然而随着 Django 3.2 + 和 MySQL 5.7.8 + 的推出,JSONField 也已经可用于 MySQL 了。
这一点为何重要?
在处理用户生成的内容或存储来自其他 API 服务的数据时,通常需要存储非结构化的数据,例如 JSON。一起来看看具体是怎么做的:
from django.db import models
class Pet(models.Model):
name = models.CharField(max_length=200)
data = models.JSONField(null=True)
def __str__(self):
return self.name
我们可能想要存储与“Pet” 有关的如下数据:
pet1 = {
"name": "Bruno",
"type": "Rat",
"nickname": "We don't talk about it",
"age": 2,
"age_interval": "months"
}
pet2 = {
"name": "Tom",
"type": "Cat",
"breed": "Mixed"
"age": 4,
"age_interval: "years",
"favorite_food": [{"brand": "Acme", "flavor": "Tuna" }]
}
pet3 = {
"name": "Stewey",
"type": "Dog",
"breed": "unknown"
"age": 34,
"age_interval: "dog years",
"nickname": "Football"
}
从上述数据中我们可以知道什么时候可能需要一个 JSONField。我们可以存储所有宠物的名字(使用 name 键)并将其他信息存储在 JSONField 中。JSONField 最酷的地方在于,可以像其他标准 Django 字段那样查询,哪怕它们使用了不同的 Schema。
Django 开发者之间一直在争论要使用哪种数据库:MySQL 或是 PostgreSQL。以前,很多人会坚持选择 PostgreSQL,因为 JSONField 只能在 PostgreSQL 中使用,但现在情况不同了。因此我们完全可以任选一个并一直使用,直到所选数据库已经无法满足自己的需求。
但为何又要使用 Redis?
Redis 是一种速度惊人的内存中数据存储技术,因此通常被用作临时数据库(稍后将详细介绍这一点)、缓存服务以及 / 或消息队列。之所以将其称之为 “临时数据库” 是因为该技术将数据保存在内存中,内存通常比磁盘存储更贵,因此将数据长期存储在内存中往往并不是可行的做法。
本例中的 Redis 和 Django 主要用于缓存和队列。
缓存:假设用户需要频繁访问某几个网页,我们希望能尽可能快速地向用户展示这些页面的数据。如果配合 Django 使用 Redis 作为缓存系统,就可以轻松实现这一点。这些页面上的数据可能是从一个 SQL 数据库中渲染出来的,但 Redis 依然可以将渲染出的数据存储在缓存中。换句话说,将 Redis 与 SQL 配合使用通常可以加快响应速度,同时减少对 SQL 数据库发出的查询数量。
队列:Redis 的另一个流行用例是将长时间运行的任务分载(Offload)到另一个进程(这通常可借助一个名为 Celery 的 Python 包实现)。如果需要这样做,即可使用 Redis 作为队列,保存需要在另一个时间完成的任务。
举例来说,如果有用户需要一份有关过去五年来所有交易的报表,软件可能需要花费几小时才能生成这样的报表。很明显,没人愿意盯着计算机等待数小时之久。因此我们可以把这种请求从用户那里分载到一个 Redis 队列中。一旦放入 Redis,就可以运行工作进程(例如将 Celery 与 Django 配合使用)来生成报表。报表创建完成后,无论该过程花费了多长时间,用户都能收到通知。和其他通知一样,这种通知也可以通过 Redis 队列与 Celery/Django 工作进程配合实现。
上述内容都是为了说明:Redis 和 MySQL 可以实现非常美妙的互补。我们可以通过 Linode Marketplace 部署自行管理的 Redis 数据库服务器。
对象存储
我们推荐的最后一个与数据有关的托管服务是 Linode Object Storage。Object Storage 负责保存我们需要存储的所有其他类型的数据,毕竟肯定没人会希望将一个视频文件的所有数据都存储在 MySQL 中。数据库可以只用来存储与视频有关的元数据,而视频文件本身,可以存储在 Object Storage 中。
我们可以使用对象存储服务来存储类似下面这些数据:
- 层叠样式表(CSS)
- JavaScript(例如 React.js、Vue.js、Vanilla.js 等)
- 视频
- 图像(原始图像和压缩后的图像)
- CSV、XLSX
- 数据库备份
- Docker 容器镜像层(如果自行管理)
- 训练后的机器学习算法和后续迭代
- Terraform State File
- PDF(无论大小)
- 任何需要频繁下载(或上传)的演示文稿
总结
按照本文推荐的思路,任何开发者都可以在自己的 Web 应用程序项目中充分发挥托管服务的力量。Django 是一种在 SQL 数据库基础上构建 Web 应用的出色解决方案,但它并非唯一。如果希望深入探寻 SQL 和 SQL 服务器的内部原理,我们认为有必要通过不断的研究来了解到底有多少成功的应用程序在使用 Django 来处理各种工作。
将 Django 与 Linode 的托管 MySQL 配合使用,可以获得包括但不限于下面列举的这些好处:
- Django 代替用户执行繁重的 SQL 任务(适用于 Flask/FastAPI 的 SQLAlchemy 等工具也是如此)
- Django 可以实现原始 SQL 命令(SQLAlchemy 之类的工具也可以支持)
- Django 可以帮助新手学习 SQL 命令
- Django 内建了对 MySQL 和 PostgreSQL 的支持(还提供了一个针对 db 的 Python 客户端)
- 可提高生产环境的部署速度
- 可提高可靠性与可恢复性
- 有助于为开发和生产环境提供几乎完全一致的数据库技术
- 让基于容器的 Django 更易用、更可靠
- 实现了从单节点部署到多节点的扩展,甚至可全面过渡至 Kubernetes
- 帮助 Django/Python 新手更容易地使用生产级系统
- 更易于跨多个 Python 应用共享数据库,并且更安全(例如 FastAPI 应用读 / 写基于 Django 的 MySQL 数据库)
- Django 的 JSONField 现已支持使用 MySQL(以前只支持 PostgreSQL)
- 更易于测试(在 CI/CD 期间,或在本地开发环境中)
- 规模完全可满足 Django 的需求
- 支持在一个 Django 项目中使用多个数据库,例如使用 MySQL 作为主要的读 / 写数据库,并用另一个 MySQL 为常用查询提供可读副本数据库
- 严格的访问控制(Linode Private IP、本地开发)
- 要求通过 SSL 证书进行连接(增加了部署复杂度,但也增强了安全性)
- 实现私有连接(同一区域内部,降低连接成本)
这篇文章的内容感觉还行吧?有没有想要立即在 Linode 平台上亲自尝试一下?别忘了,现在注册可以免费获得价值 100 美元的使用额度,快点自己动手体验本文介绍的功能和服务吧↓↓↓
出海云服务,选择Akamai Linode!
欢迎关注 Akamai,我们将定期分享各种实用技巧,帮助大家了解如何通过现代方法在 Linode 上部署 Django 应用程序,如何更好地使用托管的 MySQL 数据库,如何通过 GitHub Actions 实现 CI/CD,以及与 Terraform、Ansible 等技术有关的各类实用技巧。