Django 實現分庫


網站后端的數據庫隨着業務的不斷擴大,用戶的累積,數據庫的壓力會逐漸增大。一種辦法是優化使用方法,也就是的優化 SQL 語句啦,添加緩存以達到減少存取的目的;另外一種辦法是修改使用架構,在數據庫層面上「分庫分表」。

以前做手游服務器的時候,數據庫用的是 NxM 的結構,即 N 個數據庫,M 個表。通過用戶 ID 哈希把不同的用戶分布到不同的表中,以達到「均衡」的目的。分庫分表是很常見的解決數據庫壓力的方法,適用於很多業務場景,比如社交類app,用戶表、用戶評論這種只會不斷累加但不會刪除。

我遇到一個剛需的例子是:日志統計平台,當然少不了日志存儲。日志的特性是相互之間沒有任何關聯(業務簡單),一直會增量上報(量大),單表存儲,很快就會有查詢性能問題。這是可能最合適的分庫分表的業務場景了。

ORM 幾乎是數據庫切分的「天敵」(本質上他們有這不同的設計策略)。而我用的是 Django,Django 的 ORM 基本上就可以「分表」說再見了,一個模型對應一個表,如果要 10 個表,就要寫 10 個模型,使用上麻煩,而且不容易擴展和維護。Django 提供了同時使用多數據庫的方法,通過配置路由規則來選擇使用的數據庫,看起來是的「垂直分庫」變的可行,這篇文章將介紹在日志統計平台中如何實現日志存儲的分庫。

BTW,在此之前我的日志系統是我自己脫離 Django 直接封裝了一層 MySQL 的使用接口,實現 10*10 的日志存儲庫表結構,用了一段時間也沒出現問題。缺陷就是增加新功能的時候太過繁瑣,為不同業務的查詢封裝了多個接口,最蛋疼的時候沒有 django migrate 這樣的工具,增加、刪除字段會變的很復雜。

日志統計平台有自身的業務,以及其它要存儲的數據用 default 來存儲,日志自身的存儲將被分成 10 個庫,然后按照服務器 ID 哈希到這 10 個庫中。庫的別名為 sharding0sharding1, ..., sharding9

新建項目

首先創建一個 logstat 的項目,然后一個創建 report 的 app。

>>> django-admin startproject logstat
>>> cd logstat
>>> django-startapp report

數據庫配置

配置 setting 中配置 default DATABASES:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'logstat',
        'USER': 'logstat_user',
        'PASSWORD': 'logstat_password',
        'HOST': 'localhost',
        'PORT': '3306',
        'CHARSET': 'utf8',
    },
}

接下來配置分庫日志的數據庫,為了 demo 寫起來方便,改成 2 個庫:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'logstat',
        'USER': 'logstat_user',
        'PASSWORD': 'logstat_password',
        'HOST': 'localhost',
        'PORT': '3306',
        'CHARSET': 'utf8',
    },
    'logsharding0': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'logsharding0',
        'USER': 'logstat_user',
        'PASSWORD': 'logstat_password',
        'HOST': 'localhost',
        'PORT': '3306',
        'CHARSET': 'utf8',
    },
    'logsharding1': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'logsharding1',
        'USER': 'logstat_user',
        'PASSWORD': 'logstat_password',
        'HOST': 'localhost',
        'PORT': '3306',
        'CHARSET': 'utf8',
    },
}

創建數據庫

create database logstat charset='utf8';
grant all on logstat.* to 'logstat_user'@localhost identified by 'logstat_password';
create database logsharding0 charset='utf8';
grant all on logsharding0.* to 'logstat_user'@localhost identified by 'logstat_password';
create database logsharding1 charset='utf8';
grant all on logsharding1.* to 'logstat_user'@localhost identified by 'logstat_password';

添加模型類

在 logstat/report/models.py 中添加我們要存儲的日志格式:

class Log(models.Model):
    serverid = models.IntegerField('服務器ID')
    logid = models.IntegerField('日志類型')
    desc = models.TextField('日志內容', blank=True)
    report_dt = models.DateTimeField('上報時間')

然后將 app 添加到 INSTALLED_APPS 中。./manage.py makemigrations 產生 migrations 文件。

同步數據庫:

./manage.py migrate
./manage.py migrate --database=logsharding0
./manage.py migrate --database=logsharding1

這時候我們發現,所有的 migrations 都在 defaultlogsharding0logsharding1 分別創建了表。這顯然不是我們想要的。我們想要的效果是 report app 中的模型不在 default 中創建,只在 logshardingx 中創建,而 default 中的模型,也不希望在 logshardingx中創建。

此時我們需要添加數據庫路由器。

數據庫路由器

在 logstat/report 中創建 log_router.py 文件,添加路由規則:

class LogRouter(object):
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label == 'report':
            return db == 'logsharding0' or db == 'logsharding1'
        return None

意思是,在 migrate 時,如果是 report App,並且 database 是 logshardingx 是可以創建的,否則不創建。

在 setting.py 中添加數據庫路由器,使之生效:

DATABASE_ROUTERS = ['report.log_router.LogRouter',]

同步數據庫:

./manage.py migrate
./manage.py migrate --database=logsharding0
./manage.py migrate --database=logsharding1

這時,我們發現 report_log 表已經不再 log_stat 庫中,而只出現在 logshardingx 中。但是在 logshardingx 還是會有 auth_groupauth_group_permissions ... 這些 Django 組件的表。到現在,我們已經實現了分庫的效果,這些額外的表我們不用關心,但是總覺得不優雅,還是去比較好。

這些額外的 App 分別是 adminauthcontenttypessessionsmessagesstaticfiles,同樣我們需要為他們設置路由規則,在 logstat 下創建 default_router.py 添加路由規則,使得其余的 App 自動只選擇 default

class DefaultRouter(object):
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label == 'admin' \
           or app_label == 'auth' \
           or app_label == 'staticfiles' \
           or app_label == 'sessions' \
           or app_label == 'messages' \
           or app_label == 'contenttypes':
            return db == 'default'
        return None

同樣添加到 DATABASE_ROUTERS 中,

DATABASE_ROUTERS = [
    'logstat.default_router.DefaultRouter',
    'report.log_router.LogRouter',
]

再執行 migrate --database=xxx 時,只創建了兩個表,django_migrations 和 report_log ,這就是我們想要的效果。

備注: django_migrations 這個表是必須存在的,它是數據庫 migrate 記錄,以保證再次 migrate 時,migrations 文件不被重復執行。

分庫使用

Django 在多數據庫文檔中提供了指定數據庫的用法,但是我個人傾向於一個簡單的規則:「同時只操作一個庫」。從那個庫中查詢的數據,無論是修改、保存還是刪除,都只操作同一個庫。像:

>>> user_obj.save(using='new_users')
>>> user_obj.delete(using='legacy_users')

這種用戶從一個表遷移到另外一個表,應該寫的更明確一些,應在業務上遷移而不是利用 using 關鍵字和 Django 的設計取巧。盡管這非常方便,但是對於維護代碼的人簡直就是災難!所謂業務上的遷移是,首先創建一個新的用戶(User.objects.using('new_users').create()),待新用戶創建以后,再刪除舊用戶(User.objects.using('legacy_users').filter().delete()),邏輯清晰。

自動選擇一個庫

日志存儲的需求是:基於 serverid 的 hash 值選擇一個存儲庫中的模型,封裝一個函數即可:

def db_slice(serverid):
    slice_list = (
        'logsharding0',
        'logsharding0'
    )
    return slice_list[serverid % 2]

使用實例:

# 創建對象
Log.objects.using(db_slice(1)).create(
    serverid=1,
    logid=1001,
    desc='lalala',
    report_dt=datetime.now()
)

# 查詢對象
Log.objects.using(db_slice(1)).all()

結尾

自此,一個分庫的例子就講完了,也比較簡單。聊幾句個人想法,選擇框架和選擇技術,因為業務場景不同,很難有一個完美的解決方案,總是要做一些取舍。

比如說,自己寫一個直接獨立於 Django 的分庫分表策略並不難,但是脫離了 Django 這一套東西,常用的 API 用不了(createfilterdelete etc),為每一個操作封裝一個 SQL 操作,測試起來比較麻煩,靈活性太強,擴展性差,重要的是還要防止 SQL 注入。

如果用 Django 的多 DB 實現策略,也有問題。首先是路由,新的 app 如果忘了設置路由規則,很容易把表生成到不想生成的地方,而且 Django 官方文檔也說了,並不會檢查非 default 遷移的一致性(1.10之后版本可能支持)。其次是外鍵,使用了分庫之后外鍵約束自然就沒有了,這也不算一個問題。還有在使用上,每次訪問數據庫都必須要用 using 顯式的選擇一個數據庫。忘了如果路由規則設置沒問題會直接報錯這倒還好,如果選擇錯了,就蛋疼了,因為他們每個庫的結構都是一樣的,很難查出問題。這將為寫代碼增加復雜性。

我的建議是,既然沒有一個完美的方案,就應該盡量的保證邏輯簡單、清晰,不要過分的依賴框架,少使用 hack 技巧。像上面那種

>>> user_obj.save(using='new_users')
>>> user_obj.delete(using='legacy_users')

盡管可行,但個人認為這是非常不可取的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM