本文参考《极客时间》- 从0开始学架构
高性能数据库集群
第一种方式是“读写分离”,其本质是将访问压力分散到集群中的多个节点,但是没有分散存储压力;
第二种方式是“分库分表”,既可以分散访问压力,又可以分散存储压力。
“读写分离”
读写分离的基本原理是将数据库读写操作分散到不同的节点上。
读写分离的基本实现是:
- 数据库服务器搭建主从集群,一主一从、一主多从都可以。
- 数据库主机负责读写操作,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
需要注意的是,这里用的是“主从集群”,而不是“主备集群”。
“从机”是需要提供读数据的功能的;而“备机”一般被认为仅仅 提供备份功能,不提供访问功能。
所以使用“主从”还是“主备”,是要看场景的,这两个词并不是完全等同的。
写分离的实现逻辑并不复杂,但有两个细节点将引入设计复杂度: 主从复制延迟 和 分配机制 。
复制延迟
以MySQL为例,主从复制延迟可能达到1秒,如果有大量数据同步,延迟1分钟也是有可能的。
主从复制延迟会带来一个问题:如果业务服务器将数据写入到数据库主服务器后立刻 (1秒内)进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。
例如,用户刚注册完后立刻登录,业 务服务器会提示他“你还没有注册”,而用户明明刚才已经注册成功了。
- 解决主从复制延迟有几种常见的方法:
(1)写操作后的读操作指定发给数据库主服务器
例如,注册账号完成后,登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定,对业务的侵入和影响较大,如果哪个新来的程序员不知道这样写代码,就会导致一个bug。
(2)读从机失败后再读一次主机 这就是通常所说的“二次读取”,二次读取和业务无绑定,只需要对底层数据库访问的API进行封装即可,实现代价较小,不足之处在于如果有很多二次读取,将大大增加主机的读操
作压力。例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃
(3)关键业务读写操作全部指向主机,非关键业务采用读写分离
例如,对于一个用户管理系统来说,注册+登录的业务读写操作全部访问主机,用户的介绍、爱好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时 却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。
备注:当然,用户登录这种情况,可以通过redis缓存来处理。注册之后,用户信息存缓存。登录先查缓存
并不是说一有性能问题就上读写分离,而是应该先优化,例如优化慢查询,调整不合理的业务逻辑,引入缓存等,只有确定系统没有优化空间后,才考虑读写分离或者集群。
分配机制
- 将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。
(1)程序代码封装 程序代码封装指在代码中抽象一个数据访问层(所以有的文章也称这种方式为“中间层封装”),实现读写操作分离和数据库服务器连接的管理。
例如,基于Hibernate进行简单封装,就可以实现读写分离,基本架构是:
目前开源的实现方案中,淘宝的TDDL(Taobao Distributed Data Layer,外号:头都大了)是比较有名的。它是一个通用数据访问层,所有功能封装在jar包中提供给业务代码调 用。
其基本原理是一个基于集中式配置的 jdbc datasource实现,具有主备、读写分离、动态数据库配置等功能,基本架构是:
.中间件封装
(2)中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供SQL兼容的协议,业务服务器无须自己进行读写分离。对于业务服务 器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。其基本架构是:
数据库中间件的方式具备的特点是: 能够支持多种编程语言,因为数据库中间件对业务服务器提供的是标准SQL接口。
数据库中间件要支持完整的SQL语法和数据库服务器的协议(例如,MySQL客户端和服务器的连接协议),实现比较复杂,细节特别多,很容易出现bug,需要较长的时间才能稳定。
数据库中间件自己不执行真正的读写操作,但所有的数据库操作请求都要经过中间件,中间件的性能要求也很高。
数据库主从切换对业务服务器无感知,数据库中间件可以探测数据库服务器的主从状态。
例如,向某个测试表写入一条数据,成功的就是主机,失败的就是从机。
由于数据库中间件的复杂度要比程序代码封装高出一个数量级,一般情况下建议采用程序语言封装的方式,或者使用成熟的开源数据库中间件。如果是大公司,可以投入人力去实现数据库中间件,因为这个系统一旦做好,接入的业务系统越多,节省的程序开发投入就越多,价值也越大。
目前的开源数据库中间件方案中,MySQL官方先是提供了MySQL Proxy,但MySQL Proxy一直没有正式GA,现在MySQL官方推荐MySQL Router。MySQL Router的主要功能 有读写分离、故障自动切换、负载均衡、连接池等,其基本架构如下:
“分库分表”
读写分离分散了数据库读写操作的压力,但没有分散存储压力。
当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面:
1)数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
2)数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
3)数据文件越大,极端情况下丢失数据的风险越高(例如,机房火灾导致数据库主备机都发生故障)。
做分库分表前依次尝试这些方式是否可以达到目的:
1.做硬件优化,例如从机械硬盘改成使用固态硬盘,当然固态硬盘不适合服务器使用,只是举个例子
2.先做数据库服务器的调优操作,例如增加索引,oracle有很多的参数调整;
3.引入缓存技术,例如Redis,减少数据库压力
4.程序与数据库表优化,重构,例如根据业务逻辑对程序逻辑做优化,减少不必要的查询;
5.在这些操作都不能大幅度优化性能的情况下,不能满足将来的发展,再考虑分库分表,也要有预估性
分库分表的实施前提:
- 数据库存在性能问题,且用加索引、慢查询优化、缓存和读写分离都无法彻底解决问题的时候
- 复杂查询对应的单表数据量级一般超过千万以上,或简单查询的单表数据量级一般超过5000万以上 3. 对一致性要求不是特别高,只要求最终一致性。
- 用hadoop等大数据技术因为业务需求、技术成本、时间成本无法解决该问题
其他: - 业务对数据的操作主要集中在某些字段上,比较适合垂直分表
- 业务对数据的操作在整个表层面较均匀分布,适合水平分表
常见的分散存储的方法“分库分表”,其中包括“分库”和“分表”两大类。
业务分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器、
例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放 到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。
虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题:
1)join操作问题
业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用SQL的join查询。
不同库的数据无法联表查询。只能通过id一步步子查询。
(备注:不过现在很多系统中,推荐只使用子查询。尽量减少联合查询)
2)事务问题:业务分库后,表分散到不同库中。无法通过事务统一修改
3)成本问题:业务分库同时也带来了成本的代价,本来1台服务器搞定的事情,现在要3台,如果考虑备份,那就是2台变成了6台
- 基于上述原因,对于小公司初创业务,并不建议一开始就这样拆分,主要有几个原因:
1)初创业务存在很大的不确定性,业务不一定能发展起来,业务开始的时候并没有真正的存储和访问压力,业务分库并不能为业务带来价值。
2) 业务分库后,表之间的join查询、数据库事务无法简单实现了。
3) 业务分库后,因为不同的数据要读写不同的数据库,代码中需要增加根据数据类型映射到不同数据库的逻辑,增加了工作量。而业务初创期间最重要的是快速实现、快速验证,业务分库会拖慢业务节奏。
如果业务发展的真的很快,后面进行分库分表也不迟。
业务分库带来的代码和业务复杂可以通过增加人来解决,成本问题可以通过增加资金解决。
单台系统服务器性能没有想象的弱。一般来说,单台数据库服务器能够支撑10万用户量量级的业务,初创业务从0发展到10万级用户,并不是想象得那么快。
分表
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务。
如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。
例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。
垂直分表:按列来分。是表记录数相同但包含不同的列。一张表300个字段。从某一列拆分
水平分表:按行来分。表的列相同但包含不同的行数据。一亿条数据,拆分为10张表。
单表切分后,是否需要将切分后的多个表分散在不同库,根据实际效果来。不强制要求单表切分后,分散到不同库中。因为,单表切分后,新的表即使同库,也能带来性能的很大提升,如果性能能满足业务要求,是可以不拆分为多台库的。
分表能够有效地分散存储压力和带来性能提升,但和分库一样,也会引入各种复杂性。
分表之垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。
垂直分表主要带来的是表操作数据增加。原本一次查询的需要2次查询。其复杂性相对较小。
分表之水平分表:
水平分表适合表行数特别大的表,有的公司要求单表行数超过5000万就必须进行分表,数字作为参考不是绝对标准,关键还是要看表的访问性能。
对于一些比较复杂的表,可能超过1000万就要分表了;
而对于一些简单的表,即使存储数据超过1亿行,也可以不分表。
但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来, 因为这很可能是架构的性能瓶颈或者隐患。
水平分表带来的复杂性,主要是:
第一点:路由
水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。
常见的路由算法有:
A.范围路由:
选取有序的数据列(例如,整形、时间戳等)作为路由的条件,不同分段分散到不同的数据库表中。以最常见的用户ID为例,路由算法可以按照1000000的范围大小进行 分段,1 ~ 999999放到数据库1的表中,1000000 ~ 1999999放到数据库2的表中,以此类推。
问题点在于:分段大小选取上,分段太小导致切分后子表数量过多,增加维护复杂度;分表太大仍然存在性能问题。一般建议分段大小在100万到2000万之间(具体业务具体分析)。
优点在于:随着数据的增加平滑地扩充新的表。例如,现在的用户是100万,如果增加到1000万,只需要增加新的表就可以了,原有的数据不需要动。
缺点在于:分布不均匀,假如按照1000万来进行分表,有可能某个分段实际存储的数据量只有1000条,而另外一个分段实际存储的数据量有900万条。B.Hash路由
选取某个列(或者某几个列组合也可以)的值进行Hash运算,然后根据Hash结果分散到不同的数据库表中。同样以用户ID为例,假如我们一开始就规划了10个数据库 表,路由算法可以简单地用user_id % 10的值来表示数据所属的数据库表编号,ID为985的用户放到编号为5的子表中,ID为10086的用户放到编号为6的字表中。
问题点在于初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了Hash路由后,增加字表数量是非常麻烦的, 所有数据都要重分布。
Hash路由的优缺点和范围路由基本相反,Hash路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。
- C.配置路由
配置路由就是路由表,用一张独立的表来记录路由信息。同样以用户ID为例,我们新增一张user_router表,这个表包含user_id和table_id两列,根据user_id就可以查询对应的table_id。
配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。
配置路由的缺点就是必须多查询一次,会影响整体性能;
而且路由表本身如果太大(例如,几亿条数据),性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。
第二点:join操作
水平分表后,数据分散在多个表中,如果需要与其他表进行join查询,需要在业务代码或者数据库中间件中进行多次join查询,然后将结果合并。
第三点:count()操作
水平分表后,虽然物理上数据分散到多个表中,但某些业务逻辑上还是会将这些表当作一个表来处理。
例如,获取记录总数用于分页或者展示,水平分表前用一个count()就能完成的操作,在分表后没那么简单。
常见的处理方式有下面两种:
count()相加:具体做法是在业务代码或者数据库中间件中对每个表进行count()操作,然后将结果相加。这种方式实现简单,缺点就是性能比较低。例如,水平分表后切分为20张
表,则要进行20次count(*)操作,如果串行的话,可能需要几秒钟才能得到结果。
记录数表:具体做法是新建一张表,假如表名为“记录数表”,包含table_name、row_count两个字段,每次插入或者删除子表数据成功后,都更新“记录数表”。
这种性能要优于count()相加的方式,因为只需要一次简单查询。
缺点是复杂度增加不少,对子表的操作要同步操作“记录数表”,如果有一个 业务逻辑遗漏了,数据就会不一致;且针对“记录数表”的操作和针对子表的操作无法放在同一事务中进行处理,异常的情况下会出现操作子表成功了而操作记录数表失败,同样会导 致数据不一致。
此外,记录数表的方式也增加了数据库的写压力,因为每次针对子表的insert和delete操作都要update记录数表,所以对于一些不要求记录数实时保持精确的业务,也可以通过后台 定时更新记录数表。定时更新实际上就是“count()相加”和“记录数表”的结合,即定时通过count()相加计算表的记录数,然后更新记录数表中的数据。
第四点:order by操作
水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。
实现方法
和数据库读写分离类似,分库分表具体的实现方式也是“程序代码封装”和“中间件封装”,但实现会更复杂。读写分离实现时只要识别SQL操作是读操作还是写操作,通过简单的判 断SELECT、UPDATE、INSERT、DELETE几个关键字就可以做到,而分库分表的实现除了要判断操作类型外,还要判断SQL中具体需要操作的表、操作函数(例如count函 数)、order by、group by操作等,然后再根据不同的操作进行不同的处理。例如order by操作,需要先从多个库查询到各个库的数据,然后再重新order by才能得到最终的结 果。