微服务架构已成为目前互联网架构的趋势,关于微服务的讨论,几乎占据了各种技术大会的绝大多数版面。国内使用最多的服务治理框架非阿里开源的 dubbo 莫属,千米网也选择了 dubbo 作为微服务治理框架。另一方面,和大多数互联网公司一样,千米的开发语言是多样的,大多数后端业务由 java 支撑,而每个业务线有各自开发语言的选择权,便出现了 nodejs,python,go 多语言调用的问题。
创新互联公司是网站建设专家,致力于互联网品牌建设与网络营销,专业领域包括成都网站建设、成都网站制作、电商网站制作开发、微信小程序定制开发、微信营销、系统平台开发,与其他网站设计及系统开发公司不同,我们的整合解决方案结合了恒基网络品牌建设经验和互联网整合营销的理念,并将策略和执行紧密结合,且不断评估并优化我们的方案,为客户提供全方位的互联网品牌整合方案!
跨语言调用是一个很大的话题,也是一个很有挑战的技术活,目前业界经常被提及的解决方案有如下几种,不妨拿出来老生常谈一番:
当我们再聊跨语言调用时我们在聊什么?纵观上述几个较为通用,成熟的解决方案,可以得出结论:解决跨语言调用的思路无非是两种:
如果一个新型的团队面临技术选型,我认为上述的方案都可以纳入参考,可考虑到遗留系统的兼容性问题
旧系统的迁移成本
这也关键的选型因素。我们做出的第一个尝试,便是在 RPC 协议上下功夫。
通用协议的跨语言支持
springmvc的美好时代
springmvc
springmvc
在没有实现真正的跨语言调用之前,想要实现“跨语言”大多数方案是使用 http 协议做一层转换,最常见的手段莫过于借助 springmvc 提供的 controller/restController,间接调用 dubbo provider。这种方案的优势和劣势显而易见
通用协议的支持
事实上,大多数服务治理框架都支持多种协议,dubbo 框架除默认的 dubbo 协议之外,还有当当网扩展的 rest协议和千米网扩展的 json-rpc 协议可供选择。这两者都是通用的跨语言协议。
rest 协议为满足 JAX-RS 2.0 标准规范,在开发过程中引入了 @Path,@POST,@GET 等注解,习惯于编写传统 rpc 接口的人可能不太习惯 rest 风格的 rpc 接口。一方面这样会影响开发体验,另一方面,独树一帜的接口风格使得它与其他协议不太兼容,旧接口的共生和迁移都无法实现。如果没有遗留系统,rest 协议无疑是跨语言方案最简易的实现,绝大多数语言支持 rest 协议。
和 rest 协议类似,json-rpc 的实现也是文本序列化http 协议。dubbox 在 restful 接口上已经做出了尝试,但是 rest 架构和 dubbo 原有的 rpc 架构是有区别的,rest 架构需要对资源(Resources)进行定义, 需要用到 http 协议的基本操作 GET、POST、PUT、DELETE。在我们看来,restful 更合适互联网系统之间的调用,而 rpc 更适合一个系统内的调用。使用 json-rpc 协议使得旧接口得以兼顾,开发习惯仍旧保留,同时获得了跨语言的能力。
千米网在早期实践中采用了 json-rpc 作为 dubbo 的跨语言协议实现,并开源了基于 json-rpc 协议下的 python 客户端 dubbo-client-py 和 node 客户端 dubbo-node-client,使用 python 和 nodejs 的小伙伴可以借助于它们直接调用 dubbo-provider-java 提供的 rpc 服务。系统中大多数 java 服务之间的互相调用还是以 dubbo 协议为主,考虑到新旧协议的适配,在不影响原有服务的基础上,我们配置了双协议。
dubbo 协议主要支持 java 间的相互调用,适配老接口;json-rpc 协议主要支持异构语言的调用。
定制协议的跨语言支持
微服务框架所谓的协议(protocol)可以简单理解为:报文格式和序列化方案。服务治理框架一般都提供了众多的协议配置项供使用者选择,除去上述两种通用协议,还存在一些定制化的协议,如 dubbo 框架的默认协议:dubbo 协议以及 motan 框架提供的跨语言协议:motan2。
motan2协议的跨语言支持
motan2
motan2
motan2 协议被设计用来满足跨语言的需求主要体现在两个细节中—MetaData 和 motan-go。在最初的 motan 协议中,协议报文仅由 Header+Body 组成,这样导致 path,param,group 等存储在 Body 中的数据需要反序列得到,这对异构语言来说是很不友好的,所以在 motan2 中修改了协议的组成;weibo 开源了 motan-go ,motan-php ,motan-openresty ,并借助于 motan-go 充当了 agent 这一翻译官的角色,使用 simple 序列化方案来序列化协议报文的 Body 部分(simple 序列化是一种较弱的序列化方案)。
agent
agent
仔细揣摩下可以发现这么做和双协议的配置区别并不是大,只不过这里的 agent 是隐式存在的,与主服务共生。明显的区别在于 agent 方案中异构语言并不直接交互。
dubbo协议的跨语言支持
dubbo 协议设计之初只考虑到了常规的 rpc 调用场景,它并不是为跨语言而设计,但跨语言支持从来不是只有支持、不支持两种选择,而是要按难易程度来划分。是的,dubbo 协议的跨语言调用可能并不好做,但并非无法实现。千米网便实现了这一点,nodejs 构建的前端业务是异构语言的主战场,最终实现了 dubbo2.js,打通了 nodejs 和原生 dubbo 协议。作为本文第二部分的核心内容,重点介绍下我们使用 dubbo2.js 干了什么事。
Dubbo协议报文格式
dubbo协议
dubbo协议
dubbo协议报文消息头详解:
magic:类似java字节码文件里的魔数,用来判断是不是 dubbo 协议的数据包。魔数是常量 0xdabb
flag:标志位, 一共8个地址位。低四位用来表示消息体数据用的序列化工具的类型(默认 hessian),高四位中,第一位为 1 表示是 request 请求,第二位为 1 表示双向传输(即有返回 response),第三位为 1 表示是心跳 ping 事件。
status:状态位, 设置请求响应状态,dubbo 定义了一些响应的类型。具体类型见com.alibaba.dubbo.remoting.exchange.Response
invoke id:消息 id, long 类型。每一个请求的唯一识别 id(由于采用异步通讯的方式,用来把请求 request 和返回的 response 对应上)
body length:消息体 body 长度, int 类型,即记录 Body Content 有多少个字节
body content:请求参数,响应参数的抽象序列化之后存储于此。
协议报文最终都会变成字节,使用 tcp 传输,任何语言只要支持网络模块,有类似 Socket 之类的封装,那么通信就不成问题。那,跨语言难在哪儿?以其他语言调用 java 来说,主要有两个难点:
ps:dubbo 协议通讯demo( )
Dubbo是 Alibaba 开源的分布式服务框架远程调用框架,在网络间传输数据,就需要通信协议和序列化。
Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多种协议,但是Dubbo官网是推荐我们使用Dubbo协议的,默认也是用的dubbo协议。
先介绍几种常见的协议:
缺省协议,使用基于mina1.1.7+hessian3.2.1的tbremoting交互。
连接个数:单连接
连接方式:长连接
传输协议:TCP
传输方式:NIO异步传输
序列化:Hessian二进制序列化
适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串。
适用场景:常规远程服务方法调用
1、dubbo默认采用dubbo协议,dubbo协议采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况
2、他不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。
配置如下:
dubbo:protocol name="dubbo" port="20880" /
dubbo:protocol name=“dubbo” port=“9090” server=“netty” client=“netty” codec=“dubbo”
serialization=“hessian2” charset=“UTF-8” threadpool=“fixed” threads=“100” queues=“0” iothreads=“9”
buffer=“8192” accepts=“1000” payload=“8388608” /
3、Dubbo协议缺省每服务每提供者每消费者使用单一长连接,如果数据量较大,可以使用多个连接。
dubbo:protocol name="dubbo" connections="2" /
4、为防止被大量连接撑挂,可在服务提供方限制大接收连接数,以实现服务提供方自我保护
dubbo:protocol name="dubbo" accepts="1000" /
Java标准的远程调用协议。
连接个数:多连接
连接方式:短连接
传输协议:TCP
传输方式:同步传输
序列化:Java标准二进制序列化
适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。
适用场景:常规远程服务方法调用,与原生RMI服务互操作
RMI协议采用JDK标准的java.rmi.*实现,采用阻塞式短连接和JDK标准序列化方式 。
基于Hessian的远程调用协议。
连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:表单序列化
适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。
适用场景:需同时给应用程序和浏览器JS使用的服务。
1、Hessian协议用于集成Hessian的服务,Hessian底层采用Http通讯,采用Servlet暴露服务,Dubbo缺省内嵌Jetty作为服务器实现。
2、Hessian是Caucho开源的一个RPC框架: ,其通讯效率高于WebService和Java自带的序列化。
基于http表单的远程调用协议。参见:[HTTP协议使用说明]
连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:表单序列化
适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。
适用场景:需同时给应用程序和浏览器JS使用的服务。
基于WebService的远程调用协议。
连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:SOAP文本序列化
适用场景:系统集成,跨语言调用
序列化是将一个对象变成一个二进制流就是序列化, 反序列化是将二进制流转换成对象
为什么要序列化?
Dubbo序列化支持java、compactedjava、nativejava、fastjson、dubbo、fst、hessian2、kryo,其中默认hessian2。其中java、compactedjava、nativejava属于原生java的序列化。
hessian2序列化:hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的hessian2序列化,而是阿里修改过的,它是dubbo RPC默认启用的序列化方式。
json序列化:目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,但其实现都不是特别成熟,而且json这种文本序列化性能一般不如上面两种二进制序列化。
java序列化:主要是采用JDK自带的Java序列化实现,性能很不理想。
在 2021-10-17 dubbogo 基础使用 中,介绍了如何用 go 写一个部署到 zookeeper 的 dubbo 服务,这次编写一个 go 语言的 dubbo 调用端。并且支持同时连接到多个 zookeeper ,根据需要调用不同 zk 上的同一个服务
这里我们部署两个 zookeeper ,端口号分别是 2181 和 2182
这里我们配置了两个 UserService 的引用,并且设置了不同的别名。指定了每个引用连接各自的 zk .
注意,这里 service.UserService 为服务提供方的接口,我们直接继承他
服务1 的日志如下:
服务2 的日志如下:
github-dubbogodemo
近几年诞生了很多微服务框架,比如JAVA的Spring Cloud、Dubbo;Golang的GoKit和GoMicro以及NodeJs的Seneca。几乎每种主流语言都有其对应的微服务框架。
Go在微服务框架中有其独特的优势,至于优势在哪,自行google。
1、GoKit框架
这是一个工具包的集合,可以帮助攻城狮构建强大、可靠和可维护的微服务。提供了用于实现系统监控和弹性模式组件的库,例如日志、跟踪、限流、熔断等。
基于这个框架的应用程序架构由三个主要的部分组成:
传输层:用于网络通信,服务通常使用HTTP或者gRPC等网络传输协议,或者使用NATS等发布订阅系统相互通信。
接口层:是服务器和客户端的基本构建块。每个对外提供的接口方法都会定义为一个Endpoint,一遍在服务器和客户端之间进行网络通信,每个端点使用传输层通过HTTP或gRPC等具体通信模式对外提供服务
服务成:具体的业务逻辑实现
2、GoMicro框架
这是一个基于Go语言实现的插件化RPC微服务框架。提供了服务发现、负载均衡、同步传输、异步通信以及事件驱动等机制,尝试简化分布式系统之间的通信,让开发者更专注于自身业务逻辑的开发。
GoMicro的设计哲学是可插拔的架构理念,提供了可快速构建系统的组件,并且可以根据自身的需求对GoMicro提供的默认实现进行定制。所有插件都可在仓库github.com/micro/go-plugins 中找到。
互联网架构下,大部分系统已经转型分布式。其中服务注册发现中心,分布式服务中非常重要的组成部分。按需选择合适的注册中心,也变的尤为重要。
Eureka是SpringCloud全家桶中非常重要的一个组件,主要是实现服务的注册和发现。Eureka做到了CAP理论中的AP,强调服务的高可用性。实现中分Eureka Server和Eureka Client两部分。
Eureka客户端会向Eureka注册中心注册为服务,并通过心跳来更新它的服务租约。同时也可以从服务端查询当前注册的服务信息并把他们缓存到本地并周期性的刷新服务状态。若服务集群出现分区故障时,Eureka会转入自动保护模式,允许分区故障的节点继续提供服务;若分区故障恢复,集群中其他分区会把他们的状态再次同步回来。
SpringCloud对其做了非常好的集成封装,是官方推荐的注册中心。
Zookeeper是大数据Hadoop中的一个分布式调度组件,强调数据一致性和扩展性,可用于服务的注册和发现。她是dubbo中默认的服务注册中心,也是目前使用最广泛的分布式服务发现组件。注重CAP理论中的CP。
Consul是一个高可用的分布式服务注册中心,由HashiCorp公司推出,Golang实现的开源共享的服务工具。Consul在分布式服务注册与发现方面有自己的特色,解决方案更加“一站式”,不再需要依赖其他工具。
1、通过HTTP接口和DNS协议调用API存储键值对,使服务注册和服务发现更容易;
2、支持 健康 检查,可以快速的告警在集群中的操作
3、支持key/value存储动态配置
4、支持任意数量的区域
ETCD是一个高可用的分布式键值数据库,可用于共享配置、服务的注册和发现。ETCD采用Raft一致性算法,基于Go语言实现。ETCD作为后起之秀,又非常大的优势。
1、基于HTTP+JSON的API,使用简单;
2、可选SSL客户认证机制,更安全;
3、单个实例支持每秒千次写操作,快速。
4、采用Raft一致性算法保证分布式。
Protocol 还有一个实现分支是 AbstractProxyProtocol,如下图所示:
从图中我们可以看到:gRPC、HTTP、WebService、Hessian、Thrift 等协议对应的 Protocol 实现,都是继承自 AbstractProxyProtocol 抽象类。
目前互联网的技术栈百花齐放,很多公司会使用 Node.js、Python、Rails、Go 等语言来开发 一些 Web 端应用,同时又有很多服务会使用 Java 技术栈实现,这就出现了大量的跨语言调用的需求。Dubbo 作为一个 RPC 框架,自然也希望能实现这种跨语言的调用,目前 Dubbo 中使用“HTTP 协议 + JSON-RPC”的方式来达到这一目的,其中 HTTP 协议和 JSON 都是天然跨语言的标准,在各种语言中都有成熟的类库。
下面就重点来分析 Dubbo 对 HTTP 协议的支持。首先,会介绍 JSON-RPC 的基础,并通过一个示例,快速入门,然后介绍 Dubbo 中 HttpProtocol 的具体实现,也就是如何将 HTTP 协议与 JSON-RPC 结合使用,实现跨语言调用的效果。
Dubbo 中支持的 HTTP 协议实际上使用的是 JSON-RPC 协议。
JSON-RPC 是基于 JSON 的跨语言远程调用协议。Dubbo 中的 dubbo-rpc-xml、dubbo-rpc-webservice 等模块支持的 XML-RPC、WebService 等协议与 JSON-RPC 一样,都是基于文本的协议,只不过 JSON 的格式比 XML、WebService 等格式更加简洁、紧凑。与 Dubbo 协议、Hessian 协议等二进制协议相比,JSON-RPC 更便于调试和实现,可见 JSON-RPC 协议还是一款非常优秀的远程调用协议。
在 Java 体系中,有很多成熟的 JSON-RPC 框架,例如 jsonrpc4j、jpoxy 等,其中,jsonrpc4j 本身体积小巧,使用方便,既可以独立使用,也可以与 Spring 无缝集合,非常适合基于 Spring 的项目。
下面先来看看 JSON-RPC 协议中请求的基本格式:
JSON-RPC请求中各个字段的含义如下:
在 JSON-RPC 的服务端收到调用请求之后,会查找到相应的方法并进行调用,然后将方法的返回值整理成如下格式,返回给客户端:
JSON-RPC响应中各个字段的含义如下:
Dubbo 使用 jsonrpc4j 库来实现 JSON-RPC 协议,下面使用 jsonrpc4j 编写一个简单的 JSON-RPC 服务端示例程序和客户端示例程序,并通过这两个示例程序说明 jsonrpc4j 最基本的使用方式。
首先,需要创建服务端和客户端都需要的 domain 类以及服务接口。先来创建一个 User 类,作为最基础的数据对象:
接下来创建一个 UserService 接口作为服务接口,其中定义了 5 个方法,分别用来创建 User、查询 User 以及相关信息、删除 User:
UserServiceImpl 是 UserService 接口的实现类,其中使用一个 ArrayList 集合管理 User 对象,具体实现如下:
整个用户管理业务的核心大致如此。下面我们来看服务端如何将 UserService 与 JSON-RPC 关联起来。
首先,创建 RpcServlet 类,它是 HttpServlet 的子类,并覆盖了 HttpServlet 的 service() 方法。我们知道,HttpServlet 在收到 GET 和 POST 请求的时候,最终会调用其 service() 方法进行处理;HttpServlet 还会将 HTTP 请求和响应封装成 HttpServletRequest 和 HttpServletResponse 传入 service() 方法之中。这里的 RpcServlet 实现之中会创建一个 JsonRpcServer,并在 service() 方法中将 HTTP 请求委托给 JsonRpcServer 进行处理:
最后,创建一个 JsonRpcServer 作为服务端的入口类,在其 main() 方法中会启动 Jetty 作为 Web 容器,具体实现如下:
这里使用到的 web.xml 配置文件如下:
完成服务端的编写之后,下面再继续编写 JSON-RPC 的客户端。在 JsonRpcClient 中会创建 JsonRpcHttpClient,并通过 JsonRpcHttpClient 请求服务端:
在 AbstractProxyProtocol 的 export() 方法中,首先会根据 URL 检查 exporterMap 缓存,如果查询失败,则会调用 ProxyFactory.getProxy() 方法将 Invoker 封装成业务接口的代理类,然后通过子类实现的 doExport() 方法启动底层的 ProxyProtocolServer,并初始化 serverMap 集合。具体实现如下:
在 HttpProtocol 的 doExport() 方法中,与前面介绍的 DubboProtocol 的实现类似,也要启动一个 RemotingServer。为了适配各种 HTTP 服务器,例如,Tomcat、Jetty 等,Dubbo 在 Transporter 层抽象出了一个 HttpServer 的接口。
dubbo-remoting-http 模块的入口是 HttpBinder 接口,它被 @SPI 注解修饰,是一个扩展接口,有三个扩展实现,默认使用的是 JettyHttpBinder 实现,如下图所示:
HttpBinder 接口中的 bind() 方法被 @Adaptive 注解修饰,会根据 URL 的 server 参数选择相应的 HttpBinder 扩展实现,不同 HttpBinder 实现返回相应的 HttpServer 实现。HttpServer 的继承关系如下图所示:
这里以 JettyHttpServer 为例简单介绍 HttpServer 的实现,在 JettyHttpServer 中会初始化 Jetty Server,其中会配置 Jetty Server 使用到的线程池以及处理请求 Handler:
可以看到 JettyHttpServer 收到的全部请求将委托给 DispatcherServlet 这个 HttpServlet 实现,而 DispatcherServlet 的 service() 方法会把请求委托给对应接端口的 HttpHandler 处理:
了解了 Dubbo 对 HttpServer 的抽象以及 JettyHttpServer 的核心之后,回到 HttpProtocol 中的 doExport() 方法继续分析。
在 HttpProtocol.doExport() 方法中会通过 HttpBinder 创建前面介绍的 HttpServer 对象,并记录到 serverMap 中用来接收 HTTP 请求。这里初始化 HttpServer 以及处理请求用到的 HttpHandler 是 HttpProtocol 中的内部类,在其他使用 HTTP 协议作为基础的 RPC 协议实现中也有类似的 HttpHandler 实现类,如下图所示:
在 HttpProtocol.InternalHandler 中的 handle() 实现中,会将请求委托给 skeletonMap 集合中记录的 JsonRpcServer 对象进行处理:
skeletonMap 集合中的 JsonRpcServer 是与 HttpServer 对象一同在 doExport() 方法中初始化的。最后,我们来看 HttpProtocol.doExport() 方法的实现:
介绍完 HttpProtocol 暴露服务的相关实现之后,下面再来看 HttpProtocol 中引用服务相关的方法实现,即 protocolBindinRefer() 方法实现。该方法首先通过 doRefer() 方法创建业务接口的代理,这里会使用到 jsonrpc4j 库中的 JsonProxyFactoryBean 与 Spring 进行集成,在其 afterPropertiesSet() 方法中会创建 JsonRpcHttpClient 对象:
下面来看 doRefer() 方法的具体实现:
在 AbstractProxyProtocol.protocolBindingRefer() 方法中,会通过 ProxyFactory.getInvoker() 方法将 doRefer() 方法返回的代理对象转换成 Invoker 对象,并记录到 Invokers 集合中,具体实现如下:
本文重点介绍了在 Dubbo 中如何通过“HTTP 协议 + JSON-RPC”的方案实现跨语言调用。首先介绍了 JSON-RPC 中请求和响应的基本格式,以及其实现库 jsonrpc4j 的基本使用;接下来我们还详细介绍了 Dubbo 中 AbstractProxyProtocol、HttpProtocol 等核心类,剖析了 Dubbo 中“HTTP 协议 + JSON-RPC”方案的落地实现。