参考
http://dubbo.io/Developer+Guide-zh.htm
摘要
背景
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。
- 单一应用架构
- 当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。
- 此时,用于简化增删改查工作量的 数据访问框架(ORM) 是关键。
- 垂直应用架构
- 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。
- 此时,用于加速前端页面开发的 Web框架(MVC) 是关键。
- 分布式服务架构
- 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。
- 此时,用于提高业务复用及整合的 分布式服务框架(RPC) 是关键。
- 流动计算架构
- 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。
- 此时,用于提高机器利用率的 资源调度和治理中心(SOA) 是关键。
需求
在大规模服务化之前,应用可能只是通过RMI或Hessian等工具,简单的暴露和引用远程服务,通过配置服务的URL地址进行调用,通过F5等硬件进行负载均衡。
(1) 当服务越来越多时,服务URL配置管理变得非常困难,F5硬件负载均衡器的单点压力也越来越大。
- 此时需要一个服务注册中心,动态的注册和发现服务,使服务的位置透明。
- 并通过在消费方获取服务提供方地址列表,实现软负载均衡和Failover,降低对F5硬件负载均衡器的依赖,也能减少部分成本。
(2) 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。
- 这时,需要自动画出应用间的依赖关系图,以帮助架构师理清理关系。
(3) 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器?
- 为了解决这些问题,第一步,要将服务现在每天的调用量,响应时间,都统计出来,作为容量规划的参考指标。
- 其次,要可以动态调整权重,在线上,将某台机器的权重一直加大,并在加大的过程中记录响应时间的变化,直到响应时间到达阀值,记录此时的访问量,再以此访问量乘以机器数反推总容量。
- 以上是Dubbo最基本的几个需求,更多服务治理问题参见:http://code.alibabatech.com/blog/experience_1402/service-governance-process.html
架构
服务提供者暴露一个服务的详细过程
上图是服务提供者暴露服务的主过程:
首先ServiceConfig类拿到对外提供服务的实际类ref(如:HelloWorldImpl),然后通过ProxyFactory类的getInvoker方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成具体服务到Invoker的转化。接下来就是Invoker转换到Exporter的过程。
Dubbo处理服务暴露的关键就在Invoker转换到Exporter的过程(如上图中的红色部分),下面我们以Dubbo和RMI这两种典型协议的实现来进行说明:
Dubbo的实现 : Dubbo协议的Invoker转为Exporter发生在DubboProtocol类的export方法,它主要是打开socket侦听服务,并接收客户端发来的各种请求,通讯细节由Dubbo自己实现。
RMI的实现 : RMI协议的Invoker转为Exporter发生在RmiProtocol类的export方法,
它通过Spring或Dubbo或JDK来实现RMI服务,通讯细节这一块由JDK底层来实现,这就省了不少工作量。
服务消费者消费一个服务的详细过程
上图是服务消费的主过程:
首先ReferenceConfig类的init方法调用Protocol的refer方法生成Invoker实例(如上图中的红色部分),这是服务消费的关键。接下来把Invoker转换为客户端需要的接口(如:HelloWorld)。
关于每种协议如RMI/Dubbo/Web service等它们在调用refer方法生成Invoker实例的细节和上一章节所描述的类似。
节点角色说明:
- Provider: 暴露服务的服务提供方。
- Consumer: 调用远程服务的服务消费方。
- Registry: 服务注册与发现的注册中心。
- Monitor: 统计服务的调用次调和调用时间的监控中心。
- Container: 服务运行容器。
调用关系说明:
- 0. 服务容器负责启动,加载,运行服务提供者。
- 1. 服务提供者在启动时,向注册中心注册自己提供的服务。
- 2. 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 3. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 4. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 5. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
(1) 连通性:
- 注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小
- 监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示
- 服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销
- 服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销
- 注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外
- 注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者
- 注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表
- 注册中心和监控中心都是可选的,服务消费者可以直连服务提供者
(2) 健状性:
- 监控中心宕掉不影响使用,只是丢失部分采样数据
- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
- 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
- 服务提供者无状态,任意一台宕掉后,不影响使用
- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
(3) 伸缩性:
- 注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心
- 服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者
(4) 升级性:
- 当服务集群规模进一步扩大,带动IT治理结构进一步升级,需要实现动态部署,进行流动计算,现有分布式服务架构不会带来阻力:
配置
最小配置
Consumer
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="consumer-of-helloworld-app" />
<!-- 使用multicast广播注册中心暴露发现服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" />
</beans>
Provider
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="hello-world-app" />
<!-- 使用multicast广播注册中心暴露服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" />
<!-- 和本地bean一样实现服务 -->
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
</beans>
配置项
Config | Side | Usage |
<dubbo:application/> | Both | 元数据 :应用配置,用于配置当前应用信息,不管该应用是提供者还是消费者。 |
<dubbo:module/> | Both | 元数据 :模块配置,用于配置当前模块信息,可选。 |
<dubbo:registry/> | Both | 注册中心配置,用于配置连接注册中心相关信息。 |
<dubbo:monitor/> | Both | 监控中心配置,用于配置连接监控中心相关信息,可选。 |
<dubbo:protocol/> | Provider | 协议配置,用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受。 |
<dubbo:provider/> | Provider | 提供方的缺省值,当ProtocolConfig和ServiceConfig某属性没有配置时,采用此缺省值,可选。 |
<dubbo:service/> | Provider | 服务配置,用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心。 |
<dubbo:consumer/> | Consumer | 消费方缺省配置,当ReferenceConfig某属性没有配置时,采用此缺省值,可选。 |
<dubbo:reference/> | Consumer | 引用配置,用于创建一个远程服务代理,一个引用可以指向多个注册中心。 |
<dubbo:method/> | Both | 方法配置,用于ServiceConfig和ReferenceConfig指定方法级的配置信息。 |
<dubbo:argument/> | Both | 用于指定方法参数配置。 |
配置优先级
可配置项包括 :timeout, retries, loadbalance, actives,等等。最终作用到方法调用。Provider提供默认配置,Consumer获取默认配置,并可以更改。全局配置定义在Consumer/Provider上,Service/Reference可以进行覆盖,Method上面可再行覆盖。
- Reference Method 覆盖 Service Method,覆盖
- Reference 覆盖 Service,覆盖
- Consumer 覆盖 Provider,覆盖
配置方式
- JVM -D 覆盖
- dubbo.properties 覆盖
- spring.xml
调用链
- 这里的Invoker是Provider的一个可调用Service的抽象,Invoker封装了Provider地址及Service接口信息。
- Directory代表多个Invoker,可以把它看成List<Invoker>,但与List不同的是,它的值可能是动态变化的,比如注册中心推送变更。
- Cluster将Directory中的多个Invoker伪装成一个Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个。
- Router负责从多个Invoker中按路由规则选出子集,比如读写分离,应用隔离等。
- LoadBalance负责从多个Invoker中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选。
容错模式
Failover Cluster | Yes | 读 |
|
Failback Cluster | Yes | 消息通知 |
|
Failfast Cluster | No | 写 |
|
Failsafe Cluster | No | 日志 |
|
Forking Cluster |
| ||
Broadcast Cluster |
|
调用上下文环境
上下文
上下文中存放的是当前调用过程中所需的环境信息。所有配置信息都将转换为URL的参数,参见《配置项一览表》中的“对应URL参数”一列。RpcContext是一个ThreadLocal的临时状态记录器,当接收到RPC请求,或发起RPC请求时,RpcContext的状态都会变化。比如:A调B,B再调C,则B机器上,在B调C之前,RpcContext记录的是A调B的信息,在B调C之后,RpcContext记录的是B调C的信息。
/**
* 消费方
*/
// 第一次 : 远程调用
xxxService.xxx();
// 本端是否为消费端,这里会返回true
boolean isConsumerSide = RpcContext.getContext().isConsumerSide();
// 获取最后一次调用的提供方IP地址
String serverIP = RpcContext.getContext().getRemoteHost();
// 获取当前服务配置信息,所有配置信息都将转换为URL的参数
String application = RpcContext.getContext().getUrl().getParameter("application");
// ...
// 第二次 :远程调用。注意:每发起RPC调用,上下文状态会变化
yyyService.yyy();
// ...
/**
* 生产方
*/
public class XxxServiceImpl implements XxxService {
// 服务方法实现
public void xxx() {
// 本端是否为提供端,这里会返回true
boolean isProviderSide = RpcContext.getContext().isProviderSide();
// 获取调用方IP地址
String clientIP = RpcContext.getContext().getRemoteHost();
// 获取当前服务配置信息,所有配置信息都将转换为URL的参数
String application = RpcContext.getContext().getUrl().getParameter("application");
// ...
// 注意:每发起RPC调用,上下文状态会变化
yyyService.yyy();
// 此时本端变成消费端,这里会返回false
boolean isProviderSide = RpcContext.getContext().isProviderSide();
// ...
}
}
上下文参数传递
path,group,version,dubbo,token,timeout几个key有特殊处理,请使用其它key值。
/**
* 消费方 : 传递参数
*/
// 隐式传参,后面的远程调用都会隐式将这些参数发送到服务器端,类似cookie,用于框架集成,不建议常规业务使用
RpcContext.getContext().setAttachment("index", "1");
// 远程调用
xxxService.xxx();
// ...
/**
* 生产方 : 使用参数
*/
public class XxxServiceImpl implements XxxService {
// 服务方法实现
public void xxx() {
// 获取客户端隐式传入的参数,用于框架集成,不建议常规业务使用
String index = RpcContext.getContext().getAttachment("index");
// ...
}
}
异步调用
基于NIO的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
- async : 启用一部调
- sent : 是否等待请求发送成功
- sent="true" 等待消息发出,消息发送失败将抛出异常。
- sent="false" 不等待消息发出,将消息放入IO队列,即刻返回。
- return : 是否需要返回值,如果不需要,则可省去创建/等待Future对象
<dubbo:reference interface="com.alibaba.foo.FooService">
<dubbo:method name="findFoo" async="true" />
</dubbo:reference>
<dubbo:reference interface="com.alibaba.bar.BarService">
<dubbo:method name="findBar" async="true" />
</dubbo:reference>
// 此调用会立即返回null
fooService.findFoo(fooId);
// 拿到调用的Future引用,当结果返回后,会被通知和设置到此Future。
Future<Foo> fooFuture = RpcContext.getContext().getFuture();
// 此调用会立即返回null
barService.findBar(barId);
// 拿到调用的Future引用,当结果返回后,会被通知和设置到此Future。
Future<Bar> barFuture = RpcContext.getContext().getFuture();
// 此时findFoo和findBar的请求同时在执行,客户端不需要启动多线程来支持并行,而是借助NIO的非阻塞完成。
// 如果foo已返回,直接拿到返回值,否则线程wait住,等待foo返回后,线程会被notify唤醒。
Foo foo = fooFuture.get();
// 同理等待bar返回。
Bar bar = barFuture.get();
// 如果foo需要5秒返回,bar需要6秒返回,实际只需等6秒,即可获取到foo和bar,进行接下来的处理。
参数回调
参数回调方式与调用本地callback或listener相同,只需要在Spring的配置文件中声明哪个参数是callback类型即可,Dubbo将基于长连接生成反向代理,这样就可以从服务器端调用客户端逻辑。
生产端配置
<bean id="callbackService" class="com.callback.impl.CallbackServiceImpl" />
<dubbo:service interface="com.callback.CallbackService" ref="callbackService"
connections="1"
callbacks="1000">
<dubbo:method name="addListener">
<dubbo:argument index="1" callback="true" />
<!--也可以通过指定类型的方式-->
<!--<dubbo:argument type="com.demo.CallbackListener" callback="true" />-->
</dubbo:method>
</dubbo:service>
生产端触发Event
package com.callback.impl;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.callback.CallbackListener;
import com.callback.CallbackService;
public class CallbackServiceImpl implements CallbackService {
private final Map<String, CallbackListener> listeners = new ConcurrentHashMap<String, CallbackListener>();
public CallbackServiceImpl() {
Thread t = new Thread(new Runnable() {
public void run() {
while(true) {
try {
for(Map.Entry<String, CallbackListener> entry : listeners.entrySet()){
try {
entry.getValue().changed(getChanged(entry.getKey()));
} catch (Throwable t) {
listeners.remove(entry.getKey());
}
}
Thread.sleep(5000); // 定时触发变更通知
} catch (Throwable t) { // 防御容错
t.printStackTrace();
}
}
}
});
t.setDaemon(true);
t.start();
}
public void addListener(String key, CallbackListener listener) {
listeners.put(key, listener);
listener.changed(getChanged(key)); // 发送变更通知
}
private String getChanged(String key) {
return "Changed: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
}
消费端注册Listener
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:consumer.xml");
context.start();
CallbackService callbackService = (CallbackService) context.getBean("callbackService");
callbackService.addListener("http://10.20.160.198/wiki/display/dubbo/foo.bar", new CallbackListener(){
public void changed(String msg) {
System.out.println("callback1:" + msg);
}
});
事件通知
在调用之前,调用之后,出现异常时,会触发oninvoke, onreturn, onthrow三个事件,可以配置当事件发生时,通知哪个类的哪个方法。callback与async功能正交分解:async=true,表示结果是否马上返回. onreturn 表示是否需要回调. 组合情况:(async=false 默认)
- 异步回调模式:async=true onreturn="xxx"
- 同步回调模式:async=false onreturn="xxx"
- 异步无回调:async=true
- 同步无回调:async=false
消费端配置
<dubbo:reference interface="com.alibaba.dubbo.callback.implicit.IDemoService" >
<!-- 调用该方法时需要通知 -->
<dubbo:method name="get" async="true"
<!-- 处理onreturn的方法 -->
onreturn="demoCallback.onreturn"
<!-- 处理onthrow的方法 -->
onthrow="demoCallback.onthrow" />
</dubbo:reference>
<!-- 通知处理类 -->
<bean id ="demoCallback" class = "com.alibaba.dubbo.callback.implicit.NofifyImpl" />
通知实现
class NofifyImpl implements Nofify {
public Map<Integer, Person> ret = new HashMap<Integer, Person>();
public Map<Integer, Throwable> errors = new HashMap<Integer, Throwable>();
public void onreturn(Person msg, Integer id) {
System.out.println("onreturn:" + msg);
ret.put(id, msg);
}
public void onthrow(Throwable ex, Integer id) {
errors.put(id, ex);
}
}
本地存根
远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做ThreadLocal缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在API中带上Stub,客户端生成Proxy实例,会把Proxy通过构造函数传给Stub,然后把Stub暴露组给用户,Stub可以决定要不要去调Proxy。Stub必须有可传入Proxy的构造函数。
服务器端声称存根
<!-- 指定存根 -->
<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />
package com.foo
public class BarServiceStub implements BarService {
private final BarService barService;
// 构造函数传入真正的远程代理对象
public (BarService barService) {
this.barService = barService;
}
public String sayHello(String name) {
// 此代码在客户端执行
// 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
try {
return barService.sayHello(name);
} catch (Exception e) {
// 你可以容错,可以做任何AOP拦截事项
return "容错数据";
}
}
}
API jar包
com.foo.BarService
// 在API旁边放一个Stub实现,它实现BarService接口,并有一个传入远程BarService实例的构造函数
com.foo.BarServiceStub
本地伪装
Mock通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过Mock数据返回授权失败。Mock是Stub的一个子集,便于服务提供方在客户端执行容错逻辑,因经常需要在出现RpcException(比如网络失败,超时等)时进行容错,而在出现业务异常(比如登录用户名密码错误)时不需要容错,如果用Stub,可能就需要捕获并依赖RpcException类,而用Mock就可以不依赖RpcException,因为它的约定就是只有出现RpcException时才执行。
服务器端生成伪装
<!-- 指定伪装 -->
<dubbo:service interface="com.foo.BarService" mock="com.foo.BarServiceMock" />
package com.foo
public class BarServiceMock implements BarService {
public String sayHello(String name) {
// 你可以伪造容错数据,此方法只在出现RpcException时被执行
return "容错数据";
}
}
API jar包
com.foo.BarService
// 在API旁边放一个Stub实现,它实现BarService接口,并有一个传入远程BarService实例的构造函数
com.foo.BarServiceStub
调用协议
<!-- 多协议配置 -->
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="rmi" port="1099" />
<!-- 不同服务使用不同协议 -->
<dubbo:service interface="com.alibaba.hello.api.HelloService" ref="helloService"
protocol="dubbo" />
<dubbo:service interface="com.alibaba.hello.api.DemoService" ref="demoService"
protocol="rmi" />
<!-- 同一服务使用不同协议 -->
<dubbo:service interface="com.alibaba.hello.api.DemoService" ref="demoService"
protocol="dubbo,hessian" />
默认 | dubbo | rmi | hessian | http | webservice | thrift |
---|---|---|---|---|---|---|
序列化 | Hessian二进制 | Java标准二进制 | Hessian二进制 | 表单序列化 | SOAP文本序列化 | |
连接个数 | 单连接 | 多连接 | 多连接 | 多连接 | 多连接 | |
连接方式 | 长连接 | 短连接 | 短连接 | 短连接 | 短连接 | |
传输协议 | TCP | TCP | HTTP | HTTP | HTTP | |
传输方式 | NIO异步传输 | 同步传输 | 同步传输 | 同步传输 | 同步传输 |
dubbo
推荐使用Dubbo协议。Dubbo缺省协议采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。Dubbo缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低
- 为什么要消费者比提供者个数多:根据测试经验数据每条消费端连接最多只能压满7MByte(不同的环境可能不一样,供参考),因dubbo协议采用单一长连接,假设网络为千兆网卡(1024Mbit=128MByte),理论上1个服务提供者需要20(128M/7M)个服务消费者才能压满网卡。
- 为什么不能传大包:假设每次请求的数据包大小为500KByte,假设网络为千兆网卡(1024Mbit=128MByte),每条连接最大7MByte(不同的环境可能不一样,供参考),如果能接受如下TPS, 可以考虑使用,否则网络将成为瓶颈。
- 单个服务提供者的TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。
- 单个消费者调用单个服务提供者的TPS(每秒处理事务数)最大为:7MByte / 500KByte = 14。
- 为什么采用异步单一长连接:因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如Morgan的提供者只有6台提供者,却有上百台消费者,每天有1.5亿次调用,如果采用常规的hessian服务,服务提供者很容易就被压跨,
- 通过单一连接,保证单一消费者不会压死提供者,
- 长连接,减少连接握手验证等,
- 并使用异步IO,复用线程池,防止C10K问题。
- 约束:
- 参数及返回值需实现Serializable接口
- 参数及返回值不能自定义实现List, Map, Number, Date, Calendar等接口,只能用JDK自带的实现,因为hessian会做特殊处理,自定义实现类中的属性值都会丢失。
- Hessian序列化,只传成员属性值和值的类型,不传方法或静态变量,
<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” />
rmi
- 如果服务接口继承了java.rmi.Remote接口,可以和原生RMI互操作,即:
- 提供者用Dubbo的RMI协议暴露服务,消费者用标准RMI 调用。
- 提供者用标准RMI 暴露服务,消费者用Dubbo的RMI协议调用。
- 如果服务接口没有继承java.rmi.Remote接口,
- 缺省Dubbo将自动生成一个com.xxx.XxxService$Remote的接口,并继承java.rmi.Remote接口,并以此接口暴露服务,
- 但如果设置了<dubbo:protocol name="rmi" codec="spring" />,将不生成$Remote接口,而使用Spring的RmiInvocationHandler接口暴露服务,和Spring兼容。
- 约束:
- 参数及返回值需实现Serializable接口
- dubbo配置中的超时时间对rmi无效,需使用java启动参数设置:-Dsun.rmi.transport.tcp.responseTimeout=3000。
hessian
Hessian协议用于集成Hessian的服务,Hessian底层采用Http通讯,采用Servlet暴露服务,Dubbo缺省内嵌Jetty作为服务器实现。Hessian是Caucho开源的一个RPC框架:http://hessian.caucho.com,其通讯效率高于WebService和Java自带的序列化。
可以和原生Hessian服务互操作,即:
- 提供者用Dubbo的Hessian协议暴露服务,消费者用标准Hessian 调用,
- 提供者用标准Hessian 暴露服务,消费者用Dubbo的Hessian协议调用。
约束
- 参数及返回值需实现Serializable接口
- 参数及返回值不能自定义实现List, Map, Number, Date, Calendar等接口,只能用JDK自带的实现,因为hessian会做特殊处理,自定义实现类中的属性值都会丢失。
如果使用servlet派发请求:
- 协议的端口<dubbo:protocol port="8080" />必须与servlet容器的端口相同,
- 协议的上下文路径<dubbo:protocol contextpath="foo" />必须与servlet应用的上下文路径相同。
http
如果使用servlet派发请求:
- 协议的端口<dubbo:protocol port="8080" />必须与servlet容器的端口相同,
- 协议的上下文路径<dubbo:protocol contextpath="foo" />必须与servlet应用的上下文路径相同。
webservice
可以和原生WebService服务互操作,即:
- 提供者用Dubbo的WebService暴露服务,消费者用标准WebService 调用,
- 提供者用标准WebService 暴露服务,消费者用Dubbo的WebService调用。
<dubbo:reference id="helloService" interface="HelloWorld" url="webservice://10.20.153.10:8080/com.foo.HelloWorld" />
http://10.20.153.10:8080/com.foo.HelloWorld?wsdl
Cache
memcached
<dubbo:reference id="cache" interface="com.foo.CacheService"
url="memcached://10.20.153.10:11211" />
方法名建议和memcached的标准方法名相同,即:get(key), set(key, value), delete(key)。如果方法名和memcached的标准方法名不相同,则需要配置映射关系:(其中"p:xxx"为spring的标准p标签)
<dubbo:reference id="cache" interface="com.foo.CacheService"
url="memcached://10.20.153.10:11211"
p:set="putFoo"
p:get="getFoo"
p:delete="removeFoo" />
redis
<dubbo:reference id="store" interface="com.foo.StoreService"
url="redis://10.20.153.10:6379" />
方法名建议和redis的标准方法名相同,即:get(key), set(key, value), delete(key)。如果方法名和redis的标准方法名不相同,则需要配置映射关系:(其中"p:xxx"为spring的标准p标签)
<dubbo:reference id="cache" interface="com.foo.CacheService"
url="memcached://10.20.153.10:11211"
p:set="putFoo"
p:get="getFoo"
p:delete="removeFoo" />
负载均衡
<!-- 服务器端定义 -->
<dubbo:service interface="com.foo.BarService"
loadbalance="leastactive" />
<!-- 客户端定义 -->
<dubbo:reference interface="com.foo.BarService"
loadbalance="leastactive" />
RoundRobin LoadBalance |
|
Random LoadBalance |
|
LeastActive LoadBalance |
|
ConsistentHash LoadBalance |
|
线程模型
- 短事务 :如果事件处理的逻辑能迅速完成,并且不会发起新的IO请求,比如只是在内存中记个标识,则直接在IO线程上处理更快,因为减少了线程池调度。
- 长事务 :如果事件处理逻辑较慢,或者需要发起新的IO请求,比如需要查询数据库,则必须派发到线程池,否则IO线程阻塞,将导致不能接收其它请求。
- 如果用IO线程处理事件,又在事件处理过程中发起新的IO请求,比如在连接事件中发起登录请求,会报“可能引发死锁”异常,但不会真死锁。
<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" />
Dispatcher
all | 所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。 |
direct | 所有消息都不派发到线程池,全部在IO线程上直接执行。 |
message | 只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在IO线程上执行。 |
execution | 只请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在IO线程上执行。 |
connection | 在IO线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。 |
ThreadPool
fixed | 固定大小线程池,启动时建立线程,不关闭,一直持有。(缺省) |
cached | 缓存线程池,空闲一分钟自动删除,需要时重建。 |
limited | 可伸缩线程池,但池中的线程数只会增长不会收缩。(为避免收缩时突然来了大流量引起的性能问题)。 |
连接控制
限制服务器端接受的连接不能超过10个:(以连接在Server上,所以配置在Provider上)
<!-- 服务器端最多接受10个链接 -->
<dubbo:provider protocol="dubbo"
accepts="10" />
<!-- 或者定义在 protocol上 -->
<dubbo:protocol name="dubbo"
accepts="10" />
限制客户端服务使用连接连接数:(如果是长连接,比如Dubbo协议,connections表示该服务对每个提供者建立的长连接数)
<!-- 客户端在此服务上面最多发起10个链接 -->
<dubbo:reference interface="com.foo.BarService"
connections="10" />
并发控制
<!-- 服务器端定义 -->
<dubbo:service interface="com.foo.BarService"
<!-- 服务器端总共10个线程 -->
executes="10"
<!-- 每个客户端最多2个线程 -->
actives="2" />
<!-- 客户端定义 -->
<dubbo:reference interface="com.foo.BarService"
<!-- 对此服务最多2个线程调用 -->
actives="2" />
服务注册
<!-- 服务自动注册 :自动上下线>
<dubbo:registry address="10.20.141.150:9090"/>
<!-- 服务手动注册 : 手动上下线 -->
<dubbo:registry address="10.20.141.150:9090" dynamic="false" />
<!-- 禁用服务注册 : 只作为消费者 -->
<dubbo:registry address="10.20.153.10:9090" register="false" />
<!-- 禁用服务获取 : 只作为提供者 -->
<dubbo:registry id="hzRegistry" address="10.20.153.10:9090" />
<dubbo:registry id="qdRegistry" address="10.20.141.150:9090" subscribe="false" />
<!-- 定义多注册中心 -->
<dubbo:registry id="hangzhouRegistry" address="10.20.141.150:9090" />
<dubbo:registry id="qingdaoRegistry" address="10.20.141.151:9010" default="false" />
<!-- 注册 :向多中心注册 -->
<dubbo:service interface="com.alibaba.hello.api.HelloService" ref="helloService"
registry="hangzhouRegistry,qingdaoRegistry" />
<!-- 注册 :向多中心注册不同服务 -->
<dubbo:service interface="com.alibaba.hello.api.HelloService" ref="helloService"
registry="hangzhouRegistry" />
<dubbo:service interface="com.alibaba.hello.api.DemoService" ref="demoService"
registry="qingdaoRegistry" />
<!-- 引用 :向多中心注册 -->
<dubbo:reference id="chinaHelloService" interface="com.alibaba.hello.api.HelloService"
registry="hangzhouRegistry" />
<dubbo:reference id="intlHelloService" interface="com.alibaba.hello.api.HelloService"
registry="qingdaoRegistry" />
注册中心
Multicast注册中心
不需要启动任何中心节点,只要广播地址一样,就可以互相发现。组播受网络结构限制,只适合小规模应用或开发阶段使用。组播地址段: 224.0.0.0 - 239.255.255.255。
<dubbo:registry address="multicast://224.5.6.7:1234?unicast=false" />
- 提供方启动时广播自己的地址。
- 消费方启动时广播订阅请求。
- 提供方收到订阅请求时,单播自己的地址给订阅者,如果设置了unicast=false,则广播给订阅者。
- 消费方收到提供方地址时,连接该地址进行RPC调用。
- 为了减少广播量,Dubbo缺省使用单播发送提供者地址信息给消费者,如果一个机器上同时启了多个消费者进程,消费者需声明unicast=false,否则只会有一个消费者能收到消息。
Zookeeper注册中心
- 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息。
- 当注册中心重启时,能自动恢复注册数据,以及订阅请求。
- 当会话过期时,能自动恢复注册数据,以及订阅请求。
- 当设置<dubbo:registry check="false" />时,记录失败注册和订阅请求,后台定时重试。
- 可通过<dubbo:registry username="admin" password="1234" />设置zookeeper登录信息。
- 可通过<dubbo:registry group="dubbo" />设置zookeeper的根节点,不设置将使用无根树。
- 支持*号通配符<dubbo:reference group="*" version="*" />,可订阅服务的所有分组和所有版本的提供者。
<dubbo:registry protocol="zookeeper" address="10.20.153.10:2181,10.20.153.11:2181,10.20.153.12:2181" />
Redis注册中心
<dubbo:registry protocol="redis" address="10.20.153.10:6379,10.20.153.11:6379,10.20.153.12:6379" />
Simple注册中心
其它
- 优雅停机
- 路由规则
- 配置规则
- 主机绑定
- 日志适配
- 访问日志
- 服务容器
- Simple监控中心
- Reference Config缓存
ReferenceConfig实例很重,封装了与注册中心的连接以及与提供者的连接,需要缓存,否则重复生成ReferenceConfig可能造成性能问题并且会有内存和连接泄漏。API方式编程时,容易忽略此问题。
- 令牌验证
- 粘滞连接
- 延迟连接
- 分组 : 当一个接口有多种实现时,可以用group区分。
<!-- 通过组来区分不同的实现 -->
<dubbo:service group="feedback" interface="com.xxx.IndexService" />
<dubbo:service group="member" interface="com.xxx.IndexService" />
<!-- 调用"相应"组提供的服务 -->
<dubbo:reference id="feedbackIndexService" group="feedback" interface="com.xxx.IndexService" />
<dubbo:reference id="memberIndexService" group="member" interface="com.xxx.IndexService" />
<!-- 调用"任意"组提供的服务 -->
<dubbo:reference id="barService" interface="com.foo.BarService" group="*" />
- 分组聚合
- 版本 : 当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
<!-- 通过版本来区分不同的实现 -->
<dubbo:service interface="com.foo.BarService" version="1.0.0" />
<dubbo:service interface="com.foo.BarService" version="2.0.0" />
<!-- 调用指定版本提供的服务 -->
<dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />
<dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />
<!-- 不区分版本 -->
<dubbo:reference id="barService" interface="com.foo.BarService" version="*" />
- 参数验证
- 结果缓存 :结果缓存,用于加速热门数据的访问速度,Dubbo提供声明式缓存,以减少用户加缓存的工作量。
- lru 基于最近最少使用原则删除多余缓存,保持最热的数据被缓存。
- threadlocal 当前线程缓存,比如一个页面渲染,用到很多portal,每个portal都要去查用户信息,通过线程缓存,可以减少这种多余访问。
- jcache 与JSR107集成,可以桥接各种缓存实现。
<!-- 消费方开启缓存 -->
<dubbo:reference interface="com.foo.BarService" cache="lru" />
<!-- 消费方方法开启缓存 -->
<dubbo:reference interface="com.foo.BarService">
<dubbo:method name="findBar" cache="lru" />
</dubbo:reference>
测试相关
- 泛化调用 (消费端) : 泛接口调用方式主要用于客户端没有API接口及模型类元的情况,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如:实现一个通用的服务测试框架,可通过GenericService调用所有服务实现。
<!-- 泛化引用 -->
<dubbo:reference id="barService" interface="com.foo.BarService" generic="true" />
import com.alibaba.dubbo.rpc.service.GenericService;
...
GenericService barService = (GenericService) applicationContext.getBean("barService");
Object result = barService.$invoke("sayHello", new String[] { "java.lang.String" }, new Object[] { "World" });
// 用Map表示POJO参数,如果返回值为POJO也将自动转成Map
Map<String, Object> person = new HashMap<String, Object>();
person.put("name", "xxx");
person.put("password", "yyy");
Object result = genericService.$invoke("findPerson", new String[]{"com.xxx.Person"}, new Object[]{person}); // 如果返回POJO将自动转成Map
...
- 泛化实现 (生产端) : 泛接口实现方式主要用于服务器端没有API接口及模型类元的情况,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如:实现一个通用的远程服务Mock框架,可通过实现GenericService接口处理所有服务请求。
<dubbo:service interface="com.foo.BarService" ref="genericService" />
<bean id="genericService" class="com.foo.MyGenericService" />
package com.foo;
public class MyGenericService implements GenericService {
public Object $invoke(String methodName, String[] parameterTypes, Object[] args) throws GenericException {
if ("sayHello".equals(methodName)) {
return "Welcome " + args[0];
}
}
}
- 回声测试