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

Data replication or API Gateway Aggregation: which one to choose using microservices?

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

Identity 服务可以在 /api/users/{id}:

使用其 API 提供用户的 public 个人资料
// GET /api/users/1 HTTP/1.1
// Host: my-identity-service

{
  "id": 1,
  "username": "cat_sun_dog"
}

Social 服务可以在 /api/posts/{id}:

处提供 API API
// 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
}

太好了,但是我的客户,一个 Web 应用程序,想要显示带有 作者姓名 的 post,它最好接收以下 JSON 单个 REST 请求中的数据。

{
  "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 and Microsoft's guide for communication between microservices 中所述,微服务可以通过设置事件总线(例如 RabbitMQ)并使用来自其他服务的事件来复制所需的数据:

And finally (and this is where most of the issues arise when building microservices), if your initial microservice needs data that's originally owned by other microservices, do not rely on making synchronous requests for that data. Instead, replicate or propagate that data (only the attributes you need) into the initial service's database by using eventual consistency (typically by using integration events, as explained in upcoming sections).

因此,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"
  }
}

好处:

不足与疑问:

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网关上聚合使用事件在什么情况下使用哪一个,如何正确实施它们?

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

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

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

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

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

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