image1080×478 86.9 KB
Dubbo 最开始是应用于淘宝网,由阿里巴巴开源的一款优秀的高性能服务框架,由 Java 开发,后来贡献给了 Apache 开源基金会组织。
下面以官网的一个说明来了解一下架构的演变过程,从而了解 Dubbo 的诞生原因:
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的 Web 框架(MVC)是关键。
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。
image884×644 68.1 KB
Dubbo 比较有特点的地方就是这个注册中心,平常我们测试较多的 HTTP 接口,直接请求接口,调用后端服务即可;而 Dubbo 是要先走注册中心获取服务的位置,下面来举个现实生活中的例子来说明。
现实举例
好比大家平常约朋友一起出去吃饭,听说川菜馆“赠李白”不错,然后需要找这家饭店在哪(用小蓝或小黄App),知道了具体的地址才出发,至于是走路,打车还是骑车,就随意了。
这里 App 就相当于注册中心(Registry
),我们这群吃货就是消费者(Consumer
),商家属于生产者(Provider
)。商家把自己的信息注册在 App 上,消费者根据 App 查询到商家的信息,再根据信息找到商家进行消费。
2.1、Zookeeper 简介
之前经常有小伙伴问我 zk 干啥的?怎么用?下面就来简单了解一哈:
ZK,全称就是zookeeper
,是 Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。
下面的图示也可以清晰的说明zk的部署和工作的一些方式(具体的技术细节需要的话可以针对zk专门搜索学习):
Leader:集群工作的核心,事务请求的唯一调度和处理者,保证事务处理的顺序性。对于有写操作的请求,需统一转发给Leader处理。Leader需决定编号执行操作。
Follower:处理客户端非事务请求,转发事务请求转发给Leader,参与Leader选举。
Observer观察者:进行非事务请求的独立处理,对于事务请求,则转发给Leader服务器进行处理.不参与投票。
所谓的 Dubbo 接口,其实就是一个个 Dubbo 服务中的方法,而测试 Dubbo 接口就相当于我们测试人员充当消费者或者创造消费者去"消费"这个方法。
具体的方式有很多,代码、工具、命令皆可,在接下来的内容中来一一演示。
以下我将以本地的一个简单的 Dubbo 服务 demo 为例,演示 Dubbo 测试的各种方法。
interface
只有两个,分别是OrderService
和UserService
OrderService
package com.qinzhen.testmall.service;
import com.qinzhen.testmall.bean.UserAddress;
import java.util.List;
public interface OrderService {
/**
* 初始化订单
* @param userID
*/
public List initOrder(String userID);
}
UserService
package com.qinzhen.testmall.service;
import com.qinzhen.testmall.bean.UserAddress;
import java.util.List;
/**
* 用户服务
*/
public interface UserService {
/**
* 按照userId返回所有的收获地址
* @param userId
* @return
*/
public List getUserAddressList(String userId);
/**
* 返回所有的收获地址
* @param
* @return
*/
public List getUserAddressList();
}
JavaBean 对象 UserAddress
如下:
package com.qinzhen.testmall.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
@AllArgsConstructor
@Data
public class UserAddress implements Serializable {
private Integer id;
private String userAddress; //用户地址
private String userId; //用户ID
private String consignee; //收货人
private String phoneNum; //电话号码
private String isDefault; //是否为默认地址 Y-是 N-否
public UserAddress(){
}
}
创建一个provider
来实现UserService
的Interface
:
实现方法中,根据 id 返回对应的用户地址信息即可:
···
package com.qinzhen.testmall.bootuserserviceprovider.service.impl;
import com.alibaba.dubbo.config.annotation.Service;
import com.qinzhen.testmall.bean.UserAddress;
import com.qinzhen.testmall.service.UserService;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Component
@Service //暴露服务
public class UserServiceImpl implements UserService {
private UserAddress userAddress1 = new UserAddress(1, "杭州市西湖区XX公司", "1", "qz", "12345678", "Y");
private UserAddress userAddress2 = new UserAddress(2, "杭州市西湖区花园", "2", "qz", "12345678", "N");
@Override
public List getUserAddressList(String userId) {
if (userId.equals("1")){
return Collections.singletonList(userAddress1);
}
else if (userId.equals("2")){
return Collections.singletonList(userAddress2);
}
else {
return Arrays.asList(userAddress1, userAddress2);
}
}
@Override
public List getUserAddressList(){
return Arrays.asList(userAddress1, userAddress2);
}
}
···
4.1 Java consumer 代码
下面我们编写
consumer
代码,让服务消费者去注册中心订阅服务提供者的服务地址,以RPC
方式,获取远程服务代理,从而执行远程方法,代码也很简单,如下:
OrderService
中的initOrder()
方法,初始化订单,初始化中直接调用userService
的getUserAddressLis(java.lang.String)
方法,具体代码如下: package com.qinzhen.testmall.service.impl;
import com.qinzhen.testmall.bean.UserAddress;
import com.qinzhen.testmall.service.OrderService;
import com.qinzhen.testmall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 1、讲服务提供者注册到注册中心(暴露服务)
* 1)导入dubbo依赖:操作zookeeper的客户端(curator)
* 2、让服务消费者去注册中心订阅服务提供者的服务地址
*/
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
UserService userService;
public List initOrder(String userId) {
//1.查询用户的收货地址
System.out.println("用户ID为:" + userId);
List userAddressList = userService.getUserAddressList(userId);
return userAddressList;
}
}
package com.qinzhen.testmall;
import com.qinzhen.testmall.bean.UserAddress;
import com.qinzhen.testmall.service.OrderService;
import com.qinzhen.testmall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
/**
* 1、将服务提供者注册到注册中心(暴露服务)
* 1)导入dubbo依赖:操作zookeeper的客户端(curator)
* 2、让服务消费者去注册中心订阅服务提供者的服务地址
*/
@Service
public class MainApplication {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"consumer.xml"});
context.start();
OrderService orderService = context.getBean(OrderService.class); // 获取远程服务代理
List userAddresses = orderService.initOrder("3");// 执行远程方法
System.out.println(userAddresses);
System.out.println("调用完成。。。");
System.in.read();
}
}
首先确保provider
已启动:
image1080×276 121 KB
运行consumer
,可以看到成功调用到dubbo方法,获取地址列表信息:
image1080×151 57.6 KB
4.2 telnet+invoke
我们使用 telnet 命令可以直接访问对应的服务,但是前提是你需要知道服务对应的ip+端口。
如下配置文件,我们可以知道服务暴露在本地的20880端口
dubbo.application.name=boot-user-service-provider
dubbo.registry.address=127.0.0.1:2181
dubbo.registry.protocol=zookeeper
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
使用 telnet 命令进行访问,如下出现 Dubbo 字样时说明连接成功:
% telnet localhost 20880
Trying ::1...
Connected to localhost.
Escape character is '^]'.
dubbo>
Dubbo 内建的 telnet 命令的说明和用法如下
ls
ls
: 显示服务列表
ls -l
: 显示服务详细信息列表
ls XxxService
: 显示服务的方法列表
ls -l XxxService
: 显示服务的方法详细信息列表
dubbo>ls
com.qinzhen.testmall.service.UserService
dubbo>ls -l
com.qinzhen.testmall.service.UserService -> dubbo://192.168.2.xxx:20880/com.qinzhen.testmall.service.UserService?anyhost=true&application=boot-user-service-provider&bind.ip=192.168.2.xxx&bind.port=20880&dubbo=2.6.2&generic=false&interface=com.qinzhen.testmall.service.UserService&methods=getUserAddressList&pid=55472&qos.enable=false&side=provider×tamp=1615088321885
dubbo>dubbo>ls com.qinzhen.testmall.service.UserService
getUserAddressList
getUserAddressList
dubbo>dubbo>ls -l com.qinzhen.testmall.service.UserService
java.util.List getUserAddressList(java.lang.String)
java.util.List getUserAddressList()
invoke
invoke XxxService.xxxMethod(1234, "abcd", {"prop" : "value"})
: 调用服务的方法
invoke com.xxx.XxxService.XxxService.xxxMethod(1234, "abcd", {"prop" : "value"})
: 调用全路径服务的方法
invoke xxxMethod(1234, "abcd", {"prop" : "value"})
: 调用服务的方法(自动查找包含此方法的服务)
invoke xxxMethod({"name":"zhangsan","age":12,"class":"org.apache.dubbo.qos.legacy.service.Person"})
:当有参数重载,或者类型转换失败的时候,可以通过增加class属性指定需要转换类
当参数为Map
,key
的类型为Integer
时,建议指定类型。例如invoke com.xxx.xxxApiService({"3":0.123, "class":"java.util.HashMap"})
然后我们使用invoke
命令对dubbo方法getUserAddressList()
进行调用,如下:
dubbo>invoke getUserAddressList()
[{"consignee":"qz","id":1,"isDefault":"Y","phoneNum":"12345678","userAddress":"杭州市西湖区xx公司","userId":"1"},{"consignee":"qz","id":2,"isDefault":"N","phoneNum":"12345678","userAddress":"杭州市西湖区xx花园","userId":"2"}]
dubbo>invoke getUserAddressList("1")
[{"consignee":"qz","id":1,"isDefault":"Y","phoneNum":"12345678","userAddress":"杭州市西湖区xx公司","userId":"1"}]
elapsed: 14 ms.
学习链接:
其他 Telnet 命令相关操作,需要可参考 Dubbo 官网:
4.3 JMeter
对于 JMeter 测试 Dubbo 接口的方法,可参考往期文章:
《基于 Jmeter 完成 Dubbo 接口的测试》 6
4.4 Dubbo-admin
对于 Dubbo-admin 的安装调试,可参考文章:
《dubbo-admin+zookeeper 的环境搭建实操与 Could not extract archive 报错踩坑》
4.5 泛化调用
测试 Dubbo 服务的时候,我们需要服务端的同学给我们提供 API,没有这个 API 我们是测不了的,而为了解决这个问题,Dubbo 官方又给我们提供了另外一个方法,就是泛化调用,来看看官方的解释:
image1023×204 32.6 KB
Dubbo 给我们提供了一个接口GenericService
,这个接口只有一个方法,就是$invoke
,它接受三个参数,分别为方法名
、方法参数类型数组
和参数值数组
;
下面我们直接上代码演示:
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.rpc.service.GenericService;
import org.junit.jupiter.api.Test;
public class TestDemo {
@Test
void testDubboGenericService(){
// 引用远程服务
// 该实例很重量,里面封装了所有与注册中心及服务提供方连接,请缓存
ReferenceConfig reference = new ReferenceConfig();
// 弱类型接口名
reference.setApplication(new ApplicationConfig("order-service-consumer"));
reference.setInterface("com.qinzhen.testmall.service.UserService");
reference.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
// 声明为泛化接口
reference.setGeneric(true);
// 用org.apache.dubbo.rpc.service.GenericService可以替代所有接口引用
GenericService genericService = reference.get();
Object result = genericService.$invoke("getUserAddressList", new String[] {"java.lang.String"}, new Object[] {"2"});
System.out.println(result);
}
}
运行后我们来看看结果,咦~也成功访问了:
image1080×198 64.9 KB
我们通过 debug 跟入 Dubbo 的源码中,可以得到如下的调用链:
服务消费端:
image1080×239 15.8 KB
服务提供端:
image1080×331 26 KB
从上面的调用链可以知道完成一次泛化调用,Dubbo 框架经历了很多过滤器,我们分别选取两端链路中的最后一步的 Filter 来简单了解一下泛化调用做了哪些事.
简化后的调用关系就如下:
先来看consumer
端的GenericImplFilter
,大概看下核心的处理步骤:
// 判断是否为泛化调用
if (invocation.getMethodName().equals(Constants.$INVOKE)
&& invocation.getArguments() != null
&& invocation.getArguments().length == 3
&& ProtocolUtils.isGeneric(generic)) {
// 获取泛化调用参数
Object[] args = (Object[]) invocation.getArguments()[2];
// 判断是否为nativejava方式
if (ProtocolUtils.isJavaGenericSerialization(generic)) {
for (Object arg : args) {
if (!(byte[].class == arg.getClass())) {
error(byte[].class.getName(), arg.getClass().getName());
}
}
// 判断是否为bean方式
} else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
for (Object arg : args) {
if (!(arg instanceof JavaBeanDescriptor)) {
error(JavaBeanDescriptor.class.getName(), arg.getClass().getName());
}
}
}
// 设置为泛化调用方式
((RpcInvocation) invocation).setAttachment(
Constants.GENERIC_KEY, invoker.getUrl().getParameter(Constants.GENERIC_KEY));
}
// 发起远程调用
return invoker.invoke(invocation);
再来看provider端的GenericFilter,大概的核心处理步骤如下:
package com.alibaba.dubbo.rpc.filter;
import ...
/**
* GenericInvokerFilter.
*/
@Activate(group = Constants.PROVIDER, order = -20000)
public class GenericFilter implements Filter {
@Override
public Result invoke(Invoker> invoker, Invocation inv) throws RpcException {
// 判断是否为泛化请求
if (inv.getMethodName().equals(Constants.$INVOKE)
&& inv.getArguments() != null
&& inv.getArguments().length == 3
&& !ProtocolUtils.isGeneric(invoker.getUrl().getParameter(Constants.GENERIC_KEY))) {
// 获取参数名称、参数类型、参数值
String name = ((String) inv.getArguments()[0]).trim();
String[] types = (String[]) inv.getArguments()[1];
Object[] args = (Object[]) inv.getArguments()[2];
try {
// 使用反射获取调用方法
Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
Class>[] params = method.getParameterTypes();
if (args == null) {
args = new Object[params.length];
}
// 获取泛化引用方式使用的泛化类型
String generic = inv.getAttachment(Constants.GENERIC_KEY);
// 泛化类型为空的话就使用generic=true的方式
if (StringUtils.isEmpty(generic)
|| ProtocolUtils.isDefaultGenericSerialization(generic)) {
args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
// 判断是否为generic=nativejava方式
} else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
for (int i = 0; i < args.length; i++) {
if (byte[].class == args[i].getClass()) {
try {
UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i]);
args[i] = ExtensionLoader.getExtensionLoader(Serialization.class)
.getExtension(Constants.GENERIC_SERIALIZATION_NATIVE_JAVA)
.deserialize(null, is).readObject();
} catch (Exception e) {
。。。
}
} else {
。。。
}
}
// 判断是否为generic=bean方式
} else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof JavaBeanDescriptor) {
args[i] = JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) args[i]);
} else {
。。。
}
}
}
// 传递请求,执行服务
Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));
。。。
}
上面的代码很多,着重来提取一小段看一下:
Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
Class>[] params = method.getParameterTypes();
从上面的代码中我们便可以得知原来泛化调用中使用了Java的反射技术来获取对应的方法信息完成调用的
4.6 用 Python 来测试 Dubbo
我们知道 Dubbo 是个 Java 项目,测试 Dubbo 就是模拟消费者去调用 Dubbo 的 Java 方法,那显而易见,用 Python 是肯定没法去直接调用Java的,但是在日常的工作中,很多小伙伴可能是 Pyhton技术栈的,或者因为一些测试条件限制亦或历史原因,必须要将 Dubbo 测试用 Python 实现以满足各种接口测试的一个组合。
Dubbo
是支持hessian+http
协议调用的,hessian
是一种二进制序列化的方式。
了解到可以通过这种方式实现,具体没有尝试过,还需要开发在项目中将序列化的方式改为hessian
,并且需要知道URL,有兴趣的小伙伴可以去了解一下。
telnetlib
是Python3自带的一个库,可以调用telnet
命令,其实也就相当于上面说到的使用telnet
方式访问dubbo
的方法
我们可以使用 Java 来开发一个 Dubbo 测试的 Web 服务,实现上就可以使用 Dubbo 的泛化调用,然后我们再用 HTTP 访问的形式去访问这个服务,将我们的测试参数信息传过去,剩下的就交给 Java 去处理就好了。
这样经过封装设计后,可以实现 Python 端的使用者在访问 Dubbo 时就像在测试HTTP
接口一样(例如 Python 的request
库);另外服务的 IP、端口、注册中心等信息都不用出现在测试的工程中,只需要用环境标签做区分,在服务端进行请求转发即可,也保证了一定安全性。
大体上的思路流程如下:
image988×137 9.26 KB
以上,期待与大家多交流学习。