Please enable Javascript to view the contents

Django 性能之分库分表

 ·  ☕ 3 分钟

1. 碰到的问题

前端请求量大,并发高,访问速度慢,瓶颈主要表现在:

  • 单表大
  • 单库大
  • 网络 IO 慢
  • 磁盘 IO 慢

网络、磁盘 IO 优化,主要依靠硬件升级。理论上,数据库对单库、单表的大小没有限制,但是过大的单库、单表会导致更多的请求落到单机上,给 IO 造成压力。

理想情况是,通过增加机器,能不断地增加系统并发能力。当 MySQL 单表数据量达到百万级别时,我们就应该开始存储相关的知识,以应对可能的问题。

2. 数据库架构设计的三种模式

为了解决数据库的性能问题,除了使用性能更好的硬件之外,另外一个思路就是从架构方面考虑。将一个数据库切分成多个部分放到不同的数据库上,从而缓解单一数据库的性能问题。

数据库构架设计中主要有 Shared Everthting、Shared Nothing、和 Shared Disk。通常说的 Sharding,实际上指的就是 Shared Nothing,通过增加处理单元来扩展处理能力。

2.1 Shared Everthting

通常是单个主机,完全共享 CPU、Memory、IO,并行处理能力差。例如,SQL Server。

2.2 Shared Disk

各个处理单元使用私有的 CPU、Memory,共享 IO。可以通过增加节点来提高并行处理能力,直到存储接口达到瓶颈为止。例如 Oracle Rac。

2.3 Shared Nothing

各个处理单元都有自己私有的 CPU、Memory、IO。各个处理单元之间,通过协议通信,例如:Hadopp。

3. 拆分策略

3.1 垂直拆分

将关系紧密的数据聚合在一起,拆分到不同的 Server。

  • 分表:基于字段。
  • 分库:基于业务。

3.2 水平拆分

将同类数据,拆分到不同的 Server。

  • 分表:基于某种规则(hash 等)。
  • 分库:基于表结构相同,但数据集不同。

拆分后的问题

  • 主键生成(唯一 ID)
  • 数据的路由(分布、节点伸缩)
  • 事务支持。由数据库本身,转向了应用层。
  • 跨库 Join。由应用层组装。
  • count、group by、order by 等聚合

在生产环境中,通常会混合垂直、水平拆分实施。将原有数据库切分为矩阵一样,可以根据需要,无限拆分。

4. Django 中的 Sharding

4.1 分表方案

Django 分表方案,主要是自定义 Model 的 db_table 属性指定 ORM 操作的表名。

下面的例子中,使用一个 Proxy 类,通过取余算法,将不同用户的数据分配到不同表中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.db import models

# 表分片的数量
SHARD_TABLE_NUMBER = 2


class UserProxy(models.Model):
    @classmethod
    def get_sharding_model(cls, uid=None):
        piece = uid % SHARD_TABLE_NUMBER

        class Meta:
            db_table = 'user_%s' % piece
        attrs = {
            '__module__': cls.__module__,
            '__doc__': 'using user_%s table' % piece,
            'Meta': Meta
        }
        return type(str('User%s' % piece), (cls, ), attrs)
    username = models.CharField(max_length=255)

    class Meta:
        abstract = True

User2 = UserProxy.get_sharding_model(uid=2)

需要考虑的新问题:

  • 分片数量改变后,如何保证一致性
  • 新建数据如何选择表
  • 如何同步表结构

4.2 分库方案

Django 原生支持分库,只需要在 settings.py 文件中,新增数据库配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'db_name1',
        'USER': 'db_user1',
        'PASSWORD': 'db_password1',
        'HOST': '127.0.0.0',
        'PORT': 3306,
    },
    'mydb2': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'db_name2',
        'USER': 'db_user2',
        'PASSWORD': 'db_password2',
        'HOST': '127.0.0.0',
        'PORT': 3306,
    }
}

有两种使用方法:

  1. 使用 using,代码入侵比较强。
1
Author.objects.using('mydb').all()
  1. 使用 Database Router

第一步,编写 Database Router,指定匹配的 app_label 使用某个 DB。

需要实现 db_for_read、db_for_write、allow_relationy 以及 allow_migrate 方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class MyRouter(object):
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'myapp_label':
            return 'mydb2'
        return None

    def db_for_write(self, model, **hints):
        pass

    def allow_relation(self, obj1, obj2, **hints):
        pass

    def allow_migrate(self, db, model):
        pass

第二步,在 settings.py 文件中配置 Database Router。

1
DATABASE_ROUTERS = ['db_router.MyRouter']

第三步,配置 Model 的 app_label

1
2
3
4
5
class MyModel(models.Model):
    pass

    class Meta:
        app_label = 'myapp_label'

微信公众号
作者
微信公众号