我的团队在 AWS 中运行的一项服务充分利用了 Memcached(通过 ElastiCache 产品)。我说“好”的使用是因为我们在大多数时候设法达到了 98% 左右的命中率,尽管现在我意识到这是以巨大的成本为代价的——当这个缓存被删除时,它会对应用程序造成重大损失.与传统上缓存 MySQL 查询结果的其他应用程序不同,此特定应用程序存储 GOB 编码的 二进制元数据,但该应用程序所做的超出了本文的范围。当缓存的条目不存在时,应用程序必须做一些合理的工作来重新生成它并将其存储回去。
最近我观察到,当我们的一个 ElastiCache 节点重新启动时(这可能是为了维护或由于系统故障),我们已经看到对应用程序的影响不太理想。对于相同的整体集群容量,我们可以通过在集群中拥有更多实例且每个实例的容量更少来最大限度地减少这种影响。因此,从我们损失 33% 的缓存容量的 3 个节点到我们将损失 12.5% 的缓存容量的 8 个节点是一个更好的情况。我还意识到我们可以升级到最新一代的缓存节点,这使交易变得更加有利。
出现的问题是:如何在对应用程序和用户体验的影响最小的情况下循环退出 ElastiCache 集群?在这里长话短说,我会告诉你,没有办法将集群中的单个节点更改为不同的类型,如果你在 CloudFormation 中维护你的配置并在那里更改实例类型,你将破坏整个集群然后重新创建它——在这个过程中丢失你的缓存(事实上你将在短时间内没有任何缓存)。我决定完全创建一个新的 CloudFormation 堆栈,预热缓存,然后慢慢地使其运行。
如何预热缓存?理想情况下,您可以转储全部内容并将其简单地插入到新集群中(很像 MySQL 转储或备份),但使用 Memcached 这是不可能的。 Memcached 有一个
stats cachedump
命令,它能够转储给定 slab 的前 2MB 键。如果您不知道 Memcached 如何存储其数据,它会将内存分配分成各种大小递增的“板”,并将值存储在最适合它的大小的板中(尽管总是四舍五入)。因此,数据在内部被分段。您可以使用
stats slabs
列出所有当前 slab 的统计信息,然后使用
stats cachedump {slab} {limit}
执行密钥转储。
这有几个问题。一个是前面提到的对返回数据的 2MB 限制,在我的例子中,这实际上限制了这种方法的有用性。有些 slab 有几十万个对象,我几乎无法检索整个键空间。其次,围绕 Memcached 的开发者社区反对这个命令的持续生命周期,它可能会在未来被删除(也许它已经是,我不确定,但至少它仍然存在于 1.4.14 我' m using) – 我相信他们有充分的理由这样做。我还担心使用该命令会锁定内部数据结构并导致访问服务器的应用程序出现操作问题。
您可以
在这里看到描述此操作的锁定特性的不太令人放心的函数注释
。果然,关键部分被
pthread_mutex_lock
正确锁定在 slab 的 LRU 锁上,我认为这意味着只有缓存逐出会受到此锁的影响。基于一些测试(和常识),我怀疑它只是名义上的 LRU 锁,更普遍的是在写入的情况下锁定数据结构(尽管它也在某处记录缓存访问统计信息,也许在另一个结构中) .在任何情况下,如前所述,我只能从我的集群中检索总密钥空间的一小部分,因此除了是一项危险的练习外,使用
stats cachedump
命令对我最初的目的没有用。
当天晚些时候,我决定改为检索最近几天的 Elastic LoadBalancer 日志,对它们运行 awk 以提取请求路径(对于某些会触发缓存填充的请求),然后简单地向新集群发出相同的请求.由于 ELB 日志可能非常大,而且不幸的是没有压缩,因此预先需要付出更多的努力,但幸运的是 awk 非常快。这种方法(或与此相关的任何方法)的第二部分是使用 Vegeta 来“攻击”您的新机器集群,重播您从 ELB 日志中提取的先前请求。
一种更具冒险性的方法可能是使用 Elastic MapReduce 解析日志,提取请求路径,并使用流式 API 调用将向 ELB 发出 HTTP 请求的外部脚本。这样你就可以很好地分担从更长的时间段发出大量并行请求的工作,以便更彻底地用历史请求预热缓存。或者经常轮询您的日志存储,并在 ELB 请求发生在您的主集群上后将其重播到新集群,只需很短的延迟。如果您尝试其中任何一个并取得了一些成功,请告诉我!