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

问题描述

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

  • 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/posts/{id} 发布带有其 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
}

太好了,但是我的客户端,一个网络应用程序,想要显示带有作者姓名的帖子,并且它最好在一个 REST 请求中接收以下 JSON 数据。

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

我找到了两种主要的方法解决这个问题。

数据复制

Microsoft's guide for dataMicrosoft'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,"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,"username": "cat_sun_dog"
  }
}

好处:

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

缺点和问题:

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

有这两个选项:API 网关上的聚合使用事件的单个微服务上的数据复制哪个用于哪种情况,以及如何使用正确实施它们?

解决方法

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

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

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

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

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

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...