微服务:服务间如何通信?
在微服务架构中,会将一个完整的应用程序拆分成一组服务。这些服务之间需要经过协作,通过接口调用,才能组成一个完整的应用。
不同的服务部署在不同的机器上,或者同一个机器的多个容器中,进程间进行通信就不可避免了,也变得非常重要。
按种类来分,进程间的通信方式有很多种,比如远程过程调用的 RESTful API 和 gRPC 、基于消息机制的异步方式等。
RESTful API :现在前后端分离比较流行,RESTful API 大家应该都比较熟悉。REST 是一种使用 HTTP 协议的进程间通信机制,一般使用 Json 来传递数据; gRPC :是一个高性能、开源和通用的 RPC 框架,基于 ProtoBuf ( Protocol Buffers ) 序列化协议开发,支持众多开发语言。面向服务端和移动端,基于 HTTP/2 设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特性; 异步消息:使用消息中间件来实现,比如 RabbitMQ、Kafka 等。
按照交互方式来分,会有同步、异步。
同步:客户端向服务端发起请求、等待服务端响应,等待的过程会造成阻塞; 异步:客户端向服务端发起请求,服务端立即响应,不会造成阻塞,比如说消息队列的发布、订阅方式。
这里有几个概念需要统一下语言:接口、客户端、服务端
接口:如果使用的是消息机制,那么接口就是由消息通道、类型和消息格式组成的;如果是基于 HTTP ,则是由 URL、HTTP 动词和请求响应的格式来组成;当然,也可以是提供一组方法的类; 客户端、服务端:说起客户端,第一印象容易想到浏览器、移动 APP ,这里的客户端是指在调用和被调用的调用方,服务端就是被调用方。而接口的定义是由服务端来进行定义。
在前后端分离之前的单体应用中,当接口方法有变化时,进行代码编译就知道哪些地方需要调整,或者在 IDE 中进行接口方法引用的查找,也能很容易处理。
在微服务中,不同的服务可能是不同的团队来进行开发,服务端接口的修改、滚动发布等都需要有很好的兼容性和可用性。
一种方式是在接口中向下兼容,但时间越长代码就会变得越复杂,比如:
添加一些控制性的参数; 接口方法新添加的参数需要给默认值; 返回值可能也需要进行特殊处理。
另一种方式就是不向下兼容,所有的客户端需要进行代码的调整来适应新的接口。在客户端代码还没有完全调整完之前,新老接口需要共存,共存有两种方式:
使用 URL 地址中添加版本号,比如:/api/v1/xxx , /api/v2/xxx ; 在请求头或消息体中添加版本号,接口方法中根据版本号来进行适配,调用对应版本的逻辑然后返回给客户端。
所以,一个设计良好的接口可以在暴露有用功能的同时隐藏实现细节,对于细节,可以进行扩展,修改,并不会影响到客户端的调用,这就要求在接口设计之前,需要先进行定义,经过多轮评审后再进行编码实现。
好的设计自带防腐层。
因为客户端和服务端是互相独立的,服务端有时在特定时间内无法完全响应客户端的请求,可能是自己本身的故障,也可能是超过了负载。这时,客户端请求就会被阻塞,无限地等待。
有几种方式可以来解决这个问题:
设置超时:在等待请求响应时,不要无限阻塞,设置一个超时时间,超过时间就返回; 根据负载能力,限制客户端请求的数量,超过上限,后面的请求直接返回失败; 对客户端请求进行监控,如果失败的比例超过阈值,就进行熔断,让调用立即失败。
现在有一些成熟的框架可以方便进行熔断的处理,比如:.NET 中的 Polly、Spring Cloud 中的 Sentinel、Hystrix 。
在传统软件中,经常使用环境变量和配置文件来进行静态地址的配置,而部署在云端的分布式微服务程序中,地址是动态的,那客户端怎么能找到这些地址呢?这就需要用到服务发现。
服务发现就是客户端不再依赖一个静态的固定地址去寻找服务端,而是根据一个路由名称在服务注册表去寻找服务端地址,服务端部署后会将地址写入服务注册表。
在微服务框架中,也有相关的框架来实现服务发现,比如:.NET 中的 consul 、Spring Cloud 中的 Eureka、Nacos 等。
对于实时性要求不高的场景,可以采用异步消息的方式来实现。比如删除数据时,需要删除数据中对应的附件信息、各种操作的日志记录、流程流转中需要发送消息通知等。
使用异步消息有下面几个好处:
不需要知道是接收方的地址,只需要将消息发出去就行,发送方和接收方充分解耦; 消息的消费者可以是一个,也可以是多个,当处理速度不够时,可以横向扩展多个消费者来进行处理; 消息中间件在发送方和接收方中间起到一个缓冲的作用。
现在流行的开源中间件有 RabbitMQ、ActiveMQ、RokcetMQ、Kafka 等,选择这些中间件时需要考虑:
支持的编程语言; 支持的消息标准; 是否支持持久化? 延迟是否在接受范围之内? 消息在处理时能否保持顺序?
很多工作流引擎使用的是消息驱动机制,流程在流转过程中需要保证消息是顺序处理的,否则流程数据可能出现错乱,如何在保证消息顺序处理的情况下又能横向进行扩展,这是一个挑战。在 Kafka 中可以使用分片的方式进行解决。
上面介绍的是服务间通信的一些常用方式,了解了基本逻辑,在具体实践时,无论是使用 .NET 技术栈还是 Java 技术栈来做微服务,就都不是什么难事了。
希望本文对您有所帮助!
相关阅读: