02月22, 2017

openstack如何优雅的更新数据库

我们在维护openstack过程中,发现有些功能无法满足,需要修改代码,而且要修改数据库的表结构,我们发现neutron中是使用alembic来做db的版本控制,于是我们摒弃了直接alter table方式,通过alembic来修改表结构,这样以后自动化部署和升级都会非常方便。

alembic入门

在项目开发中,因为程序变动更新数据库表结构不可避免,传统方式是直接alter table,这种方式有诸多弊端,不利于后期维护,回滚不方便等,所以SQLAlchemy的作者提供了一个db的版本控制工具-alembic

首先安装mysql

yum -y install mysql-server mysql mysql-devel
service mysqld start

安装 alembic

pip install alembic

创建myalembic目录,在目录下执行

alembic init alembic

我们这里需要编辑alembic.ini文件,该ini中最重要的就是下面配置好sqlalchemy要连接的db url,其他的都是关于log的配置,使用默认的即可.

sqlalchemy.url = mysql://user:pass@host:port/database

然后使用alembic revision来创建新的revision,alembic中每一次的db操作,都会被视为一个新的版本,可以upgrade可以downgrade,就是一个db的版本控制操作。

alembic revision -m "create account table"
  Generating 
myalembic/alembic/versions/3d48b370ea59_create_account_table.py ... done

然后会在version目录下生成一个新的migrate文件3d48b370ea59_create_account_table.py。 alt

这里migrate的模板都给我们写好了,我们只需要编辑其中的upgrade操作来更新db,和downgrade来执行回滚操作即可。

这里我们修改一下upgrade函数和downgrade回滚操作如下: upgrade db更新操作就是添加一个account表,回滚操作就是直接drop该表. alt

然后执行alembic upgrade head看看效果,当然这里可以指定具体revision id为3d48b370ea59,但是一般都执行默认指向head指针就行,都是最新的, 原来test是空的,现在生成了acount 表。 alt

alembic downgrade base 执行downgrade函数进行回滚操作,发现test中的acount表已经被drop掉了. alt

现在如果我们需要在当前acount表上继续添加一个字段,这时候可以继续添加一个revision。 命令如下:

alembic revision -m "Add a column"

然后编辑自动生成的迁移脚本. alt

然后执行alembic upgrade head

alembic upgrade head
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade e2a588d5cec8 -> 3b950b16e221, Add a column

然后查看db,column 添加成功.

mysql> desc account;
+-----------------------+--------------+------+-----+---------+----------------+
| Field                 | Type         | Null | Key | Default | Extra          |
+-----------------------+--------------+------+-----+---------+----------------+
| id                    | int(11)      | NO   | PRI | NULL    | auto_increment |
| name                  | varchar(50)  | NO   |     | NULL    |                |
| description           | varchar(200) | YES  |     | NULL    |                |
| last_transaction_date | datetime     | YES  |     | NULL    |                |
+-----------------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

执行alembic downgrade -1回滚到上一次更改,如下:

+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int(11)      | NO   | PRI | NULL    | auto_increment |
| name        | varchar(50)  | NO   |     | NULL    |                |
| description | varchar(200) | YES  |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

neutron中是如何使用alembic模块的

下面我们来分析一下neutron是如何使用alembic模块的,我们模拟一个场景,比如我们要修改dnsnameservers这个表。

在neutron中基于alembic library对alembic的cli进行了封装,但是neutron是只支持upgrade,不支持downgrade。

neutron-db-manage其实就是alembic的一个wrapper。这里我们可以具体看一下 我们查看/usr/bin/neutron-db-manage,可见其加载的是migration目录下的cli.py中main函数

该main函数如下:

def main():
    CONF(project='neutron')
    config = get_alembic_config()
    config.neutron_config = CONF
    #TODO(gongysh) enable logging
    CONF.command.func(config, CONF.command.name)

这里思路就是加载neutron配置,然后通过alembic.ini来读alembic模块需要的配置参数,和后续的migrations脚本目录。然后执行alembic的基本cli操作。

其中do_upgrade(config, cmd)函数就是alembic upgrade命令的封装,do_revision(config, cmd)就是alembic revison的wrapper,所以neutron-db-manage的revision命令和upgrade命令最后都会调用alembic revison和alembic upgrade执行。

下面我们使用neutron-db-manage来给dnsnameservers自动修改表结构。 使用neutron-db-manage revision添加一个version。

revision -m "add order to dnsnameservers"

然后alembic会给我们生成2fad26ad1543_add_order_to_dnsnameservers.py,然后修改其中的upgrade函数,即在dnsnameservers表中添加order字段。

revision = '2fad26ad1543'
down_revision = '444dc0e62832'
from alembic import op
import sqlalchemy as sa
def upgrade():
    op.add_column('dnsnameservers',
                sa.Column('order', sa.Integer(),
                        server_default='0', nullable=False))

然后执行neutron-db-manage upgrade heads alt

最后我们去db中查看验证一下,确实又添加了一个order字段,order_id是我以前手动添加的。

MySQL [neutron]> desc dnsnameservers;
+-----------+--------------+------+-----+---------+-------+
| Field     | Type         | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| address   | varchar(128) | NO   | PRI | NULL    |       |
| subnet_id | varchar(36)  | NO   | PRI | NULL    |       |
| order_id  | int(11)      | NO   |     | 0       |       |
| order     | int(11)      | NO   |     | 0       |       |
+-----------+--------------+------+-----+---------+-------+
4 rows in set (0.00 sec)

综上所述,我们就可以很方便的使用neutron-db-manage来对我们的db,进行alter table和create table操作,在我们现在的使用中暂时还没有涉及到drop table和删除column等影响neutron server进程的操作。

在线上升级的时候我们不可能执行neutron-db-manage revision,所以这里需要我们直接给version目录中添加一个versionid号,然后在versions/HEADS中编辑好versionid号,最后直接执行neutron-db-manage upgrade heads,neutron-db-manage会从versions/heads中读取该versionid号,然后加载该versionid对应的migration代码。

alt

这里的revison id 和down_revision_id是非常重要的,revison表示当前version的指针,down_revision表示前一version的指针。HEAD文件中的第一个id号表示我们要升级到的版本的id号。

但是在升级的时候咋知道我们当前db的版本号是多少呢?这个neutron有一张表 alembic_version,该表里记录了当前db的版本号,如下,当前我们安装好的kilo版本的db version id号都是kilo。

MySQL [neutron]> desc alembic_version;
+-------------+-------------+------+-----+---------+-------+
| Field       | Type        | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+-------+
| version_num | varchar(32) | NO   |     | NULL    |       |
+-------------+-------------+------+-----+---------+-------+
1 row in set (0.00 sec)
MySQL [neutron]> select * from alembic_version;
+-------------+
| version_num |
+-------------+
| kilo        |
+-------------+
1 row in set (0.00 sec)

neutron db upgrade

在neutron 的db upgrade中,每一次的upgrade都是必须有顺序的。这个顺序就是又每个迁移脚本中的revison id 和 down_revision 来记录,revison id 表示当前版本指针,down_revision表示前一个版本的指针,形成一个单链表结构。

这样看似一大堆杂乱的db迁移脚本,通过每个脚本的当前指针和后继指针,形成一个有序的单链表,这样db在升级的时候就有序的进行升级。 比如在我们的neutron 中有87个db migrate scripts。这87脚本通过revision_id号形成一个单链表。

alt

比如:38495dc99731_ml2_tunnel_endpoints_table.py,当前versionid是38495dc99731,他的前驱后继指针分别是哪个id呢?如下可以看得出,该脚本的前驱指针(version id)是57086602ca0a, 而他的后继指针(version id) 就是 4dbe243cd84d。这样db在upgrade的时候,只要这三个version id号在可升级范围内,就会依次 57086602ca0a -> 38495dc99731 -> 4dbe243cd84d来升级。

alt

我们可以通过查看migrate history来验证一下

neutron-db-manage history|grep 38495dc99731
38495dc99731 -> 4dbe243cd84d, nsxv
57086602ca0a -> 38495dc99731, ml2_tunnel_endpoints_table

总之,在执行neutron-db-manage的时候,首先会从db alembic_version中读取当前db的version id号 A,然后和HEAD文件中需要升级到的版本id B相比较。

  • 如果A = B,则不做任何upgrade操作。
  • 如果A < B,则将从A的后继开始升级,遍历单链表指针,一直到B
  • 如果A > B,则报错,因为这是相当于在做downgrade操作,neutron已经停掉了降版本操作。

结语

通过以上分析,你是不是对升级db又有了新的认识呢?

本文链接:https://www.opsdev.cn/post/how-to-update-db.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。