联系
Knight's Tale » 架构

分布式 数据库表 sharding 综述

2015-08-20 08:33

数据库表 sharding 综述

数据库sharding

基本思路

  • 表多:垂直划分
  • 表不多但表的数据很多:水平划分

切分策略

先垂直后水平

  • 垂直:“聚集“,聚合
  • 水平:聚合根

举例:

  • 社交网站:根据用户区分
  • 论坛:
    • 垂直shard:用户和论坛
    • 水平shard:Form是聚合根

Tips:

  • 只读字典或变化较小的表:每个shard里维护一份,可以加速读取速度(join)
  • 同时进行垂直和水平切分时,在垂直方向上的切分将不再以“功能模块”进行划分,而是需要更加细粒度的垂直切分,而这个粒度与领域驱动设计中的“聚合”概念不谋而合,甚至可以说是完全一致,每个shard的主表正是一个聚合中的聚合根!
  • 这样切分下来你会发现数据库分被切分地过于分散了(shard的数量会比较多,但是shard里的表却不多),为了避免管理过多的数据源,充分利用每一个数据库服务器的资源,可以考虑将业务上相近,并且具有相近数据增长速率(主表数据量在同一数量级上)的两个或多个shard放到同一个数据源里,每个shard依然是独立的,它们有各自的主表,并使用各自主表ID进行散列,不同的只是它们的散列取模(即节点数量)必需是一致的。

问题

  • 事务问题
    • 使用分布式事务
      • 优点:交由数据库管理,简单有效
      • 缺点:性能代价高,特别是shard越来越多时
    • 由应用程序和数据库共同控制
      • 将一个跨多个数据库的分布式事务分拆成多个仅处 于单个数据库上面的小事务,并通过应用程序来总控 各个小事务。
      • 优点:性能上有优势
      • 缺点:需要应用程序在事务控制上做灵活设计。如果使用
        了spring的事务管理,改动起来会面临一定的困难。
  • 跨节点Join的问题
    • 只要是进行切分,跨节点Join的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。解决这一问题的普遍做法是分两次查询实现。在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。
  • 跨节点的count,order by,group by以及聚合函数问题
    • 这些是一类问题,因为它们都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作。解决方案:与解决跨节点join问题的类似,分别在各个节点上得到结果后在应用程序端进行合并。和join不同的是每个结点的查询可以并行执行,因此很多时候它的速度要比单一大表快很多。但如果结果集很大,对应用程序内存的消耗是一个问题。

拆分实施策略和示例演示

image

准备阶段

领域模型

分析阶段

  • 垂直切分
  • 水平切分
    • 垂直切分后,需要对shard内表格的数据量和增速进一步分析,以确定是否需要进行水平切分
      • 若划分到一起的表格数据增长缓慢,在产品上线后可遇见的足够长的时期内均可以由单一数据库承载,则不需要进行水平切分
      • 若划分到一起的表格数据量巨大,增速迅猛,需要进一步进行水平分割
        • 结合业务逻辑和表间关系,将当前shard划分成多个更小的shard,通常情况下,这些更小的shard每一个都只包含一个主表(将以该表ID进行散列的表)和多个与其关联或间接关联的次表。这种一个shard一张主表多张次表的状况是水平切分的必然结果。
        • 这样切分下来,shard数量就会迅速增多。如果每一个shard代表一个独立的数据库,那么管理和维护数据库将会非常麻烦,而且这些小shard往往只有两三张表,为此而建立一个新库,利用率并不高,因此,在水平切分完成后可再进行一次“反向的Merge”,即:将业务上相近,并且具有相近数据增长速率(主表数据量在同一数量级上)的两个或多个shard放到同一个数据库上,在逻辑上它们依然是独立的shard,有各自的主表,并依据各自主表的ID进行散列,不同的只是它们的散列取模(即节点数量)必需是一致的。这样,每个数据库结点上的表格数量就相对平均了。
      • 所有表格均划分到合适的shard之后,所有跨越shard的表间关联都必须打断,在书写sql时,跨shard的join、group by、order by都将被禁止,需要在应用程序层面协调解决这些问题。

实施阶段

如果项目在开发伊始就决定进行分库分表,则严格按照分析设计方案推进即可。如果是在中期架构演进中实施,除搭建实现sharding逻辑的基础设施外(关于该话题会在下篇文章中进行阐述),还需要对原有SQL逐一过滤分析,修改那些因为sharding而受到影响的sql.

全局主键生成策略

一些常见的主键生成策略

  • UUID
    • 索引慢问题
  • Sequence表:
    • nextId
    • tableName
    • 单点问题:使用master-slave
    • 访问压力问题:暂没有好的解决方案

一种极为优秀的主键生成策略

flickr

常见分库方式

无单一主键生成方式

假设分成N个 shard,那么 为每个shard的表采用 auto increase N 方式.

  • 优点:不必使用主键生成中间件
  • 缺点:
    • 由于是在数据库上进行increase控制,因此 扩展非常困难,迁移数据很坑爹
    • 无法生成连续ID

单一主键生成方式

使用某种主键中间件生成主键,通过hash取余将数据分配。

  • 优点:可以生成连续ID,在迁移时比“无单一主键生成方式”稍微简单
  • 缺点:迁移困难

单一主键生成方式 + 路由表

使用某种主键中间件生成主键,随机散列到shard里,将关联关系放到 路由表里

  • 优点:没有迁移问题
  • 缺点:路由表将成为瓶颈
    • 改进方式:将路由表读取通过分布式缓存进行控制(解决读慢的问题),但还是无法解决写的压力(多次写和数据库表大引起的写压力)

增量区间进行路由

按增量区间进行路由(如每1千万条数据或是每一个月的数据存放在一个节点上 ),虽然可以避免数据的迁移,却有可能带来“热点”问题,也就是近期系统的读写都集中在最新创建的节点上(很多系统都有此类特点:新生数据的读写频率明显高于旧有数据),从而影响了系统性能。面对这种两难的处境,Sharding扩容显得异常困难。

散列路由 + 增量区间路由

常见路由层算法实现

  • DAO层实现:大部分技术团队采用
    • 优点:快速、简单
    • 缺点:与业务代码耦合
  • 在ORM框架层实现
    • 实现O-R Mapping的前提下同时提供sharding支持
  • 在JDBC API层实现:实现难度大
  • 在介于DAO与JDBC之间的Spring数据访问封装层实现:
    • 优点:spring提供了很多方便的连接管理
  • 在应用服务器与数据库之间通过代理实现

多数据源的事务处理

  • 分布式事务:两阶段提交
  • 基于Best Efforts 1PC模式的事务
  • 事务补偿机制: pikaq