geektutu / blog

极客兔兔的博客,Coding Coding 创建有趣的开源项目。
https://geektutu.com
Apache License 2.0
166 stars 21 forks source link

动手写分布式缓存 - GeeCache第五天 分布式节点 | 极客兔兔 #68

Open geektutu opened 4 years ago

geektutu commented 4 years ago

https://geektutu.com/post/geecache-day5.html

7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了为 GeeCache 添加了注册节点与选择节点的功能,并实现了 HTTP 客户端,与远程节点的服务端通信。

ylwangs commented 4 years ago

感谢博主,一系列的博文看完后,感觉自己的知识架构更加清楚了。 想请教一下博主,resp是否需要先判空后再defer resp.Body.Close()?如果不判空的话,会留下隐患

PegasusWang commented 4 years ago

main.go 58 行。addrs := make([]string, 3) 应该是 addrs := make([]string, 0) 吧,如果一开始长度是 3 ,后边再 append 会得到前边是三个空字符串的切片。

geektutu commented 4 years ago

@ylwang1122 如果 err != nil,前面已经返回了,不会到 defer 语句,我看了下 http.Get 的实现,如果 err == nil,resp 是不会为空的。

geektutu commented 4 years ago

@PegasusWang,感谢纠错,已经更正。因为后面用了 append,而不是按索引赋值,所以可以不用初始化,改成了:var addrs []string

Kingpie commented 4 years ago

感谢博主的文章,是不是还没有实现节点异常的处理。

geektutu commented 4 years ago

@Kingpie 仿照的 groupcache 的实现,假定节点是可用的,不包含异常的处理。

geecache 对一致性没有要求,所以也没有必要。需要异常处理的一般是实现 CAP 理论中的 CP,比如分布式数据库,消息系统等。有节点宕机时,需要重新选举 master 以保证一致性。后续会考虑此类系统的实现。

catwithtudou commented 4 years ago

感谢博主的文章, 这里有一个疑惑想问一下博主,如果要将GeeCache进行横向扩展的话,应该如何部署,可不可以将peer部署到其他机器上

geektutu commented 4 years ago

@catwithtudou 把 IP 和端口换成部署机器的 IP 和端口就可以了。只是测试用例中,在本机启动了三个实例,基于网络通信,部署在哪里都可以。

walkmiao commented 4 years ago

是不是漏掉了从远程节点拿到缓存后更新本地缓存这一步?

geektutu commented 4 years ago

是不是漏掉了从远程节点拿到缓存后更新本地缓存这一步?

groupcache 中缓存值只淘汰不更新,也没有超时淘汰机制,这样取舍简化了设计。

walkmiao commented 4 years ago

@geektutu

是不是漏掉了从远程节点拿到缓存后更新本地缓存这一步?

groupcache 中缓存值只淘汰不更新,也没有超时淘汰机制,这样取舍简化了设计。

我的意思是当请求当前缓存服务器时 此服务器本地没有缓存 接着由当前服务器去请求其他节点服务器 当拿回来缓存值后 不应该更新此服务器的本地缓存吗 ?

geektutu commented 4 years ago

@walkmiao 分布式缓存的目的是不同key缓存在不同的节点上,增加总的吞吐量。如果大家转发请求后,都再备份一次,每台机器上都缓存了相同的数据,就失去意义了。每个节点缓存1G数据,理论上10个节点总共可以缓存10G不同的数据。

当然对于热点数据,每个节点拿到值后,本机备份一次是有价值的,增加热点数据的吞吐量。groupcache 的原生实现中,有1/10的概率会在本机存一次。这样10个节点,理论上可以缓存9G不同的数据,算是一种取舍。

walkmiao commented 4 years ago

@geektutu @walkmiao 分布式缓存的目的是不同key缓存在不同的节点上,增加总的吞吐量。如果大家转发请求后,都再备份一次,每台机器上都缓存了相同的数据,就失去意义了。每个节点缓存1G数据,理论上10个节点总共可以缓存10G不同的数据。

当然对于热点数据,每个节点拿到值后,本机备份一次是有价值的,增加热点数据的吞吐量。groupcache 的原生实现中,有1/10的概率会在本机存一次。这样10个节点,理论上可以缓存9G不同的数据,算是一种取舍。

学到很多 谢谢

mjyuser commented 4 years ago

var PeerPicker = (*HTTPPool)(nil) var PeerGetter = (*httpGetter)(nil) 请问这两个操作有什么意义呢, 不是很明白

walkmiao commented 4 years ago

@mjyuser var PeerPicker = (*HTTPPool)(nil) var PeerGetter = (*httpGetter)(nil) 请问这两个操作有什么意义呢, 不是很明白

确保这个类型实现了这个接口 如果没有实现会报错的

201732110125 commented 4 years ago

请教博主: 如何才能缓存多个group? 是通过go 开辟新goroutine吗?或者是给多个group注册相同的HTTPPool?

geektutu commented 4 years ago

如何才能缓存多个group?

都可以,group 和 HTTPPool 是解耦的,可以复用,也可以各自搭配各自的。

Assiduous-donkey commented 4 years ago

博主你好,请问回调函数中获取数据的数据源,是不是可以不一定来自节点本地数据库。

Leoooo-tqp commented 3 years ago

请教一下: 之前在RegisterPeers中如果已经有注册的peers,则不能注册,在main函数中,如果设置了三个端口号那不是在同一个group上注册了三个不同端口的HTTPPool吗?这里是怎么实现的呀?

wangy2333 commented 3 years ago

请教一下, 整片文章看了一遍之后, 我还是有点不明白. 用户只知晓API:9999.那么API与分布式缓存是一个1对3的关系. 那么当用户查询的时候,首先查询的get是三个缓存中的哪一个呢? 还是说API本身是一个group 本地有, 没有再去三个缓存中找呢? 这个模型实在是没搞清晰 希望指点

ghost commented 3 years ago

请教一下, 整片文章看了一遍之后, 我还是有点不明白. 用户只知晓API:9999.那么API与分布式缓存是一个1对3的关系. 那么当用户查询的时候,首先查询的get是三个缓存中的哪一个呢? 还是说API本身是一个group 本地有, 没有再去三个缓存中找呢? 这个模型实在是没搞清晰 希望指点

API用consistent hasher 去算出应该去哪个port,每个port都被放到了hash环上。

Willendless commented 3 years ago

第一步就抽象出接口,感觉不是很好理解。根据这篇文章 https://blog.chewxy.com/2018/03/18/golang-interfaces/ ,是否可以考虑延迟定义接口。即首先定义类型/struct,在会使用到接口的时候再定义接口。

wilgx0 commented 3 years ago

成功被大佬精湛的技术绕晕了

geektutu commented 3 years ago

@Willendless 一般抽象出接口是为了扩展性,很多场景下需要优先抽象接口。比如 RPC 通信需要支持不同的编解码方式,那首先想到的是一个支持编解码的结构体需要支持哪些方法,即接口。GeeRPC第一天 服务端与消息编码 在这篇文章中体现了这种思考方式。

我觉得你说的也是有道理的,如果是比较确定的业务,优先 struct,需要扩展时再抽象接口更符合直觉一些。不过对于实现框架的童鞋来说,可扩展性是第一位的,所以一般都会优先设计接口。比如 go-micro 这个微服务框架,所有的参数都是接口类型的,这个框架是完全可插拔的,允许用户替换任意的类,只要 struct 实现了接口就行。

Pissssofshit commented 3 years ago

@wilgx0 成功被大佬精湛的技术绕晕了

group代表一类资源,一个group结构体代表的是一个节点,存储了一部分缓存数据,具体一个值是到哪个具体节点读取则是由算法决定的(这里是一致性哈希算法,一致性哈希算法可以理解为把地址空间化成一个圈,键值的Key先遇到谁就是谁,具体的看代码实现)。group中注册了一个peers数组,相当于是其他节点的电话簿,是用作在缓存miss的时候向其他节点请求数据用。而这个peer其实就是先前定义的httppool,定义了节点所在的Ip、端口。 以上个人理解,有错误请帮忙指出。

limaoxiaoer commented 3 years ago

我觉得难理解的地方是从 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迷了

geektutu commented 3 years ago

@limaoxiaoer 感谢你的建议,这部分相比 groupcache 的原生实现还是简化了很多了。确实是需要一定的基础,不然看上去会比较费力。比如 http.Get(u) 直接到了 ServeHTTP,这一块如果没有用 Go 实现过网站,就挺不好理解的。ServeHTTPnet/http 的接口,实现了这个接口,请求就会被路由到实现函数来。

另外,geecache 既作为存储的实例,提供 http 接口,又可以作为 API 层,供应用程序直接调用,这两个功能的切换也可能会比较绕。

这个系列最大程度还原 groupcache 的实现,而且做了大量的简化,不过博客的解释可能确实不够清晰,见谅。

UnbearableFate commented 3 years ago

是我自己理解错了 因为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 commented 3 years ago

我怎么感觉这个代码有点问题,因为节点之间可能会循环请求对方的缓存

FinaLone commented 3 years ago

@wanboyan 我怎么感觉这个代码有点问题,因为节点之间可能会循环请求对方的缓存

每个节点的hash算法都是一致的,对于一个key,所有节点算出来的远程节点都是同一个。所以应该不存在循环请求的问题

jeffmingup commented 3 years ago

说一个我遇到的坑😭😭,在访问localhost:8001这个url的时候,我本地一直会报 curl: (52) Empty reply from server。改成127.0.0.1:8001或者干脆别用这个端口就没有这个问题。有碰到这个问题的同学可以试下

geektutu commented 3 years ago

@jeffmingup 感谢反馈,可能和 hosts 的设置有关系。

monkeyskings commented 3 years ago

@geektutu @Kingpie 仿照的 groupcache 的实现,假定节点是可用的,不包含异常的处理。

geecache 对一致性没有要求,所以也没有必要。需要异常处理的一般是实现 CAP 理论中的 CP,比如分布式数据库,消息系统等。有节点宕机时,需要重新选举 master 以保证一致性。后续会考虑此类系统的实现。

应该是不支持动态的横向扩展,现在需要把一致性hash中的物理节点提前写在配置中,或者map中

dayou5168 commented 3 years ago

我觉得TUTU,你这个main.go文件写的过于复杂了,不便于理解基础业务逻辑,这启动三个CacheServer这里,就需要三次启动才比较好理解,主要把注册peer表现完整就好多了。

fy403 commented 3 years ago
// 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()                                |
//                      |--------------------------------------------
yingying1999 commented 3 years ago

有一个问题不懂,感觉apiServer提供的服务更像是负载均衡,为什么要先检查是否被缓存?先找到key对应的节点,再从那个节点从找到需要的值可以吗?

ghost commented 3 years ago

那个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.

banzhihang commented 3 years ago

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 获取节点要加锁呢,那这样的话是不是无法并发查询了

zach030 commented 3 years ago

对于一个HTTPPool的peers里加入它本身,会不会PickPeer时选择到自身,导致进入死循环,我在测试的时候一直在死循环,想请教一下

cuglaiyp commented 3 years ago

@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()                                |
//                        |--------------------------------------------

请问这个是用啥工具画的呀

cuglaiyp commented 3 years ago

@zach030 对于一个HTTPPool的peers里加入它本身,会不会PickPeer时选择到自身,导致进入死循环,我在测试的时候一直在死循环,想请教一下

并不会,因为PickPeer方法里面有peer != p.self的判断

cuglaiyp commented 3 years ago

@njcongtou

请教一下, 整片文章看了一遍之后, 我还是有点不明白. 用户只知晓API:9999.那么API与分布式缓存是一个1对3的关系. 那么当用户查询的时候,首先查询的get是三个缓存中的哪一个呢? 还是说API本身是一个group 本地有, 没有再去三个缓存中找呢? 这个模型实在是没搞清晰 希望指点

API用consistent hasher 去算出应该去哪个port,每个port都被放到了hash环上。

并不是api用一致性hash算,而是api服务绑定了一个本地的geecache服务,这个服务miss的时候,才会去寻找其他结点

puck1006 commented 3 years ago

@dayou5168 我觉得TUTU,你这个main.go文件写的过于复杂了,不便于理解基础业务逻辑,这启动三个CacheServer这里,就需要三次启动才比较好理解,主要把注册peer表现完整就好多了。

是的。本来逻辑通了。 到main.go 开始又混乱了

alvinssb commented 3 years ago

小白请教一下,在调试的时候,我用协程分别起了三次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 commented 3 years ago

获取缓存数据的流程是:从本地缓存查找->从远程节点查找->回调函数,写到本地。如果是这样的话,远程节点是不是就一直没有缓存到数据。数据要么是本地缓存直接得到,要么是本地和远程都找不到,然后回调,写到本地缓存。不知道我理解的正确吗?

puck1006 commented 3 years ago

@chens72 获取缓存数据的流程是:从本地缓存查找->从远程节点查找->回调函数,写到本地。如果是这样的话,远程节点是不是就一直没有缓存到数据。数据要么是本地缓存直接得到,要么是本地和远程都找不到,然后回调,写到本地缓存。不知道我理解的正确吗?

不对。 远程节点查找的时候,如果在远程节点的缓存中找不到,是调用远程节点的回调函数,存储到远程节点

DamonGG commented 2 years ago

@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()                                |
//                      |--------------------------------------------

请问这个是用啥工具画的呀

同问呀,有知道的小伙伴么?

sgq123 commented 2 years ago

兔兔,我有个问题,就是如果我想要新增一个节点怎么办呢,需要重新启动,然后利用peers.Set(addrs...)把新增后的所有节点加进来吗

peanutzhen commented 2 years ago

兔兔,我有个问题,就是如果我想要新增一个节点怎么办呢,需要重新启动,然后利用peers.Set(addrs...)把新增后的所有节点加进来吗

对,作者他这个版本只能这样。后续可以将缓存服务注册到注册中心,通过服务发现获取所有节点IP。可以参看我实现的版本:https://github.com/peanutzhen/peanutcache

sgq123 commented 2 years ago

@peanutzhen

兔兔,我有个问题,就是如果我想要新增一个节点怎么办呢,需要重新启动,然后利用peers.Set(addrs...)把新增后的所有节点加进来吗

对,作者他这个版本只能这样。后续可以将缓存服务注册到注册中心,通过服务发现获取所有节点IP。可以参看我实现的版本:https://github.com/peanutzhen/peanutcache

已star哈哈