通过Consul Raft库打造自己的分布式系统
通用的CP系统有etcd和consul, 通用的对立面就是专用系统. 所以在某些场合是有这种需求的.
然而etcd embed的可用性极差, Windows上面跑会出现各种问题, 而且不能定制协议, 你必须得用etcd定义好的协议和客户端来和etcd集群通讯. 所以这时候的选择:
1. 忍着
2. 自己实现一个raft算法库, 在这上面做应用
有一定的可能性, 起码MIT 6.824可以做出来, 但是和工业应用还是有很大的差距
3. 找一个工业级raft库, 然后在这上面做应用
这时候到Raft Consensus Algorithm上面看看就能找到几个可选的Raft算法库, 例如braft, hashicorp/raft, lni/dragonboat.
但是呢, C++代码比较难写的, 所以就pass掉了braft. 就剩下consul raft和dragonboat.
本文就用consul raft做一个简单的KeyValue服务.
首先前端用的gin, 提供put/get/inc/delete几个接口, 三个接口都走raft状态机, 因为要支持多节点, 所以内部非leader节点就需要把请求转发给leader节点.
前端的代码类似于这样:
1 2 3 4 5 6 7 8 9 10 11 12 | func (this *ApiService) Start() error { //转发请求给leader节点 this.router.Use(this.proxyHandler()) this.router.POST( "/get" , this.Get) this.router.POST( "/put" , this.Put) this.router.POST( "/delete" , this.Delete) this.router.POST( "/inc" , this.Inc) address := fmt.Sprintf( ":%d" , this.port) return this.router.Run(address) } |
请求都很简单, 就是直接把命令, 或者叫服务提供的原语塞到Raft状态机里面等候Raft状态Apply, 然后才能拿到结果(future/promise模式), 例如put命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | func (this *ApiService) Put(ctx *gin.Context) { req := &Request{} if err := ctx.ShouldBindJSON(req); err != nil { ctx.JSON(http.StatusBadRequest, Response{ Error: err.Error(), }) return } result, err := this.raft.ApplyCommand(raft.CommandPut, req.Key, req.Value) if err != nil { ctx.JSON(http.StatusInternalServerError, Response{ Error: err.Error(), }) return } ctx.JSON(http.StatusOK, Response{ Value: result.Value, }) } |
前端还有一个转发请求到leader节点的拦截器(? 应该叫这个名字, 实际上是pipeline模式的一种)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | func (this *ApiService) proxyHandler() gin.HandlerFunc { return func (context *gin.Context) { if this.raft.IsLeader() { context.Next() } else { leaderServiceAddress := this.raft.GetLeaderServiceAddress() if this.leaderServiceAddress != leaderServiceAddress { Director := func (req *http.Request) { req.URL.Scheme = "http" req.URL.Host = leaderServiceAddress } this.leaderProxy = &httputil.ReverseProxy{ Director: Director, } this.leaderServiceAddress = leaderServiceAddress } this.leaderProxy.ServeHTTP(context.Writer, context.Request) context.Abort() } } } |
下面是对协议的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | func (this *FSM) Apply(log *raft.Log) interface {} { result := &FSMApplyResult{ Success: false, } t, cmd, err := raftLogToCommand(log) if err != nil { result.Error = err return result } binary.LittleEndian.PutUint64(keyCache, uint64(cmd.Key)) binary.LittleEndian.PutUint64(valueCache, uint64(cmd.Value)) switch t { case CommandPut: result.Success, result.Error = this.add(keyCache, valueCache) case CommandDelete: result.Success, result.Error = this.delete(keyCache) case CommandGet: result.Value, result.Error = this.get(keyCache) case CommandInc: result.Value, result.Error = this.inc(keyCache, cmd.Value) } return result } |
输入给Raft状态的命令实际上都是序列化好的, Raft状态机会自己把命令保存到Storage里面(可以是内存, 也可以是磁盘/DB等). 所以Apply命令的时候, 先对raft log进行解码, 然后switch去处理.
这边再看看例如inc的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | func (this *FSM) inc(key []byte, add int64) (int64, error) { var value int64 = 0 err := this.db.Update( func (tx *bbolt.Tx) error { b, err := tx.CreateBucketIfNotExists(BBoltBucket) if err != nil { return err } valueBytes := b.Get(key) if len(valueBytes) != 8 { logging.Errorf( "FSM.inc, key:%d, value length:%d, Reset" , int64(binary.LittleEndian.Uint64(key)), len(valueBytes)) valueBytes = make([]byte, 8) } value = int64(binary.LittleEndian.Uint64(valueBytes)) value += add binary.LittleEndian.PutUint64(valueBytes, uint64(value)) err = b.Put(key, valueBytes) return err }) if err != nil { return -1, err } return value, err } |
这个指令稍微复杂一点, 需要先到db里面去找, 找到的话, 再加一个N, 然后存储, 然后返回新的值. 因为raft状态机apply log的时候, 是顺序的, 所以不需要加锁啥的, inc本身就是原子的.
至此一个简单的分布式KeyValue服务就实现, 而且还是一个CP系统.
当然这只是一个demo, 实际的应用远远比这个复杂, 本文只是提供一种思路.
不必非要把自己绑死在Etcd上, 条条大路通罗马. 如果你的系统只需要提供有限的操作原语, 那么是可以考虑Consul Raft或者DragonBoat来制作自定义协议的CP服务. 蚂蚁的SOFARaft也可以干这种事.
参考:
1) RaftKV (https://gitee.com/egmkang/raft-kv)
2) Consul Raft (https://github.com/hashicorp/raft)
3) DragonBoat (https://github.com/lni/dragonboat)
4) Dapr (https://github.com/dapr/dapr/tree/master/cmd/placement)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律