许多架构师已经将微服务之间的通信划分为同步和异步两种模式。让我们一个一个来介绍。
当我们说到同步时,意思是客户端向服务端发出请求并等待其响应。线程将被阻塞,直到它接收到返回。实现同步通信最主要的协议是 HTTP。HTTP 可以通过 REST 或 SOAP 实现。现在 REST 在微服务方面发展迅速并超越了 SOAP。对我而言两者都很好用。
现在让我们讨论同步模式中的不同的工作流、用例,我们面临的问题以及如何去解决。
先从一个简单的例子开始。你需要一个服务 A 来调用服务 B 并等待实时数据的响应。这是实现同步的一个很好的选择,因为不会涉及到下游服务。如果使用多个实例,除了负载均衡之外,你不需要为这个用例实现任何复杂的设计模式。
现在让我们把它变得更复杂一点。服务 A 为实时数据调用多个下游服务,如服务 B、服务 C 和服务 D。
这会为通信带来复杂性。让我们一个一个地讨论。
服务 A 将与服务 B、C 和 D 耦合。它必须知道每个服务的端点(endpoint)和凭据(credentials)。
解决方案: 服务发现模式 就是用来解决这类问题的。它通过提供查询功能来帮助分离消费者和生产者应用。服务 B、C 和 D 可以将它们自己注册为服务。服务发现可以在服务端也可以在客户端实现。对于服务端,有 AWS ALB 和 NGINX,它们接受来自客户端的请求,发现服务并将请求路由到指定位置。
对于客户端,有 Spring Eureka 发现服务。使用 Eureka 的真正好处是它在客户端缓存了可用的服务信息,所以即使 Eureka 服务器宕机了一段时间,它也不会成为单点故障。除了 Eureka,etcd 和 consul 等其他服务发现工具也得到了广泛的应用。
如果服务 B,C,D 有多个实例,它们需要知道如何负载均衡。
解决方案: 负载均衡通常与服务发现携手出现。对于服务器负载平衡,可以使用 AWS ALB,对于客户端可以使用 Ribbon 或 Eureka。
如果服务 B、C 和 D 需要被保护并验证身份,我们只需要过滤这些服务的某些请求,如果服务 A 和其他服务使用不同的协议。
解决方案: API 网关模式(gateway) 有助于解决这些问题。它可以处理身份验证、过滤和将协议从 AMQP 转换为 HTTP 或其他协议。它还可以查看分布式日志、监控和分布式跟踪等可观测的指标(metrics)。Apigee、Zuul 和 Kong 就是一些这样的工具。请注意,如果服务 B、C 和 D 是可管理的 API 的一部分,我建议使用这种模式,否则使用 API 网关就太重了。下面将要读到的服务网格是它的替代解决方案。
如果服务 B、C 或 D 宕机,服务 A 仍然有某些功能来响应客户端请求,就必须相应地对其进行设计。另一个问题是:假设服务B宕机,所有请求仍然在调用服务B,并且由于它没有响应而耗尽了资源,这会使整个系统宕机,服务 A 也无法向 C 和 D 发送请求了。
解决方案: 熔断器(Circuit Breaker) 和隔板(bulkhead) 模式有助于解决这些问题。熔断器识别下游服务是否停机了一段时间,并断开开关以避免向其发送调用请求。它将在定义的时间段之后再次尝试检查,如果服务已经恢复则关闭开关以继续对其进行调用。这确实有助于避免网络阻塞和耗尽资源。隔板模式有助于隔离用于服务的资源,并避免级联故障。Spring Cloud Hystrix 就是做这样的工作,它适用于断路器和隔板模式。
API 网关 通常用于管理 API,它处理来自 ui 或其他消费者的请求,并对多个微服务进行下游调用并作出响应。但是,当一个微服务想要调用同组中的另一个微服务时,API 网关就没有必要了,它并不是为了这个目的而设计的。最终,独立的微服务将负责进行网络通信、安全验证、处理超时、处理故障、负载平衡、服务发现、监控和日志记录。对于微服务来说开销太大。
解决方案: 服务网格模式有助于处理此类 NFRs。它可以卸载我们前面讨论的所有网络功能。这样,微服务就不会直接调用其他微服务,而是通过服务网格,它将处理所有的通信。这种模式的美妙之处在于,你可以专注于用任何语言(如 Java、NodeJS 或 Python)编写业务逻辑,而不必担心这些语言是否支持所有的网络功能。Istio 和 Linkerd 解决了这些需求。我唯一不喜欢 Istio 的地方是它目前仅限于 Kubernetes。
当我们谈到异步通信时,它意味着客户端调用服务器,接收到请求的确认,然后忘记它。服务器将处理请求并完成。
现在让我们讨论一下什么时候需要异步。如果你的应用读操作很多,那么同步可能非常适合,尤其是在需要实时数据时。但是,当你处理大量写操作而又不能丢失数据记录时,你可能希望选择异步操作,因为如果下游系统宕机,你继续向其发送同步的调用,你将丢失请求和业务交易。经验法则是永远不要对实时数据读取使用异步,也永远不要对关键的业务交易写操作使用同步,除非你在写后立即需要数据。你需要在数据可用性和数据的强一致性之间进行选择。
有几种不同的方式可以实现异步:
在这种方式中,生产者将消息发送到消息代理,而消费者可以监听代理来接收消息并相应地处理它。在消息处理中有两种模式:一对一和一对多。我们讨论了同步带来的一些复杂性,但是在消息传递中,默认情况下就会消除一些复杂性。例如,服务发现变得无关紧要,因为消费者和生产者都只与消息代理对话。负载均衡是通过扩展消息系统来处理的。失败处理是内建的,主要由 message broker 负责。RabbitMQ、ActiveMQ 和 Kafka 是云平台中最流行的消息传递解决方案。
事件驱动方式看起来类似于消息传递,但它的用途不同。它不会发送消息,而是将事件细节连同负载(payload)一起发送到消息代理。消费者将确定事件是什么,以及如何对此作出响应。这会更加松散的耦合。有下面几种类型的负载可以被传递:
还有其他一些方式,比如编排(choreography),但我个人并不喜欢。它太复杂几乎无法实现,只能通过同步方式来完成。