一、分布式项目开发与联调
接口暴露与引用
暴露接口的通常做法是 接口与实现分离,服务端将 接口、模型、异常 等统一放置于一个模块,实现置于另一个模块。调用方通过Maven进行引用。
注意:在分布式项目中,不会把整个服务提供方打包成JAR并提供给消费端,而是选择单独把接口与实体类打包成JAR提供给消费端。
自动化构建与协作
当项目越来越多,服务依懒关系越发复杂的时候,为了提高协作效率,必须采用自动化工具 完成 接口从编写到构建成JAR包,最后到引用的整个过程。
流程描述:
- 服务提供者项目发人员编写Client 接口
- push 至远程仓库
- jenkins 构建指定版本
- jenkins Deploye 至私服仓库 nexus
- 服务消费者项目开发人员基于maven 从私服务仓库下载
接口平滑升级
在项目迭代过程当中, 经常会有多个项目依懒同一个接口,如下图 项目B、C都依懒了项目A当中的接口1,此时项目B业务需要,需要接口1多增加一个参数,升级完成后。项目B能正确构建上线,项目C却不行。
解决办法与原则:
- 接口要做到向下兼容:接口参数尽量以对象形式进行封装。Model属性只增不删,如果需要作废,可以添加@Deprecated 标识。
- 如果出现了不可兼容的变更,则必须通知调用方整改,并制定上线计划。
-- 不推荐,兼容性差
public interface UserService {
User getUser(Integer id, string name);
}
-- 推荐
public interface UserService {
User getUser(UserParam param);
public static class UserParam
{
Integer id;
string name;
}
}
二、Dubbo控制管理后台使用
Dubbo 控制后台的安装
#从github 中下载dubbo 项目
git clone https://github.com/apache/incubator-dubbo.git
#更新项目
git fetch
#临时切换至 dubbo-2.5.8 版本
git checkout dubbo-2.5.8
#进入 dubbo-admin 目录
cd dubbo-admin
#mvn 构建admin war 包
mvn clean pakcage -DskipTests
#得到 dubbo-admin-2.5.8.war 即可直接部署至Tomcat
#修改 dubbo.properties 配置文件
dubbo.registry.address=zookeeper://127.0.0.1:2181
三、Dubbo注册中心详解
为了到达服务集群动态扩容的目的,注册中心存储了服务的地址信息与可用状态信息,并实时推送给订阅了相关服务的客户端。
一个完整的注册中心需要实现以下功能:
- 接收服务端的注册与客户端的引用,即将引用与消费建立关联,并支持多对多。
- 当服务非正常关闭时能即时清除其状态
- 当注册中心重启时,能自动恢复注册数据,以及订阅请求
- 注册中心本身的集群
Zookeeper 注册中心
关于Zookeeper 注册中心同样需要了解其存储结构和更新机制。
Zookeper是一个树型的目录服务,本身支持变更推送相比redis的实现Publish/Subscribe功能更稳定。
注意:其中叶子节点为临时节点。
源码解析
注意:UserService是一个代理对象,由ReferenceConfig引用对象生成,并把ClusterInvoker、RegistryDirectory赋予给它。
四、Dubbo调用模块
dubbo调用模块核心功能是发起一个远程方法的调用并顺利拿到返回结果,其体系组成如下:
- 透明代理:通过动态代理技术,屏蔽远程调用细节以提高编程友好性。这里dubbo 使用了 javassist作为代理实现。
- 负载均衡:当有多个提供者是,如何选择哪个进行调用的负载算法。
- 容错机制:当服务调用失败时采取的策略
- 调用方式:支持同步调用、异步调用
负载均衡
Dubbo 目前官方支持以下负载均衡策略:
- 随机(random):按权重设置随机概率。此为默认算法.
- 轮循 (roundrobin):按公约后的权重设置轮循比率。
- 最少活跃调用数(leastactive):相同活跃数的随机,活跃数指调用前后计数差。
-
一致性Hash(consistenthash ):相同的参数总是发到同一台机器,默认初始化160个虚拟点,相对hash取余的方式,一致性Hash避免了单一服务过热以及节点数量变化后全局乱套的缺点。
容错
Dubbo 官方目前支持以下容错策略:
- 失败自动切换:调用失败后基于retries=“2” 属性重试其它服务器
- 快速失败:快速失败,只发起一次调用,失败立即报错。
- 勿略失败:失败后勿略,不抛出异常给客户端。
- 失败重试:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作
- 并行调用: 只要一个成功即返回,并行调用指定数量机器,可通过 forks="2" 来设置最大并行数。
- 广播调用:广播调用所有提供者,逐个调用,任意一台报错则报错
异步调用
异步调用是指发起远程调用之后获取结果的方式。
- 同步等待结果返回(默认)
- 异步等待结果返回
-
不需要返回结果
Dubbo 中关于异步等待结果返回的实现流程如下图:
demoService.sayHello1("han");
Future<Object> future1 = RpcContext.getContext().getFuture(); //底层ThreadLocal
demoService.sayHello2("han2");
Future<Object> future2 = RpcContext.getContext().getFuture();
Object r1 = null, r2 = null;
// wait 直到拿到结果 获超时
r1 = future1.get(); //同步
// wait 直到拿到结果 获超时
r2 = future2.get(); //同步
注意:有返回值的时候,future.get()方法执行的是同步,提升效率的关键在于多个future.get()方法同时执行时,全部完成的时间是future.get()方法最慢的那一个,而不是多个future.get()方法的总和。
注意:同步调用底层也使用了Future,与异步不同的是,同步对异步多了一层包装,里面使用了future.get()方法,一个线程死循环在访问结果是否有值,一个线程在检测线程是否超时。
五、Dubbo 调用非典型使用场景
泛化提供
是指不通过接口的方式直接将服务暴露出去。通常用于Mock框架或服务降级框架实现。
模拟出来的通用服务提供方
public class MockService implements GenericService {
private String target;
public MockService(String target) {
this.target = target;
}
// 通用方法
@Override
public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
if (target.equals("com.lin.client.UserService") && method.equals("getUser")) {
HashMap<Object, Object> map = new HashMap<>();
map.put("id", 1);
map.put("name", "克里斯");
return map;
}
return null;
}
}
public class DubboServer {
public static void main(String[] args) throws IOException {
ApplicationConfig applicationConfig = new ApplicationConfig("sample-app");
ProtocolConfig protocolConfig = new ProtocolConfig();
protocolConfig.setName("dubbo");
protocolConfig.setSerialization("fastjson");
protocolConfig.setPort(-1);//20880
RegistryConfig registryConfig = new RegistryConfig("zookeeper://192.168.0.147:2181");
ServiceConfig serviceConfig = new ServiceConfig();
serviceConfig.setInterface("com.tuling.client.UserService");
//serviceConfig.setRef(new UserServiceImpl());
setMock("com.lin.client.UserService");
serviceConfig.setRegistry(registryConfig);
serviceConfig.setProtocol(protocolConfig);
serviceConfig.setApplication(applicationConfig);
serviceConfig.export();
System.out.println("服务已暴露");
System.in.read();
}
public static void setMock(ServiceConfig serviceConfig, String server) {
serviceConfig.setRef(new MockService(server));
}
}
隐示传参
是指通过非常方法参数传递参数,类似于http 调用当中添加cookie值。通常用于分布式追踪框架的实现。使用方式如下 :
//客户端隐示设置值
RpcContext.getContext().setAttachment("index", "1"); // 隐式传参,后面的远程调用都会隐
//服务端隐示获取值
String index = RpcContext.getContext().getAttachment("index");
令牌验证
通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者,可以防止消费者绕过注册中心访问提供者,另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者
使用:
<!--随机token令牌,使用UUID生成-->
<dubbo:provider interface="com.foo.BarService" token="true" />
过滤器
类似于 WEB 中的Filter ,Dubbo本身提供了Filter 功能用于拦截远程方法的调用。其支持自定义过滤器与官方的过滤器使用:
<dubbo:provider filter="accesslog" accesslog="logs/dubbo.log"/>
以上配置 就是 为 服务提供者 添加 日志记录过滤器, 所有访问日志将会集中打印至 accesslog 当中。
自定义过滤器:
- 编写过滤器
package com.tuling.dubbo;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
@Activate(group = {CommonConstants.PROVIDER})
public class ProviderHelloFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
System.out.println("log========>");
return invoker.invoke(invocation);
}
}
- 添加扩展点
# 文件路径
META-INF/dubbo/org.apache.dubbo.rpc.Filter
#内容:
helloFilter=com.tuling.dubbo.ProviderHelloFilter
六、调用内部实现源码分析
分析代理类
在调用服务端时,是接口的形式进行调用,该接口是Duboo 动态代理之后的实现,通过反编译工具可以查看到其具体实现:
因为类是代理生成,所以采用arthas工具来反编译,具体操作如下:
#运行 arthas
java -jar arthas-boot.jar
#扫描类
sc *.proxy0
#反编译代理类
jad com.alibaba.dubbo.common.bytecode.proxy0
反编译的代码如下:
package org.apache.dubbo.common.bytecode;
import com.alibaba.dubbo.rpc.service.EchoService;
import com.tuling.client.User;
import com.tuling.client.UserService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
import org.apache.dubbo.common.bytecode.ClassGenerator;
public class proxy0 implements ClassGenerator.DC, EchoService, UserService {
public static Method[] methods;
private InvocationHandler handler;
public List findUser(String string, String string2) {
Object[] arrobject = new Object[]{string, string2};
Object object = this.handler.invoke(this, methods[0], arrobject);
return (List)object;
}
public User getUser(Integer n) {
Object[] arrobject = new Object[]{n};
Object object = this.handler.invoke(this, methods[1], arrobject);
return (User)object;
}
@Override
public Object $echo(Object object) {
Object[] arrobject = new Object[]{object};
Object object2 = this.handler.invoke(this, methods[2], arrobject);
return object2;
}
public proxy0() {
}
public proxy0(InvocationHandler invocationHandler) {
this.handler = invocationHandler;
}
}
可看出其代理实现了 UserService 接口。并且基于InvocationHandler 进行代理。实际类是 InvokerInvocationHandler 并且其中之属性为Invoker.。也就是说最终会调用Invoker进行远程调用。
Dubbo调用流程
//------7协议 调用
doInvoke:77, DubboInvoker {org.apache.dubbo.rpc.protocol.dubbo}
invoke:155, AbstractInvoker {org.apache.dubbo.rpc.protocol}
//------6异步转同步
invoke:52, AsyncToSyncInvoker {org.apache.dubbo.rpc.protocol} // 异步转同步 ,返回结果之前进行阻塞调用线程
//----- 5过滤器链
invoke:92, MonitorFilter {org.apache.dubbo.monitor.support} // 过滤链-> 监控器
invoke:54, FutureFilter {org.apache.dubbo.rpc.protocol.dubbo.filter} //过滤链-> 回调参数
invoke:14, ProviderHelloFilter {com.tuling.dubbo} // 过滤链-> 自定义过滤器
invoke:60, ConsumerContextFilter {org.apache.dubbo.rpc.filter} // 过滤链-> 消费者环境初始化
//------4集群处理
doInvoke:82, FailoverClusterInvoker {org.apache.dubbo.rpc.cluster.support} // 集服-失败重试
invoke:248, AbstractClusterInvoker {org.apache.dubbo.rpc.cluster.support} //
//----- 3Mock服务
invoke:78, MockClusterInvoker {org.apache.dubbo.rpc.cluster.support.wrapper} // mock 服务
//----- 2动态代理 --透明化
invoke:55, InvokerInvocationHandler {org.apache.dubbo.rpc.proxy}// 代理的中间接口
getUser:-1, proxy0 {org.apache.dubbo.common.bytecode} // 代理对象
//----- 1调用客户端
main:53, DubboClient {com.tuling.dubbo} // 客户端
协议-->注册协议--->MockClusterInvoker--->ClusterInvoker--->RegistryDirectory--->DubboProtcol->FilterChain-->DubboInvoker
注意:核心在于运用了责任链模式与spi扩展点的技术。
七、RPC 协议
在一个典型RPC的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中RPC协议就指明了程序如何进行网络传输和序列化 。也就是说一个RPC协议的实现就等于一个非透明的远程调用实现,如何做到的的呢?
dubbo 支持的RPC协议列表
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有多少个字节。