1. Dubbo与RPC的关系

        
1.1 什么是RPC?

维基百科这样解释:

        远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
        

        如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用,所以,对于Java程序员而言,RPC就是远程方法调用。

        如何理解RPC是一个计算机通信协议呢?我们已经知道RPC是专注于远程方法调用,如果实现远程方法调用,基本的就是通过网络,通过传输数据来进行调用。如下图所示
Dubbo的负载均衡、集群容错、服务降级等机制详解_Dubbo
        可以看到远程方法A 想要调用远程方法B,需要定义 数据类型 和 传输协议。而这些需要定义的东西作为一个协议存在于调用方和接收方,后续所有调用都遵守这个已制定的协议,这就是RPC通信协议。所以,我们其实可以看到RPC的自定义性是很高的,各个公司内部都可以实现自己的一套RPC框架,而Dubbo就是阿里所开源出来的一套RPC框架。

        RPC和 HTTP、TCP的关系就是:RPC是基于HTTP、TCP协议来传输数据的,对于所传输的数据,可以交由RPC的双方来协商定义,但基本都会包括:

  1. 调用的是哪个类或接口
  2. 调用的是哪个方法,方法名和方法参数类型(考虑方法重载)
  3. 调用方法的入参

 

1.2 Dubbo与RPC的关系

        上面说到实现RPC框架需要定义 数据类型 和 传输协议。而Dubbo作为阿里开源出来的RPC框架,已经制定好了对应的 传输数据类型 和传输协议,使用Dubbo必须遵循Dubbo制定好的规则。

Dubbo的传输协议见下文!

 

3. 自定义RPC框架思路

服务端:

  1. 注册服务到zk或redis。以map的形式保存起来,key = 服务名,value = List<服务器地址>。客户端请求可以负载到value的某个地址上。
    注意:如果只把服务放在本地缓存中,那么其他的服务将调用失败,因为不同的服务属于不同的jvm,其他服务将无法感知另一个服务中的本地缓存。
  2. 把服务和服务的实现类注册到本地缓存。以map的形式保存起来,key = 服务名,value = 服务的实现类。目的是:当服务端接受到客户端请求,可以根据客户端传来的接口名,从本地缓存中拿到其实现类,然后通过反射调用客户端想要调用的方法
  3. 根据不同的协议启动不同的服务器。如果是Http协议则启动Tomcat,如果是Dubbo协议则启动Netty。

客户端:

  1. 指定传输的数据类型,包括接口(服务)名、方法名、参数类型、参数名,并封装成一个类Invocation。

  2. 当客户端调用某个接口时,采用jdk动态代理的方式,调用invoke代理方法,在代理方法中做增强逻辑。逻辑如下:
    2.1:填充数据类型Invocation
    2.2:从zk或redis中根据服务名拉取服务器地址,并负载均衡到某一个服务器地址下
    2.3:获取客户端协议(dubbo 或 http),并根据协议向服务端发送数据Invocation

  3. 客户端DispartchServlet拦截到客户端发过来的请求。通过JSON序列化二进制数据为Invocation 对象。根据对象中的接口名,从本地缓存中拿到对应的实现类,利用反射调用客户端客户端想要调用的方法,并输出。完成了远程服务调用!

 

2. Dubbo的基本使用

首先附上dubbo官方使用文档:https://dubbo.apache.org/zh/docs/v2.7/user/examples/loadbalance/

2.1 Dubbo是什么?

Dubbo的负载均衡、集群容错、服务降级等机制详解_Dubbo_02

        Apache Dubbo 是一款高性能、轻量级的开源 Java 服务框架,提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维。

其中有以下几个关键点

  • 注册与发现:Dubbo使用zookeeper做服务的注册中心,就是服务的提供者以临时节点的形式将服务Server信息注册保存到Zookeeper的dubbo目录下的provider的节点下,供消费者发现调用。

  • 负载均衡: Dubbo支持负载均衡策略,就是同一个Dubbo服务被多台服务器启用后,会在在Zookeeper提供者节点下显示多个相同接口名称节点。消费者在调用Dubbo负载均衡服务时,采用权重的算法策略选择具体某个服务器上的服务,权重策略以*2倍数设置。

  • 容错机制:Dubbo的提供者在Zookeeper上使用的是临时节点,一旦提供者所在服务挂掉,该节点的客服端连接将会关闭,故节点自动消失。所以消费者调用接口时将不会轮询到已经挂掉的接口上(延迟例外)。

  • Dubbo容器:Dubbo在java jvm中有自己的容器,和Spring IOC的bean一样,将服务对象保存到自己的容器中。

  • 监控中心:监控中心主要是用来服务监控和服务治理。服务治理包含:负载均衡策略、服务状态、容错、路由规则限定、服务降级等。具体可以下载Dubbo监控中心客户端查看与设置。

  • Dubbo的协议:点击链接获取更多协议的详细信息

①:dubbo协议: Dubbo默认协议是dubbo协议,采用单一长连接和 NIO 异步通讯,基于hessian作为序列化协议,适合于数据量小但并发高的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。

②:hessian协议: Hessian底层采用Http通讯(同步),走hessian序列化协议。适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用

③:http协议: 走json序列化,适用于浏览器查看,同时给应用程序和浏览器JS使用的服务。

④:rmi协议:走java二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少用

⑤:webservice协议:采用SOAP文本序列化,适用HTTP传输,常用于系统集成,跨语言调用

⑥:redis协议:基于 Redis实现的 RPC 协议。

⑦:rest协议:基于标准的Java REST API实现的REST调用支持

 

2.2 负载均衡

        生产者在为某个接口暴露服务时,可以根据协议、ip、端口号、服务、group、version等六要素暴露多个接口实例,达到类似于集群的形式。如下所示:任意修改某个要素就算是这个接口已暴露的实例!在代码中可以通过修改@Service注解的值来暴露不同的服务实例@Service(interfaceName = "com.tuling.DemoService", version = "generic") 这样就会暴露http://ip:port/DemoService + generic服务,消费时要根据生产者暴露的规则来进行消费。
Dubbo的负载均衡、集群容错、服务降级等机制详解_Dubbo_03
        如果在application.properties配置文件中,配置了多个协议,Dubbo会默认会根据配置暴露多个服务实例,如果做下面的配置,那么上面的DemoService接口在zookeeper上就会有两个服务实例,一个Http的,一个Dubbo的!

# 配置多协议

# dubbo协议
dubbo.protocols.p1.id=dubbo1
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20881
dubbo.protocols.p1.host=0.0.0.0
# http协议
dubbo.protocols.p2.id=dubbo2
dubbo.protocols.p2.name=http
dubbo.protocols.p2.port=20882
dubbo.protocols.p2.host=0.0.0.0

Dubbo的负载均衡、集群容错、服务降级等机制详解_Dubbo_04

        那么面对多个服务实例,消费端调用时是如何进行选择的呢?Dubbo为我们提供了四种负载均衡策略,可以通过负载均衡策略来选择一个服务实例进行调用!默认的负载策略为 random 随机调用。四种策略如下:

  • Random 随机:按权重设置随机概率,可通过配置权重修改概率
  • RoundRobin 轮询:存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
  • LeastActive 最少活跃数:活跃数是指调用前后的计数差,服务调用越快,活跃数越小。提供者越慢,接收的请求就越少,因为越慢的提供者的调用前后计数差会越大,活跃数也会变大
  • ConsistentHash 一致性Hash:相同参数的请求总是发到同一提供者。

注意:比较难理解的是LeastActive 最少活跃数,理论上最少活跃数应该是在服务提供者端进行统计的,服务提供者统计有多少个请求正在执行中。但是Dubbo却选择在消费端进行统计最少活跃数,为什么能在消费端进行统计?逻辑如下:

  1. 消费者会缓存所调用服务的所有提供者,比如记为p1、p2、p3三个服务提供者,每个提供者内都有一个属性记为active,默认位0
  2. 消费者在调用次服务时,如果负载均衡策略是leastactive
  3. 消费者端会判断缓存的所有服务提供者的active,选择最小的,如果都相同,则随机选出某一个服务提供者后,假设位p2,Dubbo就会对p2.active+1
  4. 然后真正发出请求调用该服务
  5. 消费端收到响应结果后,对p2.active-1
  6. 这样就完成了对某个服务提供者当前活跃调用数进行了统计,并且并不影响服务调用的性能,下次调用会再次判断最小的active,这就解释了为什么服务提供者越慢,接收的请求就越少!因为它的active值大!

配置方式

  • Provider端配置:生产者通过在暴露服务的@Servic注解上进行配置:@Service(loadbalance = "roundrobin"),配置时需要注意负载均衡方式均为小写!
  • Consumer端配置:消费端通过@Reference(loadbalance = "leastactive ")

如果Provider和Consumer都配置,则以Consumer端配置的为准!

 

2.3 服务超时

在服务提供者(服务端)和服务消费者上都可以配置服务超时时间,这两者是不一样的。

@Service(version = "timeout", timeout = 4000)	//服务提供者端超时时间
@Reference(version = "timeout", timeout = 3000,retries = 1)	//服务消费者端超时时间

消费者调用一个服务,分为三步:

  1. 消费者发送请求(网络传输)
  2. 服务端执行服务
  3. 服务端返回响应(网络传输)

如果在服务端和消费端只在其中一方配置了timeout

         那么没有歧义,表示消费端调用服务的超时时间,消费端如果超过时间还没有收到响应结果,则消费端会抛超时异常,但,服务端不会抛异常,服务端在执行服务后,会检查执行该服务的时间,如果超过timeout,则会打印一个超时日志。服务会正常的执行完。

如果在服务端和消费端各配了一个timeout

那情况就比较复杂了,假设

  • 服务执行为5s
  • 消费端timeout=3s
  • 服务端timeout=6s

那么消费端调用服务时,消费端会收到超时异常(因为消费端超时了),服务端一切正常(服务端没有超时)。如果配置服务端timeout=4s,那么由于服务执行为5s,所以服务端也会打印警告,标识服务端也超时了!

 

2.4 集群容错

         一个服务提供多个实例(集群),集群容错是指:集群容错表示:服务消费者在调用某个服务时,这个服务有多个服务提供者,在经过负载均衡后选出其中一个服务提供者之后进行调用,但调用报错后,Dubbo所采取的后续处理策略。如图:如果服务实例1调用失败,则会尝试调用服务实例2或者3,默认重试2次。
Dubbo的负载均衡、集群容错、服务降级等机制详解_Dubbo_05
集群容错可以在@Service 和 @Reference注解上进行配置:如果两者都配置,以消费端为主!

@Service( cluster = "failfast")	//服务端超集群容错
@Reference(cluster = "failfast") //消费端集群容错

Dubbo提供了六种集群容错方案:

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

 

2.5 服务降级

         服务降级表示:服务消费者在调用某个服务提供者时,如果该服务提供者报错了,所采取的措施。可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。集群容错和服务降级的区别在于:

  1. 集群容错是整个集群范围内的容错
  2. 服务降级是单个服务提供者的自身容错

服务降级可以在消费端的 @Reference注解上使用mock来指定降级方案

  • mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
  • mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
 //服务降级:如果调用失败返回123
 @Reference(version = "timeout", timeout = 1000, mock = "fail: return 123")  

更多服务降级方案可参考本地伪装:https://dubbo.apache.org/zh/docs/v2.7/user/examples/local-mock/

本地伪装其实也是对Mock的应用,便于服务端在客户端执行容错逻辑

 

2.6 本地存根

        消费端通过Dubbo远程调用服务端,其业务实现基本都在服务端。但有些时候想在消费端也执行部分逻辑,比如:做 ThreadLocal 缓存(这个用处最大),提前验证参数,调用失败后伪造容错数据等等,此时就需要在@Reference中带上 Stub,消费端生成服务的代理 Proxy 实例,会把 Proxy 通过构造函数传给 Stub,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。

//本地存根,开启stub
@Reference(stub = "true")
或者
@Reference(stub = "com.foo.DemoServiceStub") //指定stub对象

还需要自定义一个类实现DemoService接口,表示为DemoService做的本地存根,这个类是放在消费端的

public class DemoServiceStub implements DemoService {

    private final DemoService demoService;

    // 构造函数传入真正的远程代理对象
    public DemoServiceStub(DemoService demoService){
        this.demoService = demoService;
    }

    @Override
    public String sayHello(String name) {
        // 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
        try {
            return demoService.sayHello(name); // safe  null
        } catch (Exception e) {
            // 你可以容错,可以做任何AOP拦截事项
            return "容错数据";
        }
    }
}

注意:实现类中必须有一个传入远程 DemoService 实例的构造函数

使用上述存根代码执行后,如果调用失败,则会执行DemoServiceStub中的容错方案,控制台打印”容错数据“!

 

2.7 参数回调

参数回调是指:当消费端调用服务成功后,希望服务端能够回调一下消费端的逻辑

既然是服务端回调消费端的逻辑,那么这个逻辑一定是存在消费端的!以DemoService服务为例

消费端调用:

    @Reference(version = "callback")
    private DemoService demoService;

	//调用服务
	demoService.sayHello("aaa", "d1", new DemoServiceListenerImpl())

上述代码new DemoServiceListenerImpl()中要包含着具体的回调逻辑

// 回调逻辑接口
public interface DemoServiceListener {
    void changed(String msg);
}
// 回调逻辑实现类
public class DemoServiceListenerImpl implements DemoServiceListener {

    @Override
    public void changed(String msg) {
        System.out.println("被回调了:"+msg);
    }
}

服务端回调

// DemoService接口
public interface DemoService {

    // 回调方法
    default String sayHello(String name, String key, DemoServiceListener listener) {
        return null;
    };
}
// @Method注明了是sayHello()中索引为2的参数参与了回调,以及最大同时支持3个回调,上述代码只有一个,如果写4个就报错
@Service(version = "callback", methods = {@Method(name = "sayHello", arguments = {@Argument(index = 2, callback = true)})}, callbacks = 3)
public class CallBackDemoService implements DemoService {
    @Override
    public String sayHello(String name, String key, DemoServiceListener callback) {
        
        callback.changed(); //代理对象直接回调消费端的changed方法
        
        return "";  // 正常访问
    }
}

在服务端回调时,需要注意:

  • sayHello()方法中的DemoServiceListener为代理对象,并不是消费端传过来的DemoServiceListenerImpl对象
  • 需要在@Service中使用@Method注明是哪个方法中哪个参数参与了回调,以及最大同时支持几个回调

结果:
在消费端打印的是changed的内容,但这个方法是在服务端被执行的
Dubbo的负载均衡、集群容错、服务降级等机制详解_Dubbo_06

 

2.8 异步调用

         上文所讲的内容都是依托于同步调用的,Dubbo也提供了异步调用方式,异步调用与同步调用的求别在于:

  • 服务端需要使用CompletableFuture.supplyAsync开启一个线程执行任务
  • 客户端需要使用CompletableFuture.whenComplete监听异步线程执行完毕

消费端代码示例:

    @Reference(version = "async")
    private DemoService demoService;


    public static void main(String[] args) throws IOException {
        ConfigurableApplicationContext context = SpringApplication.run(AsyncDubboConsumerDemo.class);

        DemoService demoService = context.getBean(DemoService.class);

        // 调用直接返回CompletableFuture
        CompletableFuture<String> future = demoService.sayHelloAsync("异步调用");  // 5

		//这个方法只有等异步线程执行结束才会调用
        future.whenComplete((v, t) -> {
            if (t != null) {
                t.printStackTrace();
            } else {
                System.out.println("Response: " + v);
            }
        });

        System.out.println("结束了");

    }

服务端代码示例

public interface DemoService {
    // 同步调用方法
    String sayHello(String name);

    // 异步调用方法
    default CompletableFuture<String> sayHelloAsync(String name) {
        return null;
    };

}

@Service(version = "async")
public class AsyncDemoService implements DemoService {

	//同步调用
    @Override
    public String sayHello(String name) {
        System.out.println("sayhello方法 " + name);
        return name;
    }

	// 主要关注这个异步调用
    @Override
    public CompletableFuture<String> sayHelloAsync(String name) {
        System.out.println("执行了异步服务" + name);

		//相当于在异步线程里执行sayHello方法!
        return CompletableFuture.supplyAsync(() -> {
            return sayHello(name);
        });
    }
}

执行结果如下:
消费端:
Dubbo的负载均衡、集群容错、服务降级等机制详解_Dubbo_07
服务端:
Dubbo的负载均衡、集群容错、服务降级等机制详解_Dubbo_08
可以看到他们之间打印的顺序也是异步的体现!

 

2.9 泛化调用、泛化服务

         泛化调用: 在Dubbo中,如果某个服务想要支持泛化调用,就可以将该服务的generic属性设置为true,那对于服务消费者来说,就可以不用依赖该服务的接口,直接利用GenericService接口来进行服务调用。泛化调用可以用来做服务测试。

@EnableAutoConfiguration
public class GenericDubboConsumerDemo {

	//调用DemoService服务,并不需要注入DemoService,也不需要引入依赖
    @Reference(id = "demoService", version = "default", interfaceName = "com.tuling.DemoService", generic = true)
    private GenericService genericService;

    public static void main(String[] args) throws IOException {
        ConfigurableApplicationContext context = SpringApplication.run(GenericDubboConsumerDemo.class);
		
        GenericService genericService = (GenericService) context.getBean("demoService");

        Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"周瑜"});
        System.out.println(result);
        
    }
}

         泛化服务: 可以不实现具体的某个接口,而是实现GenericService接口,并在@Service上标明接口名即可,在调用直接注入DemoService就可以使用!

@Service(interfaceName = "com.tuling.DemoService", version = "generic")
public class GenericDemoService implements GenericService {

    @Override
    public Object $invoke(String s, String[] strings, Object[] objects) throws GenericException {
        System.out.println("执行了generic服务");

        return "执行的方法是" + s;
    }
}

 

3. dubbo的REST协议

         dubbo支持多种远程调用方式,例如dubbo RPC(二进制序列化 + tcp协议)、http invoker(二进制序列化 + http协议)、hessian(二进制序列化 + http协议)、WebServices (文本序列化 + http协议)、REST(文本序列化 + http协议)等等的支持。

         当我们用Dubbo提供了一个服务后,如果消费者没有使用Dubbo也想调用服务,那么这个时候就可以让我们的服务支持REST协议,这样消费者就可以通过REST形式调用我们的服务了。更多REST协议内容点击查看官网!

①:服务端配置文件修改协议为rest

dubbo.protocol.name=rest

②:服务端实现:使用@Path指定Rest风格的访问路径(注意:所有暴露的服务都必须加@Path)

@Service(version = "rest")
@Path("demo")
public class RestDemoService implements DemoService {

    @GET
    @Path("say")
    @Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})
    @Override
    public String sayHello(@QueryParam("name") String name) {
        System.out.println("执行了rest服务" + name);

        URL url = RpcContext.getContext().getUrl();
        return String.format("%s: %s, Hello, %s", url.getProtocol(), url.getPort(), name);  // 正常访问
    }

}

这样就可以通过浏览器访问这个服务了,其他消费端也可以直接通过HttpCliet等非dubbo的形式去调用服务!

 

4. dubbo的控制台

 

5. dubbo的服务路由

经过服务路由可以配置黑名单、白名单、读写分离、隔离不同机房网段等等,这点在官网有很详细的解释 点击查看官网详情!!!

dubbo提供的标签路由还可以用来发布版本,什么是蓝绿发布、灰度发布?