Monzo使用Cassandra与微服务架构实现大规模支付运维过程中的事故与单点风险


系统出现严重的问题,马上公开披露技术细节,而不是让民间流言替代真相,这样的分享值得点赞:
7月29日从大约13:10开始,你可能会遇到Monzo的一些问题:
可能无法:

  • 登录应用程序
  • 发送和接收付款,或从ATM取款
  • 查看应用中的准确余额和交易
  • 通过应用内聊天或电话与我们联系

之所以发生这种情况是因为我们正在对我们的数据库进行更改,而这些更改并没有按计划实施。我们知道,当涉及到你的钱时,任何形式的问题都是完全不可接受的。我们对此感到非常抱歉,我们致力于确保它不再发生。我们在那天结束时解决了大部分问题。从那以后,我们一直在调查究竟发生了什么,并制定计划以确保它不会再发生。
本着透明的精神,我们想从技术角度分析到底出了什么问题,以及我们将来如何努力避免它。

Cassandra背景
我们使用Cassandra来存储我们的数据,并拥有所有内容的多个副本。
Cassandra是一个开源的,高度可用的分布式数据存储。我们使用Cassandra在多个服务器之间传播数据,同时仍将其作为我们服务的一个逻辑单元。
我们运行了21个服务器(我们称之为集群)的集合。我们存储的所有数据都在21台服务器中的三台上复制。这意味着如果一台服务器出现问题或我们需要进行有计划的更改,则数据完全安全,并且仍可从其他两台服务器获得。
Cassandra使用一个称为分区键的元素来决定21个集群中的哪三个服务器负责特定的数据。当我们想要读取数据时,我们可以转到集群中的任何服务器并请求特定分区密钥的一段数据。我们的服务的所有读取和写入都会发生在仲裁选举。这意味着在从Cassandra返回或写入数据之前,至少有三分之二的服务器需要参与确认该值。整个群集知道如何根据分区键转换为实际保存相同数据的三个服务器。

我们正在扩大Cassandra以保持应用和卡支付顺利运行
随着越来越多的人开始使用Monzo,我们必须扩展Cassandra,以便它可以存储所有数据并快速顺利地提供服务。我们最后在2018年10月扩大了Cassandra的规模,并预计我们目前的产能将使我们满足大约一年需求。
但在此期间,更多人开始使用Monzo,我们增加了运行的微服务数量,以支持Monzo应用程序中的所有新功能。
结果,在高峰负荷期间,我们的Cassandra集群的运行速度接近我们想要的极限。尽管这并没有影响我们的客户,但我们知道如果我们很快没有解决这个问题,我们就会开始看到服务请求的时间增加了。
对于我们的某些服务(特别是那些我们用于提供实时付款的服务),服务请求的时间越长意味着我们放慢了应用和卡支付的速度。当他们在排队的前面为他们的购物买单时,没有人愿意等待很长时间!
因此,我们计划增加群集的大小,以增加更多的计算容量并将负载分散到更多服务器上。

发生在2019年7月29日的事情
这是当天发生的事件的时间表。所有时间都在2019年7月29日的英国夏令时间(BST)。
13:10我们通过向集群添加六台新服务器来开始扩展Cassandra。我们有一个标志集,我们认为这意味着新服务器将启动并加入集群,但在我们向其传输数据之前保持非活动状态。
我们确认使用数据库和依赖它的服务(即服务器和客户端指标)的指标没有影响。为此,我们会查找错误的增加,延迟的变化或读写速率的任何偏差。所有这些指标都显示正常且不受操作影响。
13:14我们的自动警报检测到万事达卡的问题,表明一小部分卡交易失败。我们会告诉我们的付款团队这个问题。
13:14我们收到客户运营部门(COps)的报告,他们用来与客户沟通的工具没有按预期工作。这意味着我们无法通过应用聊天帮助客户,让与我们保持联系的客户等待。
13:15我们宣布了一个事件,我们的白天随叫随到的工程师对一小组工程师进行调查。
13:24发起数据库更改的工程师注意到事件并加入调查。我们讨论扩大规模的活动是否可能导致这个问题,但是因为一切看起来都很健康,所以可以忽略这种可能 没有增加错误,读取和写入速率看起来没有改变。
13:24我们的支付团队在我们的一个Mastercard服务中发现了一个小代码错误,其中特定的执行路径没有正常处理错误情况。我们认为这是万事达卡问题的原因,所以要开始修复。
13:29我们注意到我们的内部边缘正在返回HTTP 404响应。
我们的内部优势是我们编写并用于访问内部服务的服务(例如我们的客户操作工具和我们的部署管道)。它会进行检查以确保我们只允许访问Monzo员工,并将请求转发给相关地点。
这意味着我们的内部边缘找不到它想要的目的地。
13:32我们收到COps的报告称,有些客户正在注销Android和iOS应用程序。
13:33 我们更新公共状态页面,让我们的客户了解这个问题。
13:33
万事达卡修复已准备好部署,但我们注意到我们的部署工具也失败了。
13:39我们使用计划的回退机制部署Mastercard修复程序。而且我们看到在信用卡交易立即改善成功
13:46我们发现内部边缘没有正确路由内部流量。除了身份验证和授权之外,它还通过检查请求并使用我们的配置服务来匹配我们的专用网络范围来验证它是“内部的”。
配置服务是Monzo微服务,为其他服务提供简单的请求 - 响应(RPC)接口,以存储和检索表示配置的键值对。在内部,它使用Cassandra来存储其数据。
我们得出结论,配置服务已关闭,或者我们的网络范围已更改。我们很快排除后者并专注于服务。
13:48我们尝试直接从配置服务中获取数据,并意识到它为我们尝试检索的密钥返回了404(未找到)响应。我们感到困惑,因为我们相信如果配置服务不起作用,它会产生比我们看到的更广泛的影响。
13:53根据指标,我们看到对配置服务的成功读取和写入,这是令人惊讶的,因为我们刚刚看到它无法检索数据。感觉就像我们看到了相互矛盾的证据。
13:57我们在配置服务中搜索其他一些密钥key,并意识到它们仍然存在。
14:00我们绕过配置服务界面直接查询Cassandra。我们确认内部边缘使用的密钥实际上是从Cassandra中丢失的。
14:02此时我们认为我们正面临着一些我们的部分数据无法获得的事件,因此我们将注意力转向Cassandra。
14:04尽管我们早先对Mastercard进行了修复,但我们仍然看不到它仍然没有完全健康,支付团队一直在努力。这意味着少量的卡支付仍然失败
14:08我们查询新的Cassandra服务器是否取得了部分数据的所有权。鉴于我们对目前发生的事情有所了解,我们认为这是不可能的,但我们会继续调查。
14:13我们对Cassandra中的某些数据发出查询,并确认响应来自其中一个新服务器。在这一点上,我们已经确认了我们认为不可能的事情,事实上已经发生了:

新服务器加入了集群,承担了部分数据的负责(某些分区键以平衡负载),但尚未对其进行流式处理。这解释了为什么有些数据似乎缺失了。
14:18我们开始逐个淘汰新服务器,以便我们将数据所有权安全地返回到原始群集服务器。每个节点大约需要8-10分钟才能安全移除。
14:28我们完全删除了第一个节点,我们注意到正在筹集的404数量立即减少。
我们的内部客户支持工具再次开始工作,因此我们可以使用应用内聊天回复客户
15:08我们删除了最终的Cassandra节点,直接影响已经结束。对于大多数客户,Monzo开始正常工作。
15:08→23:00我们继续处理在六台新服务器主动提供读写操作时编写的所有数据。我们通过结合外部存储的重放事件和运行内部协调过程来检查数据一致性。
23:00我们确认所有客户现在都能够获得资金,正常使用Monzo,并在需要时联系客户支持。

我们误解了设置的行为
问题发生是因为我们期望新服务器加入群集,并且在我们执行进一步操作之前保持不活动状态。但事实上,当我们添加新的数据时,他们立即参与了数据的读写,尽管实际上没有任何数据。误解的来源是控制新服务器行为方式的单一设置(或“标志”)。
在Cassandra中,有一个标志auto_bootstrap,用于配置服务器启动和加入现有集群的方式。该标志控制数据是否自动从现有的Cassandra服务器流式传输到已加入群集的新服务器。至关重要的是,它还控制查询模式以继续向旧服务器提供读取请求,直到较新的服务器流式传输所有数据。
在大多数情况下,建议保留默认auto_bootstrap值为true。在此状态下的标志,服务器以“非活动”状态加入群集,分配一部分数据,并在数据读取中保持非活动状态,直到数据流过程结束。
但是,当我们在2018年10月的最后一次扩大完成时,我们为我们的生产环境设定了auto_bootstrap标志false。我们这样做是因为如果我们在集群中丢失了一台服务器(例如,由于硬件故障)并且不得不更换它,我们就会从备份中恢复数据(这将显着加快并减轻其余部分的压力)集群)而不是使用具有数据的其他服务器从头开始重建它。
当auto_bootstrap设置为false,我们预期的六个新服务器将被添加到集群,商定数据,他们负责的分区,并保持不活动,直到我们开始在每个服务器上重建/流过程中,一个接一个。
但事实并非如此。事实证明,除了数据流行为外,该标志还控制新服务器是加入活动状态还是非活动状态。一旦新服务器就他们负责的数据分区达成一致,他们就会承担全部责任而不需要任何基础数据,并开始提供查询。
由于某些数据是从尚未提供任何数据的新服务器提供的,因此该数据似乎缺失。
因此,当某些客户打开应用程序时,无法找到本应存在的交易,这导致其帐户余额显示不正确。
一旦问题得到解决,我们就能够完全恢复数据并纠正任何问题。

为了阻止这种情况再次发生,我们正在做出一些改变
我们可以从这个问题中学到一些东西,并修复以确保它不会再次发生。
我们已经确定了运营Cassandra的知识差距
虽然我们经常在Cassandra上执行滚动重启和服务器升级等操作,但某些操作(如添加更多服务器)不太常见。
我们在测试环境中测试了放大,但与生产的程度不同。
我们测试了一台,而不是添加六台服务器。
为了获得对生产部署的信心,我们在线提供了一台新服务器,并将其保持在初始的“无数据”状态几个小时。我们跨两个集群做了这个。
我们能够确认对群集的其余部分或环境的任何用户没有影响。在这一点上,我们很高兴最初的加入行为auto_bootstrap与我们预期的一样是良性的。因此,我们继续将数据流式传输到新服务器,并在整个过程中进行监控,并确认数据一致性或可用性没有问题。
我们没有考虑到的是法定人数(三个服务器同意一个值)。只有一台新服务器,如果它完全加入集群并且没有任何数据,则无关紧要。在这种情况下,我们已经与群集中的其他两个服务器达成了协议。
但是当我们将六台服务器添加到生产中时,数据所有权将两个或三个成员重新分配给新节点,这意味着我们没有相同的保证,因为基础数据没有重新分配。

我们已经修复了错误的设置
我们已经修复了我们的使用auto_bootstrap。我们还审查并记录了我们围绕其他Cassandra设置的所有决策。这意味着我们拥有更广泛的Runbook - 我们用于提供执行操作的逐步计划的操作指南,例如放大或重新启动Cassandra。这将有助于填补我们的知识空白,并确保知识传播给我们所有的工程师。
延迟我们行动的另一个关键问题是缺乏将Cassandra作为问题主要原因的指标。因此,我们还考虑公开更多指标,并为“未找到行”等指标的强烈变化添加潜在警报。

我们将单个Cassandra集群拆分为较小的集群,以减少变更可能产生的影响

很长一段时间,我们为我们的所有服务运行一个Cassandra集群。每个服务在集群中都有自己的专用区域(称为密钥空间)。但是数据本身分布在一组共享的底层服务器上。
单一集群方法对于工程师构建服务非常有利 - 他们不必担心将服务放在哪个集群上,我们只需要在操作上管理和监控一件事。但这种设计的缺点是,单一的变化会产生深远的影响,就像我们在这个问题上看到的那样。我们希望降低任何单项活动影响Monzo多个区域的可能性。

banq注:单点风险

使用一个集群,查明故障源也更加困难。在这种情况下,我们从第一个警报开始差不多一个小时,直到我们将信息拉到一个连贯的图片中,突出了Cassandra的故障。对于更小且更受限制的系统配置,我们认为这将是一个不那么复杂的问题。
从长远来看,我们计划将我们的单个大型集群拆分为多个较小的集群。这将大大降低像这样的重复问题的可能性和影响,并使我们更安全地进行大规模操作。我们希望确保我们能够做到正确; 这样做不会带来太多的操作复杂性,也不会减慢我们的工程师向客户发布新功能的速度。

​​​​​​​