从零开始学Dubbo-基础篇-线程池和路由

475 阅读6分钟

异步调用

Dubbo不只提供了堵塞式的的同步调用,同时提供了异步调用的方式。这种方式主要应用于提供者接口 响应耗时明显,消费者端可以利用调用接口的时间去做一些其他的接口调用,利用 Future 模式来异步等 待和获取结果即可。这种方式可以大大的提升消费者端的利用率。 目前这种方式可以通过XML的方式进 行引入。

新增api接口

在接口中新增一个可定义延迟时常的接口

public interface HelloDubbo {
    String sayHello(String name);
    String sayHello(String name,int timeToWait);//new
}

provider新增实现

提供者进行sleep延迟

@Service
public class HellowDubboImpl implements HelloDubbo
{
    @Override
    public String sayHello(String name) {
        return "provider2 ack:"+name;
    }
    //new
    @Override
    public String sayHello(String name, int timeToWait) {
        try {
            Thread.sleep(timeToWait);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "provider ack:"+name;
    }
}

consumer基于xml配置改造

  • 1.启动类修改 由AnnotationConfigApplicationContext改为ClassPathXmlApplicationContext
public class XMLConsumerMain {

    public static void main(String[] args) throws IOException {
        ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("consumer.xml");
        final HelloDubbo bean = context.getBean(HelloDubbo.class);
        while (true){
            System.in.read();
            String hello=bean.sayHello("test",100);
            System.out.println("consumer post:"+hello);
        }
    }

}
  • 2.新增xml配置文件
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!--服务名 -->
    <dubbo:application name="dubbo-service-consumer"/>
    <!--注册地址 -->
    <dubbo:registry  address="zookeeper://10.0.9.173:2181"/>
    <!--接口 -->
    <dubbo:reference id="helloDubbo"    interface="com.study.service.HelloDubbo">
        <!--方法名 是否异步 -->
        <dubbo:method name="sayHello" async="true"/>
    </dubbo:reference>
</beans>
    1. 启动测试 首先配置响应时间为100ms

image.png

看consumer结果

image.png

然后看provider结果

image.png

可以看到消费端没有等待结果直接返回了

获取异步结果

CompletableFuture和Future

可以通过CompletableFuture或者Future来异步获取

public class XMLConsumerMain {

    public static void main(String[] args) throws IOException {
        ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("consumer.xml");
        final HelloDubbo bean = context.getBean(HelloDubbo.class);
        while (true){
            System.in.read();
            //利用CompletableFuture和Future获取
            //获取RPC上下文 然后获取CompletableFuture对象即可
            final CompletableFuture<Object> completableFuture = RpcContext.getContext().getCompletableFuture();
            String hello=bean.sayHello("test",100);
            System.out.println("consumer post:"+hello);
            try {
                System.out.println("future result:"+completableFuture.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

执行查看结果

image.png

线程池

dubbo在使用时,都是通过创建真实的业务线程池进行操作的。目前已知的线程池模型有两个和java中 的相互对应:

  • fix: 表示创建固定大小的线程池。也是Dubbo默认的使用方式,默认创建的执行线程数为200,并且是没有任何等待队列的。所以再极端的情况下可能会存在问题,比如某个操作大量执行时,可能存在堵塞的情况。后面也会讲相关的处理办法。
  • cache: 创建非固定大小的线程池,当线程不足时,会自动创建新的线程。但是使用这种的时候需要注意,如果突然有高TPS的请求过来,方法没有及时完成,则会造成大量的线程创建,对系统的CPU和负载都是压力,执行越多反而会拖慢整个系统。

自定义线程池

在真实的使用过程中可能会因为使用fix模式的线程池,导致具体某些业务场景因为线程池中的线程数量不足而产生错误,而很多业务研发是对这些无感知的,只有当出现错误的时候才会去查看告警或者通过客户反馈出现严重的问题才去查看,结果发现是线程池满了。所以可以在创建线程池的时,通过某些手段对这个线程池进行监控,这样就可以进行及时的扩缩容机器或者告警。下面的这个程序就是这样子的,会在创建线程池后进行对其监控,并且及时作出相应处理。

1.创建模块threadpool

自定义线程池,我实现的是一个监控功能的线程池是基于FixedThreadPool的,会记录使用率以及线程池状态,如果超过阈值就会发出警告。也可以基于ThreadPool接口实现其他功能线程池。

创建一个类MyThreadPool实现Runnable接口以及实现dubbo的FixedThreadPool(非必须)

public class MyThreadPool extends FixedThreadPool implements Runnable {
    //日志
    private static final Logger logger= LoggerFactory.getLogger(MyThreadPool.class);
    //定义预警阀值
    private static final double ALARM_PERCENT=0.70;
    //当前服务
    private URL url;
    //当前线程池
    private ThreadPoolExecutor threadPoolExecutor;
    public MyThreadPool(){
        //开启监控 每隔一段时间打印线程使用情况
        Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(this,1 ,3, TimeUnit.SECONDS);
    }

    //通过父类创建线程池
    @Override
    public Executor getExecutor(URL url){
        final Executor executor=super.getExecutor(url);
        if(executor instanceof ThreadPoolExecutor){
            this.url=url;
            this.threadPoolExecutor= (ThreadPoolExecutor) executor;
        }
        return executor;
    }

    @Override
    public void run() {
        //计算数据
        final int activeCount = threadPoolExecutor.getActiveCount();
        final int corePoolSize = threadPoolExecutor.getCorePoolSize();
        double usedPercent = activeCount / (corePoolSize*1.0);
        logger.info("线程池状态:[{}/{}:{}%]",activeCount,corePoolSize,usedPercent*100);
        if(usedPercent>ALARM_PERCENT){
            logger.error("超出阈值!host:{} 当前使用率:{} URL:{}",url.getIp(),usedPercent*100,url);
        }
    }
}

2.创建配置文件

在resources目录下新建META-INF/dubbo目录,然后再以org.apache.dubbo.common.threadpool.ThreadPool命名创建文件。 在里面配置:mythreadpool=com.study.threadpool.MyThreadPool

3.生产者配置

provider引入threadpool模块依赖,并在配置文件中新增配置: dubbo.provider.threadpool=mythreadpool

4.消费者修改

修改消费者的调用方法,以便于测试。修改原有的while方法。

 public static void main(String[] args) throws IOException, InterruptedException {
        AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(ConsumerConfiguation.class);
        context.start();
        final ConsumerCompent bean = context.getBean(ConsumerCompent.class);
        while (true){
            for (int i = 0; i < 1000; i++) {
                Thread.sleep(5);
                new Thread(()->{
                    //900是provider需要等待的时间ms
                    //这样provider就会出现来不及响应的情况
                    bean.sayHello("test",900);
                }).start();
            }
        }
    }

5.测试

先将延迟时间设置为800ms查看结果:

image.png

再将延迟时间设置为900ms查看结果

image.png

可以看到触发告警系统了,这时可以连接公司短信邮件服务 通知相关负责人。

路由

路由使用

编写路由规则

在consumer项目中编写路由类

public class DubboRouterMain {
    public static void main(String[] args) {
        //获取注册中心工厂
        RegistryFactory registryFactory= ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
        //获取注册中心
        Registry registry=registryFactory.getRegistry(URL.valueOf("zookeeper://10.0.9.173:2181"));
        // 消费者限制=>提供者限制
        registry.register(URL.valueOf("condition://0.0.0.0/com.study.service.HelloDubbo?category=routers&force=true&dynamic=true&rule="+
                //此处意思是不访问ip为172.16.8.233的提供端
                URL.encode("=>host!=172.16.8.233")));
    }
}

测试效果

首先启动provider然后先执行DubboRouterMain的main方法再启动consumer,查看执行结果

image.png 可以看到找不到可用服务

路由规则详解

通过上面的程序,我们实际本质上就是通过在zookeeper中保存一个节点数据,来记录路由规则。消费 者会通过监听这个服务的路径,来感知整个服务的路由规则配置,然后进行适配。这里主要介绍路由配 置的参数。具体请参考文档, 这里只对关键的参数做说明。

  • route:// 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。
  • 0.0.0.0 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填。
  • com.study.service.HelloDubbo 表示只对指定服务生效,必填。
  • category=routers 表示该数据为动态配置类型,必填。
  • dynamic : 是否为持久数据,当指定服务重启时是否继续生效。必填。
  • runtime : 是否在设置规则时自动缓存规则,如果设置为true则会影响部分性能。
  • rule : 是整个路由最关键的配置,用于配置路由规则。
  • ... => ... 在这里 => 前面的就是表示消费者方的匹配规则,可以不填(代表全部)。 => 后方则必须填写,表示当请求过来时,如果选择提供者的配置。官方这块儿也给出了详细的示例,可以按照那里来讲。 其中使用最多的便是 host 参数。 必填