GitHub如何在Redis中使用分片的复制速率限制器扩展API

大约一年前,我们GitHub迁移了一个旧的速率限制器,以提供更多的流量并适应更具弹性的平台体系结构。我们采用了带有客户端分片的复制Redis后端。最终,效果很好,但是我们在此过程中吸取了一些教训。
 
Memcached问题
我们以前有一个很简单的旧速率限制器:

  • 对于每个请求,确定当前速率限制的“key”
  • 在Memcached中,递增该key的值,如果没有任何当前值,则将其设置为1
  • 另外,如果还没有,请使用相关的key(例如“ #{key}:reset_at”)在Memcached中设置“reset at”值
  • 递增时,如果“ reset at”值是过去的值,则忽略现有值并设置一个新的“ reset at”
  • 在每个请求的开始,如果key的值大于限制,并且“reset at”是未来的值,则拒绝该请求

(可能会有更多细微差别,但这是主要思想。)
但是,此限制器有两个问题:
  • 我们的Memcached架构是由于更改而来的。由于它主要用作缓存层,因此我们将从一个共享Memcached切换到每个数据中心的单个Memcached。尽管对于应用程序缓存来说这很好用,但是如果将客户端请求路由到不同的数据中心,它将使我们的速率限制器的行为非常奇怪。
  • Memcached的“持久性”不适用于我们。Memcached后端由速率限制器和其他应用程序缓存共享,这意味着,当内存被填满时,即使它仍处于活动状态,有时也会删除速率限制器数据。

 
Redis
经过一番讨论,我们决定为速率限制器设计一种新的设计:
  • 使用Redis,因为它具有更合适的持久性系统以及简单的分片和复制设置
  • 在应用程序内部分片:应用程序将为每个键选择要从中读取和写入的Redis集群
  • 为了减轻Redis的CPU约束性,请在每个群集中放置一个主数据库(用于写入)和几个副本(用于读取)
  • 与其在数据库中编写“ reset at”(重置为),不如使用Redis过期功能使值在不再适用时消失
  • 在Lua中实现存储逻辑,以确保操作的原子性(这是对先前设计的改进)

我们从两个出色的现有资源中汲取灵感:

解决了各种问题之后,新的速率限制器效果很好。它提高了可靠性,为客户解决了固定问题,并减少了我们的支持负载。
详细原文点击标题。