2.2 从集中式到分布式数据库——电商管家系统演进之路
2000年以后,互联网应用如雨后春笋般出现,互联网应用需要支持大量用户的并发访问,且对可用性要求极高,最好永远在线;另外,随着互联网技术发展的风起云涌,使得数据规模越来越大,一天的数据增量甚至达到TB级,这考验着单机MySQL或者PostgreSQL的性能。
和互联网类似,商业银行也有大量的系统需要接受用户高并发访问,数据量也在快速增长。商业银行还有一些内容管理方面的系统,这种系统会有非常高的读写比,对数据库访问压力极大。但传统上,商业银行使用的仍然是集中式架构,例如,最常用的Oracle、Db2等。从图2-4可以看出,接收业务访问的集中式数据库安装在单台服务器上(通常还会有个备机,但不接受业务访问),供本地用户和远程用户访问。
互联网行业和商业银行的高并发访问和数据量的快速增长,已经是一种新常态。它对单机数据库造成了极大的挑战,包括容量、性能、稳定性、高可用、维护成本等。
图2-4 集中式数据库架构
面对传统集中式架构的性能瓶颈和容量瓶颈,本节将继续以某商业银行电商管家系统为例,来说明在不同阶段可以采取的技术手段,以及这种技术手段所能达到的效果。
2.2.1 硬件扩容方案——摩尔定律失效导致效果有限
1965年,英特尔联合创始人戈登·摩尔提出以自己名字命名的摩尔定律,意指集成电路上可容纳的元器件的数量每隔18~24个月就会增加一倍,性能也将提升一倍,如图2-5所示。
图2-5 摩尔定律
到今天已经五十多年了,摩尔定律过去是每5年增长10倍,每10年增长100倍。而如今,摩尔定律每年只能增长几个百分点,每10年可能只有2倍。芯片逼近物理和经济成本上的极限,摩尔定律结束了。硅谷创业教父Steve Blank曾撰文指出,严格来说,摩尔定律其实在10年前就已经失效,只是大家没有意识到。
随着摩尔定律的失效,带来的一个效应就是,单机性能的瓶颈也已经出现。想要进一步增加单机的性能,需要的成本已经不是线性的,而是呈几何倍数的增加。
电商管家系统,某商业银行曾经尝试对数据库服务器纵向扩容,即通过增加CPU、内存等硬件资源提高单机处理性能,但实践证明这种方式换来的性能提升十分有限,表现出明显的边际收益递减。
这就需要我们去思考,如何用廉价的x86机器,甚至云上的虚拟机集群,通过分布式的方式,来达到同样的增强整体性能和容量的目的。
2.2.2 Redis缓存方案——解决高并发性能问题
电商管家系统上线后,随着交易量和数据量的增加,一旦涉及大数据量的需求,例如商品抢购“秒杀”的情景,或者是主页访问量瞬间剧增的时候,单机数据库受制于CPU和磁盘I/O性能而存在严重的性能瓶颈;在这种“秒杀”场景中,一瞬间成千上万的请求到来,需要数据库在极短的时间内完成成千上万次的读/写操作,这不是单机数据库能够承受的,极其容易造成数据库系统瘫痪,最终导致服务宕机的严重生产问题。
这种“秒杀”场景,从Web页面、缓存层以及数据库层都有相应的优化手段,本节重点从缓存层给出一种优化方案——Redis缓存方案。
在合适的场景下,Redis的性能十分优越,可以支持每秒万次以上的读/写操作,其性能远超数据库,并且还支持集群、分布式、主从同步等配置,原则上可以无限扩展,让更多的数据存储在内存中,它还支持一定的事务能力,这保证了高并发场景下数据的一致性。
什么是Redis?
Redis是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的高性能Key-Value数据库。它支持字符串(String)、链表(List)、集合(Set)、有序集合(SortedSet)、哈希表(Hash)等多种数据类型。
如图2-6所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现性能瓶颈。这时,可以使用Redis做一个缓冲操作,让请求先访问到Redis,而不是直接访问数据库。
图2-6 应用发起的高并发请求
如图2-7所示,电商管家系统采用了Redis集群方案,集群共有三个节点。这是一个分布式、容错的Redis实现。Redis集群中不存在中心节点或者代理节点,集群的主要设计目标是达到线性扩展。
图2-7 三节点Redis集群方案
在日常对数据库的访问中,经常读操作的次数远超写操作,所以读的可能性是比写的可能大得多的。当使用SQL语句访问数据库进行读写操作时,数据库就会去磁盘把对应的数据取回来,这是一个相对较慢的过程。如果把数据放在Redis中,也就是直接放在内存之中,让服务端直接去读取内存中的数据,那么这样速度明显就会快不少,并且会极大减小数据库的压力。
2.2.3 MySQL读写分离方案——解决高读写比性能问题
在2.2.2节,面对高并发的“秒杀”场景,电商管家系统采用了Redis缓存方案。但Redis缓存方案也有弊端,那就是使用内存进行数据存储开销也是比较大的。限于成本的原因,一般只是使用Redis存储一些常用和主要的数据,这就意味着无法保证所有数据被缓存,从而限制了它的使用范围。
采用Redis缓存方案后,电商管家系统面对日益增加的系统访问量,数据库的吞吐量仍然面临着巨大瓶颈,这个时候读写分离方案正式登场。
对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效地避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
如图2-8所示,针对电商管家系统,过了一段时间,数据库的读压力太大,又加了一台从库,将部分读请求路由到新搭建的从库上,从而实现了读写分离,以降低主库读的压力。
图2-8 读写分离方案——物理部署架构
通过一主多从的配置方式,可以将查询请求均匀地分散到多个数据副本,能够进一步提升系统的处理能力,这样不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。
如图2-9所示,采用读写分离方案的逻辑结构,商家表Dealer和商品表Goods在主库和从库都会存放,这样方便去从库执行只读查询。
图2-9 读写分离方案——逻辑结构
读写分离虽然可以提升系统的吞吐量和可用性,但主库与从库的复制一般不是强一致的,这带来了数据不一致的问题。例如,先在主库中插入了一条订单信息,返回了订单号;然后执行查询操作,但是可能因为读写分离被路由到了从库。但是,此时从库还没有来得及从主库同步binlog,导致从库没有这条记录,所以查询操作返回空。
在读写分离的路由实现上,可以在业务系统的代码中配置多个数据源,并根据自己的需要把应用切换到对应的主库或某个从库。但是,显然这种机制不够方便,对应用的侵入性较强。
此外,也可以简单地封装一个中间层,根据执行的事务是读操作还是写操作来判断,如果是读操作就路由到从库执行,否则就路由到主库执行。这样就可以对业务系统透明。
2.2.4 分库分表方案——解决性能和容量瓶颈问题
不论是缓存方案还是读写分离方案,都只是基于传统集中式数据库方案之上的优化,在容量、性能、可用性和运维成本这几方面已经难于满足商业银行高并发和大数据量访问场景。
从容量方面,在单库单表数据量超过一定数值的情况下,索引树层级增加,磁盘I/O也很可能出现压力,会导致很多问题。
从性能方面,由于关系数据库大多采用B+树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的I/O次数增加,进而导致查询性能的下降;同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。
从可用性方面,服务化的无状态型,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。数据库的可用性,已成为整个系统的关键。
从运维成本方面,当一个数据库实例中的数据达到阈值以上,对于DBA的运维压力就会增大。数据备份和恢复的时间成本都将随着数据量的增大而增大。
对MySQL数据库来说,单库的数据阈值在1TB之内,单表容量在100GB以内,是比较合理的范围,当单库单表数据过大时,业界通行的做法是进行拆分,拆分方法主要有以下4种,下面分别介绍。
1.垂直分表
如果单表数据量过大,还可能需要对单表进行拆分。例如,一个200列的商品表,拆分成两个子表:商品表和商品详情表。这种拆分方法对业务系统的影响很大,本来从商品表就可以获取所需数据,但现在需要关联商品表和商品详情表,也就是说,应用SQL需要改写,通常动的越多,出现生产故障的风险就越高。
2.垂直分库
拆分商家表和商品表的数据,变成两个独立的库,商家表为一个商家数据库,商品表为另外一个商品数据库。这种方式对业务系统影响更大,因为数据结构本身发生了变化,SQL语句和关联关系也必随之发生了改变。原来一条SQL语句直接把商家表和相关的商品表数据都查了出来,现在这条SQL语句不能用了,需要重新改写。先查询商家数据库,拿到这批商家对应的所有商品id,再根据商品id集合去商品库查询所有的商品信息,最后在应用代码里进行组装。
3.水平分表
水平分表,是把一张表的数据分到同一个数据库的多张子表中,每张子表的结构不变,但只有部分数据。这种拆分方式也会对应用产生影响。原来查单表的一条SQL语句,需要转换为从多个子表中查询后汇总结果。
4.水平分库
可以把一个表的数据分到多个不同的子库(通常称为分片)。每个分片只有这个表的部分数据,这些分片可以分布在不同服务器,从而使访问压力被多服务器负担,可以大大提升性能。这样对业务系统本身的代码逻辑来说,就不需要做特别大的改动,甚至可以基于一些中间件做到透明。
一般情况下,如果数据本身的读写压力较大,磁盘I/O已经成为瓶颈,那么分库比分表要好。分库将数据分散到不同的数据库实例,使用不同的磁盘,从而可以并行提升整个集群的并行处理能力;相反的情况下,可以尽量多考虑分表,降低单表的数据量,从而减少单表操作的时间,同时也能在单个数据库上使用并行操作多个表来增加处理能力。
一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库、垂直分表方案,在数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案。若数据量极大,且持续增长,再考虑水平分库、水平分表方案。
针对本章提到的某商业银行电商管家系统,在数据容量和性能方面难以满足要求时,就可以采用水平分库方案了。如图2-10所示,商家表Dealer和商品表Goods被拆分到4个分片中,此时可以在Java环境里引入TDDL或者Sharding-JDBC之类的框架,在业务代码侧,通过配置特定的分库分表规则,以及对分布式事务的控制,在保证数据一致性的情况下,提升数据库整体集群的容量,保证稳定性和性能。
图2-10 分库分表方案——物理部署架构
如图2-11所示,采用分库分表方案的逻辑结构,商家表Dealer和商品表Goods被拆分,并被分到4个分片中,从而使访问压力被4台服务器分担,可以大大提升性能。
图2-11 分库分表方案——逻辑结构
2.2.5 分布式数据库中间件方案——通过中间件透明访问数据库
从2.2.4节了解到,分库分表方案的优势主要是:可以支持各种常见的数据库,如MySQL、PostgreSQL,还有SQL Server、Oracle、Db2等,另外性能损耗也较小。但其最大的缺点是对应用有侵入性,需要开发人员直接处理和维护分库分表逻辑。
为了让应用透明地访问,业界通用的做法是采用分布式数据库中间件解决方案。使用数据库中间件,可以屏蔽分库分表后的数据库复杂性。
本节重点介绍Apache ShardingSphere方案。
Apache ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由JDBC、Proxy和Sidecar(规划中)3款相互独立却又能够混合部署配合使用的产品组成。它们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、云原生等各种多样化的应用场景。
Apache ShardingSphere定位为关系数据库中间件,旨在充分合理地在分布式的场景下利用关系数据库的计算和存储能力,而并非实现一个全新的关系数据库。主要考虑到关系数据库当今依然占有巨大市场,是各个公司核心业务的基石,未来也难以撼动,所以该方案目前阶段更加关注在原有基础上的增量,而非颠覆。
Apache ShardingSphere 5.x版本开始致力于可插拔架构,项目的功能组件能够灵活地以可插拔的方式进行扩展。目前,数据分片、读写分离、多数据副本、数据加密、影子库压测等功能,以及MySQL、PostgreSQL、SQL Server、Oracle等SQL与协议的支持,均通过插件的方式植入项目,这样开发者能够像使用积木一样定制属于自己的独特系统。
ShardingSphere已于2020年4月16日成为Apache软件基金会的顶级项目。
1.ShardingSphere-JDBC
定位为轻量级Java框架,在Java的JDBC层提供额外服务。它使用客户端直连数据库,以jar包形式提供服务,无须额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架,如图2-12所示。
(1)适用于任何基于JDBC的ORM框架,如JPA、Hibernate、MyBatis、Spring JDBC Template或直接使用JDBC。
(2)支持任何第三方的数据库连接池,如DBCP、C3P0、BoneCP、Druid、HikariCP等。
(3)支持任意实现JDBC规范的数据库。目前支持MySQL、Oracle、SQL Server、PostgreSQL以及任何遵循SQL92标准的数据库。
图2-12 ShardingSphere-JDBC方案
2.ShardingSphere-Proxy
如图2-13所示,定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。目前提供MySQL和PostgreSQL版本,它可以使用任何兼容MySQL/PostgreSQL协议的访问客户端(如MySQL Command Client、MySQL Workbench、Navicat等)操作数据,对DBA更加友好。
(1)向应用程序完全透明,可直接当作MySQL/PostgreSQL使用。
(2)适用于任何兼容MySQL/PostgreSQL协议的客户端。
图2-13 ShardingSphere-Proxy方案
3.ShardingSphere-Sidecar
如图2-14所示,定位为Kubernetes的云原生数据库代理,以Sidecar的形式代理所有对数据库的访问。通过无中心、零侵入的方案提供与数据库交互的啮合层,即Database Mesh,又可称为数据库网格。
图2-14 ShardingSphere-Sidecar方案
Database Mesh的关注重点在于如何将分布式的数据访问应用与数据库有机串联起来,它更加关注的是交互,是将杂乱无章的应用与数据库之间的交互进行有效的梳理。使用Database Mesh,访问数据库的应用和数据库终将形成一个巨大的网格体系,应用和数据库只需在网格体系中对号入座即可,它们都是被啮合层所治理的对象。
4.如何选择
ShardingSphere-JDBC采用无中心化架构,适用于Java开发的高性能轻量级OLTP应用。
ShardingSphere-Proxy提供静态入口以及异构语言的支持,适用于OLAP应用以及对分片数据库进行管理和运维的场景。
Apache ShardingSphere是多接入端共同组成的生态圈。通过混合使用ShardingSphere-JDBC和ShardingSphere-Proxy,并采用同一注册中心统一配置分片策略,能够灵活地搭建适用于各种场景的应用系统,使得架构师更加自由地调整适合于当前业务的最佳系统架构。
这三种方案的具体选择策略如表2-2所示。
表2-2 ShardingSphere三种方案选择表
对电商管家来说,数据库负载为典型的OLTP类型,应用采用Java语言编写,经评估最终选择了ShardingSphere-JDBC方案。
2.2.6 分布式数据库方案——通过数据库解决所有问题
虽然数据分片解决了容量问题,以及部分性能、可用性以及单点备份恢复等问题,但分散的架构在获得了收益的同时,也引入了新的挑战。
(1)分布式数据库需要具备容错处理能力。
单机是一个由底层系统包括硬件、BIOS和操作系统组成的整体。从集中式数据库角度看,对这台机器的状态无须过分关注。换句话说,在单机环境,操作系统不会向数据库软件报告有一个CPU或内存被拔出插槽,当然应用也不需要设计成在这种情况下还要继续工作。
但是,分布式数据库就需要处理部分节点失效,有以下两点原因。
① 传统集中式数据库部署的服务器通常是IBM小型机,IBM小型机可靠性高,分布式数环境所用的x86服务器可靠性稍差,需组成集群提升可靠性。
② 分布式数据库由多个节点组成,单个节点发生故障的情况时有发生。出现故障时,并没有分布式操作系统帮助处理,这是采用分布式数据库的最大挑战。
(2)运行在集中式数据库中的SQL语句,在分布式环境中不一定能够正确运行,例如,分页、排序、聚合分组等操作的不正确处理,这是第二个挑战。
(3)跨库的分布式事务也是要面对的棘手问题。在这种场景下,需要保持事务的强一致性。基于XA的分布式事务由于在并发度高的场景中性能无法满足需要,并未被大规模使用,它们大多采用最终一致性的柔性事务代替强一致事务。
上述挑战,正是分布式数据库所要解决的,分布式数据库这个概念也因此破茧而出了!
在本章前言中提到了Google的Spanner、阿里巴巴的OceanBase和PolarDB、AWS的Aurora等分布式数据库,它们的架构既有相似之处,也有很多差异,本节将给出一种新的分布数据库架构。
如图2-15所示,这是一种理想中的分布式数据库原型图。它首先是一个完备的数据库,支持分布式事务,可线性扩展,具备高可用能力,具体功能如下。
(1)数据按照某种规则(例如哈希算法)拆分到多个分片中,完成水平分库。
(2)数据分片数能够按照需求变化进行动态伸缩,支持横向与纵向扩展。
(3)每个数据分片包括一个主库和多个从库,从库越多,就意味着当少量从库数据丢失或宕机以后,整个系统对外提供的服务不受影响。
(4)取决于对一致性的要求,主库和从库之间可以采用Paxos/raft协议保证强一致性。
(5)应用程序以标准ANSI SQL语句访问中间件集群(图2-15中为DBProxy集群)。
(6)DBProxy对收到的SQL请求进行处理,将请求路由到一个或者多个数据库分片中,对分片返回的结果进行再次加工后,最终返回给应用程序。
(7)分布式数据库集群由专门的节点管理(图2-15中为manager节点),负责集群管理、元数据管理、自动化切换等功能,从而无须DBA在集群管理上耗费额外精力。
(8)分布式事务由专门的全局事务管理节点负责(图2-15中为GTM),控制分布式事务执行。
图2-15 分布式数据库原型方案
迄今为止,一个分布式数据库原型成功建立,2.3节将正式进入本章主题:金融级分布式数据库——GoldenDB。