你好,我是王庆友。在第16讲中,我和你介绍了很多可伸缩的架构策略和原则。那么今天,我会通过1号店订单水平分库的实际案例,和你具体介绍如何实现系统的可伸缩。
2013年,随着1号店业务的发展,每日的订单量接近100万。这个时候,订单库已有上亿条记录,订单表有上百个字段,这些数据存储在一个Oracle数据库里。当时,我们已经实现了订单的服务化改造,只有订单服务才能访问这个订单数据库,但随着单量的增长以及在线促销的常态化,单一数据库的存储容量和访问性能都已经不能满足业务需求了,订单数据库已成为系统的瓶颈。所以,对这个数据库的拆分势在必行。
数据库拆分一般有两种做法,一个是垂直分库,还有一个是水平分库。
简单来说,垂直分库就是数据库里的表太多,我们把它们分散到多个数据库,一般是根据业务进行划分,把关系密切的表放在同一个数据库里,这个改造相对比较简单。
某些表太大,单个数据库存储不下,或者数据库的读写性能有压力。通过水平分库,我们把一张表拆成多张表,每张表存放部分记录,分别保存在不同的数据库里,水平分库需要对应用做比较大的改造。
当时,1号店已经通过服务化,实现了订单库的垂直拆分,它的订单库主要包括订单基本信息表、订单商品明细表、订单扩展表。这里的问题不是表的数量太多,而是单表的数据量太大,读写性能差。所以,1号店通过水平分库,把这3张表的记录分到多个数据库当中,从而分散了数据库的存储和性能压力。
水平分库后,应用通过订单服务来访问多个订单数据库,具体的方式如下图所示:
原来的一个Oracle库被现在的多个MySQL库给取代了,每个MySQL数据库包括了1主1备2从,都支持读写分离,主备之间通过自带的同步机制来实现数据同步。所以,你可以发现,这个项目实际包含了水平分库和去Oracle两大改造目标。
我们先来讨论一下水平分库的具体策略,包括要选择哪个分库维度,数据记录如何划分,以及要分为几个数据库。
首先,我们需要考虑根据哪个字段来作为分库的维度。
这个字段选择的标准是,尽量避免应用代码和SQL性能受到影响。具体地说,就是现有的SQL在分库后,它的访问尽量落在单个数据库里,否则原来的单库访问就变成了多库扫描,不但SQL的性能会受到影响,而且相应的代码也需要进行改造。
具体到订单数据库的拆分,你可能首先会想到按照用户ID来进行拆分。这个结论是没错,但我们最好还是要有量化的数据支持,不能拍脑袋。
这里,最好的做法是,先收集所有SQL,挑选出WHERE语句中最常出现的过滤字段,比如说这里有三个候选对象,分别是用户ID、订单ID和商家ID,每个字段在SQL中都会出现三种情况:
最后,我们分别统计这三个字段的使用情况,假设共有500个SQL访问订单库,3个候选字段出现的情况如下:
从这张表来看,结论非常明显,我们应该选择用户ID来进行分库。
不过,等一等,这只是静态分析。我们知道,每个SQL访问的频率是不一样的,所以,我们还要分析每个SQL的实际访问量。
在项目中,我们分析了Top15执行次数最多的SQL (它们占总执行次数85%,具有足够代表性),按照执行的次数,如果使用用户ID进行分库,这些SQL 85%会落到单个数据库,13%落到多个数据库,只有2%需要遍历所有的数据库。所以说,从SQL动态执行次数的角度来看,用户ID分库也明显优于使用其他两个ID进行分库。
这样,通过前面的量化分析,我们知道按照用户ID分库是最优的选择,同时也大致知道了分库对现有系统会造成多大影响。比如在这个例子中,85%的SQL会落到单个数据库,那么这部分的数据访问相对于不分库来说,执行性能会得到一定的优化,这样也解决了我们之前对分库是否有效果的疑问,坚定了分库的信心。
好,分库维度确定了以后,我们如何把记录分到各个库里呢?
一般有两种数据分法:
这两种分法,各自存在优缺点,如下表所示:
在实践中,为了运维方便,选择ID取模进行分库的做法比较多。同时为了数据迁移方便,一般分库的数量是按照倍数增加的,比如说,一开始是4个库,二次分裂为8个,再分成16个。这样对于某个库的数据,在分裂的时候,一半数据会移到新库,剩余的可以不用动。与此相反,如果我们每次只增加一个库,所有记录都要按照新的模数做调整。
在这个项目中,我们结合订单数据的实际情况,最后采用的是取模的方式来拆分记录。
补充说明:按照取模进行分库,每个库记录数一般比较均匀,但也有些数据库,存在超级ID,这些ID的记录远远超过其他ID。比如在广告场景下,某个大广告主的广告数可能占很大比例。如果按照广告主ID取模进行分库,某些库的记录数会特别多,对于这些超级ID,需要提供单独库来存储记录。
现在,我们确定了记录要怎么分,但具体要分成几个数据库呢?
分库数量,首先和单库能处理的记录数有关。一般来说,MySQL单库超过了5000万条记录,Oracle单库超过了1亿条记录,DB的压力就很大(当然这也和字段数量、字段长度和查询模式有关系)。
在满足前面记录数量限制的前提下,如果分库的数量太少,我们达不到分散存储和减轻DB性能压力的目的;如果分库的数量太多,好处是单库访问性能好,但对于跨多个库的访问,应用程序需要同时访问多个库,如果我们并发地访问所有数据库,就意味着要消耗更多的线程资源;如果是串行的访问模式,执行的时间会大大地增加。
另外,分库数量还直接影响了硬件的投入,多一个库,就意味着要多投入硬件设备。所以,具体分多少个库,需要做一个综合评估,一般初次分库,我建议你分成4~8个库。在项目中,我们拆分为了6个数据库,这样可以满足较长一段时间的订单业务需求。
不过水平分库解决了单个数据库容量和性能瓶颈的同时,也给我们带来了一系列新的问题,包括数据库路由、分页以及字段映射的问题。
分库从某种意义上来说,意味着DB Schema改变了,必然会影响应用,但这种改变和业务无关,所以我们要尽量保证分库相关的逻辑都在数据访问层进行处理,对上层的订单服务透明,服务代码无需改造。
当然,要完全做到这一点会很困难。那么具体哪些改动应该由DAL(数据访问层)负责,哪些由订单服务负责,这里我给你一些可行的建议:
DAL层还可以进一步细分为底层JDBC驱动层和偏上面的数据访问层。如果我们基于JDBC层面实现分库路由,系统开发难度大,灵活性低,目前也没有很好的成功案例。
在实践中,我们一般是基于持久层框架,把它进一步封装成DDAL(Distributed Data Access Layer,分布式数据访问层),实现分库路由。1号店的DDAL就是基于iBatis进一步封装而来的。
水平分库后,分页查询的问题比较突出,因为有些分页查询需要遍历所有库。
举个例子,假设我们要按时间顺序展示某个商家的订单,每页有100条记录,由于是按商家查询,我们需要遍历所有数据库。假设库数量是8,我们来看下水平分库后的分页逻辑:
你可以看到,在分库情况下,对于每个数据库,我们要取更多的记录,并且汇总后,还要在应用里做二次排序,越是靠后的分页,系统要耗费更多的内存和执行时间。而在不分库的情况下,无论取哪一页,我们只要从单个DB里取100条记录即可,也无需在应用内部做二次排序,非常简单。
那么,我们如何解决分库情况下的分页问题呢?这需要具体情况具体分析:
分库字段只有一个,比如这里,我们用的是用户ID,如果给定用户ID,这个查询会落到具体的某个库。但我们知道,在订单服务里,根据订单ID查询的场景也很多见,不过由于订单ID不是分库字段,如果不对它做特殊处理,系统会盲目查询所有分库,从而带来不必要的资源开销。
所以,这里我们为订单ID和用户ID创建映射,保存在Lookup表里,我们就可以根据订单ID,找到相应的用户ID,从而实现单库定位。
Lookup表的记录数和订单库记录总数相等,但它只有2个字段,所以存储和查询性能都不是问题,这个表在单独的数据库里存放。在实际使用时,我们可以通过分布式缓存,来优化Lookup表的查询性能。此外,对于新增的订单,除了写订单表,我们同时还要写Lookup表。
通过以上分析,最终的1号店订单水平分库的总体技术架构如下图所示:
订单表是系统的核心业务表,它的水平拆分会影响到很多业务,订单服务本身的代码改造也很大,很容易导致依赖订单服务的应用出现问题。我们在上线时,必须谨慎考虑。
所以,为了保证订单水平分库的总体改造可以安全落地,整个方案的实施过程如下:
这里,我们把上线分成了两个阶段:第一阶段,把部分非实时的功能切换到MySQL,这个阶段主要是为了验证技术,它包括了分库代理、DDAL、Lookup表等基础设施的改造;第二阶段,主要是验证业务功能,我们把所有订单场景全面接入MySQL。1号店两个阶段的上线都是一次性成功的,特别是第二阶段的上线,100多个依赖订单服务的应用,通过简单的重启就完成了系统的升级,中间没有出现一例较大的问题。
1号店在完成订单水平分库的同时,也实现了去Oracle,设备从小型机换成了X86服务器,我们通过水平分库和去Oracle,不但支持了订单量的未来增长,并且总体成本也大幅下降。
不过由于去Oracle和订单分库一起实施,带来了双重的性能影响,我们花了很大精力做性能测试。为了模拟真实的线上场景,我们通过TCPCopy,把线上实际的查询流量引到测试环境,先后经过13轮的性能测试,最终6个MySQL库相对一个Oracle,在当时的数据量下,SQL执行时间基本持平。这样,我们在性能不降低的情况下,通过水平分库优化了架构,实现了订单处理能力的水平扩展。
1号店最终是根据用户ID后三位取模进行分库,初始分成了6个库,理论上可以支持多达768个库。同时我们还改造了订单ID的生成规则,使其包括用户ID后三位,这样新订单ID本身就包含了库定位所需信息,无需走Lookup映射机制。随着老订单归档到历史库,在前面给出的架构中,Lookup表相关的部分就可以逐渐废弃了。
如果要扩充数据库的数量,从6个升到12个,我们可以分三步走:
你可以看到,通过这样的分库方式,整个数据库扩展是非常容易的,不涉及复杂的数据跨库迁移工作。
订单的水平分库是一项系统性工作,需要大胆设计,谨慎实施。你需要把握住这几个要点:
今天我和你分享了1号店订单水平分库的实际案例,并给出了具体的做法和原因,相信你已经掌握了如何通过对数据库的水平拆分,来保证系统的高性能和可伸缩。
水平分库是针对有状态的存储节点进行水平扩展,相对于无状态的节点,系统改造的复杂性比较高,要考虑的点也比较多。通过今天的分享,希望你以后在设计一个复杂方案时,能够更全面地思考相关的细节,提升架构设计能力。
最后,给你留一道思考题:你公司的数据库有什么瓶颈吗,你计划对它做什么样的改造呢?
欢迎在留言区和我互动,我会第一时间给你反馈。如果这节课对你有帮助,也欢迎你把它分享给你的朋友。感谢阅读,我们下期再见。
评论