跳至主要內容

MySQL-高可用篇-⑧分布式数据库

holic-x...大约 18 分钟JAVAMySQL

MySQL-高可用篇-⑧分布式数据库

学习核心

  • 分布式数据库概念核心
  • 选择什么字段进行分片
  • 如何保证唯一键全局唯一
  • 开源的分布式中间件

学习资料

分布式数据库架构

​ 对比之前学习的数据库,数据都是存放在一个实例对应的物理存储上,而在分布式数据库中,数据将存放在不同的数据库实例上

image-20240715190219005

  • 计算层:之前单机数据库中的 SQL 层,用来对数据访问进行权限检查、路由访问,以及对计算结果等操作。
  • 元数据层:记录了分布式数据库集群下有多少个存储节点,对应 IP、端口等元数据信息是多少。当分布式数据库的计算层启动时,会先访问元数据层,获取所有集群信息,才能正确进行 SQL 的解析和路由等工作。另外,因为元数据信息存放在元数据层,那么分布式数据库的计算层可以有多个,用于实现性能的扩展。
  • 存储层:用来存放数据,但存储层要和计算层在同一台服务器上,甚至不求在同一个进程中。

​ 分布式数据库的优势是把数据打散到不同的服务器上,这种横向扩展的 Scale Out 能力,能解决单机数据库的性能与存储瓶颈。从理论上来看,分布式数据库的性能可以随着计算层和存储层的扩展,做到性能的线性提升。

​ 从可用性的角度看,如果存储层发生宕机,那么只会影响 1/N 的数据,N 取决于数据被打散到多少台服务器上。所以,分布式数据库的可用性对比单机会有很大提升,单机数据库要实现99.999% 的可用性或许很难,但是分布式数据库就容易多了。

​ 当然,分布式数据库也存在缺点:正因为数据被打散了,分布式数据库会引入很多新问题,比如自增实现、索引设计、分布式事务

image-20240715191153244

​ 在分布式 MySQL 架构下,客户端不再是访问 MySQL 数据库本身,而是访问一个分布式中间件。这个分布式中间件的通信协议依然采用 MySQL 通信协议(因为原先客户端是如何访问的MySQL 的,现在就如何访问分布式中间件)。分布式中间件会根据元数据信息,自动将用户请求路由到下面的 MySQL 分片中,从而将存储存取到指定的节点。

​ 分布式 MySQL 数据库架构的每一层都要由高可用,保证分布式数据库架构的高可用性。对于上层的分布式中间件,是可以平行扩展的:即用户可以访问多个分布式中间件,如果其中一个中间件发生宕机,那么直接剔除即可。

​ 因为分布式中间件是无状态的,数据保存在元数据服务中,它的高可用设计比较容易。对于元数据来说,虽然它的数据量不大,但数据非常关键,一旦宕机则可能导致中间件无法工作,所以,元数据要通过副本技术保障高可用。

​ 每个分片存储本身都有副本,通过高可用技术保证分片的可用性。即如果分片 1 的 MySQL 发生宕机,分片 1 的从服务器会接替原先的 MySQL 主服务器,继续提供服务。由于使用了分布式架构,那么即使分片 1 发生宕机,需要 60 秒的时间恢复,这段时间对于业务的访问来说,只影响了 1/N 的数据请求。

​ 分布式 MySQL 数据库架构实现了计算层与存储层的分离,每一层都可以进行 Scale Out 平行扩展,每一层又通过高可用技术,保证了计算层与存储层的连续性,大大提升了MySQL 数据库的性能和可靠性,为海量互联网业务服务打下了坚实的基础。

分布式 MySQL 架构通过一个中间件路由层屏蔽了下层 MySQL 分片的信息。由于分布式中间件通信采用 MySQL 通信协议,用户原先怎么使用 MySQL 数据库,那就怎么使用分布式中间件。对于开发来说,这些都是透明的,他们不用关心下层有多少个分片,所有的路由和计算工作,交由中间件层完成。

分布式数据库:如何正确进行数据分片?

​ 分布式数据库是把数据打散存储在一个个分片中。在基于MySQL 的分布式数据库架构中,分片就存在于 MySQL 实例中。在分布式数据库中,学会正确地把数据分片,充分发挥分布式数据库架构的优势。(可以参考前面分库分表中对分片键的基础介绍)

  • 选择合适的分片键(业务经常访问的字段,且业务之间的表大多能根据这个分片键进行单元化),进行分布式数据库的改造
  • 选择合适的分片算法(Range、Hash算法等),尽量避免热点问题
  • 随后进行分库分表设计,推荐使用不同库名、不同表名的设计,以便于后续对分片数据进行扩缩容
  • 扩容的本质是复制,实际进行扩容时,可以使用过滤复制,仅复制需要的分片数据。缩容可以理解为扩容的逆向操作

数据分片

​ 分布式数据库架构设计的原则是:选择一个适合的分片键和分片算法,把数据打散,并且业务的绝大部分查询都是根据分片键进行访问

​ 为什么互联网业务这么适合进行分布式架构的设计呢?因为互联网业务大部分是 To C 业务,分片键就是用户的 ID,业务的大部分访问都是根据用户 ID 进行查询,比如:

  • 查看某个用户下的微博/短视频;
  • 查看某个用户的商品信息/购买记录;
  • 查看某个用户自己的余额信息;

​ 分片的本质是一张张表,而不是数据库实例,严格来说:分片 = 实例 + 库 + 表 = ip@port:db_name:table_name。针对分库分表场景,一般建议每个分片的库名和表名都不一样,且满足以下条件:

  • 不同分片的数据可以在同一 MySQL 数据库实例上,便于做容量的规划和后期的扩展;
  • 同一分片键的表都在同一库下,方便做整体数据的迁移和扩容;

image-20240715193946547

按上述分布式设计,数据分片完成后,所有的库表依然是在同一个 MySQL实例上。牢记,分布式数据库并不一定要求有很多个实例,最基本的要求是将数据进行打散分片。用户可以根据自己的需要,进行扩缩容,以此实现数据库性能和容量的伸缩性,这才是分布式数据库真正的魅力所在

扩缩容

​ 对于上述的分布式数据库架构,一开始将 4 个分片数据存储在一个 MySQL 实例上,但是如果遇到一些大促活动,可以对其进行扩容,比如把 4 个分片扩容到 4 个MySQL实例上。如果完成了大促活动,又可以对资源进行回收,将分片又都放到一台 MySQL 实例上,这就是对资源进行缩容。

​ 总的来说,对分布式数据库进行扩缩容在互联网公司是一件常见的操作,比如对阿里来说,每年下半年 7 月开始,他们就要进行双 11 活动的容量评估,然后根据评估结果规划数据库的扩容。一般来说,电商的双 11 活动后,还有双 12、新年、春节,所以一般会持续到过完年再对数据库进行缩容。

​ 在 HASH 分片的例子中,把数据分片到了 4 个节点,然而在生产环境中,为了方便之后的扩缩容操作,推荐一开始就把分片的数量设置为不少于 1000 个。

不用担心分片数量太多,因为分片 1 个还是 1000 个,管理方式都是一样的,但是 1000 个,意味着可以扩容到 1000 个实例上,对于一般业务来说,1000 个实例足够满足业务的需求了(BTW,网传阿里某核心业务的分布式数据库分片数量为 10000个)。

​ 如果到了 1000 个分片依然无法满足业务的需求,这时能不能拆成 2000 个分片呢?从理论上来说是可以的,但是这意味着需要对一张表中的数据进行逻辑拆分,这个工作非常复杂,通常不推荐。所以,一开始一定要设计足够多的分片。如果一开始没有考虑好分片数量,每次经历这样的拆分工作,都是扒一层皮,反而太不值得。

​ MySQL扩容的本质是搭建一个复制架构,然后通过设置过滤复制,仅回放分片所在的数据库就行,这个数据库配置在从服务器上大致进行如下配置:

# 分片1从服务器配置
replicate_do_db ="tpch01"

​ 所以在进行扩容时,首先根据下图的方式对扩容的分片进行过滤复制的配置

image-20240715194545790

​ 然后再找一个业务低峰期,将业务的请求转向新的分片,完成最终的扩容操作

image-20240715194610910

​ 而对于缩容而言,本质上是扩容的逆向操作

分布式数据库:如何正确设计索引?

1.主键选择

​ 对主键来说,要保证在所有分片中都唯一,它本质上就是一个全局唯一的索引。如果单纯使用自增作为主键,就会发现存在很大的问题。

​ 因为自增并不能在插入前就获得值,而是要通过填 NULL 值,然后再通过函数 last_insert_id()获得自增的值。所以,如果在每个分片上通过自增去实现主键,可能会出现同样的自增值存在于不同的分片上。

所以,在分布式数据库架构下,尽量不要用自增作为表的主键,而是用自定义全局唯一的键作为主键,比如 MySQL 自动生成的有序 UUID;业务生成的全局唯一键(比如发号器);或者是开源的 UUID 生成算法,比如雪花算法(但是存在时间回溯的问题)。

​ 总之,用有序的全局唯一替代自增,是这个时代数据库主键的主流设计标准

2.索引设计

​ 此处的索引设计可以理解为如何避免跨区检索(或者是提升跨区检索的效率)。实际业务场景中可以根据分片键快速定位数据所在分区,但是如果检索条件不是分片键而是其他,那么就会面临一个跨区检索的问题(涉及检索效率、检索结果合并等多个问题),如果分区很多那么SQL就会被执行很多次。最基础的思路就是对这个非分片键再做一次分库分表操作,将其转化为”分片键“(这种方式成本太高且赘余)。常见的解决思路有两种:

  • 思路1:索引法,可以通过构建一张索引表存储检索条件和对应分片键的关联信息,通过这个”索引关系“可以快速定位数据所在分区,进而避免检索每个分区;如果后期这张所以表很大,也要考虑对其进行分库分表以优化索引性能
  • 思路2:基因法,可以将分片键的关键信息(确保唯一)作为检索字段的一部分进行存储(例如orderId中后几位存储userId信息等),在检索非分片键的时候则可通过拆解字段获取到分片键信息,从而进一步定位分区数据信息

​ 上述场景是针对唯一的二级索引查询设计,如果是非唯一的二级索引查询则仍然需要检索所有分片才能获取结果。但需要注意的是,分布式数据库架构设计的要求是业务的绝大部分请求能够根据分片键定位到 1 个分片上。如果业务大部分请求都需要扫描所有分片信息才能获得最终结果,反而脱离了概念本身,那么就不适合进行分布式架构的改造或设计

3.全局表

​ 在分布式数据库中,有时会有一些无法提供分片键的表,但这些表又非常小,一般用于保存一些全局信息,平时更新也较少,绝大多数场景仅用于查询操作。

​ 例如 tpch 库中的表 nation,用于存储国家信息,但是在我们前面的 SQL 关联查询中,又经常会使用到这张表,对于这种全局表,可以在每个分片中存储,这样就不用跨分片地进行查询了。

适当冗余表或者数据字段,可以有效避免跨分区检索的痛点

分布式数据库架构选型:分库分表 or 中间件?

​ 分布式数据库的分片设计、表结构设计、索引设计等,基于此可以构建一个分布式数据库系统。数据分好了,索引也设计好了,接下来就是思考该如何访问这些数据和索引呢?访问分布式数据库有两种模式:

  • 业务直接根据分库分表访问 MySQL 数据库节点;
  • 根据中间件访问;

1.分库分表直接访问

​ 在设计分片时,已经明确了每张表的分片键信息,所以业务或服务可以直接根据分片键对应的数据库信息,直接访问底层的 MySQL 数据节点,比如在代码里可以做类似的处理:

void InsertOrders(String orderKey, int userKey...) {
  int shard_id = userKey % 4;
  if (shard_id == 0) {
    conn = MySQLConncetion('shard1',...);
    conn.query(...);
  } else if (shard_id == 1) {
    conn = MySQLConncetion('shard2',...);
    conn.query(...);   
  } else if (shard_id == 2) {
    conn = MySQLConncetion('shard3',...);
    conn.query(...);   
  } else if (shard_id == 3) {
    conn = MySQLConncetion('shard4',...);
    conn.query(...);   
  }
}

这种方式是通过在业务代码中会嵌入分库分表的路由逻辑,在业务层计算出对应分片的信息,然后访问数据库:

  • 优点在于其实现与单实例数据库没有太大的不同,只是多了一次计算分片的操作,没有额外的开销,性能非常好(参考支付宝的分布式数据库为了最求极致的性能,用的就是直接访问分片的方式)。
  • 缺点在于业务需要知道分片信息,感知分片的变化(例如上述案例中如果分片 shard1 发生变化,又或者进行了扩容,业务就需要跟着修改)

​ 为了解决这个缺点,比较好的处理方式是使用名字服务,而不要直接通过 IP 访问分片。这样当分片发生切换,又或者扩容缩容时,业务也不需要进行很大的改动。又因为业务比较多,需要访问分布式数据库分片逻辑的地方也比较多。所以,可以把分片信息存储在缓存中,当业务启动时,自动加载分片信息。比如,在 Memcached 或 Redis 中保存如下的分片信息,key 可以是分库分表的表名,value通过 JSON 或字典的方式存放分片信息:

{

  'key': 'orders',
  'shard_info' : {
    'shard_key' : 'o_custkey',
    'shard_count' : 4'shard_host' : ['shard1.xxx.com','shard2.xxx.com','...']'shard_table' : ['tpch00/orders01','tpch01/orders02','...'],
  }
}

​ 如果要进行跨分片的访问,则需要业务自己处理相关逻辑。但还是需要理解的是分布式数据库设计要求单元化,绝大部分操作需要在一个分片中完成。如果不能,那么可能都不推荐分布数据库的改造。总之,分库分表的直接访问方式,要求业务控制一切有关分布式数据库的操作,需要明确每个分片的具体信息,做好全流程的把控。

2.中间件

​ 另一种比较流行的分布式数据库访问方式是通过分布式数据库中间件。数据库中间件本身模拟成一个 MySQL 数据库,通信协议也都遵循 MySQL 协议:业务之前怎么访问MySQL数据库的,就如何访问MySQL分布式数据库中间件。其优点在于业务不用关注分布式数据库中的分片信息,把它默认为一个单机数据库使用。

​ 通过分布式 MySQL 中间件,用户只需要访问中间件就行,下面的数据路由、分布式事务的实现等操作全部交由中间件完成。所以,分布式数据库中间件变成一个非常关键的核心组件。

​ 业界比较知名的 MySQL 分布式数据库中间件产品有:ShardingShpere、DBLE、TDSQL 等。

​ ShardingSphere于 2020 年 4 月 16 日成为 Apache 软件基金会的顶级项目、社区熟度、功能支持较多,特别是对于分布式事务的支持,有多种选择(ShardingSphere 官网地址open in new window)。

DBLE open in new window是由知名 MySQL 服务商爱可生公司开源的 MySQL 中间件产品,已用于四大行核心业务,完美支撑传统银行去 IOE,转型分布式架构的探索。除了中间件技术外,爱可生公司还有很多关于 MySQL 数据库、分布式数据库设计等方面的综合经验。

​ TDSQL MySQL 版(TDSQL for MySQLopen in new window)是腾讯打造的一款分布式数据库产品,具备强一致高可用、全球部署架构、分布式水平扩展、高性能、企业级安全等特性,同时提供智能 DBA、自动化运营、监控告警等配套设施,为客户提供完整的分布式数据库解决方案。

​ 目前 TDSQL 已经为超过500+的政企和金融机构提供数据库的公有云及私有云服务,客户覆盖银行、保险、证券、互联网金融、计费、第三方支付、物联网、互联网+、政务等领域。TDSQL MySQL 版亦凭借其高质量的产品及服务,获得了多项国际和国家认证,得到了客户及行业的一致认可。

​ 需要注意的是,使用数据库中间件虽好,但其存在一个明显的缺点,即多了一层中间层的访问,单个事务的访问耗时会有上升,对于性能敏感的业务来说,需要有这方面的意识和考虑。重要的一点是,虽然使用分布式数据库中间件后,单个事务的耗时会有所上升,但整体的吞吐率是不变的,通过增大并发数,可以有效提升分布式数据库的整体性能。

3.如何选型

​ 选择业务直连分布式数据库?还是通过数据库中间件访问?这是一个架构选型要考虑的问题。

​ 对于较小业务(高峰期每秒事务不超过 1000 的业务),选择通过数据库中间件访问分布式数据库是比较优的方式。因为这样的业务通常处于爬升期,满足业务的各项功能或许是业务的主要目标。通过分布式中间件屏蔽下面的分片信息,可以让开发人员专注于业务的开发。

​ 另一方面,通过使用中间件提供的分布式事务就能满足简单的跨分片交易,解决分布式数据库中最难的问题。

​ 但如果你的业务是一个海量互联网业务,中间件的瓶颈就会显现,单个事务的耗时会上升,低并发下,性能会有一定下降。而且中间件提供的 2PC 分布式事务性能就更不能满足业务的需求了。所以类似支付宝、阿里这样的业务,并没有使用分布式数据库中间件的架构,而是采用了业务直连的模式。

​ 可能实际应用中会考虑,如果不用数据库中间件,该怎么解决 JOIN 这些问题呢?如果直接通过业务层去实现还是很麻烦的。但换个角度思考,中间件虽然可以完成这部分的功能。但如果真是数据量比较大,跨分片的场景,中间件也不能满足要求。

​ 所以,使用分布式数据库架构是一种折中,有取必有舍,要学会放弃很多,从而才能得到更多。

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3