数据复制或 API 网关聚合:使用微服务选择哪一个?
Posted
技术标签:
【中文标题】数据复制或 API 网关聚合:使用微服务选择哪一个?【英文标题】:Data replication or API Gateway Aggregation: which one to choose using microservices? 【发布时间】:2021-07-31 09:38:15 【问题描述】:例如,假设我正在构建一个简单的社交网络。我目前有两个服务:
Identity
,管理用户、他们的个人数据(电子邮件、密码哈希等)和他们的公共配置文件(用户名)和身份验证
Social
,管理用户的帖子、他们的朋友和他们的订阅源
Identity
服务可以在/api/users/id
使用其 API 提供用户的公共个人资料:
// GET /api/users/1 HTTP/1.1
// Host: my-identity-service
"id": 1,
"username": "cat_sun_dog"
Social
服务可以通过其 API 在/api/posts/id
发布帖子:
// GET /api/posts/5 HTTP/1.1
// Host: my-social-service
"id": 5,
"content": "Cats are great, dogs are too. But, to be fair, the sun is much better.",
"authorId": 1
这很好,但是我的客户端(一个网络应用程序)希望显示带有作者姓名的帖子,并且它最好在一个 REST 请求中接收以下 JSON 数据。
"id": 5,
"content": "Cats are great, dogs are too. But, to be fair, the sun is much better.",
"author":
"id": 1,
"username": "cat_sun_dog"
我找到了两种主要的方法来解决这个问题。
数据复制
如Microsoft's guide for data 和Microsoft's guide for communication between microservices 中所述,微服务可以通过设置事件总线(例如 RabbitMQ)并使用来自其他服务的事件来复制它所需的数据:
最后(这是构建微服务时出现大多数问题的地方),如果您的初始微服务需要最初由其他微服务拥有的数据,请不要依赖对这些数据发出同步请求。相反,通过使用最终一致性(通常通过使用集成事件,如后续部分所述)将该数据(仅您需要的属性)复制或传播到初始服务的数据库中。
因此,Social
服务可以使用由Identity
服务产生的事件,例如UserCreatedEvent
和UserUpdatedEvent
。然后,Social
服务可以在其自己的数据库中拥有所有用户的副本,但只有所需的数据(他们的 Id
和 Username
,仅此而已)。
通过这种最终一致的方法,Social
服务现在拥有 UI 所需的所有数据,都在一个请求中!
// GET /api/posts/5 HTTP/1.1
// Host: my-social-service
"id": 5,
"content": "Cats are great, dogs are too. But, to be fair, the sun is much better.",
"author":
"id": 1,
"username": "cat_sun_dog"
好处:
使Social
服务完全独立于Identity
服务;没有它也可以正常工作
检索数据需要更少的网络往返
为跨服务验证提供数据(例如检查给定用户是否存在)
缺点和问题:
需要一些时间才能传播更改 如果由于a disaster 炸毁了所有复制的队列而导致某些消息无法通过,那么对于某些用户来说,系统绝对会崩溃! 如果有一天我需要来自用户的更多数据,比如他们的ProfilePicture
,该怎么办?
如果我想添加具有相同复制数据的新服务该怎么办?
API 网关聚合
如 Microsoft's guide for data 中所述,可以创建一个 API 网关来聚合来自两个请求的数据:一个到 Social
服务,另一个到 Identity
服务。
因此,我们可以在 ASP.NET Core 的伪代码中实现 API 网关操作 (/api/posts/id
):
[HttpGet("/api/posts/id")]
public async Task<IActionResult> GetPost(int id)
var post = await _postService.GetPostById(id);
if (post is null)
return NotFound();
var author = await _userService.GetUserById(post.AuthorId);
return Ok(new
Id = post.Id,
Content = post.Content,
Author = new
Id = author.Id,
Username = author.Username
);
然后,客户端只需使用 API 网关并在一次查询中获取所有数据,而无需任何客户端开销:
// GET /api/posts/5 HTTP/1.1
// Host: my-api-gateway
"id": 5,
"content": "Cats are great, dogs are too. But, to be fair, the sun is much better.",
"author":
"id": 1,
"username": "cat_sun_dog"
好处:
非常容易实现 始终提供最新数据 提供一个集中的位置来缓存 API 查询缺点和问题:
延迟增加:在这种情况下,这是由于两个连续的网络往返 如果Identity
服务关闭,则操作会中断,尽管可以使用circuit breaker pattern 缓解这种情况,但客户端无论如何都不会看到作者的名字
未使用的数据可能仍会被查询并浪费资源(但这在大多数情况下是微不足道的)
有这两个选项:API 网关上的聚合和使用事件在单个微服务上复制数据,在哪种情况下使用哪个,以及如何使用正确实施它们?
【问题讨论】:
【参考方案1】:一般来说,我强烈支持通过持久日志结构存储中的事件进行状态复制,而不是进行同步(在逻辑意义上,即使以非阻塞方式执行)查询的服务。
请注意,在足够高的级别上,所有系统最终都是一致的:因为我们不会停止世界以允许对服务进行更新,所以从更新到其他地方的可见性总是存在延迟(包括在用户的记住)。
一般来说,如果您丢失了数据存储,事情就会被毁掉。但是,不可变事件日志为您提供了几乎免费的主动-被动复制(您拥有将事件复制到另一个数据中心的该日志的使用者):在发生灾难时,您可以使被动方处于活动状态。
如果您需要的事件多于已发布的事件,您只需添加一个日志。您可以使用日志存在之前状态的合成事件的回填转储为日志播种(例如,转储所有当前的ProfilePicture
s)。
当您将事件总线视为复制日志(例如,通过使用 Kafka 实现它)时,事件的消耗不会阻止任意许多其他消费者稍后出现(它只是增加您在日志中的读取位置)。这样一来,其他消费者就可以出现并使用日志来进行自己的混音。其中一个消费者可能只是将日志复制到另一个数据中心(启用主动-被动)。
请注意,一旦您允许服务维护自己对来自其他服务的重要数据位的视图,您实际上就是在执行命令查询职责分离 (CQRS);因此,熟悉 CQRS 模式是个好主意。
【讨论】:
这看起来是一个有趣的解决方案,谢谢!但是如果我的用户服务有新的更新并且用户有一个新属性:它的Biography
,会发生什么。以前的日志中不存在此数据,这使得操作起来很棘手。另外,在添加新的微服务时,是否需要转储所有用户?如果是这样,这需要多长时间(假设有大约 200,000 个用户)以及如何正确地做到这一点?
而且,如果由于某种原因,在创建新用户后用户服务由于破坏in-memory outbox 的灾难性崩溃而未能发送消息怎么办?
我不知道为什么 SO 没有在 Biography
评论上通知我,只是现在才看到。消费服务基本上有两种选择:要么他们不需要知道Biography
,所以他们可以忽略更新的Biography
部分,或者他们确实需要知道Biography
。在前一种情况下,您对消费者进行编码,以便它忽略不需要关心的事情。在后一种情况下,最好在更新生产者之前更新消费者。
至于传递消息的灾难性失败,本文概述了一种方法(构建一个恢复过程,该过程将重建要从写入 DB 的内容发送的消息),但我个人更喜欢事件溯源方法,这是一种相反的方法:仅在您放入发件箱的消息发送后更新您的本地状态(即,如果未发布事件,则实际上并未创建用户)。
我明白了,谢谢!顺便说一句,考虑到我的 API 有一个带有 Name
、ProfilePicture
、Biography
等的“用户”对象,那么在添加新属性时实现 API 一致性的好方法是什么?我应该更新我的所有微服务,以便它们复制新添加的属性吗?以上是关于数据复制或 API 网关聚合:使用微服务选择哪一个?的主要内容,如果未能解决你的问题,请参考以下文章
Fizz企业级微服务网关-服务编排,祭出终结BFF层的大杀器
微服务项目服务管理混乱?来看这一篇生产者消费者服务实践,使用API网关实现服务聚合