分布式缓存系统(例如redis和memcached客户端)通常以以下方式工作:

  1. 应用程序通过密钥向客户端请求缓存的数据。
  2. 客户端对密钥执行一致的哈希,以确定哪个节点拥有数据
  3. 客户端节点发出网络请求
  4. 如果找到,节点将返回数据。
  5. 应用程序检查是否返回了数据,否则从数据库渲染或获取数据。
  6. 应用程序告诉客户端存储该密钥的数据。
  7. 客户端对密钥执行一致的哈希,以确定哪个节点应拥有数据。
  8. 客户端将数据存储在节点上。

    观察此流程,可以看出一些含义:

  9. 每个缓存请求都会导致节点往返,无论缓存是否命中。

  10. 您无法通过在本地缓存值来避免到节点的往返,因为远程节点可能在没有Apps知识的情况下随时使数据无效。

尽管对于大多数应用程序而言,这些影响都不是特别麻烦,但数据库的额外往返行程可能会对高性能,低延迟的应用程序产生影响。但是,还有另一种可能不是立即显而易见的含义,即惊群

惊群效应

惊群效应问题有时被称为缓存踩踏,雪崩或斜线效果。从本质上讲,就像是使系统不堪重负的大量请求。

在此讨论中,我们的惊群效应是对高速缓存未命中的响应。例如,在正常操作中,只要数据保持高速缓存,应用程序就可以在高负载下保持响应。当缓存中没有数据时,应用程序的并发实例都会尝试同时渲染或访问数据。取决于您的应用程序和服务器场中并发实例/线程的数量,这种并发工作的浪潮可能会使系统不堪重负,并导致系统拥塞,并可能导致崩溃。

为了与并发工作作斗争,您需要一个系统来同步数据的获取或呈现。幸运的是,有一个名为groupcache的golang库,可用于解决惊群效应问题并改善上述远程缓存的含义。

组缓存(GroupCache)

GroupCache与redis和memcache不同,因为它直接与您的代码集成为In Code Distributed Cache(ICDC)。这意味着该应用程序的每个实例都是分布式缓存中的一个节点。作为分布式缓存的完整成员,每个应用程序实例不仅知道如何为节点存储数据,而且还知道如何在丢失数据时获取或呈现数据。

要了解为什么它优于redis或memcached,请在使用groupcache时通过分布式缓存流进行运行。仔细阅读流时,请记住GroupCache是应用程序使用的库,它还侦听来自使用GroupCache的应用程序其他实例的传入请求。

  1. 应用程序通过密钥向GroupCache询问数据。
  2. GroupCache检查内存中的热缓存中是否有数据,如果没有数据,请继续。
  3. GroupCache对键执行一致的哈希,以确定哪个GroupCache实例具有数据。
  4. GroupCache向具有数据的GroupCache实例发出网络请求
  5. 如果内存中存在数据,GroupCache将返回该数据;如果不存在,它将要求该应用程序呈现或获取数据。
  6. GroupCache将数据返回到发起请求的GroupCache实例。

第5步在发生惊群事件的情况下非常重要,因为只有GroupCache实例之一将执行所请求数据的呈现或获取。还在从GroupCache实例中请求数据的应用程序的所有其他实例将阻塞,直到该应用程序的拥有实例成功呈现或获取数据为止。这为分布式系统中的数据访问创建了自然的同步点,并消除了惊群效应问题。

第2步也很重要,因为可以在内存中本地缓存数据,从而避免了网络往返的开销,从而提供了巨大的性能优势并降低了网络压力。由于GroupCache是应用程序的一部分,因此我们避免了GroupCache在不知道应用程序的情况下删除数据的可能性,因为任何此类删除事件都将由使用GroupCache的应用程序的所有实例共享

部署是一种公认的次要好处,但是我们中那些喜欢简单性的人可以欣赏到的好处。尽管将Redis和Memcached作为独立于应用程序的实体进行部署和保护并不是很困难,但是部署一个应用程序对于操作员来说是一件轻而易举的事情,可以使他们处理并保持最新和安全。

值得再次提及,因为它很容易被忽略。高速缓存实现在高速缓存未命中期间呈现或从数据库中获取数据的能力以及依赖于内存中的本地热高速缓存的能力,使GroupCache成为分布式高速缓存中的最佳选择。应用程序外部的任何分布式缓存都不能提供这些好处。

Groupcache作为同步工具

因为groupcache提供了很好的同步语义,所以我们发现Groupcache在创建和管理唯一资源时是分布式或数据库级锁的替代方案。

例如,我们的内部分析引擎读取数千个事件,并动态添加具有已分配统计信息的标签。由于我们有许多正在运行的引擎实例,因此必须将看到的每个新标签都视为可能的新标签。通常,这将向我们的数据库生成不断更新请求的流。通过使用groupcache,每个实例都可以使用account:tag键查询缓存。如果标签已经存在,则返回标签上的最新数据。但是,如果标签不存在,groupcache会将请求中继到拥有的实例并创建标签。以这种方式,当系统遇到新标签时,仅将单个upsert发送到数据库。

同样,我们使用groupcache对唯一计数器进行计数,其中系统仅应记录一个计数器的单个实例。因为我们使用的是Groupcache,所以我们避免完全使用分布式锁定和死锁问题。当使用nosql数据库本身很少或没有锁定或同步语义时,这特别有用。

参考资料

https://www.mailgun.com/blog/golangs-superior-cache-solution-memcached-redis/