微服务之间如何共享数据
在纽约市,两个机器人一起享受日落时分,一边喝着鸡尾酒。
当我开始使用微服务时,我对“两个服务不应该共用一个数据源”这条常见规则理解得太僵化了。
我看到这句话被到处贴在网上:“服务之间不应该共享数据库”,这确实说得通。一个服务应该有自己的数据,并且保留更改其数据结构的自由,而不改变它对外的API接口。
但这里有一个重要的细微差别,我直到后来才理解到。要正确地应用这条规则,我们需要区分共享数据来源和共享数据本身。
共享数据源为什么不好?一个例子:产品服务应该拥有 products
表,并包含其中的所有记录。他们通过 API、一个 GraphQL 查询(products
)以及一个变异(createProduct
)来创建这些记录,并通过这些方式将这些数据提供给其他团队。
产品服务掌握产品的数据源头,其他团队不应直接接触这些数据。如果他们需要数据,应该通过API向产品服务请求。在任何情况下,都不应允许直接访问数据库,否则就会失去对模式进行调整的自由。我也是从这段经历中学到的。我从这段经历中学到了这一点。
分享数据没啥问题实际上,这些服务需要用到其他服务的数据。
例如,行程服务需要访问乘客系统中的乘客和司机系统中的司机,以便提供行程概览信息。
一个包含三个服务的简单架构
行程服务会同步从每个相关服务请求数据,以完成原始请求(getTrips
)。我们可以确信数据是最新的,请求的客户端将获得数据的强一致的视角(你们中的一些人可能已经猜到我接下来要说什么了;)。
对于刚开始接触微服务的团队来说,这种同步请求响应模型用于在微服务之间传输数据,是一种非常自然的心理模型。当你需要某项数据时,你知道该从哪里获取,可以向负责该数据的服务请求,它会按需提供数据。
除此之外,为团队提供一致且最新的数据易如反掌。一致且最新的数据意味着直接来自源头(真相之源)的最新信息。提供任何非一致性的数据都是不可接受的。提供非最新的数据难道不是在自欺欺人吗?还有什么比这更不合常理的吗?
我们把这些模式视为信条,因为我们没有其他选择,最重要的是,感觉非常自然。
同步性和强一致性不容易扩展依赖于同步请求和强一致性的架构通常无法很好地扩展。有时候,并非总是需要直接从原始来源获取所需的数据来满足需求,或者在实际操作中这样做不可行。有时候,
上面提到的行程服务示例乍一看挺简洁的,但系统很少能一直保持这么简单。新的服务会不断涌现,而这些服务又需要从现有服务获取数据。如果一直采用同步请求模式,最终会导致服务之间相互请求形成一团乱麻。这里举个例子。
例如带有同步请求的步骤
- 用户完成了一个挑战后,会调用挑战服务的
completeChallenge
突变。 - 在存储完成后,挑战服务告知排行榜服务,以便更新排行榜。
- 排行榜服务向用户服务请求用户显示名和头像,以构建新的排行榜状态。
- 排行榜服务发现新的排行榜状态中出现了一位新的第一名,于是通知消息服务,这样消息服务就可以通知参与者有新的第一名了。
- 通知服务向用户服务请求该排行榜中用户的最新电子邮件地址,以便发送邮件。
用户服务很明显是这里的一个争议焦点:大家或多或少都依赖它。想象一下如果这个服务挂了:其他服务也会跟着挂掉。不仅如此,你还需要时刻给这台服务器扩容,添加更多副本和高性能数据库,来满足需求。
另外,这个请求链中的每一跳都会增加整个请求的延迟。每一跳都可能带来成倍的延迟,因为每个服务都可能向其依赖的服务发出多个请求。很快,你就会发现延迟已经到了难以接受的程度。
最后,请求链中每个额外的依赖项都会增加整个请求链失败的可能性。在一个涉及五个服务的请求链中,每个服务都具有99.9%的SLA(即每年约9小时的停机时间),复合SLA则变成99.5%。这意味着每年差不多有两天的停机时间!
我们只需问一个简单的问题就能避免这些问题:服务真的需要最新数据吗?
通知服务(步骤5)可以说确实如此。如果用户更改了地址而通知服务没有意识到这一点,它可能会将邮件误发到错误的地址,从而使通知无法送达预期接收者。
另一方面,排行榜功能可能不需要最新的昵称和头像来生成排行榜——用户看到过时的昵称或头像也不是什么大事。
就像你看到的,服务有不同的数据一致性要求。我们可以通过权衡来找到杠杆点,采用不同的数据共享方法,构建一个更强大的分布式系统。
进入最终一致性模式在我职业生涯的这个时候,我发现了服务可以保存其他服务数据的本地副本,存储在它们自己的数据库表中。这也意味着他们需要通过事件或轮询来保留这些数据,这也要求他们通过事件或轮询来保留这些数据。
此包包含数据可能在一段时间内过时,但最终一定会更新,这意味着数据是最终一致的。我们不能保证数据不会过时,但可以确保最终会更新数据。
我突然明白了,当我从后端服务依赖公共天气API获取天气数据的角度来思考时。而不是每次当有来自普里什蒂纳或柏林的用户需要天气数据时去获取这些城市的天气数据,我会每天多次缓存这些数据,并将数据存储在本地表中,为用户提供缓存的数据。我选择了最终一致性作为权衡,因为对于我的用户来说,看到最新的数据并不是至关重要,如果数据稍微过时几个小时也无妨。
回到挑战例子:比如,我们可以通过在各个服务中维护用户的一个本地副本,来切断很多与用户服务的同步依赖关系。
- Leaderboard 服务可以维护一个用户本地副本,如果数据稍微有点旧也不会有人在意,如果看到稍微旧一点的头像也不是什么大问题。
- Challenge 服务也一样;如果它提供了一个
getChallengeDetails
查询,并且需要用户的显示名和头像来展示当前挑战参与者——它可以从自己的用户表中提供这种最终一致的数据。 - Notification 服务虽然稍微敏感一点,也可以利用数据共享来摆脱对 Users 服务的依赖。它可以本地化用户数据,并通过监听用户的更新事件来尽力保持最新状态,以确保最新的电子邮件地址。
尽管我们没有详细讨论服务是如何共享这些数据的(这个问题留待以后再谈),但一个最终架构示例将会结合事件溯源和缓存。先来一窥这种架构的样子:
一个示例架构,其中包含两种主要的数据共享方法:事件发布和缓存,用来在服务之间共享数据。
如果你想了解更多,可以看看这篇文章《如何在高并发下在微服务之间共享数据》(作者是 Fiverr 的工程师 Shiran Metsuyanim,链接为 https://medium.com/fiverr-engineering/how-to-share-data-between-microservices-on-high-scale-ab2bc663898d)。这篇文章非常不错,展示了如何在添加新服务时保持系统的健壮性。文章首先介绍了限制条件,然后讨论了同步、异步和混合方案之间的权衡。
最后来个总结我想向那些和几年前的我一样的开发者传达这个观点,他们可能仍然被“不要共享数据”这句话的字面意思困扰。他们需要明白,这只是指不要共享数据的唯一真相来源。在另一个服务中维护一个服务的数据副本完全没问题,这实际上符合最终一致性的理念。
谢谢阅读。您还想了解哪些微服务相关的话题?还有哪些话题讨论得不够充分?欢迎留言!
共同学习,写下你的评论
评论加载中...
作者其他优质文章