今天群里有人问起,刚好做过相关的工作,特此分享一下当时的工作内容和感受。
背景
大概说一下这个事情的背景。在2013年大概4月份,人人网打算做一次大规模的数据迁移——评论服务。所谓评论就是指各种资源下的“评论文字”,比如照片的评论、Blog的评论、分享的评论、音乐的评论…… 早期人人网的各个开发小组各自为政,每个团队几乎都实现了一个评论服务,有各自不同的功能和数据结构,但是大体上还算相似。当时,业务部门希望能够集中这些数据做一些统一的管理,比如权限管理(控制谁能看什么评论)、比如数据内容推荐(基于用户评论人和评论的内容计算关系的紧密性等等)。而这一切的基础是评论内容的基础数据结构必须一致。
而同时,UGC这边的评论内容(数据量最大的评论服务)之前使用Mongo DB开发,有很多维护上的问题。MongoDB虽然当时乘着NoSQL的东风呼风唤雨,但是据当时的维护同事讲,维护负担不小,不如MySQL稳定。而人人当时具有国内顶级的MySQL数据团队(顺便拜一下刘启荣、周彦伟两位大佬)。于是当时高层的意见是将整个服务迁移到MySQL架构下。
此外,架构组当时正在推行新一代的SOA架构,基于Thrift开发。需要一个服务作为“参考实现”,给整个公司的研发团队做一个参照。
当时我刚刚工作2年半,进入人人的架构组不久。在做了一些“没有什么人在意”的基础架构后实在是有些无聊,就接了这个活。
问题规模
涉及到的团队是13个。
涉及到的总数据量大约在10T的量级。
访问的总量大概在10亿PV/日(因为评论在首页feed上就有)。
除了数据迁移,这个项目还需要解决
- 用新的SOA架构开发应用服务
- 重新设计一个全新的服务,大致可以照顾13个团队的不同的评论服务的全部功能(比如点赞、回帖),重新设计评论的基础服务
- 与13个团队交涉、排期、联测
- 需要与首页Feed关联,涉及到与C++互操作(因为Feed用C++,而其他组大多用Java)
- 整个开发期间不得中断服务
- 大并发访问量,所以要做严格的压测
- 热门主题的评论可能多大几万到十几万条,分页、计数都不能用常规做法实现
限于主题,本文就不叙述这些设计和开发的细节,只说数据迁移本身。
数据迁移要考虑的问题
抱歉废话了一番才说到重点。这里简单列举一些迁移时要考虑的问题。
平滑过渡
平滑过渡,即如何做到不同格式数据服务可以在用户无感知的情况下做到平滑迁移。答案是双写和可控读取路径。当用户写入评论时,每个涉及的要做迁移的服务在保持写自己的数据之外,都必须通过新版本的SOA客户端访问新的评论服务。
而双写一旦开始,我们就定义了数据迁移的时间范围。即,只迁移双写之前的数据。注意一点,这里因为只是评论,所以少许的双写不一致(造成用户评论丢失)是可以接受的。如果这里迁移的是交易记录,就得保证绝对的一致性,那么必须额外开发WAL保证一致性。
而等到数据全部迁移完毕,通过线上配置中心的开关,统一切换评论的读取路径,全部落在新的服务上。这样就彻底避免了用户可见的问题。
海量数据设计
10T的数据不是小数目,是4~5年积攒下的数据量。对于新的评论系统,容量设计必须将容量设计为“3年内不需要扩容”的。所以设计的数据量大概是在30T左右。为此,我们设计了partition方案,实现了100个虚拟的分库。这100各分库被动态的分片在10 * 3台主机上。如果未来性能无法撑住,可以增加更多的磁盘和更多的主机,最多可以达到100 * 3台主机的容量。
所谓10 * 3个主机是指每组机器组成一个3节点的replica set互为备份。人人的数据组有非常强大的工具。在没有percona等工具时,这些备份,热切机制都是人人数据组自己开发的。同时,主从切换等机制对业务开发都是完全透明的(当然,代价就是业务代码不可以有复杂SQL,都必须是简单的单表SQL)。
partition采用双重设计——主要的partition的id是评论的资源ID,比如一个照片的ID,一个Blog的ID等。因为80%的评论查询都是查询一个资源的评论列表。这样partition的好处是使得这样的查询总是只会落到一个分片上。而部分业务提供了“一个用户“的评论列表查询。所以对于这样的数据,就需要冗余一份数据重新做partition——按照”评论作者ID“进行分片。
上面说的这些其实跟数据迁移关系并不大,只不过在编写迁移数据脚本时,必须考虑到这些地方,而非仅仅是简单的往一个数据源里插入。
评论ID
原有的评论系统数据量有大有小。部分数据的ID简单的用auto increment实现,部分系统则简单的用uuid,而部分系统使用了全局序列产生器(使用Postgres sequence)。
为了保证新系统的效率,新系统采用snowflake算法来产生分布式ID,既保证新的数据的ID“大致有序”,有利于插入效率,又避免了因入中心化的ID产生机制。
出错处理
这么浩大的开发过程,不出错时完全不可能的。所以必须提前设计出错时如何追踪错误。而我们的处理是一定要把一条评论的新老两个ID在新系统都要记录下来。一旦发现数据有问题,可以立刻反查原始数据。
Emoji
新的评论服务支持emoji,对应于MySQL的utf8_mb4编码。在数据迁移时必须留意这一点。当年的MySQL中utf8_mb4并不是默认编码,必须经过配置和重启才可以。这是个小坑,要留意。
限流
迁移说白了就是把老的数据读出来,写入到新的数据源。但对于访问量极大的系统,绝对不能无节制的对数据进行读写。任何系统的带宽都是有限的,磁盘的IO也是有限的,所以一定要限流。我们的迁移脚本在读取和写入数据时都都会监控所消耗的时间。如果超过阈值就开始短暂的等待。比如读取100条数据花100ms以上,脚本机会停顿1s;如果超过200ms,脚本就会停顿5s。超过一定阈值,脚本会发邮件通知我:“迁移暂时中断,需要人工重启”等等。
这样的限流脚本,一旦开启后,大部分时间我就不用盯着,不用操心生产系统被压垮。只用每天花点时间看看进度就行了。
进度控制
我们在迁移的第一周大概估算了大概的迁移速度。然后让迁移脚本每隔一定的进度就将当前的已迁移数据比例记录在一个数据表里。因为做的比较糙,所以没有开发UI,而仅仅是做了个小脚本每天发送进度邮件给相关人员。如果有任何异常,都可以引起我们的警觉。因为这个工具我们还发现了几个组的程序的bug……
迁移幂等
迁移脚本会出错,而迁移本身是并不是原子的——因为业务的复杂性,我们无法用transaction。所以,我们的迁移必须是幂等的。我们利用了原始数据的服务名称+ID作为幂等key(比如PHOTO:12345
),以及INSERT IGNORE INTO
,配合进度控制可以实现简单的重启数据迁移。这样即便迁移脚本被强行的kill掉,同时又没有记录下精确的动作。我也可以放心大胆的随意重启它。
如果原来的评论数据变了……
尽管理论上评论时不会变的,但是有些业务的评论的确可以编辑(尽管数量很小)。这样在迁移过程中,我们不得不考虑怎么去重新同步这些变化。我用了最简单的办法,对每条记录的updated_at做索引和排序。感谢公司良好的数据表设计规范,每个表都有created_at
和updated_at
(或者created_on
和updated_on
,大家英文不统一,但只要有就足够了)。一旦发现新的数据变更,就排在一个队列里进行特别的同步。这解决了绝大部分的问题。还是那句话,好在是评论,不需要特别严格一致,所以就算是丢了那么几条的改动,也是可以接受的。
激励和进度管理
这个其实是整个项目里最难的。因为我是一个普通的研发。虽然我来自架构组,但是这并不代表别的组的人会按我所得做。每个组都有自己的KPI,有自己的排期和优先级。我想把我的工作目标插入到他们的安排中,真是各种招都用了。有的普及技术、有的找项目负责人谈合作、还有的直接吃饭KTV。但不论如何,在相对短的时间内,这项工作真真切切的落实了。现在想想觉得还是很NB。
另外这么多事情,包括数据设计、功能开发、联测、压测、不同组的沟通排期、数据迁移、切换开关在13个组里进行。现在想想,目前的项目管理和文档能力都是在那时候锻炼成的。
最后
经过2个半月的迁移和开发,这个事情终于告一段落。业务的头头们得到了统一的评论数据,用户没有骂娘,架构组的SOA基础框架也有了第一个使用样板(其实我被坑了好几次,所以架构组也没少请我吃饭抚慰我的心灵)。通过这个过程也得到了一帮好哥们,和第一次季度S绩效(后边还顺便升了个title,但是没过多久就离开了人人)。我很感谢这个经历和帮助我的团队。
收一收,通过这件事情总结一下关于数据迁移的重点:
- 精心的进度管理和控制
- 开发“低心智负担”的工具
- 平滑过渡,让用户开心和满意
最后说一句:做业务真心容易出绩效啊!