
示例工程【免费下载链接】7days-golang7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7天用Go动手写/从零实现系列项目地址https://gitcode.com/gh_mirrors/7d/7days-golang点击查看免费下载本文是「7天用 Go 从零实现分布式缓存 GeeCache」系列的第五篇主题是为 GeeCache 增加分布式节点能力借助第四天实现的一致性哈希算法注册节点、选择节点并编写约 90 行的 HTTP 客户端与远程节点服务端通信从而让缓存查询可以从「单机命中」扩展到「多节点协作」。读完本文你将掌握PeerPicker/PeerGetter两个核心接口的设计思路、HTTPPool如何同时充当服务端与客户端以及如何用 3 个本地端口 1 个 API 端口完整跑通一套多节点缓存集群。1 流程回顾今天要补上第 ⑵ 步在 GeeCache 的整体设计中一次key查询遵循如下流程是 接收 key -- 检查是否被缓存 ----- 返回缓存值 ⑴ | 否 是 |----- 是否应当从远程节点获取 ----- 与远程节点交互 -- 返回缓存值 ⑵ | 否 |----- 调用回调函数获取值并添加到缓存 -- 返回缓存值 ⑶前几天的实现已经完成了流程 ⑴从本地缓存命中并返回Group.Get中先查mainCache见 geecache.go流程 ⑶未命中时调用回调函数从数据源加载并回填缓存getLocallypopulateCache。今天要实现的正是流程 ⑵从远程节点获取缓存值。将它进一步细化使用一致性哈希选择节点 是 是 |----- 是否是远程节点 ----- HTTP 客户端访问远程节点 -- 成功----- 服务端返回返回值 | 否 ↓ 否 |---------------------------- 回退到本地节点处理。可以看到这一步需要解决三个问题如何抽象选择节点和访问节点的行为接口设计、如何用 HTTP 与远程节点通信客户端实现、如何把这两件事接入主流程load改造。2 抽象 PeerPicker 与 PeerGetter 两个接口首先要做的是面向接口编程把根据 key 选择节点和从节点获取数据两个行为抽象出来而不是直接依赖具体的 HTTP 实现。为此新增 peers.gopackage geecache // PeerPicker is the interface that must be implemented to locate // the peer that owns a specific key. type PeerPicker interface { PickPeer(key string) (peer PeerGetter, ok bool) } // PeerGetter is the interface that must be implemented by a peer. type PeerGetter interface { Get(group string, key string) ([]byte, error) }PeerPicker负责节点选择。PickPeer(key)根据传入的 key 返回应当由哪个节点PeerGetter处理该 key以及是否命中了一个有效的远程节点ok。PeerGetter负责节点数据访问也就是流程中的 HTTP 客户端。Get(group, key)从指定 group 中查找 key 对应的缓存值返回[]byte。这里的ok布尔返回值很关键当 key 经一致性哈希计算后落在本机节点上时PickPeer应当返回okfalse由调用方回退到本地处理避免节点间无限转发。3 节点选择与 HTTP 客户端第三天实现中HTTPPool已经通过ServeHTTP提供了服务端能力解析/basepath/groupname/key路径、调用group.Get、返回字节流见 http.go。但通信是双向的服务端还需要配套的客户端。本节为HTTPPool补齐客户端与节点选择能力。3.1 第一步httpGetter 实现 PeerGetter创建具体的 HTTP 客户端类httpGetter实现PeerGetter接口源码见 http.gotype httpGetter struct { baseURL string } func (h *httpGetter) Get(group string, key string) ([]byte, error) { u : fmt.Sprintf( %v%v/%v, h.baseURL, url.QueryEscape(group), url.QueryEscape(key), ) res, err : http.Get(u) if err ! nil { return nil, err } defer res.Body.Close() if res.StatusCode ! http.StatusOK { return nil, fmt.Errorf(server returned: %v, res.Status) } bytes, err : ioutil.ReadAll(res.Body) if err ! nil { return nil, fmt.Errorf(reading response body: %v, err) } return bytes, nil } var _ PeerGetter (*httpGetter)(nil)baseURL表示将要访问的远程节点的地址前缀例如http://example.com/_geecache/。注意它以basePath/_geecache/结尾与服务端的路由前缀严格对应拼接 URL 时使用url.QueryEscape对group与key做转义保证 key 中即使含特殊字符也不会破坏路径结构使用http.Get()发起请求校验状态码为200后通过ioutil.ReadAll读取响应体并转换为[]byte末尾的var _ PeerGetter (*httpGetter)(nil)是编译期断言确保httpGetter确实实现了PeerGetter接口一旦接口签名变化编译立刻报错。3.2 第二步HTTPPool 增加节点注册与选择接着为HTTPPool增加节点管理成员完整定义见 http.goconst ( defaultBasePath /_geecache/ defaultReplicas 50 ) // HTTPPool implements PeerPicker for a pool of HTTP peers. type HTTPPool struct { // this peers base URL, e.g. https://example.net:8000 self string basePath string mu sync.Mutex // guards peers and httpGetters peers *consistenthash.Map httpGetters map[string]*httpGetter // keyed by e.g. http://10.0.0.2:8008 }新增的两个成员变量peers类型为一致性哈希算法的Map第四天实现负责根据具体的 key 选择节点httpGetters映射「远程节点地址 → 对应的httpGetter」。每个远程节点都对应一个独立的httpGetter因为httpGetter携带的baseURL与节点地址强相关mu sync.Mutex保护peers与httpGetters的并发读写因为Set/PickPeer可能在多 goroutine 下被同时调用defaultReplicas 50表示每个真实节点在哈希环上生成 50 个虚拟节点虚拟节点越多节点增减时数据迁移的均衡性越好。3.3 第三步实现 PeerPicker 接口为HTTPPool实现Set与PickPeer两个方法源码见 http.go// Set updates the pools list of peers. func (p *HTTPPool) Set(peers ...string) { p.mu.Lock() defer p.mu.Unlock() p.peers consistenthash.New(defaultReplicas, nil) p.peers.Add(peers...) p.httpGetters make(map[string]*httpGetter, len(peers)) for _, peer : range peers { p.httpGetters[peer] httpGetter{baseURL: peer p.basePath} } } // PickPeer picks a peer according to key func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) { p.mu.Lock() defer p.mu.Unlock() if peer : p.peers.Get(key); peer ! peer ! p.self { p.Log(Pick peer %s, peer) return p.httpGetters[peer], true } return nil, false } var _ PeerPicker (*HTTPPool)(nil)Set()实例化一致性哈希虚拟节点数 50哈希函数默认取crc32.ChecksumIEEE把传入的全部节点地址加入哈希环同时为每个节点创建对应的httpGetter其baseURL由节点地址 basePath拼接而成PickPeer()包装一致性哈希的Get()方法根据 key 选出节点关键判断是peer ! p.self——如果选出的节点就是本机则返回(nil, false)由主流程回退到本地处理避免节点自我递归请求。至此HTTPPool一身二任既具备提供 HTTP 服务的能力ServeHTTP也具备根据具体 key 创建 HTTP 客户端、从远程节点拉取缓存值的能力PickPeerhttpGetter。关于一致性哈希的行为可以参考其单元测试 consistenthash_test.go用注入的假哈希函数验证添加节点后 key 映射到最近虚拟节点、新增节点只影响少量 key等特性这也是分布式缓存中节点变化时最小化数据迁移的基石。4 集成主流程RegisterPeers 与 load 改造最后把上述能力接入Group的主流程geecache.go。Group新增peers PeerPicker字段并增加两个方法、修改一个方法// A Group is a cache namespace and associated data loaded spread over type Group struct { name string getter Getter mainCache cache peers PeerPicker } // RegisterPeers registers a PeerPicker for choosing remote peer func (g *Group) RegisterPeers(peers PeerPicker) { if g.peers ! nil { panic(RegisterPeerPicker called more than once) } g.peers peers } func (g *Group) load(key string) (value ByteView, err error) { if g.peers ! nil { if peer, ok : g.peers.PickPeer(key); ok { if value, err g.getFromPeer(peer, key); err nil { return value, nil } log.Println([GeeCache] Failed to get from peer, err) } } return g.getLocally(key) } func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) { bytes, err : peer.Get(g.name, key) if err ! nil { return ByteView{}, err } return ByteView{b: bytes}, nil }三个关键改动RegisterPeers()将实现了PeerPicker接口的HTTPPool注入到Group中用panic防止重复注册保证节点配置只生效一次getFromPeer()使用实现了PeerGetter的httpGetter访问远程节点将返回的[]byte包装成不可变的ByteViewByteView提供了Len / ByteSlice / String等只读访问方法见 byteview.goload()改造先判断是否配置了节点选择器若是则调用PickPeer(key)选节点选中的是远程节点则调用getFromPeer()若请求失败则打印日志并回退到getLocally()本地处理——这是分布式缓存的重要容错设计单个远程节点故障不应拖垮整个查询链路。5 main 函数测试3 节点集群 API 服务5.1 main 函数结构main.go 的代码比较多但逻辑非常简单启动两种角色——用户不感知的缓存服务器8001/8002/8003和用户感知的 API 服务9999。var db map[string]string{ Tom: 630, Jack: 589, Sam: 567, } func createGroup() *geecache.Group { return geecache.NewGroup(scores, 210, geecache.GetterFunc( func(key string) ([]byte, error) { log.Println([SlowDB] search key, key) if v, ok : db[key]; ok { return []byte(v), nil } return nil, fmt.Errorf(%s not exist, key) })) } func startCacheServer(addr string, addrs []string, gee *geecache.Group) { peers : geecache.NewHTTPPool(addr) peers.Set(addrs...) gee.RegisterPeers(peers) log.Println(geecache is running at, addr) log.Fatal(http.ListenAndServe(addr[7:], peers)) } func startAPIServer(apiAddr string, gee *geecache.Group) { http.Handle(/api, http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { key : r.URL.Query().Get(key) view, err : gee.Get(key) if err ! nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set(Content-Type, application/octet-stream) w.Write(view.ByteSlice()) })) log.Println(fontend server is running at, apiAddr) log.Fatal(http.ListenAndServe(apiAddr[7:], nil)) } func main() { var port int var api bool flag.IntVar(port, port, 8001, Geecache server port) flag.BoolVar(api, api, false, Start a api server?) flag.Parse() apiAddr : http://localhost:9999 addrMap : map[int]string{ 8001: http://localhost:8001, 8002: http://localhost:8002, 8003: http://localhost:8003, } var addrs []string for _, v : range addrMap { addrs append(addrs, v) } gee : createGroup() if api { go startAPIServer(apiAddr, gee) } startCacheServer(addrMap[port], addrs, gee) }各部分职责createGroup()创建名为scores的缓存组容量 2102048 字节LRU 淘汰数据源是内存 map 模拟的「慢数据库」startCacheServer()创建HTTPPoolSet(addrs...)注册全部 3 个节点RegisterPeers注入到 Group最后用http.ListenAndServe(addr[7:], peers)启动缓存服务——addr[7:]截掉http://前缀得到监听地址peers同时充当 Handler即ServeHTTPstartAPIServer()在 9999 端口注册/api路由解析?key查询参数后调用gee.Get(key)成功则返回字节流失败返回 500main()通过命令行参数-port指定缓存端口、-api1决定是否同时启动 API 服务三台缓存节点共用同一个addrs列表因此每个节点都知道完整的集群拓扑。需要注意模块组织day5 采用独立 module 结构go.mod 中通过replace geecache ./geecache将geecache包指向本地子目录主程序与缓存库分离。5.2 run.sh一键启动集群并压测为了方便将启动命令封装为 shell 脚本 run.sh#!/bin/bash trap rm server;kill 0 EXIT go build -o server ./server -port8001 ./server -port8002 ./server -port8003 -api1 sleep 2 echo start test curl http://localhost:9999/api?keyTom curl http://localhost:9999/api?keyTom curl http://localhost:9999/api?keyTom waittrap rm server;kill 0 EXIT用于在 shell 脚本退出时删除临时编译产物并结束全部子进程避免残留后台服务依次在 8001、8002、8003 启动缓存节点其中 8003 同时开启 API 服务9999等待 2 秒后并发发起 3 个?keyTom请求进行测试。运行./run.sh输出如下$ ./run.sh 2020/02/16 21:17:43 geecache is running at http://localhost:8001 2020/02/16 21:17:43 geecache is running at http://localhost:8002 2020/02/16 21:17:43 geecache is running at http://localhost:8003 2020/02/16 21:17:43 fontend server is running at http://localhost:9999 start test 2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001 2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001 2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001 ... 630630630此时可以另开一个 shell 手动验证$ curl http://localhost:9999/api?keyTom 630 $ curl http://localhost:9999/api?keykkk kkk not exist日志清晰地展示了分布式协作过程API 服务运行在 8003 节点上收到 3 个并发请求后经一致性哈希全部选择了节点 8001由 8001 的服务端返回缓存值3 次请求最终输出630630630——节点选择与远程 HTTP 通信已经全链路打通。5.3 暴露的问题缓存击穿风险测试时并发 3 个?keyTom请求日志显示三次都选中了节点 8001这是一致性哈希算法相同 key 稳定映射到相同节点的功劳。但这同时暴露了一个隐患假如有 10 万个并发请求同一数据就会向 8001 同时发起 10 万次请求如果 8001 此时又同时向数据库发起 10 万次查询极易导致缓存被击穿cache breakdown。三次请求结果一致对相同的 key 完全可以在第一次请求后就只发一次远程请求其余请求共享结果。这正是第六天singleflight请求合并要解决的问题本文暂不展开。6 小结第五天为 GeeCache 补上了分布式缓存最关键的一环能力载体说明节点选择接口PeerPicker/PickPeer按 key 经一致性哈希选出远程节点节点访问接口PeerGetter/Get从远程 group 获取缓存值HTTP 客户端httpGetter拼接 URL、发请求、读响应约 90 行节点注册HTTPPool.Set初始化哈希环并创建全部 httpGetter主流程集成Group.load/RegisterPeers/getFromPeer远程优先、失败回退本地至此GeeCache 已经具备完整的三级查询能力本地缓存命中 → 远程节点获取 → 回调数据源加载并回填。下一步第六天将针对并发重复请求带来的缓存击穿问题引入 singleflight 机制做请求合并。进一步阅读系列前文可参考 geecache-day4.md一致性哈希、geecache-day3.mdHTTP 服务端后续演进见 geecache-day6.mdsingleflight。赞分享示例工程【免费下载链接】7days-golang7 days golang programs from scratch (web framework Gee, distributed cache GeeCache, object relational mapping ORM framework GeeORM, rpc framework GeeRPC etc) 7天用Go动手写/从零实现系列项目地址https://gitcode.com/gh_mirrors/7d/7days-golang点击查看免费下载相关推荐深入理解GeeCache分布式缓存第五天实现分布式节点深入理解GeeCache分布式缓存第五天实现分布式节点 前言 在分布式缓存系统的开发过程中节点间的通信与协作是核心挑战之一。本文将深入探讨如何为GeeCac示例工程JGit调试技巧终极指南如何追踪代码变更和问题排查JGit调试技巧终极指南如何追踪代码变更和问题排查 JGit Cookbook是一个为Java开发者提供JGit Git实现示例和代码片段的宝贵资源库。这个项示例工程7天用Go实现分布式缓存GeeCache从零到实战7天用Go实现分布式缓存GeeCache从零到实战 分布式缓存系统概述 在现代互联网应用中缓存系统扮演着至关重要的角色。一个设计良好的缓存系统能够显著提升应示例工程上一篇深度解析garage强化学习研究与开发的多功能工具下一篇ExpressoTS用例(UseCase)模式最佳实践业务逻辑分层架构指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考