在快节奏交付的今天,LinkedIn是如何使用暗金丝雀进行生产现场测试?


互联网软件行业已经从漫长的开发周期和专门的质量保证(QA)阶段转移到了快节奏的连续集成/连续交付(CI / CD)管道,在该管道中可以快速编写,提交和推送新代码。
这样做极大地提高了迭代速度,在LinkedIn上,提交新代码并将其推入生产环境并不少见。一个小时之内,一天多次。
尽管这取得了令人难以置信的成功:通过允许我们更快地进行更改提高了开发人员的生产力。但当不良代码、配置或AI模型投入生产时,它还引起了诸如站点或服务中断的问题。即使使用完整的单元和集成测试,有时也无法替代使用生产数据和生产量的生产级测试,尤其是在系统性能指标方面,例如内存消耗,CPU / GPU使用率,并发性和延迟。
但是,为了安全地进行生产级测试而不对客户造成负面影响,团队经常需要生产请求和生产数据。诸如记录和重放之类的框架提供了一种解决方案,但通常不适合快速变化的场景,例如测试 新的AI模型,该模型需要不同的输入功能才能产生输出。收集生产量的新输入需要重新记录生产请求和响应的潜在昂贵且耗时的步骤,而这对于每天进行A / B测试的多个新模型是不可行的。这篇博客文章介绍了暗金丝雀群集,作为在生产之前就发现问题的一种方法。
 
什么是暗金丝雀?
金丝雀的术语来自采矿业,矿工将不幸的金丝雀放到笼子里,以检测是否存在有毒气体危害矿工呼吸。这些金丝雀将向矿工发出预警信号,表示进入矿山并不安全。在软件工程中,金丝雀是在生产级别运行新代码,配置或AI模型的服务的实例,因此我们可以在将新代码部署到更多实例之前验证其安全性。如果新代码不正确(例如,性能降低或错误率增加),则对该“金丝雀”的请求可能会“牺牲”,直到该错误代码被回滚为止。这可能意味着一小部分用户请求将失败(banq注:因为互联网是免费的,故可以承受参与测试的失败)。
“暗”金丝雀是服务的一个实例,该服务从实际服务实例中获取重复流量,但是默认情况下会丢弃来自暗金丝雀的响应。这意味着即使在暗金丝雀中出了点问题,例如错误,更高的延迟,更高的CPU /内存消耗等,最终用户也不会受到影响。(banq注:使用免费用户做测试,但是不影响用户结果)。
例如,如果有读取配置文件的请求,则普通情况下的副作用是将对配置文件读取次数进行计数的数据写入数据库;可以通过检测到这是一个暗金丝雀请求并使用单独的数据库计数器,或者在这种情况下不写入数据库,来减轻这种副作用。
对下游服务的呼叫也必须消除副作用,或者需要减轻其影响。在使用暗金丝雀之前,还需要仔细考虑其他下游影响。
 
LinkedIn的暗金丝雀历史
早在2013年,我们的大型后端服务之一就希望在Rest.li中为暗金丝雀提供支持。当时的服务涉及复制来自一台主机的请求并将其发送到另一台主机。它是通过Python工具添加的,以填充Apache ZooKeeper中的主机到主机映射以及用于读取此映射并增加流量的过滤器。随着操作复杂性的增加(由于额外的数据中心,中端甚至前端服务中使用了暗金丝雀,以及动态伸缩实例),维护起来变得更加复杂。越来越多的团队在使用暗金丝雀,但我们的开发人员和SRE仍然受困于维护暗金丝雀的难度。例如,当暗金丝雀突然停止接收流量,工程师不得不在每个数据中心中重新创建乏味的主机到主机映射。很明显,我们需要一个新的解决方案。
 
引入暗金丝雀集群
暗金丝雀群集类似于暗金丝雀本身,只是存在多个暗金丝雀实例。将流量发送到暗金丝雀群集要求用户发现流量并将其分散到群集成员之间。LinkedIn使用Rest.li中动态发现(D2)服务发现机制将请求从服务发送到服务,因此这是添加对黑暗群集的一流支持的逻辑位置,因为D2已经可以将请求发送到群集。这种集成使整个常规服务实例集群能够将重复但被忽略的请求发送到黑暗的实例集群。这样做的好处是:

  • 我们可以轻松地向暗金丝雀发送大量流量,同时将对发送实例的影响降到最低,因为任何实例上的附加QPS都很低。
  • LinkedIn开发人员熟悉D2服务发现机制,并拥有明确的支持途径来检查代码更改,并促进跨登台区域和数据中心的更改。
  • 分叉机制(我们在其中复制请求,将其发送到暗服务,然后忽略响应)可以知道源和目标群集的大小,以便我们可以在常规服务实例和暗实例之间维持可比较的流量水平,例如与心电图

我们在LinkedIn上的团队发现,暗金丝雀群集是验证更改的简便方法,尤其是对于测试新的在线AI模型时,该模型在生产级流量下的系统性能尚不清楚。所有用例的共同点在于,这可以使我们的工程师不必担心新的变化,从而使LinkedIn网站更稳定,开发人员的工作效率更高。
 
架构
在典型的面向服务的体系结构中,服务将调用其他服务来满足用户请求。在下图中,服务A具有多个实例,接受入站请求,并调用其他服务(包括服务B)以检索必要的信息。这是一个典型的分层系统,其中服务A是中间层服务,服务B是后端服务。如果服务A是我们要验证的服务,则可以设置服务A暗金丝雀的群集。
尽管有很多方法可以将流量分配给服务A的暗金丝雀群集,但LinkedIn使用客户端库,该库可以发现服务A应当将流量分叉到的暗群集。 
在这种情况下,我们在Apache ZooKeeper中存储从源群集(“ ServiceACluster”)到一组对应的深色群集([“ DarkServiceACluster”])的自更新映射。此外,我们可以存储其他元数据,例如如何将流量增加到Dark Cluster以及分叉流量所需的其他配置。现在,每当服务A收到请求时,入站请求过滤器就会读取该ZooKeeper数据,检测到它需要向DarkServiceACluster发送暗流量,并适当地分叉该请求。

“ServiceACluster” {
  … // other properties
  “darkClusters” {
    “DarkServiceACluster” {
      “DarkClusterStrategyPrioritizedList”
        “RELATIVE_TRAFFIC”
      “multiplier” : “1.0”
    }
// map of properties for DarkServiceACluster
  }
// map of darkClusters
}
// map of properties for ServiceACluster

在这种情况下,并非绝对需要使用Apache ZooKeeper,但确实有帮助,因为它使我们能够知道常规群集和暗群集的确切计数。这使我们能够自动调整调度实例,从而独立地扩展和缩减常规集群和暗集群。发送暗流量的一种策略是使暗群集实例和常规群集实例之间的每秒查询(QPS)相同(或根据某个乘数成比例),因此可以通过验证工具进行比较。如果将实例添加到DarkServiceACluster,则可以预期来自ServiceACluster的总出站暗请求总数会增加,但是每个暗群集实例上的QPS保持不变。相反,如果我们将实例添加到ServiceACluster,那么我们希望每个实例到ServiceACluster的QPS会下降(因为总QPS保持不变,并且分布在更多实例上)。结果,我们希望每个实例的DarkServiceACluster QPS减少相应的数量。