一次合并数据库的经历

 

      当只有一个村落的时候, 世界很美好, 人们生活很幸福; 当逐渐增长到四十多个村落的时候, 人们发现需要划分出一些大的村落来管理小的村落更合适; 当逐渐增长到八十多个村落时, 人们发现需要消灭小的村落, 只留下大的村落。问题诞生了。 

  现在要做一个数据合并脚本, 将多个集群的分数据库的数据合并到一个主数据库中。

      最开始, 热情是 100%, 投入工作。 随着时间的推移, 热情逐渐降低, 任务也趋于完成状态。 当热情降到 60% 左右, 任务终于基本完成了, 意外发生了。

      1.   半路杀出个程咬金。 本来是负责5个表 A,B,C,D,E 的数据合并。 然而最近的项目又新增了一个表 C', (C 和 C’具有相似的字段, 但是针对不同的实体) , 而且在数据订正所在项目之前上线(但是此时还没有上线, 因此线上没有相关数据参考)。 必须把这个表的合并工作考虑进去, 将 C 和 C' 均合并到主数据库的 C 表中; 此时, 还是有不少热情来完成工作的; No Problem.

      2.   线上数据的主键重复。 再一次完成后, 突然又被告知: 由于历史上的手工维护, 导致 C 表的线上集群的多个分数据库的一些主键数据重复, 合并到主数据库时会导致主键冲突, 于是, 需要在主数据库的C表增加一个集群标识的字段区分出来, 原来的单字段主键变成联合字段主键。 再一次修改;

      3.   线上遗留问题隐患。 再一次完成后, 与虚拟化网络团队的成员讨论发现, 将 C 和 C' 表合并, 与线上某个已知待解决的遗留问题结合, 会导致一些小概率的不可靠, 这些不可靠可能会导致大的网络故障。 为了消除这种不可靠性, 决定再增加一个字段来解决问题。 此时, 突然发现对 C 和 C' 表存在着重大的理解错误。 原来以为 C' 表的主键数据是包含在 C 表中, 现在发现是完全无交集的关系。由于相关项目还没有上线, 无法抓取线上进行验证, 只能采用潜规则和人工沟通的方式来确认。毫无疑问, 这需要一次较大的改动。

     4.   合并验证。数据合并是必须考虑合并验证、回滚和回滚验证的。 数据验证分单表数据验证和表关联数据验证。回滚是将主数据库对应于每个集群的分数据库的数据重新迁移到原来的分数据库。回滚之前必须做备份和备份验证。每一次改动, 都必须确保“合并、 合并后验证、备份、备份后验证、 回滚、 回滚后验证”均正确无误。并且, 由于涉及到的是客户的敏感数据, 几百万条数据, 只要有一条数据订正出错, 都可能对一个或多个客户的业务产生重大影响, 导致不小的故障。 必须一遍一遍地修改,运行验证程序, 查找数据验证失败的原因, 修复, 再验证, 再修复, 再验证, 就像改论文、拍电影一样, 几个步骤翻来覆去, 真是令人头晕目眩, 思绪混乱, 濒临崩溃。即便如此, 也无法保证绝对不出问题。必须有足够的耐心和细致。 

      这里有两点需要考虑: A. 程序的可扩展性和可维护性。 当需要改动时, 不至于大动筋骨,修改很多地方; B. 程序的健壮性和容错能力。由于线上数据存在脏数据, 出现若干条数据验证失败是很容易的, 需要程序具有很好的健壮性和容错能力。 当出现数据验证失败时, 是直接终止,还是继续执行并记录下失败的数据呢? 

     5.  关键字冲突。 凭借最后的热情, 终于再一次完成。 由于部署上线要通过 DBA 系统来创建新表。 问题又出现了:  由于 D 表的字段 usage 与 mysql 关键字冲突, 该字段被 DBA 系统直接屏蔽, 不可以使用, 因此, 必须将主数据库的 usage 字段改成其他字段。 新一轮的修改又要开始了。

     6.  空值细节问题。 一些特殊值, 比如 NULL, 在 mysql 与 python 之间的转换, 从 mysql 取出的 NULL, 在 python 会变成 None ,  如果不小心, 插入到 MySQL 就会变成 'None' , 而不是 NULL, 这会导致某些查询失效。解决办法是先在 MySql 中插入 None, 再 update 为 NULL。

     7.  记录数据库更新日志。 操作过程中的所有修改, insert / delete / update 语句都必须记录日志, 发现的异常数据和不合正常约束的数据也必须记录下来以备后查。如果无法获取直接执行的SQL语句, 一定要记录下源数据, 避免操作失误覆盖原数据时可以恢复数据。

     8.  合并策略。 当要合并多个分数据库时, 存在合并策略: 是顺序地合并每一个分数据库, 还是同时并发地合并每一个分数据库。 顺序合并简单而安全, 只要性能在可接受范围就可以采用; 并发合并效率更高一点, 但是容易出现错误, 必须仔细控制。 如果集群比较少, 单数据库合并很快,可以采用顺序合并; 如果集群比较多, 并且必须为其它步骤留出时间, 就必须采用并发合并。

     9.  脚本性能。必须考虑脚本性能。 1.  插入性能。 由于每个分数据库 C 表有30w 量级的记录, 因此必须采用批量插入的方式; 每次插入 1000 条记录批量提交; 使插入操作成为短事务, 尽可能避免锁等待超时;  2. 并发情况下慎用子查询。 一方面, 子查询会锁表, 导致并发操作失败,  尤其要避免 delete from X where a in (select b from Y) 的语句, 这将锁住 Y 全表; 另一方面, 子查询在大数量级表中的性能很低, 耗费时间。 在我入职时参与的一次数据订正过程真实地发生过。 在一张百万级的表中使用了子查询, 结果插入一条记录需要几秒钟, 根本无法满足项目预期时间。教训是: 一定要考察线上数据, 避免认识不足导致重大偏差。就好比要理解真实战场, 才不至于腿还没动就被毙掉了。

     10.  环境模拟。 由于线上环境与本地环境存在差异, 必须在本地测试环境模拟线上环境。比如说, 必须导入和使用线上的库和工具, 但本地是没有的,而且也无法安装, 就需要做一层简单的 MOCK; 虽然有开发自测环境可用, 由于多人在争用,且数据存在缺失, 无法体现线上数据的复杂性, 还是选择在本地搭建测试环境,导入线上数据(只导入一个集群的分数据库数据, 模拟合并多个集群的分数据库情况)。 此外, 由于文件系统的差异, 本地 windows 无法使用 /tmp/xxx.log 的文件路径, 而线上必须使用, 必须维护两个版本, 在测试版本和发布版本切换。很容易忘记切换。

        编程是一场思维与意志的战役,是与自己思维缺陷不断抗争的过程。解决问题,引起新的问题,再消解问题,一步步小心地缩小问题的生存空间,直到幸运地发现问题能缩减到可以接受的范围,或者郁闷地发现问题在逐级扩大无法收拾只得从头再来。思维能力和意志力有多强大,决定了编程能力所能达到的高度。有时技术的强弱并不是最重要的, 耐心和意志反而是最关键的。就像马拉松一样, 速度并不是最重要的, 耐心、策略和坚持才是最有力的品质。