dubbo参数调优

心不动则不痛 提交于 2020-01-22 08:26:24

        dubbo作为一个服务治理框架,功能相对比较完善,性能也挺不错。但很多朋友在使用dubbo的时候,只是简单的参考官方说明进行搭建,并没有过多的去思考一些关键参数的意义(也可能是时间紧任务多,没空出来研究),最终做出来的效果有一定的打折。 这里我根据目前我们项目的使用情况列出几个性能调优的参数及其意义,供大家参考。

        在介绍参数之前,我们先了解下dubbo中配置的优先级,以免出现调优参数设置了却没发现效果实际是配置被覆盖导致这样的问题。dubbo分为consumer和provider端,在配置各个参数时,其优先级如下:

1、consumer的method配置 

2、provider的method配置

3、consumer的reference配置

4、provider的service配置

5、consumer的consumer节点配置

6、provider的provider节点配置

可以看到,方法级的配置优先级高于接口级,consumer的优先级高于provider。同时,在本地参数配置还存在一层优先级:

1、系统参数(-D),如-Ddubbo.protocol.port=20881

2、xml配置

3、property文件

         了解了这两个优先级,调优起来才会更加清晰,省去了一些诸如配置设置了不生效这样的麻烦。注意,其实dubbo中还可以通过将配置写入注册中心的方式覆盖用户配置,优先级高于系统参数(向注册中心写入动态配置覆盖规则 。该功能通常由监控中心或治理中心的页面完成。)

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
//在注册中心(zookeeper)的configurators节点下创建一个持久的子节点(dynamic=false指定)
//子节点的url如下,用url的参数(比如timeout)动态覆盖消费者本地缓存的提供者url的参数。
//以此达到在无需重启应用的情况下,动态调整RPC调用行为的目的
//详见http://dubbo.apache.org/zh-cn/docs/user/demos/config-rule.html
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&timeout=1000"));

接下来我们看看dubbo的几个比较重要的调优参数,及其影响的方式和大概实现。

          

参数名 作用范围 默认值 说明 备注
actives consumer 0 每服务消费者每服务每方法最大并发调用数 0表示不限制
connections consumer   对每个提供者的最大连接数,rmi、http、hessian等短连接协议表示限制连接数,dubbo等长连接协表示建立的长连接个数 dubbo时为1,及复用单链接
accepts provider 0 服务提供方最大可接受连接数 0表示不限制
iothreads provider cpu个数+1 io线程池大小(固定大小)  
threads provider 200 业务线程池大小(固定大小)  
executes provider 0 服务提供者每服务每方法最大可并行执行请求数 0表示不限制
tps provider   指定时间内(默认60s)最大的可执行次数,注意与executes的区别 默认不开启




        注意表中参数与图中的对应关系:

        1、当consumer发起一个请求时,首先经过active limit(参数actives)进行方法级别的限制,其实现方式为CHM中存放计数器(AtomicInteger),请求时加1,请求完成(包括异常)减1,如果超过actives则等待有其他请求完成后重试或者超时后失败;

//限制 com.foo.BarService 的每个方法,每客户端并发执行(或占用连接的请求数)不能超过 10 个:
<dubbo:reference interface="com.foo.BarService" actives="10" />
//限制 com.foo.BarService 的 sayHello 方法,每客户端并发执行(或占用连接的请求数)不能超过 10 个:
<dubbo:service interface="com.foo.BarService">
    <dubbo:method name="sayHello" actives="10" />
</dubbo:service>

@Activate(group = Constants.CONSUMER, value = Constants.ACTIVES_KEY)
public class ActiveLimitFilter implements Filter {

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();
        int max = invoker.getUrl().getMethodParameter(methodName, Constants.ACTIVES_KEY, 0);
        RpcStatus count = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
        if (max > 0) {
            long timeout = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.TIMEOUT_KEY, 0);
            long start = System.currentTimeMillis();
            long remain = timeout;
            int active = count.getActive();
            if (active >= max) {
                synchronized (count) {
                    while ((active = count.getActive()) >= max) {
                        try {
                            count.wait(remain);
                        } catch (InterruptedException e) {
                        }
                        long elapsed = System.currentTimeMillis() - start;
                        remain = timeout - elapsed;
                        if (remain <= 0) {
                            throw new RpcException("Waiting concurrent invoke timeout in client-side for service:  "
                                                   + invoker.getInterface().getName() + ", method: "
                                                   + invocation.getMethodName() + ", elapsed: " + elapsed
                                                   + ", timeout: " + timeout + ". concurrent invokes: " + active
                                                   + ". max concurrent invoke limit: " + max);
                        }
                    }
                }
            }
        }
        try {
            long begin = System.currentTimeMillis();
            RpcStatus.beginCount(url, methodName);
            try {
                Result result = invoker.invoke(invocation);
                RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, true);
                return result;
            } catch (RuntimeException t) {
                RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, false);
                throw t;
            }
        } finally {
            if(max>0){
                synchronized (count) {
                    count.notify();
                } 
            }
        }
    }

}

这个参数基本上和executes(详见下)一样,但是有一点不同。代码上大致和executes一样,唯一不同的就是多了一个等待时间,当前执行该方法的线程超出了最大限制,那么可以等待一个timeout时间,如果时间过了还是超出了最大限制,那么就抛出异常。这个相对余executes来说温柔那么点。这就是那点不同!

        2、从多个连接(connections)中选择一个连接发送数据,对于默认的netty实现来说,由于可以复用连接,默认一个连接就可以。不过如果你在压测,且只有一个consumer,一个provider,此时适当的加大connections确实能够增强网络传输能力。但线上业务由于有多个consumer多个provider,因此不建议增加connections参数;

限制客户端服务使用连接不能超过 10 个:
<dubbo:reference interface="com.foo.BarService" connections="10" />

在项目初始化的时候,RegistryDirectory#refreshInvoker 会将 provider 提供的接口刷新为 Invoker 类,如果接口未配置 connections 属性,则使用共享连接,如使用 dubbo 协议的方式 DubboProtocol#getClients:

private ExchangeClient[] getClients(URL url){
        // 是否共享连接
        boolean service_share_connect = false;
        int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
        // 如果connections不配置,则共享连接,否则每服务每连接
        if (connections == 0){
            service_share_connect = true;
            connections = 1;
        }
        
        ExchangeClient[] clients = new ExchangeClient[connections];
        for (int i = 0; i < clients.length; i++) {
            if (service_share_connect){
                clients[i] = getSharedClient(url);
            } else {
                clients[i] = initClient(url);
            }
        }
        return clients;
    }

对于同一服务来源:

  • 如果未配置 connections 属性,则单个消费者调用单个服务提供者之间共享一个连接,供所有接口使用
  • 配置了 connections 属性,则针对该接口,独立创建配置的 n 个连接

在这里插入图片描述
Invoker 类如 DubboInvoker ,会持有一个连接数组 ExchangeClient[],每次调用远程接口前,会随机取出一个 ExchangeClient:
在这里插入图片描述

        3、连接到达provider时(如dubbo的初次连接),首先会判断总连接数是否超限(acceps),超过限制连接将被拒绝;这个参数是告知服务提供端能接收多少个消费端连接该服务提供方

限制服务器端接受的连接不能超过 10 个:
<dubbo:provider protocol="dubbo" accepts="10" /><dubbo:protocol name="dubbo" accepts="10" />

参数的体现是在类AbstractServer中。代码如下:  
@Override
    public void connected(Channel ch) throws RemotingException {
        Collection<Channel> channels = getChannels();
        if (accepts > 0 && channels.size() > accepts) {
            logger.error("Close channel " + ch + ", cause: The server " + ch.getLocalAddress() + " connections greater than max config " + accepts);
            ch.close();
            return;
        }
        super.connected(ch);
    }

        4、连接成功后,具体的请求交给io thread处理。io threads虽然是处理数据的读写,但io部分为异步,更多的消耗的是cpu,因此iothreads默认cpu个数+1是比较合理的设置,不建议调整此参数;

在这里插入图片描述
每个NioWorker都是一个runnable任务,内部都有一个selector负责监控套接字,每个NioWorker都由一个线程死循环地执行,线程会扫描selector,当有套接字就绪,便取出套接字进行IO事件的读写,之后再提交到业务线程池处理。由此可见,长连接下,NioWorker通过内部持有的selector可完成线程与套接字的绑定
在这里插入图片描述

        5、数据读取并反序列化以后,交给业务线程池处理,默认情况下线程池为fixed,且排队队列为0(queues),这种情况下,最大并发等于业务线程池大小(threads),如果希望有请求的堆积能力,可以调整queues参数。如果希望快速失败由其他节点处理(官方推荐方式),则不修改queues,只调整threads;

<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" />

        6、execute limit(参数executes)是方法级别的并发限制,原理与actives类似,只是少了等待的过程,即受限后立即失败;

//限制 com.foo.BarService 的每个方法,服务器端并发执行(或占用线程池线程数)不能超过 10 个:
<dubbo:service interface="com.foo.BarService" executes="10" />
//限制 com.foo.BarService 的 sayHello 方法,服务器端并发执行(或占用线程池线程数)不能超过 10 个:
<dubbo:service interface="com.foo.BarService">
    <dubbo:method name="sayHello" executes="10" />
</dubbo:service>

@Activate(group = Constants.PROVIDER, value = Constants.EXECUTES_KEY)
public class ExecuteLimitFilter implements Filter {

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();
        int max = url.getMethodParameter(methodName, Constants.EXECUTES_KEY, 0);
        if (max > 0) {
            RpcStatus count = RpcStatus.getStatus(url, invocation.getMethodName());
            if (count.getActive() >= max) {
                throw new RpcException("Failed to invoke method " + invocation.getMethodName() + " in provider " + url + ", cause: The service using threads greater than <dubbo:service executes=\"" + max + "\" /> limited.");
            }
        }
        long begin = System.currentTimeMillis();
        boolean isException = false;
        RpcStatus.beginCount(url, methodName);
        try {
            Result result = invoker.invoke(invocation);
            return result;
        } catch (Throwable t) {
            isException = true;
            if(t instanceof RuntimeException) {
                throw (RuntimeException) t;
            }
            else {
                throw new RpcException("unexpected exception when ExecuteLimitFilter", t);
            }
        }
        finally {
            RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, isException);
        }
    }

}

RpcStatus.java
public static void beginCount(URL url, String methodName) {
        beginCount(getStatus(url));
        beginCount(getStatus(url, methodName));
    }
    
    private static void beginCount(RpcStatus status) {
        status.active.incrementAndGet();
    }

execute参数与active类似,都是通过统计每个服务每个方法的活跃调用数(负载均衡策略里也有最少活跃数),并与最大值进行比较,然后决定是否限流

        7、tps,控制指定时间内(默认60s)的请求数。注意目前dubbo默认没有支持该参数,需要加一个META-INF/dubbo/com.alibaba.dubbo.rpc.Filter文件,文件内容为:

                   tps=com.alibaba.dubbo.rpc.filter.TpsLimitFilter

@Activate(group = Constants.PROVIDER, value = Constants.TPS_LIMIT_RATE_KEY)
public class TpsLimitFilter implements Filter {

    private final TPSLimiter tpsLimiter = new DefaultTPSLimiter();

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        if (!tpsLimiter.isAllowable(invoker.getUrl(), invocation)) {
            throw new RpcException(
                    new StringBuilder(64)
                            .append("Failed to invoke service ")
                            .append(invoker.getInterface().getName())
                            .append(".")
                            .append(invocation.getMethodName())
                            .append(" because exceed max service tps.")
                            .toString());
        }

        return invoker.invoke(invocation);
    }

}

//tpsLimiter.isAllowable代码:
 public boolean isAllowable(URL url, Invocation invocation) {
        int rate = url.getParameter(Constants.TPS_LIMIT_RATE_KEY, -1);
        long interval = url.getParameter(Constants.TPS_LIMIT_INTERVAL_KEY,
                                         Constants.DEFAULT_TPS_LIMIT_INTERVAL);
        String serviceKey = url.getServiceKey();
        if (rate > 0) {
            StatItem statItem = stats.get(serviceKey);
            if (statItem == null) {
                stats.putIfAbsent(serviceKey,
                                  new StatItem(serviceKey, rate, interval));
                statItem = stats.get(serviceKey);
            }
            return statItem.isAllowable(url, invocation);
        } else {
            StatItem statItem = stats.get(serviceKey);
            if (statItem != null) {
                stats.remove(serviceKey);
            }
        }

        return true;
    }

其实就是限流算法中的计数器法,计数器法会有临界问题,比如在第一秒的后500ms来了100个请求,第2秒的前500ms来了100个请求,那在这1秒内其实最大QPS为200。如下图:
在这里插入图片描述
也就是说在临界的时候,计数器被重置了,然后不法分子钻了这个空子。这点可以通过滑动时间窗口解决
限流算法涉及知识点比较多,后续再接着分析。


        从上面的分析,可以看出如果consumer数*接口的方法数*actives > provider数*threads,且queues=0,则会存在部分请求无法申请到资源,重试也有很大几率失败。 当需要对一个接口的不同方法进行不同的并发控制时使用executes,否则调整threads就可以。

参考:
https://blog.csdn.net/youaremoon/article/details/51884644
https://my.oschina.net/bieber/blog/390818
https://blog.csdn.net/u012099869/article/details/88575052
https://github.com/farmerjohngit/myblog/issues/18
http://dubbo.apache.org/zh-cn/docs/user/demos/config-rule.html
https://www.wandouip.com/t5i418910/
Dubbo2.5.4源码、netty-3.2.5Final.jar源码

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!