5.12.1再试一次

 
其实,对我们来说知道订单被捕获并被处理就足够了,因为可以后面再对仓库的提取表做 一次插人操作。我们可以把这部分操作放在一个队列或者日志文件中,之后再尝试对其进 行触发。对于某些操作来说这是合理的,但要保证重试能够修复这个问题。
 
很多地方会把这种形式叫作最终一致性。相对于使用事务来保证系统处于一致的状态,最 终一致性可以接受系统在未来的某个时间达到一致。这种方法对于长时间的操作来说尤其 管用。在第11章中我们会讨论扩展(scaling)模式,届时会对该话题做进一步讨论。
 
5.12.2终止整个操作
 
另一个选择是拒绝整个操作。在这种情况下,我们需要把系统重置到某种一致的状态。提 取表的处理比较简单,因为插入失败会导致事务的回退。但是订单表已经提交了的事务该 怎么处理呢?解决方案是,再发起一个补偿事务来抵消之前的操作。对于我们来说,可能 就是简单的一个DELETE操作来把订单从数据库中删除。然后还需要向用户报告该操作失败 了。在单块系统中这些情况很好处理,但是分开之后怎么做呢?发起补偿事务的代码应该 在客户服务、订单服务,还是其他什么地方呢?
 
那如果补偿事务失败了该怎么办呢?这显然是有可能的,这时在订单表中就会有一条记录 在提取表中没有对应的记录。在这种情况下,你要么重试补偿事务,要么使用一些后台任 务来清除这些不一致的状态。可以给后台的维护人员提供一个界面来进行该操作,或者将 其自动化。
 
现在考虑,如果需要同步的操作不仅仅是两个,而是三个、四个,甚至五个,你该如何处 理?不同情况下的补偿事务会非常难以理解,更不用说实现了。
 
5.12.3分布式事务
 
手动编配补偿事务非常难以操作,一种替代方案是使用分布式事务。分布式事务会横跨多 个事务,然后使用一个叫作事务管理器的工具来统一编配其他底层系统中运行的事务。就 像普通的事务一样,一个分布式的事务会保证整个系统处于一致的状态。唯一不同的是, 这里的事务会运行在不同系统的不同进程中,通常它们之间使用网络进行通信。
 
处理分布式事务(尤其是上面处理客户订单这类的短事务)常用的算法是两阶段提交。在 这种方式中,首先是投票阶段。在这个阶段,每个参与者(在这个上下文中叫作cohort) 会告诉事务管理器它是否应该继续。如果事务管理器收到的所有投票都是成功,则会告知 它们进行提交操作。只要收到一个否定的投票,事务管理器就会让所有的参与者回退。
 
这种方式会使得所有的参与者暂停并等待中央协调进程的指令,从而很容易导致系统的中 断。如果事务管理器宕机了,处于等待状态的事务就永远无法完成。如果一个cohort在投 票阶段发送消息失败,则所有其他参与者都会被阻塞,投票结束之后的提交也有可能会失 败。该算法隐式地认为上述这些情况不会发生,即如果一个cohort在投票阶段投了赞成 票,则它一定能提交成功。cohort需要一种机制来保证这件事情的发生。这意味着此算法 并不是万无一失的,而只是尝试捕获大部分的失败场景。
 
协调进程也会使用锁,也就是说,进行中的事务可能会对某些资源持有一个锁。很多人会 对在资源上加锁有担忧,因为它会使系统很难扩展,尤其是在分布式系统的上下文中。
 
分布式事务在某些特定的技术栈上已有现成的实现,比如Java的事务AP1,该API允许你 把类似数据库和消息队列这样完全不同的资源,放在一个事务中进行操作。很多算法都很 复杂且容島出错,所以我建议你避免自己去创建这套API。如果你确定这就是你要采取的 方式的话,尽量使用现有的实现。
 
5.12.4应该怎么办呢
 
所有这些方案都会增加复杂性。如你所见,分布式事务很容易出错,而且不利于扩展。这 种通过重试和补偿达成最终一致性的方式,会使得定位问题更加困难,而且有可能需要其 他的补偿措施来修复潜在数据的不一致。
 
如果现在有一个业务操作发生在跨系统的单个事务中,那么问问自己是否真的需要这么 做。是否可以简单地把它们放到不同的本地事务中,然后依赖于最终一致性的概念?这种 系统的构建和扩展都会比较容易(第11章会对此做进一步讨论)。
 
如果你遇到的场景确实需要保持一致性,那么尽量避免把它们放在不同的地方,一定要尽 量这样做。如果实在不行,那么要避免仅仅从纯技术(比如数据库事务)的角度考虑,而 是显式地创建一个概念来表示这个事务。你可以把这个概念当作一个句柄或者钩子,在此 之上,能够相对容易地进行类似补偿事务这样的操作,这也是在系统中监控这些复杂概念 的一种方式。举个例子,你可以创建一个叫作“处理中的订单”的概念,围绕这个概念可 以把所有与订单相关的端到端操作(及相应的异常)管理起来。
 
5.13报告
 
如我们已经看到的,在对服务进行分离的同时,可能也需要对数据存储进行分离。但是就 会在进行一个很常见的操作时出问题,这个操作就是报告。
 
把架构往微服务的方向进行调整会颠覆很多东西,但这并不意味着我们需要拋弃现有的一 切。报告系统的用户和其他用户一样,他们的需求也应该得到满足。修改架构然后让用户 去适应,这种做法也未免太过傲慢。我并不是说报告这部分不能进行颠覆(当然是可以 的),但是首先需要搞清楚现有流程是如何工作的。有时候你需要选择好战场。
 
5.14报告数据库
 
报告通常需要来自组织内各个部分的数据来生成有用的输出。举个例子,一个可能的需 求是在账目信息的报告中包含售出物品的描述等,而该信息需要从产品目录中获取。另一个例子是,看看那些高价值客户的购物行为,这个报告需要他们的购买记录和客户详 情信息。
 
在标准的单块服务架构中,所有的数据都存储在一个大数据库中,所以很容易获取到所有 的信息,通过SQL查询对几张表做一个连接即可。通常为了防止对主系统性能产生影响, 报告系统会从副本数据库中读取数据,如图5-12所示。
 
这种方式有一个很大的好处,即所有的数据存储在同一个地方,因此可以使用非常简单的 工具来做查询。但也存在一些缺点。首先,数据库结构成了单块服务和报告系统之间的共 享API,所以对表结构的修改需要非常小心。事实上,这也会阻碍所有人去做类似的修改。
 
其次,无论是在线上系统还是报告系统的数据库中,可用的优化手段都比较有限。一些数 据库允I午我们在只读的备份库上做一些优化,以加快读取速度,从而更高效地生成报告。 比如在MySQL中可以停用事务管理。但由于产品数据库的限制,报告数据库的表结构是 无法随意优化的。所以通常来讲,这个表结构要么非常适用于其中一种场景,但对其他的 来说不好用,或者取二者的最小公约数,也就是两种场景都不够好用。
 
最后,来看看有哪些数据库可供选择,显然近几年来这个选择的范围得到了极大的扩展。 标准的关系型数据库使用SQL作为查询接口,它能够和很多现成的报告工具协同工作,但 不一定是适用干产品数据库的最佳选择。比如说,有可能我们的应用程序数据更适合建模 成为一个图,就像Neo4j那样;或者像MongoDB这样的文档存储。类似地,对于报告系 统来说,可以尝试Cassandra这种基于列的数据库,因为它对大数据量处理得很好。如果 只能使用一种数据库,那么就很难做这些选择及尝试新技术。
 
所以,虽然现状(使用单快表结构)并不完美,但至少它(几乎)是工作的。如果把信息存储到不同的系统 中,又该如何处理呢?有什么办法可以把所有的数据放在一起生成报告呢?是否能够同时 找到一些方法来消除与标准的报告数据库模型相关的那些缺点呢?
事实上有多种不同的替代方案,需要考虑多个因素来决定哪种方案更适合你。接下来会介 绍几种我见过的实践。
 
5.15通过服务调用来获取数据
 
这个模型有很多变体,但它们都依赖API调用来获取想要的数据。对于一个非常简单的报 告系统(比如展示过去15分钟内下的订单数量的系统)来说,这是可行的。为了从两个 或者多个系统中获取数据,你需要进行多次调用,然后进行组装。
 
但是当你需要访问大量数据时,这种方法就完全不适用了。比如我们想要看看过去24个 月内客户在音乐商店的购买行为,并从中寻找到客户行为的趋势及其对收入的影响。为了 完成这个需求,我们至少需要从客户和财务两个系统中获取大量的数据。在报告系统本地 保留这些数据的副本是非常危险的,因为我们不知道它们是否已经发生了修改(即使是历 史数据也可能会被修改),所以为了生成一份精准的报告,需要获取过去两年的所有财务 记录和客户记录。即使客户数量不多,这个操作也会非常慢。
 
报告系统也经常依赖于一些第三方工具来获取数据,而使用SQL接口能够简化报告工具链 的集成。虽然定期把数据拉入SQL数据库是可行的,但它还是会带来一些问题。
 
一个主要的问题是,不同的微服务暴露的API不一定能够很好地适用于报告这个场景。举 个例子,你可以在客户服务中根据ID查询客户,或者根据一些字段来搜索客户,但是客 户服务不会提供API来获取所有的客户。所以,如果想要获取到所有的数据,就要发起很 多调用。对于用户这个例子来说,就是遍历包含所有用户的列表,然后对每个用户分别发 起请求来获取数据。这种方式不但对报告系统来说非常低效,而且也会对服务器产生过大 的负载。
 
对于某些服务暴露的资源来说,可以通过添加一些缓存头来加快数据的获取速度,还可以 把这些数据缓存在反向代理之类的地方。但是报告天然就允许用户访问不同时期的历史数 据,这意味着,如果用户访问的资源是别人没有访问过的(或者在很长一段时间内没有人 访问),则缓存无法命中。
 
你可以提供枇量API来简化这个过程。举个例子,我们的客户服务可以允许你传过来一 组客户ID来批量获取数据,或者甚至提供一个接口来分页访问所有的客户。一个更极 端的版本是,把对所有用户的请求建模成为一个资源。发起调用的系统可以POST—个 BatchRequest,其中携带一个位置信息,服务器可以将所有数据写入该文件。客户服务会 返回HTTP 202响应码来表示请求已经接受了,但还没有处理。调用系统接下来轮询这个 资源,直到得到一个201 Created状态,这表示请求已经被满足了,然后发起调用的系统就 可以获取这个数据。通过这种方式可以将大数据文件导出,而不需要HTTP之上的开销, 只是简单地把一个CSV文件存储到共享的位置而已。
我见过使用上述方法来批量插入数据,而且能够工作得很好。但是对于报告系统而言,我 并不是很喜欢这种方式,因为我觉得有其他潜在的更简单的方式,而且还能够在处理传统 报告领域时,更有效地应对伸缩性问题。
 
5.16数据导出
 
和报告系统拉取数据的方式相比,我们还可以把数据推送到报告系统中。使用标准的 HTTP来进行大量调用时,会带来很大的额外开销,更不用提为报告系统创建专用API所 带来的开销。一种替代方式是,使用一个独立的程序直接访问其他服务使用的那些数据 库,把这些数据导出到单独的报告数据库,如图5-13所示。
图5-13:使用数据导出技术来周期性地把数据推送到报告数据库
 
这时你会说:“但是Sam,你说过使用数据库集成是不好的! ”很高兴你这么问,不枉我 前面多次强调这个问题!但如果实现得好的话,这个场景可以是一个例外,因为它使得报 告这件事情变得足够简单,从而可以抵消耦合带来的缺点。
 
一开始,相应服务的维护团队可以负责数据的导出工作。简单地使用Cron去触发一些命 令行程序就可以完成这个任务。这个程序需要同时使用服务和报告系统的数据库。导出任 务的职责是,把一种形式映射成为另一种形式。通过让同一个团队来维护服务本身和数据 导出,可以缓解二者之间的耦合。事实上,我会建议你对服务和数据导出程序统一做版本 管理,并且把数据导出的构建,作为服务本身构建的一个生成物给创建出来,当然这里假 设服务和报告总是同时部署的。因为二者总是一起部署,而且服务和报告系统之外的实体 不会访问这些数据,所以传统的数据库集成所带来的问题,很大程度上得到了缓解。(也就是导出的数据只与报告数据库和服务相关,这样数据的修改也只影响这两者)
 
关于报告系统表结构的耦合仍然存在,但是我们可以把它看作一个类似公共API的比较稳 定的东西。一些数据库提供的技术能够帮助我们进一步消除这些问题。图5-14显示了一 个关系型数据库的例子,在报告数据库中包含了所有服务的表结构,然后使用视图之类的 技术来创建一个聚合。使用这种方式,导出数据的服务只需要知道自己的报告视图结构即 可。但是这种方式的性能就取决于你所选用的数据库系统了。
当然,这里其实是把集成的复杂度推到了表结构这个层次,然后依靠数据库的能力来确保 这种方式是可行的。虽然数据导出一般来讲是合理并且容易实施的建议,但我认为,分割 的视图结构所带来的复杂度还是得不偿失的,尤其当你要在数据库中管理修改时。
 
另一个方向
 
在曾经做过的另一个项目中,我们把一系列数据以JSON格式导出到AWS S3,有效地把 S3变成了一个巨大的数据集市!这种方式一直都很好,直到后来规模变得越来越大,且出 现了把这些数据导出到类似Excel和Tableau这种标准报告工具中的需求,这种方法的效 果就不够好了。
 
5.17事件数据导出
 
第4章提到过,每个微服务可以在其管理的实体发生状态改变时发送一些事件。比如,我 们的客户服务可能会在客户增删改时发送一些事件。对于这些暴露事件聚合(feed)的微 服务来说,可以编写自己的事件订阅器把数据导出到报告数据库中,如图5-15所示。
图5-15:基于状态改变事件来将事件数据导出到报告数据库中
使用这种方式的话,与源微服务底层数据库之间的耦合就被消除掉了。我们只需要绑定到 服务所发送的事件即可,而设计这些事件,本来就是用来暴露给外部消费者的。考虑事件 是具有时效性的,也很容易确定什么样的数据应该被发送到中央报告系统的存储中。因为 可以在事件发生时就给报告系统发送数据,而不是靠原有的周期性数据导出,所以数据就 能更快地流入报告系统。
 
还有,如果我们记录了哪些事件已经被处理,而且发现老的事件已经被导人到报告系统 中,那么每次只需要对新事件进行处理即可。这意味着插入操作会更加高效,因为只需要 发送增量数据。我们可以在数据导出的方式中做类似的事情,但是需要自己实现,然而事 件流(x在时间戳y上发生)的时效性特性能够帮助我们很好地完成这个目标。
 
因为事件数据导出的方式与服务的内部实现耦合很小,所以可以把这部分工作交给另一个 独立的团队(而非持有数据的那个团队)来维护。只要事件流的设计没有造成订阅者和服 务之间的耦合,则这个事件映射器就可以独立于它所订阅的服务来进行演化。
 
这个方法主要的缺点是,所有需要的信息都必须以事件的形式广播出去,所以在数据量比 较大时,不容易像数据导出方式那样直接在数据库级别进行扩展。不管怎样,如果你已经 暴露出了合适的事件,我还是建议你考虑这种方式,因为它能够带来低耦合及更好的数据 时效性。
 
5.18数据导出的备份
 
Netflix使用过一种方法,该方法利用现有的备份方案解决了他们遇到的与扩展相关的问 题。而接下来要讨论的方法就基干Netflix使用的这种方法。从某种角度来看,你可以认为 这是数据导出的一种特殊形式,但这个有趣的方案确实值得一提。
 
Netflix已经决定把Cassandra作为在众多服务中进行数据备份的标准方式。他们投入了大 量的时间来构建相应的工具来提高Cassandra的易用性,其中大部分工作已经通过开源项 目的方式共享给了全世界。显然对于Netflix来说,数据得到适当的备份是很重要的。为 了备份Cassandra数据,标准的方式是复制一份数据,然后将其放到另外一个安全的地方。 Netflix存储的这些文件叫作SSTables,它们被存储在Amazon的S3对象存储服务中,而 该服务提供了非常好的数据持久化保证。
 
在进行报告时,Netflix需要对所有这些数据进行处理,但由于要考虑扩展性问题,所以是 一个非凡的挑战。它使用了 Hadoop,将SSTable的备份数据作为任务的数据源。慢慢地, Netflix实现了一个能够处理大量数据的流水线,并且开源了出来,它就是Aegisfhus项目 (https://github.com/Netflix/aegisthus)。但和数据导出一样,这个模式仍然会引人与最终报告 系统表结构(或者说目标系统)之间的耦合。
 
使用映射器来处理备份数据的方式与上述的方法很类似,这个方法在其他的上下文中也能够工作得很好。如果你已经在用Cassandra 了,那么Netflix已经为你做了很多事情!
 
5.19走向实时
 
前面列出;T很多把数据从不同的地方汇聚到同一个地方的模式。但是否所有的报告都必须 从一个地方出呢?我们有仪表盘、告警、财务报告、用户分析等应用。这些使用场景对于 时效性的要求不同,所以需要使用不同的技术。在第8章中会讲到,现在我们越来越靠近 能够把数据按需路由到多个不同地方的通用事件系统了。
 
5.20修改的代价
 
贯穿本书,你会看到我一直在强调做小的、增量修改的各种原因,但其中一个关键的好处 是,能够理解做出的那些改变会造成什么影响。这会帮助我们更好地消除错误的代价,但 不会完全避免错误的产生。我们可以,也一定会犯错误,需要接受这个事实。但是另外一 件我们应该做的事情是,理解如何降低这些错误所造成的影响。
 
如我们所见到的,在同一个代码库中移动代码的代价是相当小的。很多工具可以帮助我们 做这件事情,而且引入的问题也都比较容易修复。然而分割数据库的工作量就要大得多, 而且回退数据库的修改也非常复杂。类似地,解开服务之间的耦合,或者完全重写一个很 多消费者都在使用的API是非常巨大的工作。巨大的修改代价意味着风险的增大。如何才 能控制这些风险?我的方式是在影响最小的地方犯错误。
 
我喜欢在修改和犯错误的代价都很小的地方进行思考,也就是白板。把设计画在白板上。 在你认为的服务边界上运行用例,然后看看会发生什么。比如对于我们的音乐商店来说, 想象一下,当客户搜索一条记录、在网站上注册,或者在购买专辑时会发生什么样的调 用?你看到奇怪的循环引用了吗?你看到两个服务之间的通信过多,以至于它们应该被合 并成为一个吗?
 
这里我采用了一种在设计面向对象系统时的典型技术:CRC (class-responsibility-collaboration,类-职责-交互)卡片。你可以在一张卡片写上类的名字、它的职责及与谁 进行交互。当我进行设计时,会把每个服务的职责列出来,写清楚它提供了什么能力,和 哪些服务之间有协作关系。遍历的用例越多,你就越能知道这些组件是否以正确的方式在 一起工作。
 
5.21理解根本原因
 
我们做了很多关于如何把大服务拆分成一些小服务的讨论,但是这些大服务又是怎么产生 的呢?第一件需要理解的事情是,服务一定会慢慢变大,直至大到需要拆分。我们希望系统的架构随着时间的推移增量地进行变化。关键是要在拆分这件事情变得太过昂贵之前, 意识到你需要做这个拆分。
 
但是事实上,我们中的很多人都见过服务变大到非常不健康的状态。尽管知道相比于手中 巨大的怪兽,一系列的小服务更容易应对,但我们仍然在一点点地帮助它成长。这又是为 什么呢?
 
知道从哪里开始是解决方案的重要组成部分,所以希望本章的内容对你有所帮助。但另一 个挑战是拆分服务所带来的代价。找一个环境来做好配置并启动一个服务等任务并不简 单。所以要怎么做呢?好吧,如果某些对的事情做起来很困难,那么应该尽量把它们变得 简单。对库和轻量级服务框架的投资能够减小创建新服务的代价。给人们提供自助的虚拟 机创建服务,或者甚至提供一个可用的PaaS,这些措施能够大大简化系统环境的创建及在 此之上的测试工作。贯穿本书的剩余部分,我们会讨论一些方法以减小这个代价。
 
5.22 小结
 
我们通过寻找服务边界把系统分解开来,且这可以是一个增量的过程。在最开始就要养成 及时寻找接缝的好习惯,从而减少分割服务的代价,这样才能够在未来遇到新需求时继续 演化我们的系统。如你所见,有些工作是很艰难的。但由于可以增量进行,所以也没什么 好怕的。
 
那么现在可以开始对服务进行分割了,但是我们也引人了一些新的问题:需要部署上线的 组件变多了!所以接下来让我们进入到部署的世界。
 
 
posted @ 2019-12-05 21:33  mongotea  阅读(123)  评论(0编辑  收藏  举报