微服务架构设计-4接口协议选择
确定了微服务的服务划分是关键的一步,接下来需要考虑选择合适的接口协议来实现微服务之间的数据通信。目前,主流的接口调用方式可以分为两大类:RPC(远程过程调用)和REST。
🔆 RPC(Remote Procedure Call): 以本地方法调用的形式处理远程方法调用的模型。常用的有:SOAP、RMI、Thrift、Avro、gRPC、Dubbo协议。
🔆 REST(Representational State Transfer):以资源为中心,描述资源状态变更的模型。常见于HTTP协议。
1. RPC
RPC是Bruce_Jay_Nelson在1984年的《Implementing Remote Procedure Calls》文章中首次提出的,用于构建简单、高效、通用的通信机制。以gRPC、Dubbo为代表的RPC方式的优点有:
- 可定制协议/传输类型,可实现高性能通讯
- 可用于强格式约束场景
使用RPC一般而言要先定义IDL(Interface description language,接口描述言),以gRPC为例,我们需要定义类似的proto文件:
// 定义服务名称
service Greeter {
// 定义方法
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 方法入参消息体
message HelloRequest {
string name = 1;
}
// 方法出参消息体
message HelloReply {
string message = 1;
}
对于Java而言,可以使用protobuf-maven-plugin 这一Maven插件将上述代码编译成Java文件,之后在服务端就可以实现对应的方法,如
private class GreeterImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
具体示例官方都有,这里不赘述,当然我们也可以使用不同的工具生成C++、NodeJS、C#、Go等不同的语言的实现。
由于Dubbo只支持JVM平台,所以它的IDL只要定义Java的接口即可。
我们可以看出RPC存在一些不足之处,包括以下方面:
- 多语言约束: 不同调用方之间存在语言约束。例如,跨语言RPC(如Thrift、Avro、gRPC)需要定义接口描述语言(IDL),而无需显式指定IDL的RPC(如Dubbo、RMI)对协议的参与方有语言限制。
- 字段变更导致重新部署: 当有新的服务调用方加入,需要重用某一接口并为其添加新字段时,通常需要修改IDL。大多数RPC框架要求所有调用方同步更新,这可能导致频繁的部署操作。
再看REST时,它主要基于HTTP协议实现,具有以下优点:
- 通用性高,无语言约束: REST具有较高的通用性,不受语言的限制。主流的编程语言都提供了对REST的良好支持。
- 弱格式约束,字段变更不需要重新部署: REST相对宽松的格式约束使得在接口字段发生变更时,不需要所有调用方都重新部署。这为系统的灵活性和演化提供了一定的便利。Dubbo需要版本控制,可以达到支持多版本并行的能力也算不错。
- 防火墙友好: 由于REST基于HTTP协议,而HTTP是广泛被接受的协议,因此REST更容易被防火墙接受。这使得在跨越网络边界时更容易实现通信。
2.REST
REST一般会基于Json或XML做为交互格式,两者均为跨语言、弱约束的格式。我们不需要先定义IDL再生成对应的语言文件,接口的变更除受影响的参与方需要修改外不需要全局重新部署,例如:
GET /user/{id}
{
"name":""
}
这是我们的获取用户信息接口,返回用户姓名,有4个业务方调用,但后来有其中一个业务方要求再返回年龄信息,那么我们接口可以修改成:
GET /user/{id}
{
"name":"",
"age":0
}
这里增加了age字段只为某一业务方使用,其它业务方可以不用同步修改。在实际生产中我们一般会为核心接口增加版本号字段以更好地做多版本兼容。
为REST API添加版本有三种方式:
- 版本信息放在Path或Query中,如
api.example.com/v1
api_v1.example.com
api.example.com/xxx?version=v1
- 使用自定义Header, 如
Accept-Version:v1
Accept-Version:v2
- 使用内容协商
Accept: application/vnd.example.v1+json
Accept: application/vnd.example+json;version=1.0
从REST设计的初衷而言,URL对于表述资源而与版本无关,因此纯学术的建议是使用第2、3种方式,但我们也看了大量第1种方式的API,其中不乏有知名的IT厂商,所以笔者觉得这3种都可以,如何选择完全看团队的风格。更多讨论。
引入API版本控制后,确实可以在一定程度上提高接口的向下兼容性,允许更自由地进行业务修改。然而,多版本维护和兼容性确实会对服务架构提出更高的要求。
在实践中,对于服务逻辑变更较大的情况,发布一个新的服务版本是一种常见的策略。这种方式可以在保持向下兼容性的同时,为新的业务逻辑提供更大的灵活性。而对于变更相对有限的情况,可以考虑在服务中增加兼容/适配层,以平滑地过渡到新的版本。
对于REST的局限性,确实存在一些挑战。传输效率相对较低,因为REST通常使用文本格式(如JSON或XML),而不像一些RPC框架那样使用二进制协议,导致传输的数据量相对较大。此外,由于REST缺乏强约定规范,对字段和结构的修改可能会导致已接入调用方的异常。
总体而言,选择API版本控制策略和服务架构设计都需要根据具体情况和需求进行权衡。不同的项目可能会采用不同的方法,取决于团队的技术栈、发展阶段和对灵活性与稳定性的重视程度。
那么我们究竟如何选择呢?
在选择通信协议和架构模式时,需要综合考虑多个因素,包括性能、灵活性、维护成本等。下面以微服务架构为背景,对比REST(代表Spring Cloud)和RPC(代表Dubbo)的一些考虑因素:
- 性能: REST相对于RPC在IO性能上的一些损失。如果对于实际的业务调用场景而言,通信性能的差距可以被接受,并且可以通过缓存、压缩等手段进行优化,那么这可能不是一个决定性的因素。
- 异构系统的通讯: REST的无状态和基于标准协议的特性使其更容易与异构系统进行通信。如果系统中有多种语言和平台,REST可能更为方便。
- 接口修改的影响: 对于REST,确实需要通过一定的开发规范来防范接口修改可能带来的影响。这包括规范修改的方式,例如不允许删除或修改字段,而是使用新增字段来解决。
- 效率与锲约检查: 对于一些业务相对稳定、高频调用的服务,尤其是核心的公共服务,RPC可能更适合。RPC框架通常提供了强约定和类型检查,这有助于提高效率并防止一些潜在的错误。
- 兼容性和集成: 目前没有一种通用的框架同时支持REST和RPC。在微服务架构中,可以根据具体的业务需求和服务特性选择不同的通信方式,甚至可以混合使用。
针对车贷系统,考虑到整体并发要求不高,核心接口的 TPS 不会超过1000,选择REST作为通信协议是一个自然而合理的决定。REST在与异构系统通信以及保持接口灵活性方面具有优势,适合当前的业务需求。
但随着项目的后续版本迭代,将一些核心服务改成dubbo,以提升性能和稳定性。dubbo作为一种高效的RPC框架,可以在需要更高效通信的情况下发挥作用。这种逐步的演进方式允许在项目的不同阶段选择适合的通信方式,同时确保了对未来需求的灵活响应。
这样的决策考虑到了当前的业务需求和未来可能的性能提升需求,为系统架构的演进提供了一种合理的方法。