跳转到主要内容

管理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

  1. DATABASES_ROUTERS 设置为使用pindb路由器

  2. 在测试中将 PINDB_ENABLED 设置为False

  3. 定义数据库master和副本集。

  4. 使用 pindb.populate_replicas 填充 DATABASES

  5. PinDbMiddleware 添加到您的中间件中。

  6. (如果需要)与celery集成。

  7. 为显式跳过pinning的位置进行配置。

更明确地

pindb.StrictPinDbRouterpindb.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_DATABASESDATABASE_SETS

  • 您的 MASTER_DATABASES 不包括“默认”并且 populate_replicas 调用时未传递 unmanaged_default=True

  • MASTER_DATABASES 中声明一个没有相关 DATABASE_SETS 条目的别名

UnpinnedWriteException 可能由…引起

  • 在没有先前调用 pindb.pin 对主数据库进行 Model.objects.createModel.saveqs.updateqs.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_readdb_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 (20.5 kB 查看哈希值)

上传时间 源代码

由以下机构支持