微服务间通信

许多架构师已经将微服务之间的通信划分为同步和异步两种模式。让我们一个一个来介绍。

同步模式

当我们说到同步时,意思是客户端向服务端发出请求并等待其响应。线程将被阻塞,直到它接收到返回。实现同步通信最主要的协议是 HTTP。HTTP 可以通过 REST 或 SOAP 实现。现在 REST 在微服务方面发展迅速并超越了 SOAP。对我而言两者都很好用。

现在让我们讨论同步模式中的不同的工作流、用例,我们面临的问题以及如何去解决。

先从一个简单的例子开始。你需要一个服务 A 来调用服务 B 并等待实时数据的响应。这是实现同步的一个很好的选择,因为不会涉及到下游服务。如果使用多个实例,除了负载均衡之外,你不需要为这个用例实现任何复杂的设计模式。

10_微服务间通信.png

现在让我们把它变得更复杂一点。服务 A 为实时数据调用多个下游服务,如服务 B、服务 C 和服务 D。

  • 服务 B、服务 C 和服务 D 都必须按顺序调用——当服务相互依赖以检索数据,或者是有一个事件序列的功能需要被执行,就会出现这种情况。
  • 服务 B、服务 C 和服务 D 可以并行调用——这种场景被使用在服务彼此独立或服务 A 可能扮演协调者(Orchestrator)角色时。

11_微服务间通信.png

这会为通信带来复杂性。让我们一个一个地讨论。

紧密耦合

服务 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 是云平台中最流行的消息传递解决方案。

12_微服务间通信.png

事件驱动

事件驱动方式看起来类似于消息传递,但它的用途不同。它不会发送消息,而是将事件细节连同负载(payload)一起发送到消息代理。消费者将确定事件是什么,以及如何对此作出响应。这会更加松散的耦合。有下面几种类型的负载可以被传递:

  • 全负载 —— 这将包含与消费者采取进一步行动所需的事件相关的所有数据。然而,这使得它的耦合更加紧密。
  • 资源 URL —— 这是一个指向代表事件的资源的URL。
  • 仅事件 —— 不会发送负载,消费者将基于事件名称知道如何从其他源(如数据库或队列)检索相关数据。

13_微服务间通信.png

还有其他一些方式,比如编排(choreography),但我个人并不喜欢。它太复杂几乎无法实现,只能通过同步方式来完成。