数据复制或 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 服务产生的事件,例如UserCreatedEventUserUpdatedEvent。然后,Social 服务可以在其自己的数据库中拥有所有用户的副本,但只有所需的数据(他们的 IdUsername,仅此而已)。

通过这种最终一致的方法,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】:

一般来说,我强烈支持通过持久日志结构存储中的事件进行状态复制,而不是进行同步(在逻辑意义上,即使以非阻塞方式执行)查询的服务。

请注意,在足够高的级别上,所有系统最终都是一致的:因为我们不会停止世界以允许对服务进行更新,所以从更新到其他地方的可见性总是存在延迟(包括在用户的记住)。

一般来说,如果您丢失了数据存储,事情就会被毁掉。但是,不可变事件日志为您提供了几乎免费的主动-被动复制(您拥有将事件复制到另一个数据中心的该日志的使用者):在发生灾难时,您可以使被动方处于活动状态。

如果您需要的事件多于已发布的事件,您只需添加一个日志。您可以使用日志存在之前状态的合成事件的回填转储为日志播种(例如,转储所有当前的ProfilePictures)。

当您将事件总线视为复制日志(例如,通过使用 Kafka 实现它)时,事件的消耗不会阻止任意许多其他消费者稍后出现(它只是增加您在日志中的读取位置)。这样一来,其他消费者就可以出现并使用日志来进行自己的混音。其中一个消费者可能只是将日志复制到另一个数据中心(启用主动-被动)。

请注意,一旦您允许服务维护自己对来自其他服务的重要数据位的视图,您实际上就是在执行命令查询职责分离 (CQRS);因此,熟悉 CQRS 模式是个好主意。

【讨论】:

这看起来是一个有趣的解决方案,谢谢!但是如果我的用户服务有新的更新并且用户有一个新属性:它的Biography,会发生什么。以前的日志中不存在此数据,这使得操作起来很棘手。另外,在添加新的微服务时,是否需要转储所有用户?如果是这样,这需要多长时间(假设有大约 200,000 个用户)以及如何正确地做到这一点? 而且,如果由于某种原因,在创建新用户后用户服务由于破坏in-memory outbox 的灾难性崩溃而未能发送消息怎么办? 我不知道为什么 SO 没有在 Biography 评论上通知我,只是现在才看到。消费服务基本上有两种选择:要么他们不需要知道Biography,所以他们可以忽略更新的Biography 部分,或者他们确实需要知道Biography。在前一种情况下,您对消费者进行编码,以便它忽略不需要关心的事情。在后一种情况下,最好在更新生产者之前更新消费者。 至于传递消息的灾难性失败,本文概述了一种方法(构建一个恢复过程,该过程将重建要从写入 DB 的内容发送的消息),但我个人更喜欢事件溯源方法,这是一种相反的方法:仅在您放入发件箱的消息发送后更新您的本地状态(即,如果未发布事件,则实际上并未创建用户)。 我明白了,谢谢!顺便说一句,考虑到我的 API 有一个带有 NameProfilePictureBiography 等的“用户”对象,那么在添加新属性时实现 API 一致性的好方法是什么?我应该更新我的所有微服务,以便它们复制新添加的属性吗?

以上是关于数据复制或 API 网关聚合:使用微服务选择哪一个?的主要内容,如果未能解决你的问题,请参考以下文章

Fizz企业级微服务网关-服务编排,祭出终结BFF层的大杀器

微服务项目服务管理混乱?来看这一篇生产者消费者服务实践,使用API网关实现服务聚合

微服务项目服务管理混乱?来看这一篇生产者消费者服务实践,使用API网关实现服务聚合

SpringCloud:服务网关zuul-API网关

如何使用API 网关做服务编排?

API 网关中的数据聚合 - Zuul