当前位置: 首页 > 科技观察

或许是东半球直接自信的分库分表实践

时间:2023-03-15 19:30:09 科技观察

背景不久前发表了两篇分表文章:分表实践踩坑浅谈分表后需要注意的两三点从标题可以看出我们当时出来的时候只做分表;还是因为业务发展,到现在我们也做了分库,到现在好像还比较顺利,所以还是记得清楚的做个review。首先我们来回顾下分库分表的整个过程,整个过程也比较通俗易懂,基本符合大部分公司的一个发展方向。很少有业务一开始就设计成分库分表的。虽然这样会减少后续的坑,但有些公司一开始就专注于业务。当业务发展到单表无法支撑的时候,自然要考虑分表甚至分库。所以本文会做一个总结,之前提到的内容可能会再次重复。首先,什么样的情况适合分表?根据我的经验,当某个表的数据量已经达到几千万甚至上亿的时候,而且每天的数据量增长都在2%以上。当然,这些数字并不是绝对的。最重要的是,这张表的写入和查询都影响了正常的业务执行,比如查询速度明显下降,数据库整体IO居高不下。说到分表,我们主要关注水平分表;即通过一定的路由算法,将一个大表中的数据尽可能均匀地分布到N个小表中。范围和表拆分策略有多种,适用于不同的场景。首先,第一个是按范围划分的。比如我们可以将一个表的创建时间按日期划分,保存为月表;我们还可以按范围划分表的主键,比如表中[1~10000],表中[10001~20000]等。这样的分表适合归档数据。例如,系统默认只提供近三个月历史数据的查询功能,操作起来也很方便;只需要删除三个月前的数据备份保存即可)。这种方案有利也有弊:优点是自带横向扩展,不需要过多干预。缺点是可能会出现数据不均匀的情况(比如某月请求突然增加)。hash简单的就是按照日期的范围来分表,但是适用范围还是比较窄;毕竟我们大部分的数据查询都不想带时间。比如一个用户想查询所有他生成的订单信息,这是一个很常见的需求。所以我们要改变分表的维度,分表算法可以采用主流的hash+mod的组合。这是一个经典的算法,大名鼎鼎的HashMap也是这样存储数据的。假设我们把原来的大表的订单信息分成64个分表:这里的hash就是对我们需要分表的字段进行hash运算,这样hash后的数据尽量统一,不重复。当然,如果字段本身是整数且不重复,也可以省略这一步,直接进行Mod获取表的下标。分表个数的选择至于这里的分表个数(64),也是有一定取值的。具体设置没有标准值。需要根据自身业务发展和数据增量进行预估。根据我个人的经验,业务发展的几年内,至少要保证分出来的小表不会出现单表数据过多(比如达到千万)的情况。我更喜欢在数据库可以接受的范围内,尽可能的增加分表的数量。毕竟,如果后续的小表也达到瓶颈,需要再次扩容子表,那将是非常痛苦的。目前笔者没有经历过这一步,所以本文不做相关介绍。但是这个数字不是随机选择的。和HashMap一样,也推荐为2^n,方便扩容时迁移尽可能少的数据。Range+Hash当然还有一个思路,Range和Hash是否可以混合。比如我们一开始使用的是Hash分表,但是巨大的数据增长导致每个分表的数据很快达到了瓶颈,所以我们不得不扩容,比如从64张表扩容到256张表。但是,在扩容过程中不停机迁移数据是非常困难的。即使有宕机,宕机时间是多久?这很难说。那么是否可以将Mod分表划分为月表,借助Range自身的扩展性,不需要考虑后续的数据迁移。这种方法理论上是可行的,但是我没有在实践中使用过。让我为您的想法提供参考。烦人的数据迁移和分表规则其实只是分表的第一步。真正麻烦的是数据迁移,或者说如何实现对业务影响最小的数据迁移。除非一开始就拆表,否则数据迁移这一步是绝对逃不掉的。下面总结一下我们目前的做法,供大家参考:子表一旦上线,所有的数据写入和查询都是针对子表的,所以必须把原来大表的数据迁移到子表上,否则会对业务造成很大的影响。我们估计迁移一个2亿左右的表,自己写的迁移程序大概需要4到5天的时间才能完成迁移。意味着在这段时间内,之前的数据对于用户来说是不可见的,这显然是业务不能接受的。所以我们做了一个兼容的过程:分表改造上线后,所有新产生的数据都会写入分表,但是对历史数据的操作还是按照旧的形式,省去了数据迁移的步骤。只需要在操作数据前做路由判断即可。当产生足够多的新数据时(我们在两个月以内),几乎所有的操作都是针对分表的,然后从数据库开始数据迁移。数据迁移完成后,将原来的路由判断去掉。最后,所有的数据都是从子表中生成并写入的。至此整个分表操作完成。业务兼容同时,分表后需要兼容其他业务;比如原来的报表业务,分页查询等,下面看看我们是怎么处理的。报告首先是一份报告。在分表之前,可以通过查询一张表来完成。现在不一样了,从一张表变成了N张表。所以,原来的查询要改成遍历所有的子表。考虑到性能,可以使用多线程并发查询分表数据,然后汇总。但是,单纯依靠Java来对如此大的数据量进行统计分析,仍然是不现实的。一开始可以处理,后面必须要借助一个大数据平台来处理。第二个查询是query。不得再使用原来的分页查询。毕竟上亿条数据分页是没有意义的。只能通过分表字段提供查询,比如根据订单ID分表,那么查询条件必须包含该字段,否则会涉及到遍历所有表。这也是所有表分表后都会遇到的问题,除非不使用MySQL等关系型数据库。分库分表完成后,单表的压力可以解决,但是数据库本身的压力并没有下降。在我们完成分表后的一个月内,由于数据库中写入“其他表”导致整个数据库的IO增加,而这些“其他表”与业务关系不大。也就是说,一些可有可无的数据影响了整体业务,这是很不划算的。所以我们把这些表搬到了一个新的数据库中,与现有业务完全隔离。这会涉及到几个改造:应用自己对这些数据的查询和写入必须改成调用一个独立的Dubbo服务,这个服务会操作迁移后的表。数据迁移暂时不会做,所以查询的时候一定要兼容分表。如果查询旧数据,必须在当前数据库中查询,新数据必须调用Dubbo接口查询。一些对这些表的关联查询也得转化为查询Dubbo接口,可以拼接在内存中。如果数据量确实很大,也可以将同步的Dubbo接口换成写入消息队列来提高吞吐量。目前,我们将这类数据量巨大但对业务影响不大的表迁移到单库后,数据库整体IO下降明显,业务恢复正常。综上所述,我们还需要做一个历史数据归档的步骤。定期将N个月前的数据迁移到HBASE等存储中,保证MySQL中的数据始终保持在可接受的范围内。归档数据的查询依赖于大数据提供服务。这种分库分表是非常难得的实际操作。网上的资料大多是汽车出厂前换过轮胎。而我们遇到的场景大多是在高速公路上行驶的汽车换轮胎,一不小心就撞车撞死人了。