问题描述
举个例子,假设我正在构建一个简单的社交网络。我目前有两项服务:
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 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,"username": "cat_sun_dog"
}
}
好处:
缺点和问题:
- 更改传播需要一些时间
- 如果由于 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"
}
}
好处:
缺点和问题:
- 延迟增加:在本例中,这是由于两次连续的网络往返
- 如果
Identity
服务关闭,操作会中断,尽管可以使用 circuit breaker pattern 缓解这种情况,但客户端无论如何都不会看到作者的姓名 - 未使用的数据可能仍会被查询并浪费资源(但这在大多数情况下是微不足道的)
有这两个选项:API 网关上的聚合和使用事件的单个微服务上的数据复制,哪个用于哪种情况,以及如何使用正确实施它们?
解决方法
总的来说,我强烈支持通过持久日志结构存储中的事件进行状态复制,而不是进行同步(在逻辑意义上,即使以非阻塞方式执行)查询的服务。
请注意,所有系统在足够高的级别上都是最终一致的:因为我们不会阻止世界允许更新服务发生,所以从更新到其他地方的可见性总是存在延迟(包括在用户的心)。
一般来说,如果您丢失了数据存储,事情就会被毁掉。但是,不可变事件的日志几乎可以免费为您提供主动-被动复制(您拥有该日志的使用者,可以将事件复制到另一个数据中心):在灾难中,您可以使被动侧处于活动状态。
如果您需要的事件多于您已经发布的事件,您只需添加一个日志。您可以使用日志存在之前状态的合成事件的回填转储为日志播种(例如,转储所有当前的 ProfilePicture
)。
当您将事件总线视为复制日志时(例如通过使用 Kafka 实现它),事件的消费并不会阻止任意许多其他消费者随后出现(它只是增加了您在日志中的读取位置) )。因此,这允许其他消费者出现并使用日志来进行他们自己的混音。其中一个消费者可能只是将日志复制到另一个数据中心(启用主动-被动)。
请注意,一旦您允许服务维护自己对来自其他服务的重要数据位的看法,您实际上就是在执行命令查询职责分离 (CQRS);因此,熟悉 CQRS 模式是个好主意。