逻辑谬误
1.简介
实现微服务架构的过程本质上意味着构建复杂的分布式系统。 可以说,大多数现实世界的软件系统远非简单,但是微服务的分布式特性却大大增加了复杂性。
在本教程的这一部分中,我们将讨论许多开发人员可能会陷入的一些常见陷阱,即分布式计算的谬误 。 所有这些错误的假设都不会误导我们,我们将花费大量的时间讨论构建弹性微服务的不同模式和技术。
任何复杂的系统都可能(并且将)以令人惊讶的方式失败……– https://queue.acm.org/detail.cfm?id=2353017
目录
2.本地!=分布式
有多少次您惊奇地发现,看似无害的方法或函数的调用引起了远程调用的风暴? 确实,这些天来,大多数框架和库都将真正重要的细节隐藏在方便抽象的多个级别之后,试图使我们相信本地(进程内)调用与远程调用之间没有区别。 但事实是,网络不可靠,网络延迟不等于零。
尽管我们的大多数主题将以传统的请求/响应通信方式为中心,但是异步消息驱动的微服务也不是没有麻烦。 您仍然必须联系远程代理,并准备处理幂等和重复数据删除。
3. SLA
我们将从服务级别协议 (或简称为SLA)开始 。 这是一个经常被忽略的主题,但是您的微服务集合中的每个服务最好都定义一个。 这是一个艰难而周到的过程,这对于所讨论的服务的性质是唯一的,并且应考虑许多不同的约束。
为什么如此重要? 首先,它使开发团队在选择正确的技术堆栈时具有一定的自由度。 其次,它暗示服务的使用者在响应时间和可用性方面期望什么(以便使用者可以得出自己的SLA )。
在下一部分中,我们将讨论消费者(通常是其他服务)可以用来保护自己免受所依赖服务的不稳定或中断影响的多种技术。
4.健康检查
有没有一种快速的方法来检查服务是否已启动并正在运行,甚至在进行潜在的复杂业务交易之前? 健康检查是服务报告其准备工作的标准做法。
JCG租车平台的所有服务默认情况下都公开运行状况检查端点。 下面,选择了客户服务来展示运行状况检查 :
$ curl http: //localhost :18800 /health {
"checks" : [
{
"data" : {},
"name" : "db" ,
"state" : "UP"
}
],
"outcome" : "UP" }
正如我们稍后将要看到的那样,基础结构和业务流程层会积极使用健康检查来探测服务,发出警报或/和应用补偿措施。
5.超时
当一方通过电线呼叫另一方时,配置适当的超时(连接,读取,写入,请求等)可能是最简单但最有效的策略。 我们已经在本教程的前面部分中看到了它,这里仅是一小部分。
final CompletableFuture customer = client
.prepareGet( " http://localhost:8080/api/customers/ " + uuid)
.setRequestTimeout( 500 )
.setReadTimeout( 100 )
.execute()
.toCompletableFuture();
当另一方React迟钝或通信渠道不可靠时,无限期地等待以希望最终能收到响应不是最佳选择。 现在,问题是应将超时设置为什么? 没有一个魔幻数字适合所有人,但我们前面讨论的服务SLA是回答此问题的关键信息来源。
太好了,所以让我们假设正确的值就位了,但是如果对服务的调用超时,消费者应该怎么办? 除非用户不再关心响应,否则这种情况下的典型策略是重试呼叫。 让我们谈论一下。
6.重试
从消费者的角度来看,在间歇性故障的情况下重试对服务的请求是最容易的事情。 出于这些目的,像Spring Retry , failsafe或resilience4j之类的库有很大的帮助,提供了一系列重试和退避策略。 例如,下面的代码段演示了Spring Retry方法。
final SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy( 5 ); final ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval( 1000 ); backOffPolicy.setMaxInterval( 20000 ); final RetryTemplate template = new RetryTemplate(); template.setRetryPolicy(retryPolicy); template.setBackOffPolicy(backOffPolicy); final Result result = template.execute( new RetryCallback<Result, IOException>() {
public Result doWithRetry(RetryContext context) throws IOException {
// Any logic which needs retry here
return ...;
} });
除了这些通用功能外,大多数库和框架还具有内置的惯用机制来执行重试。 下面的示例来自Spring Reactive WebClient,我们已经在本教程的上一部分中进行了介绍 。
final WebClient client = WebClient
.builder()
.clientConnector( new ReactorClientHttpConnector(httpClient))
.baseUrl(“http: //localhost:8080/api/customers”)
.build(); final Mono customer = client
.get()
.uri( "/{uuid}" , uuid)
.retrieve()
.bodyToMono(Customer. class )
.retryBackoff( 5 , Duration.ofSeconds( 1 ));
不应忽略退缩政策而不是固定的拖延的重要性。 重试风暴(通常称为雷群问题 )通常会造成中断,因为所有消费者都可能决定同时重试该请求。
最后但并非最不重要的一点是,在使用任何重试策略时要认真考虑的是幂等性 :应同时从消费者和服务两方面采取预防措施 ,以确保没有意外的副作用。
7.大标题
舱壁的概念是从造船业借来的,并在软件开发实践中找到了直接的类比。
隔板 用于船舶,以形成单独的水密隔室,以限制故障的发生,理想情况下可防止船舶下沉。 – https://skife.org/architecture/fault-tolerance/2009/12/31/bulkheads.html
尽管我们不是在建造船舶而是在建造软件,但其主要思想是保持不变的:将应用程序故障的影响降至最低,理想情况下可防止崩溃或无响应。 让我们来讨论其中几个方案舱壁体现自身,特别是在微服务 。
JCG租车平台的一部分, 预订服务可能会被要求检索特定客户的所有预订。 为此,它首先咨询客户服务以确保客户存在,并且在成功响应的情况下,从基础数据存储中获取可用的保留,从而将结果限制为前20条记录。
@Autowired private WebClient customers; @Autowired private ReservationsByCustomersRepository repository; @Autowired private ConversionService conversion; @GetMapping ( "/customers/{uuid}/reservations" ) public Flux findByCustomerId( @PathVariable UUID uuid) {
return customers
.get()
.uri( "/{uuid}" , uuid)
.retrieve()
.bodyToMono(Customer. class )
.flatMapMany(c -> repository
.findByCustomerId(uuid)
.take( 20 )
.map(entity -> conversion.convert(entity, Reservation. class ))); }
Spring Reactive堆栈的简洁性是惊人的,不是吗? 那么此代码段可能是什么问题? 实际上,这完全取决于repository
。 如果调用被阻塞,则由于偶数循环也将被阻塞(请记住Reactor模式 ),因此灾难将要发生。 相反,应该将阻塞调用隔离并卸载到专用池(使用subscribeOn
)。
return customers
.get()
.uri( "/{uuid}" , uuid)
.retrieve()
.bodyToMono(Customer. class )
.flatMapMany(c -> repository
.findByCustomerId(uuid)
.take( 20 )
.map(entity -> conversion.convert(entity, Reservation. class ))
.subscribeOn(Schedulers.elastic()));
可以说,这是隔板的一个示例,可以使用专用线程池,队列或进程来最大程度地减少对应用程序关键部分的影响。 在服务的多个实例上进行部署和平衡,在多租户应用程序中隔离租户,对请求处理进行优先级排序,协调后台工作人员和前台工作人员之间的资源利用率,这只是您可能会遇到的有趣挑战的一小段。
8.断路器
真棒,所以我们已经了解了重试策略和舱壁 ,我们知道如何运用这些原则来隔离故障,并逐步把工作做好。 但是,我们的目标并不是真的,我们必须保持响应并履行SLA的承诺。 即使您没有,也必须在合理的时间内做出回应。 迈克尔·尼加德 ( Michael Nygard)大力推广的断路器模式,强烈建议读者阅读Release It! 书,这是我们真正需要的。
断路器的实现可能会非常复杂,但是我们将重点关注它的两个核心功能:能够跟踪远程调用的状态以及在发生故障或超时时使用回退功能。 有很多优秀的库提供了断路器的实现。 除了我们之前提到的故障安全和恢复能力 4j之外,还有Hystrix , Apache Polygene和Akka 。 Hystrix可能是迄今为止最有名且经过考验的断路器实现,也是我们将要使用的一种。
回到我们的预订服务 ,让我们看看如何将Hystrix集成到React流中。
public Flux findByCustomerId( @PathVariable UUID uuid) {
final Publisher customer = customers
.get()
.uri( "/{uuid}" , uuid)
.retrieve()
.bodyToMono(Customer. class );
final Publisher fallback = HystrixCommands
.from(customer)
.eager()
.commandName( "get-customer" )
.fallback(Mono.empty())
.build();
return Mono
.from(fallback)
.flatMapMany(c -> repository
.findByCustomerId(uuid)
.take( 20 )
.map(entity -> conversion.convert(entity, Reservation. class ))); }
在此示例中,我们没有调整任何Hystrix配置 ,但是如果您想了解有关内部结构的更多信息,请查看本文 。
断路器的使用不仅可以帮助用户根据操作统计信息做出明智的决策,还可以帮助服务提供商更快地从间歇性负载状况中恢复。
9.预算
断路器以及敏感的超时和重试策略可帮助您的服务处理故障,但是它们也会消耗您的服务SLA预算。 当服务最终获得组装最终响应所需的所有数据时,另一侧绝对不感兴趣,并且很早以前就断开了连接。
尽管有一种非常简单的技术可以应用,但这是一个很难解决的问题:考虑在逐步满足请求的同时计算服务所具有的大致时间预算。 超出预算应该是一个例外,而不是规则,但是,一旦发生预算,您就可以做好准备,以减少不必要的工作。
10.持久队列
这在某种程度上是显而易见的,但是如果您的微服务体系结构是使用异步消息传递构建的,则存储消息或事件的队列必须是持久性的(并且非常希望复制)。 我们之前讨论的大多数消息代理都支持开箱即用的持久性持久存储,但是在某些特殊情况下,您可能会陷入困境。
回到让客户成功注册后发送确认电子邮件的示例 , 该示例是使用异步CDI 2.0事件实现的。
customerRegisteredEvent
.fireAsync( new CustomerRegistered(entity.getUuid()))
.whenComplete((r, ex) -> {
if (ex != null ) {
LOG.error( "Customer registration post-processing failed" , ex);
}
});
这种方法的问题在于事件队列正在内存中全部发生。 如果进程在事件传递给侦听器之前崩溃,那么它将永远丢失。 也许在确认电子邮件的情况下没什么大不了的,但是问题仍然存在。
对于不希望丢失此类事件或消息的情况,一种选择是使用持久的进程内队列,例如Chronicle Queue 。 但是从长远来看,使用专用消息代理或数据存储可能总体上是更好的选择。
11.速率限制器
您应该准备服务的不愉快但不幸的是非常现实的情况之一就是与虐待客户打交道。 我们将排除故意的恶意和DDoS攻击,因为它们需要复杂的缓解解决方案。 但是确实会发生错误,甚至内部消费者也可能会发疯,并试图将您的服务搁浅。
速率限制是一种有效的技术,可以控制来自特定源的请求的速率并在违反限制的情况下减轻负载。
尽管可以将速率限制烘烤到每个服务中(例如,使用Redis来协调所有服务实例),但将此类责任转移给API网关和业务流程层更有意义。 我们将在本教程的后面部分更详细地回到该主题。
12.萨加斯
让我们暂时忘掉单个微服务 ,然后看一下全局 。 典型的业务流程通常是一个多步骤流程,并且依赖于几个微服务才能成功地发挥作用。 JCG租车公司实施的预订流程就是一个很好的例子。 至少涉及三个服务:
- 库存服务必须确认车辆的可用性
- 预订服务必须检查尚未预订的车辆并进行预订
- 付款服务必须处理费用(或退款)
流程略有简化,但要点是,由于各种原因,每个步骤都可能失败。 巨石采取的传统方法是将所有内容包装在庞大的“要么有要么无”数据库事务中,但是它在这里行不通。 那有什么选择呢?
其中之一是使用分布式事务和两阶段提交协议,它带来了所有复杂性和可伸缩性问题。 更加符合微服务架构的另一种方法是使用sagas 。
传奇是一系列本地交易。 每个本地事务都会更新数据库并发布消息或事件以触发传奇中的下一个本地事务。 如果本地事务因为违反业务规则而失败,那么该传奇将执行一系列补偿事务,以撤消先前的本地事务所做的更改。 – https://microservices.io/patterns/data/saga.html
在实现跨越多个微服务的业务流程时,很有可能需要依靠sagas 。 Axon和Eventuate Tram Saga是支持sagas的框架的两个示例,但最终陷入DIY情况的机会很高。
13.混乱
在这一点上,构建微服务似乎是在与混乱作斗争:任何地方的任何事物都可能崩溃,您必须以某种方式应对。 从某种意义上说是正确的,这可能就是为什么混沌工程学科诞生的原因。
混沌工程学是在分布式系统上进行实验的学科, 目的是建立对该系统在生产中承受动荡条件的能力的信心。 – https://principlesofchaos.org/
混乱工程的目标不是使系统崩溃,而是确保缓解策略起作用并发现问题(如果有)。 在专用于测试的教程部分中,我们将花一些时间讨论故障注入,但是如果您想立即学习更多信息,请查看这篇出色的入门文章 。
14.结论
在本教程的这一部分中,我们讨论了在实现微服务体系结构时考虑和缓解故障的重要性。 网络是不可靠的,保持弹性和响应能力应该是舰队中每个微服务遵循的核心指导原则。
我们已经介绍了一组普遍适用的技术和实践,但这只是冰山一角。 诸如Java GC暂停检测或负载平衡之类的高级方法不在我们的讨论范围之内,但是本教程的后续部分将深入探讨其中的一些内容。
要提前一点,值得一提的是,许多以前由应用程序负责的问题正在转移到基础架构或业务流程层。 但是,知道存在此类问题以及如何解决这些问题仍然很有价值。
15.下一步是什么
在本教程的下一部分中,我们将讨论安全性和秘密管理,这是将一切都部署到公共云时代的非常重要的主题。
逻辑谬误