管理Django的master/replica pinning
项目描述
pindb是Django的一个master/slave路由工具包。它提供了数据库副本pinning、从副本的轮询读取、与托管数据库并行使用非托管数据库,以及用于在多个复制的数据库集之间进行决策的代理路由器。
TL;DR API
# Try to save without pinning: foo = Model.objects.all()[0] # UnpinnedWriteException raised under strict mode, or master pinning occurs under greedy mode. foo.save() # read only from the master, allow writes. pindb.pin(alias) # Initialize/end the container: pindb.unpin_all() with unpinned_replica(alias): ... # read from replicas despite pinning state # or queryset.using(pindb.get_replica('master-alias')) with master(alias): ... # write to master despite pinning state
术语
master指的是可写数据库。副本(或slave)指的是只读数据库,其数据来自master的写入。一个或多个master/slave数据库集可以帮助扩展读取并避免master上的锁竞争。通常所有读取都发送到副本,直到发生写入操作 - 然后,所有后续的读取也发送到master,以避免由于复制延迟而导致的不一致读取。pinning基于时间,并通过cookie在Web请求之间进行往返。有关更多信息,请参阅下面的“设计说明”。
安装
pip install pindb
TL;DR
将 DATABASES_ROUTERS 设置为使用pindb路由器
在测试中将 PINDB_ENABLED 设置为False
定义数据库master和副本集。
使用 pindb.populate_replicas 填充 DATABASES
将 PinDbMiddleware 添加到您的中间件中。
(如果需要)与celery集成。
为显式跳过pinning的位置进行配置。
更明确地
将 pindb.StrictPinDbRouter 或 pindb.GreedyPinDbRouter 添加到 DATABASE_ROUTERS。
StrictPinDbRouter 需要在尝试写入之前声明绑定。优点是尽可能多地使用只读副本。缺点是您的代码需要许多声明来显式允许和放弃绑定。
GreedyPinDbRouter 一旦发生写入就会将绑定到主数据库。优点是大多数代码将正常工作。缺点是您将比可能更少地使用只读副本。您也可能遇到更多您的数据库状态在背后发生变化的情况:您可能从滞后副本中读取,然后根据旧信息执行写入(这将使您绑定到主数据库)。
PINDB_ENABLED 可用于在测试中禁用 pindb。每个通过 TEST_MIRROR 映射的别名都获得自己的连接(因此是事务),这在 Django 的 TestCase 中存在问题,其中主写入在副本连接下是不可见的。
pindb 拥有详尽的测试套件;在自己的测试套件中禁用它是合理/推荐的。
如果您需要管理多个主/副本集,请为 pindb 添加 PINDB_DELEGATE_ROUTERS 以在数据库集选择时进行委派。这只是一个 Django 的另一个 数据库路由器,它应该返回一个主别名;pindb 然后根据返回主数据库的当前绑定状态选择主数据库或副本。
定义 MASTER_DATABASES,与 DATABASES 具有相同的模式
DATABASES = { "unmanaged": { # HOST0, etc. } } MASTER_DATABASES = { "default": { # HOST1, etc. }, "some_other_master": { ... } }
定义 DATABASE_SETS,它覆盖特定副本的设置
DATABASE_SETS = { "default": [{HOST:HOST1}, {HOST:HOST2}, ...], "some_other_master": [...] # zero or more replicas is fine. }
使用 pindb.populate_replicas 确定最终 DATABASES
DATABASES.update(populate_replicas(MASTER_DATABASES, DATABASE_SETS))
在预期数据库访问之前将 pindb.middleware.PinDbMiddleware 添加到您的 MIDDLEWARE_CLASSES
可选地,在您的代码库中,如果打算写入,尽早声明以避免从相关副本的不一致读取
pin("default")
这将导致所有后续读取都使用主数据库。
在 celery 中使用时,将 celery.signals.task_postrun 钩子连接到调用 pindb.unpin_all
import pindb from celery.signals import task_postrun def end_pinning(**kwargs): pindb.unpin_all() task_postrun.connect(end_pinning)
异常及其避免
异常
PinDbConfigError 可能由…引起
您的设置不包括 MASTER_DATABASES 和 DATABASE_SETS
您的 MASTER_DATABASES 不包括“默认”并且 populate_replicas 调用时未传递 unmanaged_default=True。
在 MASTER_DATABASES 中声明一个没有相关 DATABASE_SETS 条目的别名
UnpinnedWriteException 可能由…引起
在没有先前调用 pindb.pin 对主数据库进行 Model.objects.create、Model.save、qs.update 或 qs.delete 的情况下写入
请注意,向未管理别名(即未列入 MASTER_DATABASES 及其相关 DATABASE_SETS 的别名)的写入在任何时间都是允许的。
覆盖绑定
如果您希望在之前已绑定主数据库的情况下从副本读取,可以这样做:
with pindb.unpinned_replica(alias): # code which reads from replicas
如果您希望在未绑定到它的情况下向主数据库写入,可以这样做:
with pindb.master(alias): # code which writes to the DB
需求和设计注意事项
我们有多个独立的主数据库(不一定分片)。让我们称一个主数据库及其副本的组为一个“数据库集”。
我们希望拥有这些主数据库的只读副本,并尽可能多地从副本读取,同时希望所有写入操作都发送到该组的master。但我们还希望读取操作与写入操作保持一致性。
我们希望这不仅在web请求周期中可行,对于像任务或shell脚本这样的工作单元也同样适用。因此,我们将这个工作单元称为“固定上下文”。
对特定master的写入应继续从master读取,以避免在复制延迟窗口中出现不一致,因此将有一个API来声明这一点。声明(或优先选择)需要一组master的操作称为“固定”,所有数据库集的固定组称为“固定集”。
计划写入(或需要最新数据)的代码应尽可能早地声明,以从master获得完全一致的观点。
如果我们固定了错误(即,在从集读取后写入),将是一个明显的错误。这里的问题是,如果我们允许读取(不知道即将进行写入),这将给我们一个不一致的窗口。例如,一个进程从副本读取,获取在master中已被删除的PK,写入master,失败。或者获取在master中被修改的PK,因此不应该被处理等。
需要写入但不固定整个容器(例如,日志表)的代码应能够绕过固定。
我们应该能够在具有最小重复性的设置中管理数据库集,并且应该与多个设置文件很好地组合。
方法
我们使用threadlocal来保存固定集。
然后数据库路由器将尊重固定集。
设置中的DATABASES字典在意义上是“最终”的,因为它没有使用任何master/replica语义进行结构化。因此,我们使用中间设置来定义集。
MASTER_DATABASES = { 'master-alias': { 'HOST':"a", ...normal settings }, ... } DATABASE_SETS = { 'master-alias': [{'HOST':'someotherhost',...},], # override some of the master settings }
并且副本配置可以最终确定……
DATABASES = DATABASES.update(populate_replicas(MASTER_DATABASES, DATABASE_SETS))
……结果类似于……
DATABASES = { 'master-alias': { 'HOST':"a", ...normal settings }, 'master-alias-1': { 'HOST':"someotherhost", ...merged settings, TEST_MIRROR='master-alias' }, ...}
如果没有命名为“default”的master,则第一个数据库集的master也将被别名为“default”。你应该使用django.utils.datastructures.SortedDict来确保稳定性。
如果你有多个数据库集,你还将想要将固定与适当的集的选择结合起来。为此,有一个额外的设置:DATABASE_ROUTER_DELEGATE。它具有与正常DATABASE_ROUTER相同的接口,但db_for_read和db_for_write必须只返回master别名。然后将为该DB集选择适当的master或副本。
更具体地说,假设你有2个不同的master,每个master都有一个读副本。你的代理路由器(在使用pindb之前)很可能基于应用程序语义选择哪个master。继续这样做。然后pindb的路由器将选择一个来自DB集的读副本,该DB集的master是现有(现在代理)路由器选择的。
严格路由器如果在未声明允许的情况下调用db_for_write将抛出错误。正确的方法是在从副本读取之前固定你打算写入的数据库。
要明确优先选择读副本而忽略固定,请使用以下任一方法……
with pindb.unpinned_replica('master-alias'): ...
……或查询集的.using方法。
如果你想明确使用副本,pindb.get_replica()将返回一个副本别名。
固定集的持续时间与固定上下文的持续时间相同:一旦固定,你不应该解固定数据库。如果你想在无需固定容器的情况下写入数据库,可以使用查询集的.using方法,这将绕过db_for_write。小心使用这个工具。
声明一个引脚...
pindb.pin('master-alias')
TODO:如果可用,请使用签名cookie(dj 1.4+)进行Web引脚上下文。
覆盖率
查看 pindb 的覆盖率。
$ PYTHONPATH=.:$PYTHONPATH coverage run setup.py test $ coverage html
示例配置
MASTER_DATABASES = { 'default': { 'NAME': 'db1', 'ENGINE': DB_ENGINE, 'USER': '...', 'PASSWORD': '...', 'HOST': '10.0.1.0', 'PORT': 3306, 'OPTIONS': DB_OPTIONS }, 'api': { 'NAME': 'db2', 'ENGINE': DB_ENGINE, 'USER': '...', 'PASSWORD': '...', 'HOST': '10.0.2.0', 'PORT': 3306, 'OPTIONS': DB_OPTIONS }, } DATABASE_SETS = { "default": [{'HOST': '10.0.1.1'},{'HOST': '10.0.1.2'}], "api": [{'HOST': '10.0.2.1'}] } DATABASES = {...} DATABASES.update(pindb.populate_replicas(MASTER_DATABASES, DATABASE_SETS)) PINDB_DELEGATE_ROUTERS = ["myapp.router.Router"] DATABASE_ROUTERS = ['pindb.GreedyPinDbRouter'] # default values which you can override: PINDB_ENABLED = True PINDB_PINNING_COOKIE = 'pindb_pinned_set' PINDB_PINNING_SECONDS = 15
项目详情
django-pindb-0.1.12.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | e255c5241ff293c9078b4661352f2c3418aa66b70d914a280b8efb3fa2b3bbb6 |
|
MD5 | 743af728cf610a8d52a8790bc4a6fef8 |
|
BLAKE2b-256 | 3e25359d719840c2c62ee6676d12103cff005d5ae0a326e344f0bd7dc0adb008 |