Django生产环境中的数据库迁移

22

对于在非传统生产环境中拥有Django应用程序的人,您如何处理数据库迁移? 我知道有 south,但似乎如果涉及任何实质性内容,它会缺失很多。

另外两个选项(我能想到或使用过的)是在测试数据库上进行更改,然后(离线应用程序并)导入SQL导出文件。 或者,也许是一种更冒险的选择,在生产数据库中实时进行必要的更改,如果出现任何问题,则恢复备份。

您通常如何处理数据库迁移和架构更改?


1
我认识的大多数Django开发人员都使用South。我自己也使用South。你认为它会“错过很多”吗?我对South有些不满,但大部分时间它都能正常工作。 - super9
你的应用程序大约有多少容量? - David542
6个回答

23
我认为这个问题有两个部分。
首先是管理数据库架构及其更改。我们使用South来实现这一点,将工作模型和迁移文件都保存在版本控制库中。出于安全(或偏执)考虑,在运行任何迁移之前(如果我们真的害怕),我们会将数据库备份。到目前为止,South已经足够满足我们的所有需求。
其次是部署架构更改,这超出了仅运行由South生成的迁移文件。根据我的经验,对数据库进行更改通常需要更改部署的代码。如果您有一个小的Web Farm,使部署代码与当前版本的数据库架构保持同步可能并不容易,如果考虑到不同的缓存层和已经活跃的网站用户的影响,情况就会变得更糟。不同的网站以不同的方式处理此问题,我认为没有一种通用的解决方案。
解决此问题的第二部分并不一定直截了当。我认为没有一种通用的方法,并且没有足够的关于您的网站和环境的信息来建议最适合您情况的解决方案。然而,我认为可以记住一些注意事项来帮助指导大多数情况下的部署。
在某些情况下,关闭整个站点(Web服务器和数据库)是一种选择。这肯定是管理更新最直接的方式。但频繁的停机时间(即使是计划好的)可能会很快让您的业务停业,即使是小的代码更改也会令人厌烦,并且如果您有大量数据集和/或复杂的迁移,则可能需要多个小时。尽管如此,对于我帮助管理的网站(它们都是内部的,并且通常仅在工作日的工作时间使用),这种方法非常有效。
如果您在主数据库的副本上进行更改,请小心。主要问题在于您的网站仍然在线,并且可能正在接受写入数据库的操作。当您忙于为以后使用而迁移克隆时,写入主数据库的数据将会发生什么情况?您的网站必须要么全程停机,要么暂时处于某种只读状态,否则您将失去它们。如果您的更改是向后兼容的,并且您拥有一个Web农场,有时您可以通过更新生产数据库服务器来逐渐地更新节点并将它们从负载均衡器中取出一小段时间,这可能会起作用 - 但是主要问题在于如果已经更新的节点发送了对旧节点不支持的URL的请求,则无法在负载平衡器级别上进行管理,您将会失败。
我见过/听说过一些其他方法也很好。
第一种方法是将所有代码更改包装在功能锁中,然后通过一些站点范围配置选项在运行时进行配置。这基本上意味着您可以发布关闭所有更改的代码,然后在对服务器进行所有必要的更新之后,将配置选项更改为启用该功能。但是这样会使代码变得非常繁重...
第二种方法是让代码管理迁移。我听说过更改代码的站点以这种方式编写,以便它在运行时处理迁移。它能够检测正在使用的架构版本以及返回的数据格式-如果数据来自旧架构,则它会直接进行迁移,如果数据已经来自新架构,则不做任何操作。从自然站点使用率来看,大部分数据都将由使用站点的人员迁移,其余部分可以在任何时候使用迁移脚本完成。
但是我认为此时Google会成为您的朋友,因为正如我所说,解决方案非常具体化,我担心这个答案开始变得毫无意义...搜索类似于“零停机部署”的内容,您将获得此类结果以及许多想法...

谢谢您的回答。您可以简要解释一下您如何完成这个问题的第二部分吗? - David542
1
我已經為問題的第二部分添加了一些詳細信息 - 希望能有所幫助。這也取決於您的規模 - Facebook必須以非常不同的方式處理此問題,而您從臥室中運行的博客可能需要採用不同的方法! - Mark Streatfield
2
对于任何新来的人,帖子底部的 exortech.com 链接已经失效。这里是 Wayback Machine 的链接,可以访问最近的非重定向版本:https://web.archive.org/web/20140822052754/http://www.exortech.com/blog/2009/02/01/weekly-release-blog-11-zero-downtime-database-deployment/ - Joe C.
南方现在似乎已经被弃用了。https://dev59.com/J1QK5IYBdhLWcg3wNtTA#52259197 - Srinath Ganesh

4
我在一个代码库约为40K行的生产服务器上使用South,迄今为止我们没有遇到任何问题。我们还经历了一些模型的重大重构,但是我们没有遇到任何问题。 我们还有模型的版本控制,这有助于我们回滚我们在软件方面对模型所做的任何更改,而South更多地用于实际数据。我们使用Django Reversion

1
我只是简单地阅读了Django Reversion的文档,但我不清楚为什么你会优先使用它来管理模型,而不是使用“正式”的源代码管理工具(如SVN或git),以与站点的其余代码一起管理模型。我错过了什么吗? - Mark Streatfield
@MarkStreatfield:回滚是用于管理模型内容随时间变化的,而不是结构。 - Matthew Schinckel
啊,我明白了,谢谢你的解释 - 我完全没注意到那个。所以这对于做类似于这个问题描述的事情会很有用 https://dev59.com/xHVD5IYBdhLWcg3wQJOT#126468 ? - Mark Streatfield

3

对于这个问题,我有时会采取一种非常规的方法(读了其他答案后,也许并不那么非常规)。 我之前从未尝试过在django中使用它,所以我进行了一些实验。

简而言之,我让代码捕获由旧架构引起的异常,并应用适当的架构升级。 我并不希望这成为被接受的答案-它只适用于某些情况(有人可能会说永远不适用)。 但我认为它具有丑小鸭般的优雅。

当然,我有一个测试环境,可以随时将其重置为生产状态。 使用该测试环境,我更新我的架构并编写针对其的代码-如往常一样。

然后我还原架构更改并再次测试新代码。 我捕获结果错误,执行架构升级,然后重新尝试出错的查询。

升级函数必须编写成“无害”的形式,以便在多次调用时(可能发生在投入生产时)仅执行一次。

实际的Python代码-我将其放在我的settings.py文件的末尾以测试概念,但您可能希望将其保留在单独的模块中:

from django.db.models.sql.compiler import SQLCompiler
from MySQLdb import OperationalError

orig_exec = SQLCompiler.execute_sql
def new_exec(self, *args, **kw):
    try:
        return orig_exec(self, *args, **kw)
    except OperationalError, e:
        if e[0] != 1054: # unknown column
            raise
        upgradeSchema(self.connection)
        return orig_exec(self, *args, **kw)
SQLCompiler.execute_sql = new_exec

def upgradeSchema(conn):
    cursor = conn.cursor()
    try:
        cursor.execute("alter table users add phone varchar(255)")
    except OperationalError, e:
        if e[0] != 1060: # duplicate column name
            raise

一旦您的生产环境更新完毕,您可以从代码库中删除此自升级代码。但即使您不这样做,该代码也不会执行任何重要的不必要工作。
您需要调整异常类(在我的情况下是MySQLdb.OperationalError)和数字(在我的情况下是1054“未知列”/ 1060“重复列”)以适应您的数据库引擎和模式更改,但这应该很容易。
您可能希望添加一些额外的检查,以确保执行的SQL实际上是由于涉及模式更改而出错,而不是其他问题,但即使您不这样做,也应重新抛出无关异常。唯一的惩罚是在这种情况下,您将尝试升级和错误查询两次,然后才引发异常。
Python 的一个我最喜欢的特性是能够轻松地在运行时覆盖系统方法,就像这样。它提供了如此多的灵活性。

2
如果您的数据库规模较大且使用Postgresql,那么在SQL方面有很多优秀的选择,包括:
  • 快照和回滚
  • 实时复制到备份服务器
  • 试升级后立即上线
试升级选项不错(但最好与快照一起完成)。
su postgres
pg_dump <db> > $(date "+%Y%m%d_%H%M").sql
psql template1
# create database upgrade_test template current_db
# \c upgradetest
# \i upgrade_file.sql
...assuming all well...
# \q
pg_dump <db> > $(date "+%Y%m%d_%H%M").sql # we're paranoid
psql <db>
# \i upgrade_file.sql

如果您喜欢上述的安排,但担心运行两次升级所需的时间,可以将db锁定以进行写操作,然后如果升级到upgradetest成功,则可以将db重命名为dbold,并将upgradetest重命名为db。有许多选择。
如果您有一个列出要进行更改的所有更改的SQL文件,则非常方便的psql命令是\set ON_ERROR_STOP 1。这会在出现问题时立即停止升级脚本。通过大量测试,您可以确保没有任何问题发生。
有许多可用的数据库模式比较工具,在StackOverflow答案中提到了许多工具。但是,手动完成基本上非常容易...
pg_dump --schema-only production_db > production_schema.sql
pg_dump --schema-only upgraded_db > upgrade_schema.sql
vimdiff production_schema.sql upgrade_schema.sql
or
diff -Naur production_schema.sql upgrade_schema.sql > changes.patch
vim changes.patch (to check/edit)

1

South并不是在所有地方都使用。比如在我们的组织中,我们有三个级别的代码测试。一个是本地开发环境,一个是暂存开发环境,第三个是生产环境。

本地开发环境由开发人员掌控,可以根据自己的需求进行操作。然后是暂存开发环境,它与生产环境保持相同,当然,在对现场数据库进行更改之前,我们会先在暂存环境中进行数据库更改,并检查是否一切正常,然后再手动更改生产数据库,使其再次与暂存环境相同。


0
如果不是微不足道的问题,你应该有一个预生产数据库/应用程序来模拟生产环境,以避免生产环境停机。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接