Golang项目 ymdb-一个简易的分布式键值存储系统

本文首发于我的个人博客,欢迎访问 ymdb-分布式键值存储系统

ymdb 是我开源的一款简易的分布式键值存储系统,适用于分布式系统初学者练手或者应届生写上简历,这篇文章将对 ymdb 做一个全面的介绍(建议仅用作学习用途,应用于生产是危险的,因为 ymdb 尚不完善)。

ymdb 使用 Go 语言开发,从面试情况来看,面试官还是很喜欢问这个项目的,并且有几个面试官表示这样的项目比较抓眼球,因为它是一个偏底层的并且不是一个千篇一律(俗称“烂大街”)的项目,因此写在简历上不失为一个好的选择。

技术选型

要做一个分布式存储系统,需要考虑以下三个点:

  1. 存储
  2. 分区
  3. 复制

存储是设计一个存储系统必须要考虑的,而分区和复制则是设计一个分布式系统需要考虑的。

在存储部分,当前的键值存储有三种技术方案,一种是 RocksDB 采用的 LSMT(Log Structured Merge Tree) 方案;一种是 RoseDB 采用的 Bitcask 方案;一种是 Redis 的方案。考虑到我想做一个持久的键值存储系统,所以 Redis 这种基于内存的方式首先排除,Redis 的持久化仅用作备份以及主从复制,并不会从磁盘去拿数据。LSMT 实现起来较为复杂,且其虽然写效率很高,但读效率却较差,主要由于 LSMT 是在磁盘上的多层结构,可能要查好几层才能查到数据。因此,我最终选择 RoseDB 的方案来实现键值存储,即将所有的 key 存储在内存中,并将 key 对应的值在磁盘中的位置与 key 一同存放,而将 value 都存储在磁盘上,读取数据只需要一次磁盘IO,写入数据由于是追加写,可以利用到 page cache ,这样读写效率都很高。

在分区部分,我们要做的是对数据的分区,由于我们将所有的 key 都存放在内存中,所以内存的大小就是我们可以存储的数据量的瓶颈,所以我们需要进行数据分区,保证每个节点上只存储部分数据而不是全量数据,这样就使得存储系统中可以存储的数据总量不受单台计算机的内存和磁盘大小的约束。借鉴 Redis ,我采用一致性哈希来做数据分区,不过没有 Redis 那么复杂。

在容错部分,做为一个分布式的系统,必须具有容错的能力,现有的知名的分布式系统例如 hdfsgfs 以及 kafka 等都是通过将一份数据存储多份副本来保证可靠性的,ymdb 亦是如此,我设计一个分区内的多个节点存储当前分区数据的多个副本来提供容错性。我采用 Raft 算法来实现分区内节点间的分布式共识,首先 Raft 易于理解,其次 Raft 被许多知名开源软件广泛使用,例如 etcdtikvConsul 等。

##整体架构

ymdb整体架构

ymdb 的整体架构如图所示,客户端通过一致性哈希去确定数据所在的分区,每个分区内包含多个节点,存储本分区内数据的多个副本,在某一个具体的节点上,内存中存储所有的 key ,并使用跳表(skip-list)来作为 key 的索引结构以加快查找,value 以追加写的方式存储在磁盘上。

存储

内存

ymdb 在内存中存储所有的 key ,并采用跳表作为 key 的索引。选择跳表的原因在于其易于实现、维护成本低且范围查询友好,由于是内存索引,所以没有必要选择平衡树徒增维护成本。

相关代码位于项目的 index 包下,我这里直接采用了对第三方跳表实现的封装,put() 方法将 key 以及对应 value 所在的位置写入跳表;get()方法通过 key 获得其对应 value 在磁盘上的位置;delete()方法仅在跳表中删除指定 key

虽然我这里使用了第三方的跳表实现,但是跳表易于实现,也可以自己实现,我之前使用 C++ 实现过一版,具体思路在于使用 vector 作为跳表的节点,设置 vector 长度为跳表的高度,vector 的元素以下标顺序 0-i 分别存储该节点在跳表中的 0-i 层的信息,以随机的方式选择每个节点的高度。

磁盘

所有的 value 以追加写的方式存储在磁盘上的 WAL 文件中,这部分代码在项目的 db 包下,是 ymdb 存储引擎的实现,目前对数据的操作包含 putget 以及 delete

分区

采用一致性哈希来实现数据的分区,一致性哈希的基本原理可以看 一致性哈希算法,这里我也是采用了第三方实现,代码位于项目的 cluster-cli.go ,也就是说 ymdb 在客户端完成对于数据的定位,随后再向相应的分区发送数据的读写请求。

虽然我这里使用了第三方的一致性哈希实现,但是自己实现起来应该也较为简单,由于需要在哈希环上顺时针去寻找数据所在的分区,所以首先我们需要一个有序的数据结构,比如红黑树,首先对分区做哈希得到32位哈希值,存到红黑树中,之后对数据做哈希得到32位哈希值,在红黑树中搜索第一个比自己大的节点就是这个数据对应的分区。如果需要增加虚拟节点,只需要给真实分区加上一些信息,依旧做哈希得到哈希值,如果有数据映射到该分区,则可以进一步映射到真实的分区上。

复制

采用 Raft 算法来实现节点间的分布式共识,Raft 算法的基本原理可以看 Raft 共识算法总结。当客户端给一个分区发出写数据指令,该分区的 Leader 会来处理这个指令,Leader 负责将这条指令复制给其余的节点,其余的节点执行指令实现数据存储,并返回给 Leader ,当 Leader 收到集群中半数以上节点的响应时,就认为这条指令可以提交了,于是执行指令并给客户端返回。

ymdb 基于 Raft 算法,因此 ymdb 整体上是一个 CP 型的分布式系统,也就是说,保证强一致性并且具有分区容错性。

我采用了 HashiCorpRaft 实现,代码位于项目的 raft 包下,application.go 下定义了自己的 FSM ,即有限状态机,主要是需要实现 Apply() 方法来定义日志应用的逻辑。由于我们客户端使用 grpc 来与 ymdb 集群做交互,所以还需要编写 proto 文件来定义 grpc 通信所需要的消息和服务,同时还需要在 application.go 中实现一个 rpc 服务端,实现 SendMsg() 方法处理 rpc 请求。另外,Raft 集群节点间的通信是第三方库实现好的,无需考虑。

通信

客户端采用 grpc 与多个 Raft 集群进行通信,客户端代码位于 cluster-cli.go 中,根据不同的指令和 key 向不同的数据分区发送不同的 rpc 请求,集群中任意一个节点收到 rpc 请求会把请求转发给 Leader 进行处理,Leader 处理消息的逻辑见 application.go 下的 rpcInterfaceSendMsg() 方法,首先应用日志,之后从响应 channel 中取得结果;应用日志的具体逻辑在 dataTrackerApply() 方法中,这里应用日志也就是交给存储引擎来执行数据读写命令,这里把消息放在了一个消息队列也就是 MsQueue 中,存储引擎会从中取命令然后执行,达到一个异步处理的效果。

崩溃恢复

为了实现崩溃一致性,节点应该具有崩溃恢复的能力,ymdb 在磁盘上额外存放了一个用于恢复的 WAL 文件,记录修改数据的指令,在每次执行修改类型的命令之前,首先将命令写入到日志中,类似于 MySQLredo log ,这样即使节点崩溃。在节点恢复之后只需要执行日志中的指令即可恢复数据,实现崩溃一致性。

写入日志的代码位于项目的 route 包下,在 Handle() 方法中,根据消息类型执行不同的操作,只有 PUTDELETE 操作才会去调用 writeWAL() 方法写入恢复日志。

崩溃恢复的代码位于项目的 db 包下的 Restore() 方法以及 NewDB() 方法,在创建 DB 对象时,如果发现指定目录下已经有日志文件,则置恢复状态位 isRestore 为真,执行恢复操作,否则创建新的日志文件。

优化

ymdb 目前尚有大量优化空间,我能想到的如下:

  1. 增加缓存系统。对于随机读取,无法利用到操作系统的 page cache ,导致每次查询稳定一次磁盘IO,是拖慢速度的,如果增加了缓存系统,便可以大大提升效率。然而引入缓存系统就必须要考虑其带来的数据不一致性,尚需要斟酌。
  2. 服务发现。目前系统中的节点数量和地址信息在数据库启动时就已经确定了,无法动态扩展,可以增加服务注册与发现机制来实现节点的动态增删,具体可以使用 zookeeper 来实现。
  3. 节点间数据迁移。由于使用了一致性哈希作为数据分区算法,增加和删除分区必然导致分区数据的变化,所以需要实现分区间数据的迁移,可以使用子进程来实现。

具体 ymdb 的使用可以参考 README.md 以及 示例

面试

面试中对这个项目的考察多从以下几点进行提问:

  1. 为什么选择跳表作为内存索引,不选择别的
  2. 跳表的原理是什么,应该如何实现
  3. 为什么选择一致性哈希,还了解别的数据分区算法吗
  4. 一致性哈希知道怎么实现吗
  5. 一致性哈希存在哪些问题,如何解决
  6. 为什么选择 Raft 算法来做分布式共识,还了解别的分布式共识算法吗
  7. Raft 的领导选举介绍一下
  8. RaftLeader 挂了会发生什么
  9. Raft 是强一致的吗,如何保证强一致的
  10. Raft 是如何解决脑裂问题的
  11. CAP 定理介绍一下
  12. ymdbRedis 集群有哪些不同之处
  13. 做过压测吗,性能如何

这就是我对 ymdb 的整体介绍,包括之后的优化思路以及面试常见问题,有任何问题可在评论区交流。

MIT6.824 的实验最终也会实现一个分布式的键值存储系统,然如果时间不充裕,听完 MIT 的全英课再自己实现 Raft 也需要较长时间,出于练手的考虑,直接采用第三方 Raft 快速构建一个分布式系统也是可以的,并且面试官很少会关注到 Raft 算法的具体实现。

  • 15
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 我们可以使用Golang来构建一个分布式缓存系统。一般来说,分布式缓存系统的实现包括:缓存服务器、缓存客户端、分布式缓存服务器、分布式缓存客户端、分布式缓存存储引擎和缓存管理服务器。使用Golang可以构建一个可靠、可扩展、高性能的分布式缓存系统。 ### 回答2: 分布式缓存系统是一个将数据存储在多台机器上的缓存系统,可以提高数据访问的性能和并发能力。在golang中,我们可以使用一些库和技术来实现一个分布式缓存系统。 首先,我们需要选择一个合适的分布式存储技术来存储缓存数据,比如使用Redis或Memcached。这些技术可以让我们将缓存数据分布在多个节点上,并提供高性能的读写操作。 其次,我们需要设计一个对外提供接口的服务来操作缓存。可以使用golang的HTTP服务器来实现这个服务。通过定义一些RESTful API,如GET、PUT、DELETE等,客户端可以向服务器发送请求来获取、设置或删除缓存数据。 在服务器端的代码中,我们需要处理接收到的请求,并将其映射到相应的缓存操作。当有GET请求时,我们需要先检查缓存中是否存在所需的数据,如果存在则直接返回,否则再从存储系统获取数据并放入缓存中。当有PUT请求时,需要将数据存储到缓存和存储系统中。当有DELETE请求时,需要从缓存和存储系统中删除相应的数据。 为了实现缓存的分布式特性,我们可以使用一致性哈希算法或一致性哈希环来将缓存数据分布到不同的节点上。这样可以确保当节点发生故障或增加时,仅会影响到部分数据,而不会影响全部缓存数据。 最后,我们需要定期清理缓存中的过期数据,以防止缓存数据占用过多的存储空间。可以使用定时任务或定时器来实现这个功能。 以上是一个简单的分布式缓存系统的设计和实现过程。当然,实际的分布式缓存系统可能还涉及到一些其他的问题,如并发控制、持久化存储等,但是以上提到的内容可以帮助我们开始构建一个基本的分布式缓存系统。 ### 回答3: 分布式缓存系统是用来提高系统的读取性能和减轻数据库压力的重要组件,能够将数据存储在多个节点上,提供快速的访问速度和高可用性。 在使用golang编写一个分布式缓存系统时,可以先考虑以下几个关键点: 1. 数据分片:将数据按照一定的规则分散存储到不同的节点上,可以使用一致性哈希算法或分片算法来实现。 2. 节点管理:需要设计节点的动态增删、负载均衡以及容错机制。可以使用集群管理工具如etcd或者zookeeper来实现。 3. 数据存储:使用内存数据库如Redis或Memcached来存储缓存数据,并保证数据的一致性和高可用性。可以选择golang中的redis或memcache客户端库进行数据读写。 4. 缓存更新和失效:提供缓存的自动更新机制,当数据发生变化时,需要及时更新缓存数据,同时设置合理的缓存失效策略,避免使用过期的数据。 5. 高可用性:保证缓存系统的高可用性,当节点出现故障时,能够自动切换到其他可用的节点上,并进行数据恢复。 6. 监控和日志:实现对缓存系统的监控和日志记录,可以使用Prometheus和Grafana等工具进行监控和性能分析。 在实现分布式缓存系统时,需要综合考虑不同的因素,并做好合理的设计和优化。同时,需要进行大规模测试和性能调优,确保系统的稳定和高效运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值