Open geektutu opened 4 years ago
感谢博主,一系列的博文看完后,感觉自己的知识架构更加清楚了。 想请教一下博主,resp是否需要先判空后再defer resp.Body.Close()?如果不判空的话,会留下隐患
main.go 58 行。addrs := make([]string, 3)
应该是 addrs := make([]string, 0)
吧,如果一开始长度是 3 ,后边再 append 会得到前边是三个空字符串的切片。
@ylwang1122 如果 err != nil
,前面已经返回了,不会到 defer 语句,我看了下 http.Get
的实现,如果 err == nil
,resp 是不会为空的。
@PegasusWang,感谢纠错,已经更正。因为后面用了 append,而不是按索引赋值,所以可以不用初始化,改成了:var addrs []string
感谢博主的文章,是不是还没有实现节点异常的处理。
@Kingpie 仿照的 groupcache 的实现,假定节点是可用的,不包含异常的处理。
geecache 对一致性没有要求,所以也没有必要。需要异常处理的一般是实现 CAP 理论中的 CP,比如分布式数据库,消息系统等。有节点宕机时,需要重新选举 master 以保证一致性。后续会考虑此类系统的实现。
感谢博主的文章, 这里有一个疑惑想问一下博主,如果要将GeeCache进行横向扩展的话,应该如何部署,可不可以将peer部署到其他机器上
@catwithtudou 把 IP 和端口换成部署机器的 IP 和端口就可以了。只是测试用例中,在本机启动了三个实例,基于网络通信,部署在哪里都可以。
是不是漏掉了从远程节点拿到缓存后更新本地缓存这一步?
是不是漏掉了从远程节点拿到缓存后更新本地缓存这一步?
groupcache 中缓存值只淘汰不更新,也没有超时淘汰机制,这样取舍简化了设计。
@geektutu
是不是漏掉了从远程节点拿到缓存后更新本地缓存这一步?
groupcache 中缓存值只淘汰不更新,也没有超时淘汰机制,这样取舍简化了设计。
我的意思是当请求当前缓存服务器时 此服务器本地没有缓存 接着由当前服务器去请求其他节点服务器 当拿回来缓存值后 不应该更新此服务器的本地缓存吗 ?
@walkmiao 分布式缓存的目的是不同key缓存在不同的节点上,增加总的吞吐量。如果大家转发请求后,都再备份一次,每台机器上都缓存了相同的数据,就失去意义了。每个节点缓存1G数据,理论上10个节点总共可以缓存10G不同的数据。
当然对于热点数据,每个节点拿到值后,本机备份一次是有价值的,增加热点数据的吞吐量。groupcache 的原生实现中,有1/10的概率会在本机存一次。这样10个节点,理论上可以缓存9G不同的数据,算是一种取舍。
@geektutu @walkmiao 分布式缓存的目的是不同key缓存在不同的节点上,增加总的吞吐量。如果大家转发请求后,都再备份一次,每台机器上都缓存了相同的数据,就失去意义了。每个节点缓存1G数据,理论上10个节点总共可以缓存10G不同的数据。
当然对于热点数据,每个节点拿到值后,本机备份一次是有价值的,增加热点数据的吞吐量。groupcache 的原生实现中,有1/10的概率会在本机存一次。这样10个节点,理论上可以缓存9G不同的数据,算是一种取舍。
学到很多 谢谢
var PeerPicker = (*HTTPPool)(nil) var PeerGetter = (*httpGetter)(nil) 请问这两个操作有什么意义呢, 不是很明白
@mjyuser var PeerPicker = (*HTTPPool)(nil) var PeerGetter = (*httpGetter)(nil) 请问这两个操作有什么意义呢, 不是很明白
确保这个类型实现了这个接口 如果没有实现会报错的
请教博主: 如何才能缓存多个group? 是通过go 开辟新goroutine吗?或者是给多个group注册相同的HTTPPool?
如何才能缓存多个group?
都可以,group 和 HTTPPool 是解耦的,可以复用,也可以各自搭配各自的。
博主你好,请问回调函数中获取数据的数据源,是不是可以不一定来自节点本地数据库。
请教一下: 之前在RegisterPeers中如果已经有注册的peers,则不能注册,在main函数中,如果设置了三个端口号那不是在同一个group上注册了三个不同端口的HTTPPool吗?这里是怎么实现的呀?
请教一下, 整片文章看了一遍之后, 我还是有点不明白. 用户只知晓API:9999.那么API与分布式缓存是一个1对3的关系. 那么当用户查询的时候,首先查询的get是三个缓存中的哪一个呢? 还是说API本身是一个group 本地有, 没有再去三个缓存中找呢? 这个模型实在是没搞清晰 希望指点
请教一下, 整片文章看了一遍之后, 我还是有点不明白. 用户只知晓API:9999.那么API与分布式缓存是一个1对3的关系. 那么当用户查询的时候,首先查询的get是三个缓存中的哪一个呢? 还是说API本身是一个group 本地有, 没有再去三个缓存中找呢? 这个模型实在是没搞清晰 希望指点
API用consistent hasher 去算出应该去哪个port,每个port都被放到了hash环上。
第一步就抽象出接口,感觉不是很好理解。根据这篇文章 https://blog.chewxy.com/2018/03/18/golang-interfaces/ ,是否可以考虑延迟定义接口。即首先定义类型/struct,在会使用到接口的时候再定义接口。
成功被大佬精湛的技术绕晕了
@Willendless 一般抽象出接口是为了扩展性,很多场景下需要优先抽象接口。比如 RPC 通信需要支持不同的编解码方式,那首先想到的是一个支持编解码的结构体需要支持哪些方法,即接口。GeeRPC第一天 服务端与消息编码 在这篇文章中体现了这种思考方式。
我觉得你说的也是有道理的,如果是比较确定的业务,优先 struct,需要扩展时再抽象接口更符合直觉一些。不过对于实现框架的童鞋来说,可扩展性是第一位的,所以一般都会优先设计接口。比如 go-micro 这个微服务框架,所有的参数都是接口类型的,这个框架是完全可插拔的,允许用户替换任意的类,只要 struct 实现了接口就行。
@wilgx0 成功被大佬精湛的技术绕晕了
group代表一类资源,一个group结构体代表的是一个节点,存储了一部分缓存数据,具体一个值是到哪个具体节点读取则是由算法决定的(这里是一致性哈希算法,一致性哈希算法可以理解为把地址空间化成一个圈,键值的Key先遇到谁就是谁,具体的看代码实现)。group中注册了一个peers数组,相当于是其他节点的电话簿,是用作在缓存miss的时候向其他节点请求数据用。而这个peer其实就是先前定义的httppool,定义了节点所在的Ip、端口。 以上个人理解,有错误请帮忙指出。
我觉得难理解的地方是从 getFromPeer开始获取bytes,
过程中调用的(h *httpGetter) Get(group string, key string) ([]byte, error)
方法
其中res, err := http.Get(u)
是直接到了ServeHTTP。
这个流程中又走了一遍(g *Group) load(key string) (value ByteView, err error)
这里有点绕。我是加了好多好多trace才看明白的。
感觉作者有必要整体梳理一下流程
主要是各种Get都Get迷了
@limaoxiaoer 感谢你的建议,这部分相比 groupcache
的原生实现还是简化了很多了。确实是需要一定的基础,不然看上去会比较费力。比如 http.Get(u)
直接到了 ServeHTTP
,这一块如果没有用 Go 实现过网站,就挺不好理解的。ServeHTTP
是 net/http
的接口,实现了这个接口,请求就会被路由到实现函数来。
另外,geecache 既作为存储的实例,提供 http 接口,又可以作为 API 层,供应用程序直接调用,这两个功能的切换也可能会比较绕。
这个系列最大程度还原 groupcache
的实现,而且做了大量的简化,不过博客的解释可能确实不够清晰,见谅。
是我自己理解错了 因为consistenthash.Map.Add(keys) 这里的keys运行时是用的URL而不是"Tom"之类数据的"key", 所以实际的数据的key还是能被分配到不同主机上的,只是作者这里的例子Tom和Jack运气不好恰好分到一台主机上了。 ———————————————————————————— 有个疑问,一致性哈希这章里使用 idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash }) 这样的寻找方式,不会导致任何查询(不管是不是同一个key)永远都停在8001这第一台主机吗
我怎么感觉这个代码有点问题,因为节点之间可能会循环请求对方的缓存
@wanboyan 我怎么感觉这个代码有点问题,因为节点之间可能会循环请求对方的缓存
每个节点的hash算法都是一致的,对于一个key,所有节点算出来的远程节点都是同一个。所以应该不存在循环请求的问题
说一个我遇到的坑😭😭,在访问localhost:8001这个url的时候,我本地一直会报 curl: (52) Empty reply from server。改成127.0.0.1:8001或者干脆别用这个端口就没有这个问题。有碰到这个问题的同学可以试下
@jeffmingup 感谢反馈,可能和 hosts 的设置有关系。
@geektutu @Kingpie 仿照的 groupcache 的实现,假定节点是可用的,不包含异常的处理。
geecache 对一致性没有要求,所以也没有必要。需要异常处理的一般是实现 CAP 理论中的 CP,比如分布式数据库,消息系统等。有节点宕机时,需要重新选举 master 以保证一致性。后续会考虑此类系统的实现。
应该是不支持动态的横向扩展,现在需要把一致性hash中的物理节点提前写在配置中,或者map中
我觉得TUTU,你这个main.go文件写的过于复杂了,不便于理解基础业务逻辑,这启动三个CacheServer这里,就需要三次启动才比较好理解,主要把注册peer表现完整就好多了。
// Overall flow char requsets local
// gee := createGroup() --------> /api Service : 9999 ------------------------------> gee.Get(key) ------> g.mainCache.Get(key)
// | ^ |
// | | |remote
// v | v
// cache Service : 800x | g.peers.PickPeer(key)
// |create hash ring & init peerGetter | |
// |registry peers write in g.peer | |p.httpGetters[p.hashRing(key)]
// v | |
// httpPool.Set(otherAddrs...) | v
// g.peers = gee.RegisterPeers(httpPool) | g.getFromPeer(peerGetter, key)
// | | |
// | | |
// v | v
// http.ListenAndServe("localhost:800x", httpPool)<------------+--------------peerGetter.Get(key)
// | |
// |requsets |
// v |
// p.ServeHttp(w, r) |
// | |
// |url.parse() |
// |--------------------------------------------
有一个问题不懂,感觉apiServer提供的服务更像是负载均衡,为什么要先检查是否被缓存?先找到key对应的节点,再从那个节点从找到需要的值可以吗?
那个load就太大了
Thanks, Cong Wang On Apr 6, 2021, 06:14 -0700, zyy @.***>, wrote:
有一个问题不懂,感觉apiServer提供的服务更像是负载均衡,为什么要先检查是否被缓存?先找到key对应的节点,再从那个节点从找到需要的值可以吗? — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or unsubscribe.
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 } 为什么根据查询key 获取节点要加锁呢,那这样的话是不是无法并发查询了
对于一个HTTPPool的peers里加入它本身,会不会PickPeer时选择到自身,导致进入死循环,我在测试的时候一直在死循环,想请教一下
@fy403
// Overall flow char requsets local // gee := createGroup() --------> /api Service : 9999 ------------------------------> gee.Get(key) ------> g.mainCache.Get(key) // | ^ | // | | |remote // v | v // cache Service : 800x | g.peers.PickPeer(key) // |create hash ring & init peerGetter | | // |registry peers write in g.peer | |p.httpGetters[p.hashRing(key)] // v | | // httpPool.Set(otherAddrs...) | v // g.peers = gee.RegisterPeers(httpPool) | g.getFromPeer(peerGetter, key) // | | | // | | | // v | v // http.ListenAndServe("localhost:800x", httpPool)<------------+--------------peerGetter.Get(key) // | | // |requsets | // v | // p.ServeHttp(w, r) | // | | // |url.parse() | // |--------------------------------------------
请问这个是用啥工具画的呀
@zach030 对于一个HTTPPool的peers里加入它本身,会不会PickPeer时选择到自身,导致进入死循环,我在测试的时候一直在死循环,想请教一下
并不会,因为PickPeer方法里面有peer != p.self
的判断
@njcongtou
请教一下, 整片文章看了一遍之后, 我还是有点不明白. 用户只知晓API:9999.那么API与分布式缓存是一个1对3的关系. 那么当用户查询的时候,首先查询的get是三个缓存中的哪一个呢? 还是说API本身是一个group 本地有, 没有再去三个缓存中找呢? 这个模型实在是没搞清晰 希望指点
API用consistent hasher 去算出应该去哪个port,每个port都被放到了hash环上。
并不是api用一致性hash算,而是api服务绑定了一个本地的geecache服务,这个服务miss的时候,才会去寻找其他结点
@dayou5168 我觉得TUTU,你这个main.go文件写的过于复杂了,不便于理解基础业务逻辑,这启动三个CacheServer这里,就需要三次启动才比较好理解,主要把注册peer表现完整就好多了。
是的。本来逻辑通了。 到main.go 开始又混乱了
小白请教一下,在调试的时候,我用协程分别起了三次startCacheServer,然而在debug的时候,发现无法debug HTTPPool.ServeHTTP()函数。我加了log发现这个函数一直在开启新的协程而不返回。请教下为啥不能用协程的方式启动三个startCacheServer呢?代码如下:
for k,_:=range addrMap{ register:=k==8003 go startCacheServer(addrMap[k],addrs,gee,register) } startAPIServer(apiAddr,gee)
获取缓存数据的流程是:从本地缓存查找->从远程节点查找->回调函数,写到本地。如果是这样的话,远程节点是不是就一直没有缓存到数据。数据要么是本地缓存直接得到,要么是本地和远程都找不到,然后回调,写到本地缓存。不知道我理解的正确吗?
@chens72 获取缓存数据的流程是:从本地缓存查找->从远程节点查找->回调函数,写到本地。如果是这样的话,远程节点是不是就一直没有缓存到数据。数据要么是本地缓存直接得到,要么是本地和远程都找不到,然后回调,写到本地缓存。不知道我理解的正确吗?
不对。 远程节点查找的时候,如果在远程节点的缓存中找不到,是调用远程节点的回调函数,存储到远程节点
@cuglaiyp
@fy403
// Overall flow char requsets local // gee := createGroup() --------> /api Service : 9999 ------------------------------> gee.Get(key) ------> g.mainCache.Get(key) // | ^ | // | | |remote // v | v // cache Service : 800x | g.peers.PickPeer(key) // |create hash ring & init peerGetter | | // |registry peers write in g.peer | |p.httpGetters[p.hashRing(key)] // v | | // httpPool.Set(otherAddrs...) | v // g.peers = gee.RegisterPeers(httpPool) | g.getFromPeer(peerGetter, key) // | | | // | | | // v | v // http.ListenAndServe("localhost:800x", httpPool)<------------+--------------peerGetter.Get(key) // | | // |requsets | // v | // p.ServeHttp(w, r) | // | | // |url.parse() | // |--------------------------------------------
请问这个是用啥工具画的呀
同问呀,有知道的小伙伴么?
兔兔,我有个问题,就是如果我想要新增一个节点怎么办呢,需要重新启动,然后利用peers.Set(addrs...)把新增后的所有节点加进来吗
兔兔,我有个问题,就是如果我想要新增一个节点怎么办呢,需要重新启动,然后利用peers.Set(addrs...)把新增后的所有节点加进来吗
对,作者他这个版本只能这样。后续可以将缓存服务注册到注册中心,通过服务发现获取所有节点IP。可以参看我实现的版本:https://github.com/peanutzhen/peanutcache
@peanutzhen
兔兔,我有个问题,就是如果我想要新增一个节点怎么办呢,需要重新启动,然后利用peers.Set(addrs...)把新增后的所有节点加进来吗
对,作者他这个版本只能这样。后续可以将缓存服务注册到注册中心,通过服务发现获取所有节点IP。可以参看我实现的版本:https://github.com/peanutzhen/peanutcache
已star哈哈
https://geektutu.com/post/geecache-day5.html
7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了为 GeeCache 添加了注册节点与选择节点的功能,并实现了 HTTP 客户端,与远程节点的服务端通信。