Dubbo

参考

http://dubbo.io/Developer+Guide-zh.htm

摘要

背景

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。

27112411_UYpw.jpg

  • 单一应用架构
    • 当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。
    • 此时,用于简化增删改查工作量的 数据访问框架(ORM) 是关键。
  • 垂直应用架构
    • 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。
    • 此时,用于加速前端页面开发的 Web框架(MVC) 是关键。
  • 分布式服务架构
    • 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。
    • 此时,用于提高业务复用及整合的 分布式服务框架(RPC) 是关键。
  • 流动计算架构
    • 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。
    • 此时,用于提高机器利用率的 资源调度和治理中心(SOA) 是关键。

需求

27112412_PjJ0.jpg

在大规模服务化之前,应用可能只是通过RMI或Hessian等工具,简单的暴露和引用远程服务,通过配置服务的URL地址进行调用,通过F5等硬件进行负载均衡。

(1) 当服务越来越多时,服务URL配置管理变得非常困难,F5硬件负载均衡器的单点压力也越来越大。

  • 此时需要一个服务注册中心,动态的注册和发现服务,使服务的位置透明。
  • 并通过在消费方获取服务提供方地址列表,实现软负载均衡和Failover,降低对F5硬件负载均衡器的依赖,也能减少部分成本。

(2) 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。

  • 这时,需要自动画出应用间的依赖关系图,以帮助架构师理清理关系。

(3) 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器?

  • 为了解决这些问题,第一步,要将服务现在每天的调用量,响应时间,都统计出来,作为容量规划的参考指标。
  • 其次,要可以动态调整权重,在线上,将某台机器的权重一直加大,并在加大的过程中记录响应时间的变化,直到响应时间到达阀值,记录此时的访问量,再以此访问量乘以机器数反推总容量。
  • 以上是Dubbo最基本的几个需求,更多服务治理问题参见:http://code.alibabatech.com/blog/experience_1402/service-governance-process.html

架构

27112414_pVo0.jpg

服务提供者暴露一个服务的详细过程

001603_21Zq_3551123.png

上图是服务提供者暴露服务的主过程:
首先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底层来实现,这就省了不少工作量。

服务消费者消费一个服务的详细过程

28001928_Q2zo.jpg

上图是服务消费的主过程:
首先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治理结构进一步升级,需要实现动态部署,进行流动计算,现有分布式服务架构不会带来阻力:

27112414_W3LU.jpg

配置

最小配置

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>

配置项

ConfigSideUsage
<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

调用链

115804_HXA0_3551123.png

  • 这里的Invoker是Provider的一个可调用Service的抽象,Invoker封装了Provider地址及Service接口信息。
  • Directory代表多个Invoker,可以把它看成List<Invoker>,但与List不同的是,它的值可能是动态变化的,比如注册中心推送变更。
  • Cluster将Directory中的多个Invoker伪装成一个Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个。
  • Router负责从多个Invoker中按路由规则选出子集,比如读写分离,应用隔离等。
  • LoadBalance负责从多个Invoker中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选。

28001929_AnXD.jpg

28001931_DoXT.jpg

28001933_4FEO.jpg

容错模式

Failover ClusterYes
  • 失败自动切换,当出现失败,重试其它服务器。(缺省)
  • 通常用于读操作,但重试会带来更长延迟。
  • 可通过retries="2"来设置重试次数(不含第一次)。
Failback ClusterYes消息通知
  • 失败自动恢复,后台记录失败请求,定时重发
  • 通常用于消息通知操作。
Failfast ClusterNo
  • 快速失败,只发起一次调用,失败立即报错。
  • 通常用于非幂等性的写操作,比如新增记录。
Failsafe ClusterNo日志
  • 失败安全,出现异常时,直接忽略
  • 通常用于写入审计日志等操作。
Forking Cluster  
  • 并行调用多个服务器,只要一个成功即返回。
  • 通常用于实时性要求较高的读操作,但需要浪费更多服务资源。
  • 可通过forks="2"来设置最大并行数。
Broadcast Cluster  
  • 广播调用所有提供者,逐个调用,任意一台报错则报错。(2.1.0开始支持)
  • 通常用于通知所有提供者更新缓存或日志等本地资源信息。

调用上下文环境

上下文

上下文中存放的是当前调用过程中所需的环境信息。所有配置信息都将转换为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值。

27203359_4gFZ.png

/**
 * 消费方 : 传递参数
 */
// 隐式传参,后面的远程调用都会隐式将这些参数发送到服务器端,类似cookie,用于框架集成,不建议常规业务使用
RpcContext.getContext().setAttachment("index", "1");
// 远程调用
xxxService.xxx();
// ...
/**
 * 生产方 : 使用参数
 */
public class XxxServiceImpl implements XxxService {
    // 服务方法实现
    public void xxx() { 
        // 获取客户端隐式传入的参数,用于框架集成,不建议常规业务使用
        String index = RpcContext.getContext().getAttachment("index");

        // ...
    }
}

异步调用

基于NIO的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。

27203401_X8eA.jpg

  • 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的构造函数。

27203403_4Wps.jpg

服务器端声称存根

<!-- 指定存根 -->
<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" />

27203404_oT3G.jpg

27203405_X4lv.jpg

默认dubbormihessianhttpwebservicethrift
序列化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
  • 一致性Hash,相同参数的请求总是发到同一提供者。
  • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
  • 缺省只对第一个参数Hash,如果要修改,请配置<dubbo:parameter key="hash.arguments" value="0,1" />
  • 缺省用160份虚拟节点,如果要修改,请配置<dubbo:parameter key="hash.nodes" value="320" />

线程模型

27133312_Ud7X.jpg

  • 短事务 :如果事件处理的逻辑能迅速完成,并且不会发起新的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" />
  1. 提供方启动时广播自己的地址。
  2. 消费方启动时广播订阅请求。
  3. 提供方收到订阅请求时,单播自己的地址给订阅者,如果设置了unicast=false,则广播给订阅者。
  4. 消费方收到提供方地址时,连接该地址进行RPC调用。
  5. 为了减少广播量,Dubbo缺省使用单播发送提供者地址信息给消费者,如果一个机器上同时启了多个消费者进程,消费者需声明unicast=false,否则只会有一个消费者能收到消息。

Zookeeper注册中心

27230310_MD2N.jpg

  • 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息。
  • 当注册中心重启时,能自动恢复注册数据,以及订阅请求。
  • 当会话过期时,能自动恢复注册数据,以及订阅请求。
  • 当设置<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];
        }
    }
 
}
  • 回声测试

 

 

 

转载于:https://my.oschina.net/u/3551123/blog/1036405

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值