数据分片
通过数据分片的方式可以实现分布式的水平扩容,一般的数据库都是通过 key hash 的方式来切分数据,比如 Redis 引入 Slot 的概念来实现数据库的水平扩展。Laser 是通过数据分片 (Shard)的方式 实现水平扩展。
分片方式
说到数据分片最简单的暴力的办法就是首先将请求的 key 进行 hash 计算,然后将 hash 值对机器个数取模,最终得到当前操作的 key 所在的机器节点。通过这种方式进行的数据分布存在一个最大的问题就是 当机器节点个数发生变化的时候所有的 key 都需要重新计算迁移,这对于有状态的数据库来说是致命的。为此我们引入了分片的概念,在每个 Laser 集群创建的时候给定一个固定的分片数,上层对 Key 进行 Hash 计算后 ,最终通过将 Hash 值对分片数取模,得到 Shard Id, 每个集群会维护一个 ShardId 对应 NodeId 的映射关系,通过 ShardId 在这个映射关系中查找对应的最终的 NodeId. 通过这种方式如果真是的服务节点发生 变化只需要修改调整 ShardId 与 NodeId 之间的映射关系即可,对应调整影响的 Shard 做对应的数据即可,不需要全量数据重新 Hash 并且做数据迁移。
通过将上层 Key 通过 Hash 的方式映射到一个 Shard 中,对于运维人员来说无需关注上游数据表业务逻辑,仅需要关注服务节点与 Shard 的映射即可,大大的降低了 Laser 分布式的运维成本,关于扩容缩容后续会详细介绍
上述介绍的分片方式仅仅是一个最基础的分片方式,前面数据模型中介绍 Laser 是一个支持多租户的数据库,用户可以根据不同的业务需求创建不同的数据库、数据表。 并且每个数据表为了在业务上具有弹性支持配置不同的分区,在逻辑上要比单层的数据库复制,对于单层没有多租户概念的情况下采用上述分片的方式即可,对于有多租户的概念 Laser 在上述分片基础上做了调整,首先通过数据库 、数据表以及 Key 整体进行 hash,将 Hash 值对当前数据表分区数取模得到当前数据表分区ID(Partition ID), 然后通过数据库、数据表、数据分区ID 再次 Hash, 通过对该 Hash 值对集群分片数取模最终得到当前操作 Key 的分片。
Laser 中分片(Shard)和分区(Partition)的概念可能大家容易混淆,分片在 Laser 中指的是整体集群的某个部分,它会和服务节点做映射。而分区是当前集群某个数据表的逻辑分区,最终一个分片里可能会映射多个不同表的不同分区。 分片的作用是主要是实现水平扩展,在集群扩容缩容的时候用关注,专门针对集群运维人员的,而表分区是一个逻辑上的概念,和数据表相关,每个数据表分区在 Laser 中会一一对应一个真实的 RocksDB 实例,分区的概念和 开发人员是息息相关的概念。比如当前业务写请求高的情况下在创建数据表的时候就可以多指定一些分区。最终分片的方式如图:
假设在一个有 10 个分片的集群,有 test_db 的数据库,test_db 下创建了 test_table 的数据表,并且 test_table 有 10 个数据表分区, 并且在集群创建的时候已经配置好分片与机器节点的配置:
ShardNumber
配置整体集群的分片数, 用作后续分片计算,假设不考虑多副本容灾的情况,当前只有一个集群组 aliyun
,当前副本是所有的分片都是 Leader 角色,从配置可以看出一共有两个服务节点,分别是 1 号节点映射
对于分片 0-4 号,2号服务节点对应 5-9号分片。当服务节点启动的时候会将当前节点服务的分片信息注册到注册中心,具体格式如下:
注册到注册中心的服务信息有 FollowerAvailableShardList
和 AvailableShardList
分别代表该节点对外可提供服务的 Follower 角色节点和 Leader 角色节点, 当知道 ShardId 后以及要请求 Shard 的角色
即可知道对应要和那个服务节点交互
在当前集群下当我们要操作获取一下 test_table 下的 test_key 这个 Key 的 Value, 需要如下几步:
- 计算当前 Key 对应的表分区(PartitionId)
- 通过表分区 ID 计算所属集群的数据分片(ShardId)
- 最终通过 ShardId 通过服务注册发现找到最终的 Ip Port
实现伪代码如下:
多副本容灾
一般的数据库都是通过主从的方式实现数据的高可用,Laser 同样也是采用一主多从的方式实现数据的高可用,主从角色对于 Laser 来说不是以服务节点为单位的,而是以数据分片(Shard)单位的。 以 Shard 位主从单位的好处就是可以更加灵活的对整体集群弹性部署,对于自动容灾、动态扩容缩容的时候调整主从角色也更加灵活方便,不过以 Shard 为主从单位的缺点是 在分片 Shard 的时候可能会出现将一对主从分片分配到同一个服务节点的情况,如果当前服务节点故障会出现整个分片不可服务的情况,这个情况对于后续运维分配 Shard 带来挑战。
Laser 为了彻底的避免将同一个分片的主从两个分片分配到同一个服务节点上,在整个集群设计的时候引入了集群组概念, 集群创建时规定每个集群组至少拥有整个集群的一个数据副本,在集群组与对应服务节点映射时,保证 不同的集群组映射不同的物理服务节点,每个集群有且只有一个主分片副本。每个分片可以有 Leader/Follower 两个角色选择,角色直接可以实时热切换。
假设在集群创建的时候有 1024 个数据分片,为了保证数据的高可用,至少创建两个副本,一个副本是 1024 个主分片, 另外一个副本是 1024 个从分片,每个副本映射一个集群组,每个集群组对应不同的物理机。
平滑升级
当集群进行升级或者调整的时候会出现短暂无法对外服务的情况,由于在线存储数据库一般是业务最底层,当集群抖动对上游的业务会造成很大的影响,为了避免服务升级或者调整频繁造成服务抖动甚至不可用的情况,Laser 实现了平滑升级。
平滑升级的基础要素是整个集群有多个对外服务的副本, 在升级或者调整服务节点之前,首先将当前的服务节点对外服务状态修改为 unavailable, 此时该服务节点就会从服务发现的对外服务列表中摘除,继续持续的间隔 查询当前服务节点的 QPS, 当 QPS 为零时即认为没有任何请求,此时再进行对应的升级和服务调整。需要注意的是一次性不能将所有的副本都 disable 掉。要保证线上服务平稳运行,建议一次最多 disable 一个副本进行升级或者 调整.
当服务升级重启后服务节点会再次进行服务注册,并且维持固定的心跳。此时就需要将服务状态自动改为 unavailable,否则在服务启动的时候会有新的请求进来,在数据库启动阶段服务需要从磁盘上加载数据,此时系统的 IO wait time 特别长,对外服务的性能特别差, 就会影响线上服务的质量, 当服务完成启动后再讲状态修改为 available, 此时当前的服务节点就会重新加入到服务发现的列表中。
Laser 整体的集群中一般由多个副本组成,但是只有一个副本是 Leader 角色的分片。为了使整体集群数据最终一致性,Laser 仅仅运行 Leader 角色的分片才有写的权限。对于 Leader 角色的分片一个集群中有且只有一个, 当要升级或调整的服务节点中存在 Leader 角色的分片时,是不可以直接升级调整的,需要将所有的 Leader 角色的分片都转化为 Follower 角色,与此同时其他服务节点对应的 Follower 角色的分片变为 Leader 角色可以继续 接受写类型的操作,转化为 Follower 角色后按照上面的平滑升级流程进行升级调整,当升级调整完成后再切换回来。
扩容缩容
在分布式系统服务节点弹性扩容缩容是比较常见的运维操作需求,如何可以低成本安全的进行扩容缩容一直贯穿整个 Laser 开发过程都在思考改进。对于扩容缩容一般有两种情况,一种是在某一个集群组里面扩容一个或多个服务节点,另外 一种是在整个集群中扩容一个新的集群组,意味着会有一个全新的副本产生。对于任何一种方式的扩容缩容操作的前提都是当前服务节点分片角色都是 Follower, 扩容缩容有 Leader 分片的节点都需要先转化为 Follower 操作,等操作 完成再切换回来。
集群组里扩容:
由于某个集群组里的分片数是固定的,即集群创建时指定的分片数,如果在集群中扩容一个或多个服务节点,其操作的基本原理就是将现有服务节点上服务的分片重新分配到新增节点上即可完成扩容。新扩容的机器在启动的时候 默认是不可能服务的状态,启动后新扩容节点会自动同步数据,待数据同步完成后可以将当前节点服务状态改为服务状态,并且将扩容从其他旧节点上分担的分片在旧节点上将服务状态改为不可服务的状态,注意此处的服务状态不是 整体服务节点的服务状态,而是该节点中不在需要的分片的服务状态
集群组里缩容:
缩容和扩容其实类似,都是当前集群组里的分片在多个服务节点上的迁移动作。假设要缩容一个或多个服务节点,首先要将这些服务节点上的服务分片都重新分配到其余的节点上,不过分配到其余节点的分片暂时的服务状态是不可用的, 从新分片完成后就开始数据迁移,当数据迁移完成后,缩容的服务节点的服务状态可以改为不可用。至此就完成一次缩容操作。
扩容集群组:
使用多个服务节点扩容一个新的集群组,以为新的集群组要将现有的所有的数据进行一次全复制,扩容集群组要比在一个集群组内扩容方便,就在在扩容服务启动的时候设置服务为不可用,待数据同步完成后修改服务状态为可用即可完成一次 集群组扩容, 不过对于 Laser 一主多从的分片架构来说,如果太多的服务副本不仅造成数据存储空间浪费,而且会导致服务 Leader 分片的对应服务节点的服务负载偏高。一般 Leader 实践中是每个机房一个副本,也就是每个机房一个 集群组,这样既可以做到多机房容灾,又可以达到分流上游读请求的作用。
缩容集群组:
在应对流量高峰过后可能需要对整体集群进行副本的缩容,对于集群组缩容一般在可以承接住上游流量的情况下,可以直接将当前集群组的服务节点服务状态改为不可用即可完成集群组缩容。不过如果缩容的集群组正好有存在 Leader 角色分片的 服务节点,需要将当前 Shard Leader 角色切换到其他集群组里,然后再操作。
上述的扩容缩容步骤都是由 Leader Control 自动化完成。对于运维来说仅需要执行对应的操作即可,另外一个需要关注的就是运维要时刻关注集群中每个分片的副本数及其同步情况
自动容灾
对于整个集群有固定分片的一定的副本组成,每个分片有一个副本是 Leader 角色,其余的都是 Follower 角色,多个副本之间的数据同步都是从 Leader 角色的分片副本上同步数据。所以对于分片纬度来说是一主多从的架构,j 对于 Laser 来说只有 Leader 角色的分片副本才可以接收写类型的操作,对于 Leader 角色的分片副本来说对于整个集群其实是单点。如果某个 Leader 角色副本所在服务节点故障了,当前分片的写操作就会失败。为了避免出现 类似的情况 Laser 实现了自动容灾功能,假设某个服务节点故障,首先会判断该节点是否有 Leader 角色的分片,如果有的话自动将该分片对应的 Follower 分片副本中同步数据最多的一个副本切换为 Leader 分片。最终永远保证 整个集群中每个分片都有一个对应的 Leader 角色的副本存在。