你好 我叫Konstantin Evteev,我在Avito工作,担任DBA部门的负责人。 我们的团队开发了Avito存储系统,可帮助选择或发布数据库及相关基础架构,支持数据库服务器的服务水平目标,我们还负责资源效率和监控,设计建议以及可能的微服务开发,与存储系统或在存储环境中开发平台的服务相关联。
我想告诉您,我们如何解决微服务架构的挑战之一-在使用每个服务模式的数据库构建的服务基础结构中进行业务交易。 我在Highload ++ Siberia 2018大会上就此主题做了演讲。

理论 越短越好
我将不详细介绍Sagas的理论。 我只给您一个简短的介绍,以便您了解上下文。
和以前一样(从Avito创立到2015年至2016年):我们生活在一个整体中,具有整体式基础和整体式应用程序。 在某些时候,这些条件开始阻止我们成长。 一方面,我们遇到了具有主数据库的服务器的性能问题,但这不是主要原因,因为可以通过例如使用分片来解决性能问题。 另一方面,整体具有非常复杂的逻辑,并且在增长的某个阶段,变更(发布)的交付变得非常漫长且不可预测:存在许多不明显且复杂的依赖关系(所有事物都是紧密联系的),也很难测试,通常存在很多问题。 解决方案是切换到微服务架构。 在此阶段,我们有一个问题,即与单一基础提供的ACID紧密相关的业务交易:尚不清楚如何迁移此业务逻辑。 使用Avito时,有几种服务会由多种服务实现,当数据的完整性和一致性非常重要时,例如,购买高级订用,借记资金,向用户应用服务,购买VAS软件包-万一发生不可预见的情况或事故,一切都不会意外发生按计划。 我们在解决方案中找到了解决方案。
我喜欢Kenneth Salem和Hector Garcia-Molina在1987年对 sagas的技术描述,这是Oracle董事会现任成员之一。 问题的表达方式:长期存在的事务数量相对较少,长期以来,这些事务会阻止执行较小的资源需求较少且更频繁的操作。 理想的结果是,您可以举一个生活中的例子:可以肯定的是,你们中的许多人排成一列来复印文档,而复印机操作员(如果他要复印整本书或只复印很多本)则不时地制作队列中其他成员的副本。 但是处理资源只是问题的一部分。 当执行资源密集型任务时,长期锁定会加剧这种情况,而这些锁定的级联将建立在您的DBMS中。 此外,在长时间的事务处理中可能会发生错误:事务处理不会完成,并且回滚将开始。 如果事务很长,那么回滚也将花费很长时间,并且可能会从应用程序中重试。 通常,“一切都很有趣”。 SAGAS技术说明中提出的解决方案是将长时间的事务分成多个部分。
在我看来,许多人甚至没有阅读本文档就采用了这种方法。 我们已经反复谈论了defproc(使用pgq实现的延迟过程)。 例如,当阻止用户进行欺诈时,我们会迅速执行简短交易并响应客户。 在这个简短的交易中,我们将任务放入交易队列中,然后以小批量的方式异步进行,例如,十个广告将其广告屏蔽。 我们通过实现Skype的事务队列来做到这一点。
但是我们今天的故事有些不同。 我们需要从另一个角度看待这些问题:将单个服务锯成使用每个服务模式使用数据库构建的微服务。
对我们来说,最重要的参数之一是达到最大切割速度。 因此,我们决定将旧功能和所有逻辑原样转移给微服务,而无需进行任何更改。 我们需要满足的其他要求:
- 提供关键业务数据的相关数据更改
- 能够设定严格的命令;
- 观察百分之一百的一致性-即使发生事故也要协调数据;
- 保证各级交易的开展。
在上述要求下,编排传奇的形式的解决方案是最合适的。
将编排的传奇作为PG传奇服务实现
这就是PG Saga服务的样子。

名称为PG,因为同步PostgreSQL被用作服务存储库。 里面还有什么:
该图还显示了sagas的服务所有者,下面是将执行saga步骤的服务。 它们可能具有不同的存储库。
如何运作
考虑购买VAS软件包的示例。 VAS(增值服务)-广告促销的付费服务。
首先,传奇的服务所有者必须在传奇服务中注册传奇的创建

之后,它会使用Payload生成一个传奇类。

此外,执行者已经在sag服务中,从存储中拾取先前创建的saga调用,并逐步执行它。 本案例的第一步是购买高级订阅。 目前,这笔费用已在结算服务中保留。

然后,在用户的服务中应用VAS操作。

然后,VAS服务已经就位,并且您的软件包已创建。 还可以采取其他步骤,但是它们对我们而言并不重要。

崩溃
任何服务都可能发生事故,但是有一些如何为事故做准备的众所周知的技巧。 在分布式系统中,了解这些技术很重要。 例如,最重要的限制之一是网络并不总是可靠的。 解决分布式系统中交互问题的方法:
- 我们会重试。
- 我们用幂等键标记每个操作。 这是避免重复操作所必需的。 有关幂等键的更多信息,请参见本文。
- 我们补偿交易-萨加斯人的行为特征。
交易补偿:如何运作
对于每笔积极的交易,我们都必须描述相反的动作:在出现问题时采取的业务情景。
在我们的实施中,我们提供以下补偿方案:
如果传奇的某个步骤不成功,并且我们做了很多重试,那么最后一次重复操作可能会成功,但是我们没有得到答案。 我们将尝试补偿交易,但是如果问题步骤的服务执行者确实崩溃并且完全无法访问,则无需执行此步骤。
在我们的示例中,它将如下所示:
- 关闭VAS程序包。

- 取消用户操作。

- 我们取消资金预留。

如果补偿无效,该怎么办
显然,我们必须在大致相同的情况下采取行动。 再次,应用重试,幂等键来补偿事务,但是例如,如果这次没有任何结果,例如,该服务不可用,则需要联系该传奇的服务所有者,以通知该传奇失败了。 此外,还会采取更严厉的措施:升级问题,例如进行手动试用或启动自动化解决此类问题。
更重要的是:假设saga服务的某些步骤不可用。 当然,这些操作的发起者将进行一些重试。 最后,您的saga服务需要执行第一步,第二步,并且其执行程序不可用,您可以取消第二步,取消第一步,并且还可能存在与缺乏隔离性相关的异常情况。 通常,这种情况下的传奇服务从事无用的工作,仍然会产生负担和错误。
怎么做? Healthchecker应该采访完成下垂步骤的服务,并查看它们是否有效。 如果该服务变得不可用,则有两种方法:补偿正在运行的sagas,防止新的sagas创建新的实例(调用),或者在不让它们充当执行者的情况下创建,以便该服务不不必要的动作。
另一种意外情况
想象一下,我们再次进行相同的高级订阅。
- 我们购买增值服务套餐并预付款。

- 我们向用户提供服务。

- 我们创建VAS软件包。

好像很好 但是突然之间,当事务完成时,事实证明在用户服务中使用了异步复制,并且在主数据库上发生了事故。 副本滞后的原因可能有多种:副本上的特定负载会减慢复制播放速度或阻止复制播放。 此外,源(主服务器)可能会过载,并且在源端会出现发送更改的延迟。 通常,由于某种原因,副本是滞后的,并且在事故后突然消失的成功完成步骤的更改突然消失了(结果/状态)。

为此,我们在系统中实现了另一个组件-我们使用检查器。 Checker将成功完成Sagas的所有步骤经历一个比所有可能的滞后都大的时间(例如12小时后),然后检查它们是否仍成功完成。 如果步骤突然失败,则传奇故事会回滚。




在某些情况下,经过12个小时后,您仍然无法取消任何内容-一切都会改变和移动。 在这种情况下,替代取消方案,解决方案可能是向传奇所有者的服务发送信号,告知该操作尚未完成。 如果无法进行取消操作,例如,您需要在向用户收费之后取消帐户,并且其余额已经为零,并且无法注销该笔钱。 我们有这样的场景总是朝着用户的方向解决。 您可能有不同的原则,这与产品代表一致。
结果,您可能已经注意到,在与sag服务集成的不同位置,您需要实现许多不同的逻辑。 因此,当客户团队想要创建传奇时,他们将有大量非常不明显的任务。 首先,我们创建一个传奇,以免重复无法进行,为此,我们正在处理创建传奇及其跟踪的幂等运算。 另外,在服务中,需要实现跟踪每个传奇的每个步骤的能力,以便一方面不执行两次,另一方面可以回答它是否实际完成。 并且所有这些机制都需要以某种方式进行服务,以便服务存储库不会溢出。 另外,可以使用多种语言来编写服务,以及大量的存储库。 在每个阶段,您需要了解理论并在不同部分中实现所有这些逻辑。 如果不这样做,则可能会犯很多错误。
正确的方法有很多,但也有很多情况可以“为肢体做好准备”。 为了使sagas正常工作,您需要将上述所有机制封装在客户端库中,以便为您的客户端透明地实现它们。
可以在客户端库中隐藏的传奇生成逻辑的示例
可以用其他方法完成,但是我建议采用以下方法。
- 我们获得了必须创建传奇的请求ID。
- 我们转到sag服务,获取其唯一标识符,将其与来自点1的请求ID一起保存在本地存储中。
- 使用sag服务中的有效负载运行saga。 一个重要的细微差别:我建议创建传奇的服务的本地操作,作为传奇的第一步进行设计。
- 当saga服务可以执行此步骤时(第3点),在一定的竞赛中,发起创建saga的后端也将执行它。 为此,我们在各处进行幂等运算:一个人执行,第二个电话仅收到“ OK”。
- 我们称第一步(第4点),然后我们才响应发起此操作的客户。
在此示例中,我们将传奇作为数据库使用。 您可以发送一个请求,然后连接可能会中断,但是将执行操作。 这大约是相同的方法。
如何检查全部
有必要涵盖下垂测试的整个服务。 您很可能会进行更改,并且在开始时编写的测试将有助于避免意外的意外。 另外,有必要检查一下藻渣本身。 例如,我们如何安排一次下垂服务的测试以及下垂顺序的测试。 有不同的测试块。 如果我们谈论下垂服务,他知道如何执行正数和补偿交易,如果补偿不起作用,他会通知该下垂服务的所有者。 我们以一般的方式编写测试来处理抽象的传奇。
另一方面,执行下垂步骤的服务上的正向交易和补偿交易是简单的API,对此部分的测试由拥有此服务的团队负责。
然后,传奇所有者团队编写端到端测试,在执行传奇时检查所有业务逻辑是否正常工作。 端到端测试在完整的开发环境上运行,引发了所有服务实例,包括sag服务,并且已经在此处测试了业务场景。

总计:
- 编写更多的单元测试;
- 编写集成测试;
- 编写端到端测试。
下一步是CDC。 微服务架构会影响测试的细节。 在Avito中,我们采用以下方法测试微服务体系结构:消费者驱动的合同。 首先,这种方法有助于突出显示可以在端到端测试中发现的问题,但是端到端测试“非常昂贵”。
CDC的本质是什么? 有一项提供合同的服务。 他有一个API-这是一个提供程序。 还有另一种调用API的服务,即使用合同-消费者。
消费者服务为提供者的合同编写测试,只有合同会检查的测试不是功能测试。 对于我们而言,重要的是要确保在更改API时,在此上下文中的步骤不会中断。 在我们编写测试之后,将出现服务代理的另一个元素-CDC测试信息记录在其中。 每次更改提供者服务时,它将引发一个隔离的环境并运行消费者编写的测试。 底线是什么:生成sagas的团队针对传奇的所有步骤编写测试并进行注册。

关于Avito如何实施CDC方法以测试微服务Frol Kryuchkov在RIT ++上发表了讲话。 摘要可以在Backend.conf网站上找到 -我建议您熟悉一下。
萨加斯的类型
按照函数调用的顺序
a)无序的-传奇的功能以任何顺序调用,并且不等待彼此完成;
b)有序的-传奇的功能以给定的顺序被调用,一个接一个地调用,直到前一个完成后才调用下一个;
c)混合-对部分功能设置顺序,但对部分功能不设置顺序,而是在执行这些功能的哪个阶段之前或之后设置。
考虑一个特定的场景。 在购买高级订阅的相同情况下,第一步是保留资金。 现在,我们可以对用户进行更改并并行创建高级软件包,并且只有这两个步骤结束后,我们才会通知用户。

通过获取函数调用的结果
a)同步-该函数的结果立即已知;
b)异步-该函数立即返回“ OK”,并通过从客户端服务到sag服务API的回调返回结果。
我要警告您一个错误:最好不要执行Sagas的同步步骤,尤其是在实施精心设计的传奇时。 如果执行同步下垂步骤,则下垂服务将等待此步骤完成。 由于这是一个额外的负担,因此在sagas的服务中存在额外的问题,因为这是一个负担,并且有很多sagas参与者。
凹陷度
扩展取决于您计划的系统的大小。 考虑具有单个存储实例的选项:
- 一个传奇步骤处理程序,分批处理步骤;
- 在n个处理程序中,我们实现一个“梳子”-在除法的其余部分中采取步骤:当每个执行者获得自己的步骤时。
- n个处理程序并跳过锁定-效率更高,更灵活。
而且只有那时,如果您事先知道您将在DBMS中遇到一台服务器的性能,则需要进行分片-可以使用其数据集的n个数据库实例。 分片可以隐藏在sag服务API的后面。
更大的灵活性
另外,在这种模式下,至少在理论上,客户端服务(执行saga步骤)可以访问并适合sag服务,并且参与saga也是可选的。 可能还有另一种情况:如果您已经发送了电子邮件,则无法补偿该操作-您无法将信件退回。 但是您可以发送一封新信,说前一封信是错误的,看起来像马马虎虎。 最好使用传奇故事只向前播放而没有任何补偿的场景。 如果没有继续播放,则有必要将有关问题通知给传奇用户。
什么时候需要锁
一般而言,关于Sagas的一小部分题为:如果您可以在没有传奇的情况下做出自己的逻辑,那就去做。 萨加斯人很难。 使用锁时,它几乎是相同的:最好避免使用锁。
当我来到计费团队谈论Sagas时,他们说他们需要锁。 我设法向他们解释了为什么不使用它更好,以及如何去做。 但是,如果您仍然需要锁,那么应该提前预见到。 在sag服务之前,我们已经在一个DBMS框架内实现了锁。 当我们首先同步执行部分操作并设置锁,然后异步完成批处理的其余工作时,使用defproc和一个用于异步阻止广告并同步阻止帐户的脚本的示例。
怎么做? , , , , , - , . . . : , , .
-, , . , , . , , . . — , , .
ACID —
, , . . — durability. . . , . - , - - ,
— - , - , , - , . , - , - .
— .
:
- , , , , .
- , . , , , , , .
- .
- payload . eventual consistency — , , , . , , , -.
监控方式
. , . . checker. . , .


(50%, 75%, 95%, 99%), , - .
, — , . . , - . , — .
. , - ( ) . healthchecker endpoint' info (keep-alive) .
. -. -, - , - . , , , end-to-end. - . , , — .
. .
:
, healthchecker, - , . , . .
, . , , . . choreography — - . , choreography- , . choreography , . , . , , , .
. , , . , + .
API
, - - ( API ), , API. API . — . API , , 100% .
, , , , . — , , . .
, , , . ( ) .
, , , , .
. , , .
saga call ID
. API , .
—
- legacy . , ( «» ). « »? - , , , , - , . , , , . , « », , -. . — . , .
我希望以务实的方式进行开发,因此要编写一个传奇的服务,应该有理由投资于编写这样的服务。而且,很可能很多人只需要我所描述的部分,而这部分将解决当前的需求。最主要的是要事先了解所有这些都需要什么。以及您有多少资源。
如果您有任何疑问或有兴趣了解有关萨加斯的更多信息,请在评论中写下。我很乐意回答。