高性能数据库集群

张开发
2026/4/6 19:18:33 15 分钟阅读

分享文章

高性能数据库集群
近年来各种存储技术飞速发展但关系数据库由于其 ACID 的特性和功能强大的 SQL 查询目前还是各种业务系统中关键和核心的存储系统很多场景下高性能的设计最核心的部分就是关系数据库的设计。不管是为了满足业务发展的需要还是为了提升自己的竞争力关系数据库厂商(Oracle、DB2、MySQL 等)在优化和提升单个数据库服务器的性能方面也做了非常多的技术优化和改进。但业务发展速度和数据增长速度远远超出数据库厂商的优化速度尤其是互联网业务兴起之后海量用户加上海量数据的特点单个数据库服务器已经难以满足业务需要必须考虑数据库集群的方式来提升性能。高性能数据库集群读写分离第一种方式是“读写分离”其本质是将访问压力分散到集群中的多个节点但是没有分散存储压力第二种方式是“分库分表”既可以分散访问压力又可以分散存储压力。读写分离原理读写分离的基本原理是将数据库读写操作分散到不同的节点上读写分离的基本实现是数据库服务器搭建主从集群一主一从、一主多从都可以。数据库主机负责读写操作从机只负责读操作。数据库主机通过复制将数据同步到从机每台数据库服务器都存储了所有的业务数据。业务服务器将写操作发给数据库主机将读操作发给数据库从机。需要注意的是这里用的是“主从集群”而不是“主备集群”。“从机”的“从”可以理解为“仆从”仆从是要帮主人干活的“从机”是需要提供读数据的功能的而“备机”一般被认为仅仅提供备份功能不提供访问功能。所以使用“主从”还是“主备”是要看场景的这两个词并不是完全等同的。读写分离的实现逻辑并不复杂但有两个细节点将引入设计复杂度主从复制延迟和分配机制。复制延迟以 MySQL 为例主从复制延迟可能达到 1 秒如果有大量数据同步延迟 1 分钟也是有可能的。主从复制延迟会带来一个问题如果业务服务器将数据写入到数据库主服务器后立刻(1 秒内)进行读取此时读操作访问的是从机主机还没有将数据复制过来到从机读取数据是读不到最新数据的业务上就可能出现问题。例如用户刚注册完后立刻登录业务服务器会提示他“你还没有注册”而用户明明刚才已经注册成功了。解决主从复制延迟有几种常见的方法1. 写操作后的读操作指定发给数据库主服务器例如注册账号完成后登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定对业务的侵入和影响较大如果哪个新来的程序员不知道这样写代码就会导致一个 bug。2. 读从机失败后再读一次主机这就是通常所说的“二次读取”二次读取和业务无绑定只需要对底层数据库访问的 API 进行封装即可实现代价较小不足之处在于如果有很多二次读取将大大增加主机的读操作压力。例如黑客暴力破解账号会导致大量的二次读取操作主机可能顶不住读操作的压力从而崩溃。3. 关键业务读写操作全部指向主机非关键业务采用读写分离例如对于一个用户管理系统来说注册 登录的业务读写操作全部访问主机用户的介绍、爱好、等级等业务可以采用读写分离因为即使用户改了自己的自我介绍在查询时却看到了自我介绍还是旧的业务影响与不能登录相比就小很多还可以忍受。分配机制将读写操作区分开来然后访问不同的数据库服务器一般有两种方式程序代码封装和中间件封装。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 的主要功能有读写分离、故障自动切换、负载均衡、连接池等其基本架构如下360 公司也开源了自己的数据库中间件 AtlasAtlas 是基于 MySQL Proxy 实现的基本架构如下Atlas 是一个位于应用程序与 MySQL 之间中间件。在后端 DB 看来Atlas 相当于连接它的客户端在前端应用看来Atlas 相当于一个 DB。Atlas 作为服务端与应用程序通信它实现了 MySQL 的客户端和服务端协议同时作为客户端与 MySQL 通信。它对应用程序屏蔽了 DB 的细节同时为了降低 MySQL 负担它还维护了连接池。高性能数据库集群分库分表读写分离分散了数据库读写操作的压力但没有分散存储压力当数据量达到千万甚至上亿条的时候单台数据库服务器的存储能力会成为系统的瓶颈主要体现在这几个方面数据量太大读写的性能会下降即使有索引索引也会变得很大性能同样会下降。数据文件会变得很大数据库备份和恢复需要耗费很长时间。数据文件越大极端情况下丢失数据的风险越高(例如机房火灾导致数据库主备机都发生故障)。基于上述原因单个数据库服务器存储的数据量不能太大需要控制在一定的范围内。为了满足业务数据存储的需求就需要将存储分散到多台数据库服务器上。绍常见的分散存储的方法“分库分表”其中包括“分库”和“分表”两大类。业务分库业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如一个简单的电商网站包括用户、商品、订单三个业务模块我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上而不是将所有数据都放在一台数据库服务器上。虽然业务分库能够分散存储和访问压力但同时也带来了新的问题1.join 操作问题业务分库后原本在同一个数据库中的表分散到不同数据库中导致无法使用 SQL 的 join 查询。例如“查询购买了化妆品的用户中女性用户的列表”这个功能虽然订单数据中有用户的 ID 信息但是用户的性别数据在用户数据库中如果在同一个库中简单的 join 查询就能完成但现在数据分散在两个不同的数据库中无法做 join 查询只能采取先从订单数据库中查询购买了化妆品的用户 ID 列表然后再到用户数据库中查询这批用户 ID 中的女性用户列表这样实现就比简单的 join 查询要复杂一些。2. 事务问题原本在同一个数据库中不同的表可以在同一个事务中修改业务分库后表分散到不同的数据库中无法通过事务统一修改。虽然数据库厂商提供了一些分布式事务的解决方案(例如MySQL 的 XA)但性能实在太低与高性能存储的目标是相违背的3. 成本问题业务分库同时也带来了成本的代价本来 1 台服务器搞定的事情现在要 3 台如果考虑备份那就是 2 台变成了 6 台。基于上述原因对于小公司初创业务并不建议一开始就这样拆分主要有几个原因初创业务存在很大的不确定性业务不一定能发展起来业务开始的时候并没有真正的存储和访问压力业务分库并不能为业务带来价值。业务分库后表之间的 join 查询、数据库事务无法简单实现了。业务分库后因为不同的数据要读写不同的数据库代码中需要增加根据数据类型映射到不同数据库的逻辑增加了工作量。而业务初创期间最重要的是快速实现、快速验证业务分库会拖慢业务节奏。分表将不同业务数据分散存储到不同的数据库服务器能够支撑百万甚至千万用户规模的业务但如果业务继续发展同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。此时就需要对单表数据进行拆分。单表数据拆分有两种方式垂直分表和水平分表。示意图如下从上往下切就是垂直切分表的切分就是表记录数相同但包含不同的列。例如示意图中的垂直切分会把表切分为两个表一个表包含 ID、 name、age、sex 列另外一个表包含 ID、nickname、description 列。从左往右切就是水平切分表的切分就是表的列相同但包含不同的行数据。例如示意图中的水平切分会把表分为两个表两个表都包含 ID、 name、age、sex、nickname、description 列但是一个表包含的是 ID 从 1 到 999999 的行数据另一个表包含的是 ID 从 1000000 到 9999999 的行数据。单表进行切分后是否要将切分后的多个表分散在不同的数据库服务器中可以根据实际的切分效果来确定并不强制要求单表切分为多表后一定要分散到不同数据库中。原因在于单表切分为多表后新的表即使在同一个数据库服务器中也可能带来可观的性能提升如果性能能够满足业务要求是可以不拆分到多台数据库服务器的毕竟我们在上面业务分库的内容看到业务分库也会引入很多复杂性的问题如果单表拆分为多表后单台服务器依然无法满足性能要求那就不得不再次进行业务分库的设计了。分表能够有效地分散存储压力和带来性能提升但和分库一样也会引入各种复杂性。1. 垂直分表 垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如前面示意图中的 nickname 和 description 字段假设用户在筛选其他用户的时候主要是用 age 和 sex 两个字段进行查询而 nickname 和 description 两个字段主要用于展示一般不会在业务查询中用到。description 本身又比较长因此我们可以将这两个字段独立到另外一张表中这样在查询 age 和 sex 时就能带来一定的性能提升。垂直分表引入的复杂性主要体现在表操作的数量要增加。例如原来只要一次查询就可以获取 name、age、sex、nickname、description现在需要两次查询一次查询获取 name、age、sex另外一次查询获取 nickname、description。不过相比接下来要讲的水平分表这个复杂性就是小巫见大巫了。2. 水平分表 水平分表适合表行数特别大的表有的公司要求单表行数超过 5000 万就必须进行分表这个数字可以作为参考但并不是绝对标准关键还是要看表的访问性能。对于一些比较复杂的表可能超过 1000 万就要分表了而对于一些简单的表即使存储数据超过 1 亿行也可以不分表。但不管怎样当看到表的数据量达到千万级别时作为架构师就要警觉起来因为这很可能是架构的性能瓶颈或者隐患。水平分表相比垂直分表会引入更多的复杂性主要表现在下面几个方面路由 水平分表后某条数据具体属于哪个切分后的子表需要增加路由算法进行计算这个算法会引入一定的复杂性。常见的路由算法有范围路由选取有序的数据列(例如整形、时间戳等)作为路由的条件不同分段分散到不同的数据库表中。范围路由设计的复杂点主要体现在分段大小的选取上分段太小会导致切分后子表数量过多增加维护复杂度分段太大可能会导致单表依然存在性能问题一般建议分段大小在 100 万至 2000 万之间具体需要根据业务选取合适的分段大小。范围路由的优点是可以随着数据的增加平滑地扩充新的表。范围路由的一个比较隐含的缺点是分布不均匀Hash 路由选取某个列(或者某几个列组合也可以)的值进行 Hash 运算然后根据 Hash 结果分散到不同的数据库表中。Hash 路由设计的复杂点主要体现在初始表数量的选取上表数量太多维护比较麻烦表数量太少又可能导致单表性能存在问题。Hash 路由的优缺点和范围路由基本相反Hash 路由的优点是表分布比较均匀缺点是扩充新的表很麻烦所有数据都要重分布。配置路由配置路由就是路由表用一张独立的表来记录路由信息。同样以用户 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 才能得到最终的结果。上一章: 架构设计流程下一章: 高性能NoSQL归类: 从0开始学架构

更多文章