【分布式-5】dubbo

基础使用就不说了,看一下dubbo的SPI,以及基于SPI的一些常用技术。在此之前先看看jdk中的spi。

jdk的SPI:

jdk内置的一种服务发现机制,用法:在META-INF/service下创建一个文件,名称是接口全限定名,内容是实现类全限定名,通过ServiceLoader加载到jvm,实现类须有无参构造。 

示例:

public interface ITest {
    void saySomething();
}
public class ITestImpl1 implements ITest {
    
    public void saySomething() {
        System.out.println("Hi, mia.");
    }
}
public class ITestImpl2 implements ITest {
    @Override
    public void saySomething() {
        System.out.println("Hello, world.");
    }
}

【分布式-5】dubbo_第1张图片

测试:使用ServiceLoader加载所有的扩展点(接口实现类),迭代选择想要那一个 

import java.util.Iterator;
import java.util.ServiceLoader;

public class TestServiceLoader {
    public static void main(String[] args) {
        ServiceLoader serviceLoader = ServiceLoader.load(ITest.class);
        Iterator iTests = serviceLoader.iterator();
        while (iTests.hasNext()) {
            ITest iTest = iTests.next();
            System.out.printf("loading %s\n", iTest.getClass().getName());
            iTest.saySomething();
        }
    }
}

存在问题:

jdk的SPI会一次加载所有扩展点,包括不用的,浪费资源,有一个加载失败,所有的都不能用;也不能动态选择想要的实现,只能迭代器遍历。

dubbo的SPI:

因为jdk spi存在的问题,dubbo实现了自己的spi,可以动态选择想要的扩展点。

  • 普通使用(像jdk一样加载所有的遍历):

各项目导入dubbo依赖

接口HelloService,标注@SPI

@SPI
public interface HelloService {
    String  sayHello();
}

接口实现类:

public class DogHelloService implements HelloService{
    @Override
    public String sayHello() {
        return "wang wang";
    }
}


public class HumanHelloService implements HelloService{
   @Override
    public String sayHello() {
        return "hello 你好";
    }
}

在resource目录下创建META-INF/dubbo目录,新建文件.(这里可以配置key,方便动态加载; 如果只是像此例中的普通使用方式,可以不要配置)

【分布式-5】dubbo_第2张图片

使用:

public class DubboSpiMain {
    public static void main(String[] args) {
        // 获取扩展加载器
        ExtensionLoader  extensionLoader  = ExtensionLoader.getExtensionLoader(HelloService.class);
        // 遍历所有的支持的扩展点 META-INF.dubbo
        Set  extensions = extensionLoader.getSupportedExtensions();
        for (String extension : extensions){
            String result = extensionLoader.getExtension(extension).sayHello();
            System.out.println(result);
        }

    }
}

当然,对于上面的普通使用来说,也和jdk中一样,只能加载出全部的,然后遍历。可以石红Adaptive动态选择想要的扩展点。

  • Adaptive动态加载

使用Adaptive,需要在接口方法上添加Adaptive注解,并配合org.apache.dubbo.common.URL参数的方式实现动态选择,如下面第二个方法:

@SPI()
public interface HelloService {
    String  sayHello();

    @Adaptive
    String  sayHello(URL  url);
}
public class HumanHelloService implements HelloService{
    @Override
    public String sayHello() {
        return "hello 你好";
    }

    @Override
    public String sayHello(URL url) {
        return  "hello url";
    }
}
public class DogHelloService implements HelloService{
    @Override
    public String sayHello() {
        return "wang wang";
    }

    @Override
    public String sayHello(URL url) {
        return "wang url";
    }
}
public class DubboAdaptiveMain {
    public static void main(String[] args) {
        URL   url  = URL.valueOf("test://localhost/hello?hello.service=dog");
        HelloService  adaptiveExtension = ExtensionLoader.getExtensionLoader(HelloService.class).getAdaptiveExtension();
        String  msg = adaptiveExtension.sayHello(url);
        System.out.println(msg);
    }
}

如上,getAdaptiveExtension()可以动态选择扩展点。 URL根据业务来写(乱写也没关系),但是参数hello.service=dog不能乱写,hello.service就是接口HelloService名字的驼峰变种,dog就是META-INF/dubbo目录下,接口配置文件中指定的key。

如果url中没有指定hello.service参数,那需要在接口的注解@SPI指定默认值:@SPI("dog")。  

  • @Activate

学习了Adaptive的动态选择,感觉确实方便了一些。 不过也会发现,每一次只能主动手写代码,去选择一个扩展点。 如果有多个扩展点都想使用呢? @Activate注解可以同时激活多个扩展点(当多个扩展点都满足@Activate中指定的条件时,都能使用),该注解有3个选项:

  1. group分组(筛选条件):比如指定扩展点在提供方还是消费方使用
  2. key值(筛选条件):注解中key值指定后,通常也是主动选择时使用,在url中指定相同的参数。
  3. 排序:多个扩展点满足时使用顺序。

在dubbo的过滤器中,就是使用的这个技术, 多个拦截器可以同时被激活,下面自定义一个拦截器:

@Activate(group = {CommonConstants.CONSUMER,CommonConstants.PROVIDER})
public class DubboInvokeFilter   implements Filter {
    @Override
    public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {
        long   startTime  = System.currentTimeMillis();
        try {
            // 执行方法
            return  invoker.invoke(invocation);
        } finally {
            System.out.println("invoke time:"+(System.currentTimeMillis()-startTime) + "毫秒");
        }

    }
}

然后在META-INF/dubbo下配置好。

当定义了group的范围是消费方和提供方,那么业务方法被调用时,consumer和provider的项目中,都会打印方法执行时间。

下面再介绍几个dubbo中的技术:

负载均衡:

dubbo默认实现了几种负载均衡,如随机(默认),轮询,一致性hash… 它也是使用的spi技术,配置方式可以在方法,接口或者全局配置文件; 可以在客户端配置,也可以在服务端配置。

  1. 方法级优先,接口级次之,全局配置再次之。
  2. 如果级别一样,则消费方优先,提供方次之。

//在服务消费者一方配置负载均衡策略

@Reference(check = false,loadbalance = "random")

 

//在服务提供者一方配置负载均衡

@Service(loadbalance = "random")

public class HelloServiceImpl implements HelloService {

        public String sayHello(String name) {

                return "hello " + name;

        }

}

如果要自定义,只需实现org.apache.dubbo.rpc.cluster.LoadBalance即可,并以spi方式注入;

  • 异步调用

配置:

注解:@Reference(methods = {@Method(name = "dsd",async = true)}); 

xml: 
 
 

异步调用时,可以用RpcContext.getContext().getFuture()获取结果

  • 线程池

fix:   表示创建固定大小的线程池。也是Dubbo默认的使用方式,默认创建的执行线程数为200,并且是没有任何等待队列的。所以再极端的情况下可能会存在问题,比如某个操作大量执行时,可能存在堵塞的情况。

cache:  创建非固定大小的线程池,当线程不足时,会自动创建新的线程。但是使用这种的时候需要注意,如果突然有高TPS的请求过来,方法没有及时完成,则会造成大量的线程创建,对系统的CPU和负载都是压力,执行越多反而会拖慢整个系统。

自定义线程池: 真实使用时,可能使用比较多的是fix,但是当发生线程池满了,产生问题时去查看,可能就有点晚了。   所以在创建线程池时,通过某些手段进行监控,可以提前预警。 如下面的自定义线程池:

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.threadpool.support.fixed.FixedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.*;

public class WachingThreadPool  extends FixedThreadPool  implements  Runnable{
    private  static  final Logger  LOGGER = LoggerFactory.getLogger(WachingThreadPool.class);
    // 定义线程池使用的阀值,达到90%就报警
    private  static  final  double  ALARM_PERCENT = 0.90;
    private  final Map    THREAD_POOLS = new ConcurrentHashMap<>();
    public  WachingThreadPool(){
        // 当前类既是一个线程池,也是一个线程任务。
        // 在构造函数中创建一个单线程的线程池A(不是自己),然后将自己作为任务,提交到线程池A。
        // 线程池A每隔3秒就会执行提交的任务,也就是执行此类(自己)的run方法
        // run方法就会打印自身这个线程池中的线程情况
        Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(this,1,3, TimeUnit.SECONDS);
    }
    // 通过父类创建线程池,项目启动时会调用
    @Override
    public Executor getExecutor(URL url) {
         final  Executor executor = super.getExecutor(url);
         if(executor instanceof  ThreadPoolExecutor){
             THREAD_POOLS.put(url,(ThreadPoolExecutor)executor);
         }
         return  executor;
    }

    @Override
    public void run() {
         // 遍历线程池
         for (Map.Entry entry: THREAD_POOLS.entrySet()){
              final   URL  url = entry.getKey();
              final   ThreadPoolExecutor  executor = entry.getValue();
              // 计算相关指标
              final  int  activeCount  = executor.getActiveCount();
              final  int  poolSize = executor.getCorePoolSize();
              double  usedPercent = activeCount / (poolSize*1.0);
              LOGGER.info("线程池执行状态:[{}/{}:{}%]",activeCount,poolSize,usedPercent*100);
              if (usedPercent > ALARM_PERCENT){
                  LOGGER.error("超出警戒线! host:{} 当前使用率是:{},URL:{}",url.getIp(),usedPercent*100,url);
              }

         }
    }
}

定义好线程池后,做SPI声明,创建文件 :

META-INF/dubbo/org.apache.dubbo.common.threadpool.ThreadPool

内容:watching=包名.线程池名

在provider中引入此模块,并配置使用该线程池。

dubbo.provider.threadpool=watching

在consumer中调用provider的方法时,就能使用该线程池了

问题来了,创建线程池时候的URL是什么: 

上面知道了怎么创建线程池,可是创建线程池的时候,需要一个参数URL。把FixedThreadPool的代码贴出来:

public class FixedThreadPool implements ThreadPool {
    public FixedThreadPool() {
    }

    public Executor getExecutor(URL url) {
        String name = url.getParameter("threadname", "Dubbo");
        int threads = url.getParameter("threads", 200);
        int queues = url.getParameter("queues", 0);
        return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, (BlockingQueue)(queues == 0 ? new SynchronousQueue() : (queues < 0 ? new LinkedBlockingQueue() : new LinkedBlockingQueue(queues))), new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }
}

可以看到,url中可以指定线程名称和核心线程数,否则使用默认值。   那么这个url到底是什么?

URL主要包含以下内容:

  • protocol: 协议,一般像我们的 provider 或者 consumer 在这里都是人为具体的协议
  • host: 当前 provider 或者其他协议所具体针对的地址,比较特殊的像 override 协议所指定的
  • host就是 0.0.0.0 代表所有的机器都生效
  • port: 和上面相同,代表所处理的端口号
  • path: 服务路径,在 provider 或者 consumer 等其他中代表着我们真实的业务接口
  • key=value: 这些则代表具体的参数,这里我们可以理解为对这个地址的配置。比如我们 provider中需要具体机器的服务应用名,就可以是一个配置的方式设置上去。

既然path等有关(也就是具体的接口),那我们的provider服务中有很多接口,难不成每个URL对应创建一个线程池? 那不得炸了啊………对于这个问题我纠结了很久,网上也没找到相关的说法(可能要慢慢拔源码吧,但是我还没看)。 于是我做了测试:

  1. 在项目中增加了几个service接口和实现,并且每个service有几个方法。  
  2. 在WachingThreadPool创建线程池的方法 getExecutor(URL url) 中,打印出url。 看看provider项目启动时,用哪个url创建的线程池。
  3. 在WachingThreadPool的 run方法中,也打印url。 看看consumer端调用时,使用的url与 上一步启动时的url,有何差别?

结果发现,启动时的url里面的接口的信息,就是几个service中的其中一个(就像是随机选择的),并没有一个service对应创建一个线程池。  另外,consumer端调用时,即使调用了另一个service的方法,在run方法中打印出来的URL,和项目启动创建线程池时候的URL一模一样(连时间戳都一样)。   如下:

dubbo://10.128.7.87:20885/com.test.service.HelloService?anyhost=true&application=dubbo-demo-annotation-provider&bind.ip=10.128.7.87&bind.port=20885&channel.readonly.sent=true&codec=dubbo&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&heartbeat=60000&interface=com.test.service.HelloService&methods=sayHello&pid=1468&release=2.7.5&side=provider&threadname=DubboServerHandler-10.128.7.87:20885&threadpool=watching×tamp=1679991335736

结论:

虽然项目启动创建线程池的时候,和URL有关,并且URL中附带了某个接口,参数,或者其他一些信息。  但它不会创建多个,整个项目会共用一个线程池。   至于说,此url中带的接口和方法等信息,我感觉是随机选择的。(具体我不知道按什么规则选的,有大神知道可以说一下)

  • 服务降级 

降级是防止分布式服务发生雪崩效应,什么是雪崩?就是蝴蝶效应,当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到

服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机, 这样下去将导致整个分布式服务都瘫痪,这就是雪崩。

dubbo的降级配置方式有两种:

1、屏蔽降级:mock=force:return null,不会真正去调用接口,直接返回null。

2、容错降级:mock=fail:return null,会调用接口,失败后返回null。

(mock=return null,默认为第二种)

配置的地方:

1、管理端配置,如下:

【分布式-5】dubbo_第3张图片

 2、xml中,比如在某个接口的调用里面配置:

timeout="3000" mock="return null" />

3、注解中

@Reference(mock="force:return null")    或者   @Reference(mock="return null")

注意,除了返回null,也可以返回其他指定的默认值。

你可能感兴趣的:(分布式,dubbo)