- 容器运行时
- 云原生
【免费下载链接】docker-ce
:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:
groupcache 是 Google 开源的一套缓存与缓存填充(cache-filling)库,定位于在多数场景下替代 memcached。本文以 docker-ce 仓库 vendor 目录中实际内置的 groupcache 源码为蓝本,深入解析其与 memcached 的设计取舍、
Get("foo")的完整加载链路,以及其 LRU 子包在仓库内的真实用法,帮助读者掌握这套"进程内嵌、分布式去重"的 Go 缓存模型。
概述:groupcache 是什么
根据关联文档 groupcache/README.md 的定义,groupcache 是一个缓存与缓存填充库,其核心定位是"在多数场景下替代 memcached"。它的三个关键词是:
- 缓存(caching):与 memcached 一样,按 key 分片(shard)决定由哪个 peer 负责该 key;
- 缓存填充(cache-filling):在缓存未命中时,由全集群中唯一一个进程负责加载数据,其余调用方阻塞等待并复用同一份结果,从而消除"惊群效应";
- 进程内嵌(in-process):groupcache 既是客户端也是服务端,直接与自己的 peer 进程互连,无需单独部署一组缓存服务器。
在 docker-ce 仓库中,groupcache 以 vendor 依赖的形式内嵌于 components/engine/vendor/github.com/golang/groupcache,并在 components/engine/vendor.conf 中被锁定到提交869f871628b6baa9cfbc11732cdf6546b17c1298。也就是说,本文讲解的正是当前仓库中实际携带、可被编译构建的这套代码。
groupcache 与 memcached 的设计对比
关联文档用两组对比清单,明确了 groupcache 的设计边界。这既是它被设计出来要解决的问题,也是它"替代 memcached"时必须被理解的前提。
与 memcached 相同的点
Like memcached:groupcache 同样按 key 分片(shard by key)以确定哪个 peer 负责该 key。
这是一切分布式缓存的基础:将整个 key 空间通过哈希映射到一组节点上,任何节点收到请求时都能快速判断"这个 key 归谁管"。
与 memcached 不同的点
| 维度 | memcached | groupcache |
|---|---|---|
| 部署形态 | 需要单独运行一组缓存服务器 | 无需独立服务器,库本身既是客户端又是服务端,直接连接自己的 peer 进程 |
| 缓存未命中 | 直接返回 miss,可能引发大量客户端同时回源数据库,形成"惊群效应" | 内置缓存填充机制,整个副本集合中只有一个进程负责填充,然后多路复用(multiplex)结果给所有调用方 |
| 版本化值 | 支持 | 不支持:key"foo"的值一旦是"bar",就必须一直是"bar";既没有过期时间,也没有显式淘汰 |
| CAS / 计数器 | 支持 | 不支持:没有 CAS,也没有 Increment/Decrement |
| 热点处理 | 热门 key 容易把单机 CPU/NIC 打满(hot spotting) | 自动将超热门条目镜像到多个进程,缓解热点瓶颈 |
| 语言 | 多语言 | 目前仅支持 Go,作者(bradfitz)明确表示几乎不会移植到其他语言 |
关联文档特别指出,"没有过期时间、没有显式淘汰、没有版本化值"这个强约束,正是 groupcache 能保证"全局单次填充"的前提——只有当所有 peer 对同一 key 看到的是同一份永不变化的逻辑值时,"谁加载都一样"这个假设才成立。这也意味着:需要频繁更新或需要 TTL 过期语义的业务,不适合直接套用 groupcache 的默认模型。
Get("foo") 的完整加载链路
关联文档以"第 5 号机器上的Get("foo")"为例,给出了一个非常清晰的加载流程。整理成可执行的决策链路如下:
- 本地内存命中(热点层):
"foo"是否因为"超热门"而存在于本地内存?若是,直接使用。 - 本地内存命中(owner 层):
"foo"是否因为"peer #5(当前 peer)就是它的 owner"而存在于本地内存?若是,直接使用。 - 回源加载或 RPC 转发:在所有 N 个 peer 中,当前机器是否是
"foo"的 owner(例如一致性哈希结果是否为 5)?- 是:自己加载。若此时有其他调用方进入——无论来自本进程还是来自 peer 的 RPC 请求——它们会阻塞等待本次加载完成,然后拿到同一份结果(本地重复抑制,local dup suppression)。
- 否:向 owner peer 发起 RPC 获取答案;若 RPC 失败,则回退为本地加载(依然带本地重复抑制)。
这个流程揭示了 groupcache 三个关键机制:
- 两级本地命中:超热门条目会被自动镜像到多个进程,保证任何进程都能在本级命中,避免热点集中于单机;
- 单次填充、多路复用:同一 key 的加载动作在全集群层面只发生一次,其余调用方以阻塞等待的方式共享结果,这正是它消灭"惊群效应"的实现方式;
- RPC 失败回退:owner 不可达时不会直接失败,而是退化到本地加载,保证可用性,同时本地重复抑制仍然生效,不会造成本地重复回源。
与 docker-ce 仓库的关系:作为第三方依赖被内嵌
当前仓库 docker-ce 本身并不直接 import groupcache 的主包,而是通过go.opencensus.io这条依赖链,在分布式追踪的 Span 属性上限管理中实际使用了 groupcache 的LRU 子包。
vendor.conf 中的版本锁定
在 components/engine/vendor.conf 中可以同时看到两条相关锁定记录:
github.com/golang/groupcache 869f871628b6baa9cfbc11732cdf6546b17c1298 go.opencensus.io d835ff86be02193d324330acdb7d65546b05f814 # v0.22.3LRU 子包的真实使用:Span 属性上限管理
groupcache 的 vendor 目录虽然只内嵌了 README、LICENSE 与 lru 子包,但 lru 子包在 go.opencensus.io 中承担着实际职责。在 components/engine/vendor/go.opencensus.io/trace/lrumap.go 中,OpenCensus 用lru.Cache实现了一个带淘汰统计的lruMap:
package trace import ( "github.com/golang/groupcache/lru" ) // A simple lru.Cache wrapper that tracks the keys of the current contents and // the cumulative number of evicted items. type lruMap struct { cacheKeys map[lru.Key]bool cache *lru.Cache droppedCount int } func newLruMap(size int) *lruMap { lm := &lruMap{ cacheKeys: make(map[lru.Key]bool), cache: lru.New(size), droppedCount: 0, } lm.cache.OnEvicted = func(key lru.Key, value interface{}) { delete(lm.cacheKeys, key) lm.droppedCount++ } return lm }这段代码完整地展示了 lru 子包对外暴露的 API 面:
lru.New(size)构造一个容量为size的 LRU 缓存;OnEvicted回调在条目被淘汰时被触发,OpenCensus 借此同步清理cacheKeys索引并累计droppedCount(淘汰统计);add/get/len/keys分别对应 lru.go 中Cache的Add、Get、Len与自维护的 key 集合。
在哪里被配置与调用
- 默认上限:在 components/engine/vendor/go.opencensus.io/trace/config.go 中,
DefaultMaxAttributesPerSpan = 32,即每个 Span 默认最多保留 32 个属性; - 实例化:在 components/engine/vendor/go.opencensus.io/trace/trace.go 中,创建 Span 时执行
span.lruAttributes = newLruMap(cfg.MaxAttributesPerSpan); - 写入与读取:Span 属性的写入通过 lrumap.go 的
add/get完成;在 trace.go 的copyToCappedAttributes中逐条lruAttributes.add(a.key, a.value),并由 trace.go 的lruAttributesToAttributeMap汇总导出。
也就是说:当 Docker 引擎在 OpenCensus 追踪链路中为某个 Span 打属性时,一旦属性数量超过 32,最久未使用的属性会被 lru 机制淘汰——这就是 groupcache 的 LRU 子包在当前仓库中的真实落地。
lru 子包的内部实现与关键 API
lru/lru.go 是当前仓库中唯一可读的 groupcache 源码文件,它用 Go 标准库的container/list(双向链表)配合哈希表,实现了一个经典 LRU:
// Cache is an LRU cache. It is not safe for concurrent access. type Cache struct { // MaxEntries is the maximum number of cache entries before // an item is evicted. Zero means no limit. MaxEntries int // OnEvicted optionally specifies a callback function to be // executed when an entry is purged from the cache. OnEvicted func(key Key, value interface{}) ll *list.List cache map[interface{}]*list.Element } // A Key may be any value that is comparable. type Key interface{}几点值得注意的细节:
- 并发安全由调用方保证:注释明确写着
It is not safe for concurrent access。groupcache 主包的内存缓存是靠全局单锁保护的,而 lru 子包本身不提供锁,OpenCensus 在lruMap外层的Span方法中通过mu sync.Mutex(见 trace.go)来串行化访问。 MaxEntries = 0表示不限制:此时不触发自动淘汰,淘汰动作完全交给调用方。OnEvicted回调:在removeElement中触发(lru.go),Clear()全清时也会逐个触发(lru.go)。这正是 OpenCensus 统计droppedCount的钩子。- 核心 API:
New(maxEntries)、Add(key, value)(存在则移到链表头并更新值)、Get(key)(命中移到链表头)、Remove(key)、RemoveOldest()(淘汰链表尾)、Len()、Clear()。
从源码推断的工程注意点
- 读多写少、值不可变:从"无 TTL、无显式淘汰、无版本化值"的设计反推,groupcache 适合承载"加载开销大但值恒定不变"的数据(例如静态配置、只读的聚合结果、一次性生成的元数据),不适合承载频繁变更的业务数据。
- 热点镜像的代价:超热门条目会被自动复制到多个进程的内存中,用额外的内存开销换取 CPU/NIC 热点消除。因此 groupcache 面向的是"复制成本远低于热点故障成本"的规模化场景。
- 回源失败策略:
Get链路中 RPC 失败会退化到本地加载,这保证了可用性,但也会带来"同一数据在多个进程被重复填充"的可能,属于"局部一致性换可用性"的取舍。 - 在 docker-ce 中的启示:当前仓库对 groupcache 的消费仅限于 lru 子包(Span 属性上限管理),并未使用其分布式 peer 能力。这说明 groupcache 主包的完整分布式模型(一致性哈希、RPC、单次填充)在本仓库中属于"可学习、未启用"的状态;想要在自己的 Go 服务中落地完整能力,需要按官方文档自行引入主包并按需注册 peer 集合。
结语
groupcache 的价值不在于它提供了多少 API,而在于它用一套极其克制的设计(无过期、无淘汰、无版本化、单次填充)换取了分布式缓存场景下最强的"回源去重"保证。通过阅读 docker-ce 仓库中内嵌的这份 vendor 代码,你既能看到它与 memcached 的完整对比,也能从 OpenCensus 对lru子包的真实调用中,理解一个简单 LRU 如何在生产链路里承担"上限管理 + 淘汰统计"的职责。若想在项目中复用这一套模型,建议从 lru.go 开始研读,再结合 groupcache 主包 API 设计自己的缓存填充层。
- 容器运行时
- 云原生
【免费下载链接】docker-ce
:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:
相关推荐
跨平台资源下载工具的完整指南:解锁多平台内容获取新方案
跨平台资源下载工具的完整指南:解锁多平台内容获取新方案 在数字内容日益丰富的今天,用户经常面临无法直接保存网络资源的困扰。res downloader作为一款基
桌面应用网络音视频如何用Simple Live一站式观看多平台直播:3分钟快速上手指南
如何用Simple Live一站式观看多平台直播:3分钟快速上手指南 你是否厌倦了在不同直播应用间来回切换?是否希望有一款工具能在手机、电脑和电视上无缝观看哔哩
音视频直播groupcache缓存填充机制:解决缓存穿透与击穿问题
groupcache缓存填充机制:解决缓存穿透与击穿问题 在高并发系统中,缓存(Cache)是提升性能的关键组件,但缓存穿透(大量请求穿透缓存直击数据库)和缓存
缓存抽象后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考