在之前的文章中,我介绍了分库分表的几种表现形式和玩法,也重点介绍了垂直分库所带来的问题和解决方法。本篇中,我们将继续聊聊水平分库分表的一些技巧。
分片技术的由来
关系型数据库本身比较容易成为系统性能瓶颈,单机存储容量、连接数、处理能力等都很有限,数据库本身的“有状态性”导致了它并不像 Web 和应用服务器那么容易扩展。在互联网行业海量数据和高并发访问的考验下,聪明的技术人员提出了分库分表技术(有些地方也称为 Sharding、分片)。同时,流行的分布式系统中间件(例如 MongoDB、ElasticSearch 等)均自身友好支持 Sharding,其原理和思想都是大同小异的。
分布式全局唯一 ID
在很多中小项目中,我们往往直接使用数据库自增特性来生成主键 ID,这样确实比较简单。而在分库分表的环境中,数据分布在不同的分片上,不能再借助数据库自增长特性直接生成,否则会造成不同分片上的数据表主键会重复。简单介绍下使用和了解过的几种 ID 生成算法。
- Twitter 的 Snowflake(又名“雪花算法”)
- UUID/GUID(一般应用程序和数据库均支持)
- MongoDB ObjectID(类似 UUID 的方式)
- Ticket Server(数据库生存方式,Flickr 采用的就是这种方式)
其中,Twitter 的 Snowflake 算法是笔者近几年在分布式系统项目中使用最多的,未发现重复或并发的问题。该算法生成的是 64 位唯一 Id(由 41 位的 timestamp+ 10 位自定义的机器码 + 13 位累加计数器组成)。这里不做过多介绍,感兴趣的读者可自行查阅相关资料。
常见分片规则和策略
分片字段该如何选择
在开始分片之前,我们首先要确定分片字段(也可称为“片键”)。很多常见的例子和场景中是采用 ID 或者时间字段进行拆分。这也并不绝对的,我的建议是结合实际业务,通过对系统中执行的 sql 语句进行统计分析,选择出需要分片的那个表中最频繁被使用,或者最重要的字段来作为分片字段。
常见分片规则
常见的分片策略有随机分片和连续分片这两种,如下图所示:
当需要使用分片字段进行范围查找时,连续分片可以快速定位分片进行高效查询,大多数情况下可以有效避免跨分片查询的问题。后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移。但是,连续分片也有可能存在数据热点的问题,就像图中按时间字段分片的例子,有些节点可能会被频繁查询压力较大,热数据节点就成为了整个集群的瓶颈。而有些节点可能存的是历史数据,很少需要被查询到。
随机分片其实并不是随机的,也遵循一定规则。通常,我们会采用 Hash 取模的方式进行分片拆分,所以有些时候也被称为离散分片。随机分片的数据相对比较均匀,不容易出现热点和并发访问的瓶颈。但是,后期分片集群扩容起来需要迁移旧的数据。使用一致性 Hash 算法能够很大程度的避免这个问题,所以很多中间件的分片集群都会采用一致性 Hash 算法。离散分片也很容易面临跨分片查询的复杂问题。
数据迁移,容量规划,扩容等问题
很少有项目会在初期就开始考虑分片设计的,一般都是在业务高速发展面临性能和存储的瓶颈时才会提前准备。因此,不可避免的就需要考虑历史数据迁移的问题。一般做法就是通过程序先读出历史数据,然后按照指定的分片规则再将数据写入到各个分片节点中。
此外,我们需要根据当前的数据量和 QPS 等进行容量规划,综合成本因素,推算出大概需要多少分片(一般建议单个分片上的单表数据量不要超过 1000W)。
如果是采用随机分片,则需要考虑后期的扩容问题,相对会比较麻烦。如果是采用的范围分片,只需要添加节点就可以自动扩容。
跨分片技术问题
跨分片的排序分页
一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示:
上面图中所描述的只是最简单的一种情况(取第一页数据),看起来对性能的影响并不大。但是,如果想取出第 10 页数据,情况又将变得复杂很多,如下图所示:
有些读者可能并不太理解,为什么不能像获取第一页数据那样简单处理(排序取出前 10 条再合并、排序)。其实并不难理解,因为各分片节点中的数据可能是随机的,为了排序的准确性,必须把所有分片节点的前 N 页数据都排序好后做合并,最后再进行整体的排序。很显然,这样的操作是比较消耗资源的,用户越往后翻页,系统性能将会越差。
跨分片的函数处理
在使用 Max、Min、Sum、Count 之类的函数进行统计和计算的时候,需要先在每个分片数据源上执行相应的函数处理,然后再将各个结果集进行二次处理,最终再将处理结果返回。如下图所示:
跨分片 join
Join 是关系型数据库中最常用的特性,但是在分片集群中,join 也变得非常复杂。应该尽量避免跨分片的 join 查询(这种场景,比上面的跨分片分页更加复杂,而且对性能的影响很大)。通常有以下几种方式来避免:
全局表
全局表的概念之前在“垂直分库”时提过。基本思想一致,就是把一些类似数据字典又可能会产生 join 查询的表信息放到各分片中,从而避免跨分片的 join。
ER 分片
在关系型数据库中,表之间往往存在一些关联的关系。如果我们可以先确定好关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能很好的避免跨分片 join 问题。在一对多关系的情况下,我们通常会选择按照数据较多的那一方进行拆分。如下图所示:
这样一来,Data Node1 上面的订单表与订单详细表就可以直接关联,进行局部的 join 查询了,Data Node2 上也一样。基于 ER 分片的这种方式,能够有效避免大多数业务场景中的跨分片 join 问题。
内存计算
随着 spark 内存计算的兴起,理论上来讲,很多跨数据源的操作问题看起来似乎都能够得到解决。可以将数据丢给 spark 集群进行内存计算,最后将计算结果返回。
跨分片事务问题
跨分片事务也分布式事务,想要了解分布式事务,就需要了解“XA 接口”和“两阶段提交”。值得提到的是,MySQL5.5x 和 5.6x 中的 xa 支持是存在问题的,会导致主从数据不一致。直到 5.7x 版本中才得到修复。Java 应用程序可以采用 Atomikos 框架来实现 XA 事务(J2EE 中 JTA)。感兴趣的读者可以自行参考《分布式事务一致性解决方案》,链接地址:
http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency
我们的系统真的需要分库分表吗
读完上面内容,不禁引起有些读者的思考,我们的系统是否需要分库分表吗?
其实这点没有明确的判断标准,比较依赖实际业务情况和经验判断。依照笔者个人的经验,一般 MySQL 单表 1000W 左右的数据是没有问题的(前提是应用系统和数据库等层面设计和优化的比较好)。当然,除了考虑当前的数据量和性能情况时,作为架构师,我们需要提前考虑系统半年到一年左右的业务增长情况,对数据库服务器的 QPS、连接数、容量等做合理评估和规划,并提前做好相应的准备工作。如果单机无法满足,且很难再从其他方面优化,那么说明是需要考虑分片的。这种情况可以先去掉数据库中自增 ID,为分片和后面的数据迁移工作提前做准备。
很多人觉得“分库分表”是宜早不宜迟,应该尽早进行,因为担心越往后公司业务发展越快、系统越来越复杂、系统重构和扩展越困难…这种话听起来是有那么一点道理,但我的观点恰好相反,对于关系型数据库来讲,我认为“能不分片就别分片”,除非是系统真正需要,因为数据库分片并非低成本或者免费的。
这里笔者推荐一个比较靠谱的过渡技术–“表分区”。主流的关系型数据库中基本都支持。不同的分区在逻辑上仍是一张表,但是物理上却是分开的,能在一定程度上提高查询性能,而且对应用程序透明,无需修改任何代码。笔者曾经负责优化过一个系统,主业务表有大约 8000W 左右的数据,考虑到成本问题,当时就是采用“表分区”来做的,效果比较明显,且系统运行的很稳定。
小结
最后,有很多读者都想了解当前社区中有没有开源免费的分库分表解决方案,毕竟站在巨人的肩膀上能省力很多。当前主要有两类解决方案:
- 基于应用程序层面的 DDAL(分布式数据库访问层)
比较典型的就是淘宝半开源的 TDDL,当当网开源的 Sharding-JDBC 等。分布式数据访问层无需硬件投入,技术能力较强的大公司通常会选择自研或参照开源框架进行二次开发和定制。对应用程序的侵入性一般较大,会增加技术成本和复杂度。通常仅支持特定编程语言平台(Java 平台的居多),或者仅支持特定的数据库和特定数据访问框架技术(一般支持 MySQL 数据库,JDBC、MyBatis、Hibernate 等框架技术)。
2. 数据库中间件,比较典型的像 mycat(在阿里开源的 cobar 基础上做了很多优化和改进,属于后起之秀,也支持很多新特性),基于 Go 语言实现 kingSharding,比较老牌的 Atlas(由 360 开源)等。这些中间件在互联网企业中大量被使用。另外,MySQL 5.x 企业版中官方提供的 Fabric 组件也号称支持分片技术,不过国内使用的企业较少。
中间件也可以称为“透明网关”,大名鼎鼎的 mysql_proxy 大概是该领域的鼻祖(由 MySQL 官方提供,仅限于实现“读写分离”)。中间件一般实现了特定数据库的网络通信协议,模拟一个真实的数据库服务,屏蔽了后端真实的 Server,应用程序通常直接连接中间件即可。而在执行 SQL 操作时,中间件会按照预先定义分片规则,对 SQL 语句进行解析、路由,并对结果集做二次计算再最终返回。引入数据库中间件的技术成本更低,对应用程序来讲侵入性几乎没有,可以满足大部分的业务。增加了额外的硬件投入和运维成本,同时,中间件自身也存在性能瓶颈和单点故障问题,需要能够保证中间件自身的高可用、可扩展。
总之,不管是使用分布式数据访问层还是数据库中间件,都会带来一定的成本和复杂度,也会有一定的性能影响。所以,还需读者根据实际情况和业务发展需要慎重考虑和选择。
作者介绍
丁浪,技术架构师。关注高并发、高可用的架构设计,对系统服务化、分库分表、性能调优等方面有深入研究和丰富实践经验。热衷于技术研究和分享。
感谢郭蕾对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论 1 条评论