分布式KV存储系统的设计清单(二)

架构

  这一篇我们来聊一聊构建分布式KV系统的一般套路——架构。我们的设计目标提供一个能够在大规模数据集上提供随机Read,Write。及Scan的系统。并且能够线性扩展。结合LSM引擎的优点,通过分布式技术实现高可扩展性和可用性。先看下系统的约束。

架构概览 系统中有如下的一些节点是必须的。

可用性 跟大多数分布式存储系统一样,我们也通过数据冗余的方式来保障数据的高可靠及可用。这里面提一下服务的可靠性怎么来做。

对于MasterService——引入zookeeper通过选举出Active的MasterService来保障服务同样有多分。对于元数据的存储方式,系统的元数据决这整个系统中能够承受数据的规模上限,通常做法Master会在内存中维护一份元数据的镜像来对外提供服务,持久化的元数据用做系统重启或者恢复,利用checkpoint机制加速恢复的过程。要保存的信息有哪些呢?节点信息,目录信息,副本信息,数据分布等。元数据是必须持久化的,元数据损坏对整个系统影响非常大。

MasterService 挂掉的影响,显然关于元数据的操作会有影响了。如果要做到影响最小。Client在拿到元数据之后要在本地cache一份这些元数据。这样Master挂掉之后不至于影响现有服务的读写操作。一旦涉及到cache就必然涉及到cache一致性的问题。那么cache同步的策略又是怎样的?有几个点是必须的。

  1. 如果遇到任何数据操作失败(那么很可能是节点永久不可用了,这时Client并不能私自做任何决定)
  2. 定时同步,这里我们可以引入一个version的概念,这个版本号可以是时间,也可以是偏序递增的数字,Client定期跟Master保持同步, 如果Master发现当前的version比Client发过来的的version小。那么返回metaChange。client会重新发起请求来拉数据。否则返回notChange。

DataNodeService挂掉,显然读写服务有瞬时的影响。从Client的角度来看,此时操作请求发出返回的可能是failure也可能是timeout。对于读操作如果不要求强一致的话,client拿着自己手里的元数据,轮训探活。如果服务是在写的话。由于主副本挂掉,系统会处在一个暂时的无主的状态,所以路由到这个节点上的写服务是暂时不可用的,。client会等一段时间等待新主副本被选举出来。所以数据节点倒掉必然应发的是replica membership的变化,接着就会触发新一轮的选主操作(具体怎么选Raft协议介绍)。

数据正确性 网络问题或者磁盘损坏都会引起数据错误,这对于存储系统来说是不可接受的。需要在存储跟传输的时候进行CRC对比校验。

资源管理

  IO及内存管理 内存的管理涉及到两个比较核心的点,分配释放及系统的cache策略。整个系统构建在LSM引擎之上,分布式逻辑跟数据存储逻辑实际上是分开的。对于单机来的引擎来说,以LevelDB为例,LevelDB是一个写优先的系统。对于一个查询的处理逻辑要复杂一些。需要在memtable,immutable table ,以及各个Level之间查找。如果每次查询都走磁盘的IO势必影响性能,在LevelDB内部使用LRU Cache,其内部实现是利用分段锁+双向链表的方式实现。每个LRUCache实例内部还包含有HashTable主要作用是判断对象是否被Cache。

  不论是以什么语言来实现,最终的内存分配释放都会到malloc,free这些系统函数上,涉及到系统调用就会有内核态到用户态的切换,不免有效率的影响。对于像数据存储系统这种涉及到大块的内存分配的。用一个全局的内存池是能够提高效率的。这种分配方式可以将多次的小快的内存合并为单次进行,之后再有内存分配的请求就可以在系统内部进行了。能够减少频繁的系统函数调用(malloc/free)。具体实现可参考伙伴分配算法,或者更为简化的定长内存池。或者参考netty的内存分配的实现。

IO调度 对于在引擎之上的查询服务来说,读取文件的IO模型是典型生产者-消费者模型,数据的发送线程是消费者,数据扫描线程是生产者。为方便资源的管理,系统要有统一的IO调度机制,对于一次给定key范围的查询请求来说,流程可能是这样的。

  1. 对于给定的Key Client根据元数据找到key对应的数据分片(不论数据如何分布),以及该分片对应的物理节点,具体流程client会问MasterService去要这些元数据的数据分布信息。

TODO