数据复制或 API 网关聚合:使用微服务选择哪一种?

jeu*_*x20 1 eventual-consistency microservices asp.net-core api-gateway

举个例子,假设我正在构建一个简单的社交网络。我目前有两项服务:

  • Identity,管理用户、他们的个人数据(电子邮件、密码哈希等)及其公共配置文件(用户名)和身份验证
  • Social,管理用户的帖子、朋友和动态

Identity服务可以使用其 API 提供用户的公共资料/api/users/{id}

// GET /api/users/1 HTTP/1.1
// Host: my-identity-service

{
  "id": 1,
  "username": "cat_sun_dog"
}
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)

这很好,但我的客户端(一个 Web 应用程序)希望显示带有作者姓名的帖子,并且它最好在一个 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"
  }
}
Run Code Online (Sandbox Code Playgroud)

我发现了两种主要方法来解决这个问题。

数据复制

正如Microsoft 的数据指南Microsoft 的微服务间通信指南中所述,微服务可以通过设置事件总线(例如 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"
  }
}
Run Code Online (Sandbox Code Playgroud)

好处:

  • 使Social服务完全独立于Identity服务;没有它它也可以正常工作
  • 检索数据需要更少的网络往返次数
  • 提供跨服务验证的数据(例如检查给定用户是否存在)

缺点和问题:

  • 更改传播需要一些时间
  • 如果某些消息由于灾难破坏了所有复制队列而无法通过,那么对于某些用户来说,系统绝对会被毁!
  • 如果有一天,我需要用户提供更多数据(例如他们的 )怎么办ProfilePicture
  • 如果我想添加具有相同复制数据的新服务该怎么办?

API网关聚合

正如Microsoft 数据指南中所述,可以创建一个 API 网关来聚合来自两个请求的数据:一个请求到Social服务,另一个请求到Identity服务。

/api/posts/{id}因此,我们可以在 ASP.NET Core 的伪代码中实现这样的API 网关操作 ( ):

[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
    }
  });
}
Run Code Online (Sandbox Code Playgroud)

然后,客户端只需使用 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"
  }
}
Run Code Online (Sandbox Code Playgroud)

好处:

  • 非常容易实施
  • 始终提供最新数据
  • 提供一个集中位置来缓存 API 查询

缺点和问题:

  • 延迟增加:在本例中,这是由于两次连续的网络往返造成的
  • Identity如果服务关闭,操作就会中断,尽管可以使用断路器模式来缓解这种情况,但客户端无论如何都看不到作者的名字
  • 未使用的数据可能仍会被查询并浪费资源(但这在大多数情况下都是微不足道的)

有这两种选择:API 网关上的聚合使用事件在各个微服务上进行数据复制哪种情况使用哪一种,以及如何正确实现它们?

Lev*_*sey 5

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

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

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

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

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

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