大规模基础设施: 一次分布式系统的连锁失效


在Target(作者所在公司)我们在多个数据中心里面运行着我们的一个异构的基础设施,这些基础设施由多种不同后端来组成。之所以造成这种情况是因为历史以来我们针对不同的用户场景和应用开发部署模式构建了多种基础设施放到了生产环境去运行。Target Application Platform(TAP,应用平台)提供了一个运行和管理工作负载的统一接口,透过这个接口我们可以以对应用透明的方式把它部署到多个不同的基础设施上。这个关键让我们能更好的管理容量,充分利用已有的资源,并且方便地将应用做跨基础设施和服务提供商的迁移而不需要应用开发团队针对新平台做二次修改。

我们也有很多的核心服务作为整个企业的中心系统在运行。我们有意地推行这种方式,好处是我们可以提供高可用的系统和服务以供开发团队消费,而不需要他们在开发和管理应用之外还需要了解基础设施的运行。这些企业服务包括:数据库,Elasticsearch和消息系统。

在Target我们重度使用Kafka作为系统间的消息代理。通过Kafka,应用的数据更新或者其他事件都可以通知到其他的非直连系统。在基础设施上,我们通过Kafka将应用的日志和指标传输到对应的后端。以这种方式我们就将洞见数据的生产从消费,存储和可用性上解耦。通过TAP部署的每个应用都有日志(logging)和度量(metrics)的SideCar,这样应用团队就可以很方便的从中心化的系统里面消费这些数据而不需要做任何的额外配置。

Kafka运行在基础设施里的OpenStack上面,上周的一个晚上我们对OpenStack的网络子系统做了一次升级。按照计划这次升级应该只会对网络带来很小的中断。但由于升级过程中碰到了问题,网络中断了几个小时,导致Kafka的连接间歇性的断开。

我们在TAP下面运行了多个Kubernetes集群, 其中一个比其他的都大得多,上面运行着2000个开发环境的负载。(这是早期运行“小集群,多集群”的历史产物,那个时候我们希望知道多小的规模是合适的)。在正常情况下应用的logging sidecar只会消耗很小的CPU资源。 但是当Kafka变得间歇性失联之后,所有的SideCar同时被唤醒,虽然他们单个用的CPU资源很小,但是累加起来却很大,结果导致Kubernetes集群中所有节点的Docker deamon达到极高负载。

Docker deamon的高负载导致节点向Kubernetes汇报自己的状态为unhealthy, 这样一来Kubernetes调度器就会尝试将工作负载从这些unhealthy的节点迁移到healthy的节点上。因为Kubernetes调度器并不是平均地把负载分配到所有的节点上,结果导致有些节点的CPU奇高,而另一些没有。在重新调度以后,原来healthy的变得unhealthy,原来unhealthy的变成healthy,不停的循环。

除了logging和metrics的SideCar之外,每个负载还伴随一个Consul代理的SideCar,它的作用是让应用可以跨多个不同的基础设施组成更大的计算环境。只要工作负载一启动,Consul Agent就会把它自己注册为Consul的一个服务,开始参与Consul的网格通讯中并发布自己的成员信息。

在Kubernetes做重新调度的时候,41000个新的节点被产生然后销毁。尽管在重新调度之前Pod里面的应用还来不及启动完成,但是这个Pod在线的短短时间段已经足够让Consul代理发布自己的成员信息到Gossip Mesh网格里面去。这些新节点的注册操作带给Consul服务极大的CPU使用率, 结果就是Consul有不可容忍的响应延时。 更糟糕的是,Consul网格增加的这些新节点导致了所有部署完的工作负载有极大的CPU使用率(在之前的负载上继续累加)。

在每个循环里面Consul代理只会处理数量有限的消息, 这即是说节点消亡的信息发送到网格以后并没有被所有的网格节点所处理。 最后这在网格中造成了“波浪”效应,“影子”节点刚失效但是又立刻被加回进网格中。

在过程中,几个其他的系统也被影响。开发环境的Vault集群(给应用提供秘钥)由于不能及时与Consul通讯而自我封闭。TAP的部署引擎正常的话会通过Consul来发现基础设施后端,给应用下发token,动态配置负载均衡,这个时候也被影响了。原因是它不能及时的与Consul通信,这个时候部署开始失败。

在几天的调试研究, 尝试一种又一种方法之后,我们通过在Consul上给Gossip加密的方式恢复了Consul和Vault。 这样从有问题的Gossip网格里面发来的消息直接拒绝。之后,我们能够将部署引擎稳定化,并对整个开发环境重新部署来应用加密。 在环境重新部署完毕后,没有加密的那些实例就被终止了。

在那之后我们对Consul做了一些研究,发现在1.2.3版本里面有一个专门针对大规模集群的PR被merge了。我们对Consul做了在线升级,通过测试对某个工作负载的受控的scale up和紧跟着的破坏性scale down操作,表明一切正常,升级被认可为成功。节点可以快速的从Gossip Mesh和服务目录中去掉。

(附注:TAP的工作负载在Consul里面是以“application/cluster”的形式来注册的,application和cluster对应的是Spinnaker的术语。我们这样做是为了在应用的层次来实施Consul ACL。但是注意在1.2.3 UI里面有一个bug:不让在service name里面带/。 我基本每天都在用这个UI。在升级到1.3.0 UI以后这个问题被修复,我们最后也就用的这个版本)。

幸运的是,这个问题仅仅影响TAP的开发环境,最糟也不会有特别大的危害。但是从TAP团队的立场看,我们重在确保TAP能提供世界级的开发者体验没有任何中断。我们将开发环境作为我们的“production0”,所以这次事故被以最高优先级对待。

TAP的生产环境并没有被这个问题影响,因为它的规模要小得多。我们在生产环境(prod1)也遇到了类似问题,但是由于Kubernetes集群之间更加松散,级联效应并没有发生。稍后我们确认在生产环境中Gossip Mesh也出现了这个问题,但是由于节点的数量特别少所以影响有限,而且我们快速升级了Consul以后就修复了。

从这次事故中学到的:
  1. 我的“小集群,多集群”的理念得到验证。我们在小一些的开发集群里面跑的负载所受的影响要小得多。生产环境也是如此。如果我们把生产和开发放到一个集群那后果不堪设想。
  2. 共享的Docker daemon是一个容易出问题的点。需要研究减少单个daemon出问题而影响范围。 同样适用于“小集群,多集群”的理念。
  3. 曾经我对于SideCar的使用有过担心。但是经过这次事故之后我更坚信让每一个工作负载有自己的logging和metrics sidecar是正确的。 如果这是对于整个集群是共享的,这次的问题肯定比这更加严重也更难修复。


原文链接:On Infrastructure at Scale: A Cascading Failure of Distributed Systems(翻译:姚洪)

0 个评论

要回复文章请先登录注册