Raft算法的代码实现已经在Hashicorp Raft中体现了出现,那么尝试去自己编写一个基本的分布式KV系统
我们先从架构的角度来说明一个基本的分布式KV系统如何实现,
需要实现哪些功能,在架构设计的时候,考虑实现哪些点
我们分别从技术的深度,开发的工作量,学习的复杂度来考虑一下,一个基本的分布式的KV,至少需要如下的功能
接入协议,我们需要提供给客户端什么样的通讯协议,和接入层的API
KV操作:需要支持的KV操(赋值操作)
分布式集群,基于Raft算法实现一个分布式存储集群,存放KV数据
1.接入协议
在早期,硬件性能低,服务也不是很多,开发系统的时候,性能瓶颈是一个矛盾,但是后期,基于性能的考虑,采用UDP协议和私有的二进制协议
现在,主要的矛盾不再是性能瓶颈了,而是海量的服务和开发效率,那么就需要考虑标准的协议了
那么如果使用HTTP协议,如何实现的呢?可以考虑设计Restful风格的HTTP API,如何实现使用呢?
可以使用的是KV系统,肯定要涉及到KV操作,那么我们先针对查询设计出对应的API请求方式.比如如下的API请求
curl -XGET http://raft-cluster-host01:8091/key/foo
只要有这个key,就说明是查询的命令
还有一些平台本身的操作的API接口,比如增加移除集群的API,操作节点的API如下
http://raft-cluster-host01:8091/join
还有一件事,就是设计API的时候,如何实现路由,就是在实现了多个API,比如key和join如何将API对应的请求和对应的处理函数
可以在serveHTTP()函数中,通过检测URL路径,来设置对应的处理函数,实现路由
func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 设置HTTP请求对应的路由信息
if strings.HasPrefix(r.URL.Path, “/key”) {
s.handleKeyRequest(w, r)
} else if r.URL.Path == “/join” {
s.handleJoin(w, r)
} else {
w.WriteHeader(http.StatusNotFound)
}
}
我们检测到了URL的路径为 “/key”时候,会调用handleKeyRequest()函数,来处理KV操作请求,检测URL的路径为”/join”的时候,会调用handleJoin()函数,将指定的节点加入到集群中
这样,我们就可以基本满足这个KV系统的要求了,可以手动操作这个集群,也可以支持客户端的KV操作
2.如何设计KV操作呢?
常见的KV操作是赋值,查询,删除,我们实现这三个操作就可以了,
对于赋值操作,可以使用HTTP的POST请求,来进行指定key进行赋值,就好比下面的样子
curl -XPOST http://raft-cluster-host01:8091/key -d ‘{“foo”:”bar”}’
查询操作,可以通过HTTP GET请求,查询指定key的值
curl -XGET http://raft-cluster-host01:8091/key/foo
删除操作,可以通过HTTP DELETE的请求,删除指定key和key对应的值,就像下面的样子
curl -XDELETE http://raft-cluster-host01:8091/key/foo
如上的操作都具有幂等性,就是同一个操作,不管执行多少次,结果都是一样的,这个操作可以重复的执行,执行多少次也不会对系统产生额外的影响
共识算法能够保证达成共识后的值不会在改变了,但不能保证值只被提交了一次,也就是共识算法是一个at least once的指令执行模型,可能会出现同一个指令被重复提交的情况
当客户端收到Raft的超时响应的时候,日志没有提交,此时如果重试,发送一个新的请求,可能出创建一个新的日志项,导致两个日志项都提交了,出现了指令重复执行的情况
3.那么,如何实现分布式的集群?
Raft算法实现一个集群,就是要考虑集群的创建,移除和替换
创建集群,在Raft算法中,可以这样创建集群
先将第一个节点,通过Bootstrap的方式启动,作为领导者节点
然后其他节点和这个领导者节点通讯,将自己的配置信息发送到领导者节店,然后领导者节点调用AddVoter()函数,将新的节点加入到集群中
创建了集群后,在集群运行中,因为Raft集群的领导者不是不变的,而所有的写请求,都在领导和处理,就需要考虑下一步,如何将写请求进行保证都发给领导者呢?
4.写操作
一般来说的写操作有两种,
第一种是,跟随者接收到客户端的写请求之后,拒绝处理,并返回领导的地址,让客户端直接访问领导者,直到领导者更换
方法2:跟随者收到了客户端的写请求,将写请求转发给领导者,并且将领导者处理后的结果返回给客户端,跟随者现在扮演一个中间代理的角色
第一种需要重定向,但是总体来说复杂度并不高,而第二种,虽然能够降低客户端的感知,让客户端感觉没有变化,但是呢,引入了一个中间节点,增加了问题排查的复杂度,而且一般来说,领导者一般很稳定,这样引入中间节点,会增加大量的不必要的消息和性能消耗
并且,相比写操作,读操作会复杂一些,因为如何实现读操作,会关乎着一致性的实现,如何实现读操作
5,读操作
在系统中,我们需要实现不同级别的读一致性,来方便用户的使用和体验
分为
可能读到旧数据
一定不会读到旧数据
会读到旧数据
这个会根据NWR的模型法,来设置不同的读一致性的模型
交给用户去设置集群
curl -XGET http://raft-cluster-host02:8091/key/foo?level=consistent -L
本章中,我们了解了一个基本的分布式KV系统的架构,需要权衡折中技术细节
主要的细节如下
1.设计KV操作时候,一定要考虑幂等性,因为Raft指令可能被重复执行和提交
2.在编写写请求的实现的时候,最好是跟随者接收到客户端的写请求的时候,拒绝该请求并返回领导者的地址给客户端,客户端来访问领导者
3.Raft集群中,如何实现读操作,关乎一致性的实现,推荐实现default,consisent,stale三种一致性模型,将一致性的选择交给用户,让用户自己选择一致性强度
4.这个小型的系统不仅仅,适合配置中心,还可以进一步的进行扩展,一般来说,可以如下的设计三级缓存:分为冷热数据
热数据,经常被访问到的数据,可以放在内存中,提升访问的效率
冷数据,则将不常访问的数据,放在硬盘中,提升了存储成本
课后思考:
如何去替换一个节点?
可以在多次发出同步日志的时候,都获得超时的恢复,考虑进行节点的替换,在RemoveServer函数中,可以通过传入失效这个节点的集群的ID来进行删除