【十三】Dubbo源码分析之线程模型、限流、负载均衡、超时、重试、降级

一、dubbo调用及线程模型

上图中的每个点参数解释

参数名

作用范围

默认值

说明

备注

threads

provider

200

业务处理线程池大小

 

iothreads

provider

CPU+1

io线程池大小

 

queues

provider

0

线程池队列大小,当线程池满时,排队等待执行的队列大小,建议不要设置,当线程池满时应立即失败,重试该服务提供的其它机器,而不是排队,除非有特殊需求

 

connections

consumer

0

对每个提供者的最大连接数,rmi、http、hessian等短连接协议表示限制连接数,Dubbo等长连接协表示建立的长连接个数

Dubbo协议默认共享一个长连接

默认:

消费者---->IP+PORT只有一个connection 

actives

consumer

0

每服务消费者每服务每方法最大并发调用数

0表示不限制

acceptes

provider

0

服务提供方最大可接受连接数

0表示不限制

executes

provider

0

服务提供者每服务每方法最大可并行执行请求数

默认0表示不限制

服务消费端相关配置点的实现源码

actives

 ActiveLimitFilter.java

 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();
                }
            }
        }
    }

connections 

DubboProtocol.java

    private ExchangeClient[] getClients(URL url) {
        // whether to share connection
        boolean service_share_connect = false;
        int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
        // if not configured, connection is shared, otherwise, one connection for one service
        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;
    }

 DubboInvoker.java

protected Result doInvoke(final Invocation invocation) throws Throwable {
        RpcInvocation inv = (RpcInvocation) invocation;
        final String methodName = RpcUtils.getMethodName(invocation);
        inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
        inv.setAttachment(Constants.VERSION_KEY, version);

        ExchangeClient currentClient;
        if (clients.length == 1) {
            currentClient = clients[0];
        } else {
            currentClient = clients[index.getAndIncrement() % clients.length];
        }
        try {
            boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
            int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
            if (isOneway) {
                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
                currentClient.send(inv, isSent);
                RpcContext.getContext().setFuture(null);
                return new RpcResult();
            } else if (isAsync) {
                ResponseFuture future = currentClient.request(inv, timeout);
                RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
                return new RpcResult();
            } else {
                RpcContext.getContext().setFuture(null);
                return (Result) currentClient.request(inv, timeout).get();
            }
        } catch (TimeoutException e) {
            throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        } catch (RemotingException e) {
            throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

服务提供端相关配置点的实现源码 

accepts 

AbstractServer.java

public void connected(Channel ch) throws RemotingException {
        // If the server has entered the shutdown process, reject any new connection
        if (this.isClosing() || this.isClosed()) {
            logger.warn("Close new channel " + ch + ", cause: server is closing or has been closed. For example, receive a new connect request while in shutdown process.");
            ch.close();
            return;
        }

        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);
    }

iothreads

NettyServer.java

protected void doOpen() throws Throwable {
    NettyHelper.setNettyLoggerFactory();
    ExecutorService boss = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerBoss", true));
    ExecutorService worker = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerWorker", true));
    ChannelFactory channelFactory = new NioServerSocketChannelFactory(boss, worker, getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS));
    bootstrap = new ServerBootstrap(channelFactory);

    final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);
    channels = nettyHandler.getChannels();
    bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
        public ChannelPipeline getPipeline() {
            NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec() ,getUrl(), NettyServer.this);
            ChannelPipeline pipeline = Channels.pipeline();
            pipeline.addLast("decoder", adapter.getDecoder());
            pipeline.addLast("encoder", adapter.getEncoder());
            pipeline.addLast("handler", nettyHandler);
            return pipeline;
        }
    });
    // bind
    channel = bootstrap.bind(getBindAddress());
}

threads

FixedThreadPool.java

    @Override
    public Executor getExecutor(URL url) {
        String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
        int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS);
        int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
        return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS,
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)),
                new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

其中

线程名字:Constants.DEFAULT_THREAD_NAME="Dubbo"

核心线程数、最大线程数:Constants.DEFAULT_THREADS = 200

等待队列大小:Constants.DEFAULT_QUEUES = 0  默认值为 0,表示使用同步阻塞队列;如果 queues 设置为小于 0 的值,则使用容量为 Integer.MAX_VALUE 的阻塞链表队列;如果为其他值,则使用指定大小的阻塞链表队列。

闲置回收时间:0 即是不回收

拒绝策略:打日志、dumpJstack、抛出异常

LimitedThreadPool.java

    public Executor getExecutor(URL url) {
        String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
        int cores = url.getParameter(Constants.CORE_THREADS_KEY, Constants.DEFAULT_CORE_THREADS);
        int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS);
        int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
        return new ThreadPoolExecutor(cores, threads, Long.MAX_VALUE, TimeUnit.MILLISECONDS,
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)),
                new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

其中

线程名字:Constants.DEFAULT_THREAD_NAME="Dubbo"

核心线程数:Constants.DEFAULT_CORE_THREADS = 0

最大线程数:Constants.DEFAULT_THREADS = 200

等待队列大小:Constants.DEFAULT_QUEUES = 0  默认值为 0,表示使用同步阻塞队列;如果 queues 设置为小于 0 的值,则使用容量为 Integer.MAX_VALUE 的阻塞链表队列;如果为其他值,则使用指定大小的阻塞链表队列。

闲置回收时间:keepAliveTime=Long.MAX_VALUE

TimeUnit=TimeUnit.MILLISECONDS

拒绝策略:打日志、dumpJstack、抛出异常

executes

见本文2.2.1.ExecuteLimitFilter

二、限流

1.配置及介绍

Dubbo中能够实现服务限流的方式较多,可以划分为两类:直接限流与间接限流

直接限流:通过对连接数量直接进行限制来达到限流的目的。

间接限流:通过一些非连接数量设置来达到限制流量的目的。

按照流量的方向又分为:入口限流与出口限流

入口限流:以下图为例,就是Provider限制每个Consumer进来的流量,以防有Consumer突然流量飙升把Provider调死了

出口限流:以下图为例,Consumer限制自己调用每个Provider的流量 

2.1.1.executes限制并发数– 仅提供者端

该属性仅能设置在提供者端。可以设置为接口级别,也可以设置为方法级别。限制的是服务(方法)并发执行数量。

类级别,服务器端并发执行(或占用线程池线程数)不能超过 10 个的例子:、

<dubbo:service interface="com.foo.BarService" executes="10" />

方法级别,服务器端并发执行(或占用线程池线程数)不能超过 10 个的例子:

<dubbo:service interface="com.foo.BarService">
    <dubbo:method name="sayHello" executes="10" />
</dubbo:service>

2.1.2.accepts限制连接数 – 仅提供者端

该属性仅可设置在提供者端的<dubbo:provider/>与<dubbo:protocol/>。用于对指定协议的连接数量进行限制

2.1.3.actives限制并发数 – 两端

该限流方式与前两种不同的是,其可以设置在提供者端,也可以设置在消费者端。

可以设置为接口级别,也可以设置为方法级别

A、提供者端限流

根据消费者与提供者间建立的连接类型的不同,其意义也不同

长连接:表示当前长连接最多可以处理的请求个数。与长连接的数量没有关系

短连接:表示当前服务可以同时处理的短连接数量

类级别的例子

<dubbo:service interface="com.foo.BarService" actives="10" />

方法级别的例子

<dubbo:reference interface="com.foo.BarService">
    <dubbo:method name="sayHello" actives="10" />
</dubbo:service>

B、消费者端限流

根据消费者与提供者间建立的连接类型的不同,其意义也不同:

长连接:表示当前消费者所发出的长连接中最多可以提交的请求个数。与长连接的数量没有关系。

短连接:表示当前消费者可以提交的短连接数量

类级别的例子

<dubbo:reference interface="com.foo.BarService" actives="10" />

方法级别的例子

<dubbo:reference interface="com.foo.BarService">
    <dubbo:method name="sayHello" actives="10" />
</dubbo:service>

2.1.4. connections限制连接数 – 两端

可以设置在提供者端,也可以设置在消费者端。限定连接的个数。

对于短连接,该属性效果与actives相同。但对于长连接,其限制的是长连接的个数。

一般情况下,会使connectons与actives联用,让connections限制长连接个数,让actives限制一个长连接中可以处理的请求个数。

联用前提:使用默认的Dubbo服务暴露协议

A、提供者端限流

类级别

方法级别

B、消费者端限流

类级别

方法级别

2.1.5.延迟连接 – 仅消费者端

仅可设置在消费者端,且不能设置为方法级别。仅作用于Dubbo服务暴露协议。

将长连接的建立推迟到消费者真正调用提供者时。

可以减少长连接的数量

 <!--设置当前消费者对接口中的每个方法发出链接采用延迟加载-->
   <dubbo:reference id="userService"  lazy="true"
    interface="com.dubbo.service.UserService"/>
<!--设置当前消费者对所有接口中的所有方法发出链接采用延迟加载-->
    <dubbo:consumer lazy="true"></dubbo:consumer>

2.1.6.粘滞连接 – 仅消费者

仅能设置在消费者端,其可以设置为接口级别,也可以设置为方法级别。仅作用于Dubbo服务暴露协议

其会使客户端尽量向同一个提供者发起调用,除非该提供者挂了,其会连接另一台。只要启用了粘连连接,其就会自动启用延迟连接

其限制的是流向,而非流量

类级别

方法级别

2.限流实现源码

2.2.1.ExecuteLimitFilter

ExecuteLimitFilter ,在服务提供者,通过 <dubbo:service /> 的 "executes" 统一配置项开启:

表示每服务的每方法最大可并行执行请求数。

ExecuteLimitFilter是通过信号量来实现的对服务端的并发数的控制。

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

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();
        Semaphore executesLimit = null;
        boolean acquireResult = false;
        int max = url.getMethodParameter(methodName, Constants.EXECUTES_KEY, 0);
        if (max > 0) {
            RpcStatus count = RpcStatus.getStatus(url, invocation.getMethodName());
//            if (count.getActive() >= max) {
            /**
             * http://manzhizhen.iteye.com/blog/2386408
             * use semaphore for concurrency control (to limit thread number)
             */
            executesLimit = count.getSemaphore(max);
            if(executesLimit != null && !(acquireResult = executesLimit.tryAcquire())) {
                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 isSuccess = true;
        RpcStatus.beginCount(url, methodName);
        try {
            Result result = invoker.invoke(invocation);
            return result;
        } catch (Throwable t) {
            isSuccess = false;
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new RpcException("unexpected exception when ExecuteLimitFilter", t);
            }
        } finally {
            RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, isSuccess);
            if(acquireResult) {
                executesLimit.release();
            }
        }
    }

}

ExecuteLimitFilter执行流程:

  1. 首先会去获得服务提供者每服务每方法最大可并行执行请求数
  2. 如果每服务每方法最大可并行执行请求数大于零,那么就基于基于服务 URL + 方法维度获取一个RpcStatus实例
  3. 通过RpcStatus实例获取一个信号量,若果获取的这个信号量调用tryAcquire返回false,则抛出异常
  4. 如果没有抛异常,那么久调用RpcStatus静态方法beginCount,给这个 URL + 方法维度开始计数
  5. 调用服务
  6. 调用结束后计数调用RpcStatus静态方法endCount,计数结束
  7. 释放信号量

 RpcStatus.getStatus

    public static RpcStatus getStatus(URL url, String methodName) {
        String uri = url.toIdentityString();
        ConcurrentMap<String, RpcStatus> map = METHOD_STATISTICS.get(uri);
        if (map == null) {
            METHOD_STATISTICS.putIfAbsent(uri, new ConcurrentHashMap<String, RpcStatus>());
            map = METHOD_STATISTICS.get(uri);
        }
        RpcStatus status = map.get(methodName);
        if (status == null) {
            map.putIfAbsent(methodName, new RpcStatus());
            status = map.get(methodName);
        }
        return status;
    }

给RpcStatus这个类里面的静态属性METHOD_STATISTICS里面设值。

外层的map是以url为key,里层的map是以方法名为key。

RpcStatus.getSemaphore()

    public Semaphore getSemaphore(int maxThreadNum) {
        if(maxThreadNum <= 0) {
            return null;
        }

        if (executesLimit == null || executesPermits != maxThreadNum) {
            synchronized (this) {
                if (executesLimit == null || executesPermits != maxThreadNum) {
                    executesLimit = new Semaphore(maxThreadNum);
                    executesPermits = maxThreadNum;
                }
            }
        }

        return executesLimit;
    }

这个方法是获取信号量,如果这个实例里面的信号量是空的,那么就添加一个,如果不是空的就返回。

2.2.2.DefaultTPSLimiter

TpsLimitFilter 过滤器,用于服务提供者中,提供限流的功能。

配置方式:

1.通过 <dubbo:parameter key="tps" value="" /> 配置项,添加到 <dubbo:service /> 或 <dubbo:provider /> 或 <dubbo:protocol /> 中开启,例如:

dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoServiceImpl" protocol="injvm" >
<dubbo:parameter key="tps" value="100" />
</dubbo:service>

2.通过 <dubbo:parameter key="tps.interval" value="" /> 配置项,设置 TPS 周期。

public class DefaultTPSLimiter implements TPSLimiter {

    private final ConcurrentMap<String, StatItem> stats
            = new ConcurrentHashMap<String, StatItem>();

    @Override
    public boolean isAllowable(URL url, Invocation invocation) {
        //获取tps这个参数设置的大小
        int rate = url.getParameter(Constants.TPS_LIMIT_RATE_KEY, -1);
       //获取tps.interval这个参数设置的大小,默认60秒
        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();
        } else {
            StatItem statItem = stats.get(serviceKey);
            if (statItem != null) {
                stats.remove(serviceKey);
            }
        }

        return true;
    }

}

三、负载均衡

1.配置方式 – 两端

可以设置在消费者端,亦可设置在提供者端;

可以设置在接口级别,亦可设置在方法级别。

其限制的是流向,而非流量

配置服务的客户端的 loadbalance 属性为 leastactive,此 Loadbalance 会调用并发数最小的 Provider(Consumer端并发数)。 

<dubbo:reference interface="com.foo.BarService" loadbalance="leastactive" />

<dubbo:service interface="com.foo.BarService" loadbalance="leastactive" />

可以在方法上配置

<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>

2.多种负载方式

Random LoadBalance

随机,按权重设置随机概率。

在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。

RoundRobin LoadBalance

轮循,按公约后的权重设置轮循比率。

存在慢的提供者累积请求问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

LeastActive LoadBalance

最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。

使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。

ConsistentHash LoadBalance

一致性Hash,相同参数的请求总是发到同一提供者。

当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

算法参见:http://en.wikipedia.org/wiki/Consistent_hashing。

缺省只对第一个参数Hash,如果要修改,请配置<dubbo:parameter key="hash.arguments" value="0,1" />

缺省用160份虚拟节点,如果要修改,请配置<dubbo:parameter key="hash.nodes" value="320" />

四、超时配置及介绍

dubbo中超时是争对消费端,当消费端发起一次请求后,如果在规定时间内未得到服务端的响应,则消费端直接返回超时异常,但服务端的代码依然在执行。

1.配置方式

消费端

  • 全局控制
<dubbo:consumer timeout="1000"></dubbo:consumer>
  • 接口控制
  • 方法控制

服务端

  • 全局控制
<dubbo:provider timeout="1000"></dubbo:provider>
  • 接口控制
  • 方法控制

服务端的超时配置是消费端的缺省配置,即如果服务端设置了超时,任务消费端可以不设置超时时间,简化了配置。

另外针对控制的粒度,dubbo支持了接口级别也支持方法级别,可以根据不同的实际情况精确控制每个方法的超时时间。

2.配置生效的优先顺序

客户端方法级>服务端方法级>客户端接口级>服务端接口级>客户端全局>服务端全局

3.实现原理

4.3.1第一种实现原理

dubbo默认采用了netty做为网络组件,它属于一种NIO的模式。

消费端发起远程请求后,线程不会阻塞等待服务端的返回,而是马上得到一个ResponseFuture,消费端通过不断的轮询机制判断结果是否有返回。

因为是通过轮询,轮询有个需要特别注要的就是避免死循环,所以为了解决这个问题就引入了超时机制,只在一定时间范围内做轮询,如果超时时间就返回超时异常。

源码

DefaultFuture类的get方法

    @Override
    public Object get(int timeout) throws RemotingException {
        if (timeout <= 0) {
            timeout = Constants.DEFAULT_TIMEOUT;
        }
        if (!isDone()) {
            long start = System.currentTimeMillis();
            lock.lock();
            try {
                while (!isDone()) {
                    done.await(timeout, TimeUnit.MILLISECONDS);
                    if (isDone() || System.currentTimeMillis() - start > timeout) {
                        break;
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
            if (!isDone()) {
                throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
            }
        }
        return returnFromResponse();
    }

4.3.2第二种实现原理(异步)

dubbo是可以配置异步,那么当时异步模式的时候,在发送完消息之后,不会立即通过ResponseFuture的get方法来阻塞的获取返回结果,而是将future放入RpcContext中之后就返回了,刚才所说的在get中实现的超时机制就不会起作用了,所以dubbo还有第二种机制。

DefaultFuture源码片段

    static {
        Thread th = new Thread(new RemotingInvocationTimeoutScan(), "DubboResponseTimeoutScanTimer");
        th.setDaemon(true);
        th.start();
    }
    private static class RemotingInvocationTimeoutScan implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    for (DefaultFuture future : FUTURES.values()) {
                        if (future == null || future.isDone()) {
                            continue;
                        }
                        if (System.currentTimeMillis() - future.getStartTimestamp() > future.getTimeout()) {
                            // create exception response.
                            Response timeoutResponse = new Response(future.getId());
                            // set timeout status.
                            timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);
                            timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));
                            // handle response.
                            DefaultFuture.received(future.getChannel(), timeoutResponse);
                        }
                    }
                    Thread.sleep(30);
                } catch (Throwable e) {
                    logger.error("Exception when scan the timeout invocation of remoting.", e);
                }
            }
        }
    }

在类加载的时候就会开启了一个子线程, 这个子线程就是不断的检测还没有接收到返回报文的future,验证每一个是否已经超时,如果超时则返回相应的超时报文,这种机制就保证了异步模式也具体超时这样的特性 

五、重试配置及其介绍

dubbo默认的集群方式是failover Cluster,该方式采用失败重试的集群容错机制。也就是可以配置的属性retries,该属性只有在failover Cluster集群方式下才会生效配置不包括第一次请求在内的调用失败后重试次数

假设服务端配置10s超时,消费端配置1s超时,程序执行5s。

也就是当时间超过1s的时候消费端就会出现异常,但是服务端并不会异常停止,这时候消费端重试又会发起请求,这在消费端看来就是第二次请求。

最后结果就是服务调用失败,但是消费端执行了retries + 1次逻辑。

dubbo在调用服务不成功时,默认会重试2次。

Dubbo的路由机制,会把超时的请求路由到其他机器上,而不是本机尝试,所以 dubbo的重试机制也能一定程度的保证服务的质量。

1.配置方式

在Failover Cluster模式下

失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。

重试次数配置如下:

服务提供端:

<dubbo:service retries="2" />

服务消费端:

<dubbo:reference retries="2" />

到方法粒度

<dubbo:reference>
    <dubbo:method name="findFoo" retries="2" />
</dubbo:reference>

以上配置为缺省配置

2.实现原理

FailoverClusterInvoker.doInvoke方法


public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {

    private static final Logger logger = LoggerFactory.getLogger(FailoverClusterInvoker.class);

    public FailoverClusterInvoker(Directory<T> directory) {
        super(directory);
    }

    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        List<Invoker<T>> copyinvokers = invokers;
        checkInvokers(copyinvokers, invocation);
        int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
        if (len <= 0) {
            len = 1;
        }
        // retry loop.
        RpcException le = null; // last exception.
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
        Set<String> providers = new HashSet<String>(len);

        //如果失败会重试调用所有服务
        for (int i = 0; i < len; i++) {
            //Reselect before retry to avoid a change of candidate `invokers`.
            //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
            if (i > 0) {
                checkWhetherDestroyed();
                copyinvokers = list(invocation);
                // check again
                checkInvokers(copyinvokers, invocation);
            }

            //负载均衡获取服务调用者
            Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
            invoked.add(invoker);
            RpcContext.getContext().setInvokers((List) invoked);
            try {

                //调用成功则直接返回
                Result result = invoker.invoke(invocation);
                if (le != null && logger.isWarnEnabled()) {
                    logger.warn("Although retry the method " + invocation.getMethodName()
                            + " in the service " + getInterface().getName()
                            + " was successful by the provider " + invoker.getUrl().getAddress()
                            + ", but there have been failed providers " + providers
                            + " (" + providers.size() + "/" + copyinvokers.size()
                            + ") from the registry " + directory.getUrl().getAddress()
                            + " on the consumer " + NetUtils.getLocalHost()
                            + " using the dubbo version " + Version.getVersion() + ". Last error is: "
                            + le.getMessage(), le);
                }
                return result;

            //失败的话再尝试重新调用
            } catch (RpcException e) {
                if (e.isBiz()) { // biz exception.
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
                le = new RpcException(e.getMessage(), e);
            } finally {
                providers.add(invoker.getUrl().getAddress());
            }
        }
        throw new RpcException(le != null ? le.getCode() : 0, "Failed to invoke the method "
                + invocation.getMethodName() + " in the service " + getInterface().getName()
                + ". Tried " + len + " times of the providers " + providers
                + " (" + providers.size() + "/" + copyinvokers.size()
                + ") from the registry " + directory.getUrl().getAddress()
                + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
                + Version.getVersion() + ". Last error is: "
                + (le != null ? le.getMessage() : ""), le != null && le.getCause() != null ? le.getCause() : le);
    }

}

通过for循环依次调用服务,如果成功则直接返回,如果失败则尝试重新调用其他服务。 

六、降级

2.2.0 以上版本支持

可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。

1.服务降级常见设置

在 Dubbo 中服务降级有两种选择 

屏蔽(force):消费方对该服务的方法调用都直接强制性执行屏蔽逻辑,不发起远程调用。

容错(fail):消费方对该服务的方法调用在失败后,再执行容错逻辑。

设置的KV内容 

mock="force:return null":消费方对该服务的方法调用都直接强制性返回 null 值,不发起远程调用。

mock="fail:return null":消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。

mock="true":消费方对该服务的方法调用在失败后,会调用消费方定义的服务降级Mock 类实例的相应方法。而该 Mock 类的类名为“业务接口名+Mock”,且放在与接口相同的包中。

mock=降级类的全限定性类名:与 mock="true"功能类似,不同的是,该方式中的降级类名可以是任意名称,在任何包中

2.如何设置

6.2.1向注册中心写入动态配置覆盖规则

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));

6.2.2在dubbo-admin中设置

在配置中心(好像2.7.0+ 要使用 nacos)的情况下,可在 dubbo-admin直接设置,实时生效。

设置完成后可以在配置中心(比如ZK)该服务对应的/configurators节点下看到相关信息,比如:

override://0.0.0.0/com.sid.service.UserService?category=configurators&dynamic=false&enabled=true&register.mock=force:return null

是以 override 开头的协议,我们需要关注的参数:register.mock=force:return nul,解释为: com.sid.service.UserService.register()方法的服务降级策略为屏蔽,直接返回 null。
关于配置覆盖:

override:// 表示数据采用覆盖方式,支持 override 和 absent,可扩展,必填。

0.0.0.0 表示对所有 IP 地址生效,如果只想覆盖某个 IP 的数据,请填入具体 IP,必填。

com.alibaba.dubbo.demo.DemoService表示只对指定服务生效,必填。

category=configurators 表示该数据为动态配置类型,必填。

dynamic=false 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。

enabled=true 覆盖规则是否生效,可不填,缺省生效。

application=demo-consumer 表示只对指定应用生效,可不填,表示对所有应用生效。

mock=force:return+null表示将满足以上条件的 mock 参数的值覆盖为 force:return+null。如果想覆盖其它参数,直接加在 override 的 URL 参数上。

 3.如何实现

首先回顾下一个请求调用链:

proxy0#sayHello(String)
  —> InvokerInvocationHandler#invoke(Object, Method, Object[])
    —> MockClusterInvoker#invoke(Invocation) //服务降级逻辑
      —> AbstractClusterInvoker#invoke(Invocation)//调用子类模板方法doInvoke
        —> FailoverClusterInvoker#doInvoke(Invocation, List<Invoker<T>>, LoadBalance)集群容错、路由。此处是重试。
          —> Filter#invoke(Invoker, Invocation)  // 包含多个 Filter 调用。比如ActiveLimitFilter消费端限流
            —> ListenerInvokerWrapper#invoke(Invocation) 
              —> AbstractInvoker#invoke(Invocation) //添加信息到 RpcInvocation#attachment 
                —> DubboInvoker#doInvoke(Invocation)
                  —> ReferenceCountExchangeClient#request(Object, int)//实现引用计数功能
                    —> HeaderExchangeClient#request(Object, int)
                      —> HeaderExchangeChannel#request(Object, int)
                        —> AbstractPeer#send(Object)
                          —> AbstractClient#send(Object, boolean)
                            —> NettyChannel#send(Object, boolean)
                              —> NioClientSocketChannel#write(Object)   

服务降级就发生在这一行MockClusterInvoker#invoke(Invocation)

 MockClusterInvoker.invoke(Invocation)

 源码

@Override
    public Result invoke(Invocation invocation) throws RpcException {
        Result result = null;

         // 判断方法名.mock 的值  即上面例子的:register.mock
         //需要注意directory.getUrl() : directory 对象为 RegistryDirectory, URL 会动态变化
        String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();

        // 未设置mock 走正常 invoke
        if (value.length() == 0 || value.equalsIgnoreCase("false")) {
            //no mock
            result = this.invoker.invoke(invocation);
        } else if (value.startsWith("force")) {
            if (logger.isWarnEnabled()) {
                logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());
            }

             //屏蔽
            //force:direct mock
            result = doMockInvoke(invocation, null);
        } else {

            //容错
            //fail-mock
            try {

                //首先正常 invoke
                result = this.invoker.invoke(invocation);
            } catch (RpcException e) {
                if (e.isBiz()) {
                    throw e;
                } else {
                    if (logger.isWarnEnabled()) {
                        logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e);
                    }

                    //异常后 走mock invoke
                    result = doMockInvoke(invocation, e);
                }
            }
        }
        return result;
    }

主要做了几件事

1.先获取url中mock参数的值,然后判断

需要注意directory.getUrl() : directory 对象为 RegistryDirectory, URL 会动态变化

2.如果没有mock为空或者为false,则跳过降级逻辑,直接执行后面的调用逻辑

3.如果为force开头,则调用doMockInvoke(invocation, null);方法,执行屏蔽逻辑,类似mock=force:return+null 

4.否则,即为fail mock,类似 mock=fail:return+null。一个try-catch包裹住执行后面的调用逻辑,catch到异常就调用doMockInvoke(invocation, e);执行容错的逻辑

MockClusterInvoker.doMockInvoke方法

    private Result doMockInvoke(Invocation invocation, RpcException e) {
        Result result = null;
        Invoker<T> minvoker;

        List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);
        if (mockInvokers == null || mockInvokers.isEmpty()) {
            minvoker = (Invoker<T>) new MockInvoker(directory.getUrl());
        } else {
            minvoker = mockInvokers.get(0);
        }
        try {
            result = minvoker.invoke(invocation);
        } catch (RpcException me) {
            if (me.isBiz()) {
                result = new RpcResult(me.getCause());
            } else {
                throw new RpcException(me.getCode(), getMockExceptionMessage(e, me), me.getCause());
            }
        } catch (Throwable me) {
            throw new RpcException(getMockExceptionMessage(e, me), me.getCause());
        }
        return result;
    }

主要做了几件事

1.调用selectMockInvoker(invocation)方法,得到一组mockInvokers

2.从这一组mockInvokers里面选一个invoker出来

3.调用这个选出的来的invoker的invoke方法

MockClusterInvoker.selectMockInvoker方法

    private List<Invoker<T>> selectMockInvoker(Invocation invocation) {
        List<Invoker<T>> invokers = null;
        //TODO generic invoker?
        if (invocation instanceof RpcInvocation) {
            //Note the implicit contract (although the description is added to the interface declaration, but extensibility is a problem. The practice placed in the attachement needs to be improved)
            //存在隐含契约(虽然在接口声明中增加描述,但扩展性会存在问题.同时放在attachement中的做法需要改进
            ((RpcInvocation) invocation).setAttachment(Constants.INVOCATION_NEED_MOCK, Boolean.TRUE.toString());
            //directory will return a list of normal invokers if Constants.INVOCATION_NEED_MOCK is present in invocation, otherwise, a list of mock invokers will return.
           //directory根据invocation中attachment是否有Constants.INVOCATION_NEED_MOCK,来判断获取的是normal invokers or mock invokers

            try {
                invokers = directory.list(invocation);
            } catch (RpcException e) {
                if (logger.isInfoEnabled()) {
                    logger.info("Exception when try to invoke mock. Get mock invokers error for service:"
                            + directory.getUrl().getServiceInterface() + ", method:" + invocation.getMethodName()
                            + ", will contruct a new mock with 'new MockInvoker()'.", e);
                }
            }
        }
        return invokers;
    }

主要做了几件事

1.在attachment中设置参数invocation.need.mock=true

2.调用directory.list方法获取invokers

AbstractDirectory.list(Invocation invocation)方法

    public List<Invoker<T>> list(Invocation invocation) throws RpcException {
        if (destroyed) {
            throw new RpcException("Directory already destroyed .url: " + getUrl());
        }
        //模版方法  实例为RegistryDirectory 获得所有Invoker
        List<Invoker<T>> invokers = doList(invocation);
        //路由策略 默认会在routers集合中添加MockInvokersSelector
        List<Router> localRouters = this.routers; // local reference
        if (localRouters != null && !localRouters.isEmpty()) {
            for (Router router : localRouters) {
                try {
                    if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) {
                        invokers = router.route(invokers, getConsumerUrl(), invocation);
                    }
                } catch (Throwable t) {
                    logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
                }
            }
        }
        return invokers;
    }

主要做了几件事

1.从服务字典中获得所有的服务,然后进行路由过滤把过滤后的服务列表返回。 

MockInvokersSelector.route

    public <T> List<Invoker<T>> route(final List<Invoker<T>> invokers,
                                      URL url, final Invocation invocation) throws RpcException {
        if (invocation.getAttachments() == null) {
            return getNormalInvokers(invokers);
        } else {
           // invocation.need.mock  就是在MockClusterInvoker.selectMockInvoker 中添加的
            String value = invocation.getAttachments().get(Constants.INVOCATION_NEED_MOCK);
            if (value == null)
                return getNormalInvokers(invokers);
            else if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
                return getMockedInvokers(invokers);
            }
        }
        return invokers;
    }

 主要做了几件事

1.主要就是判断 invocation.need.mock 的值是否为 true 。如果为 true 调用getMockedInvokers,否则调用getNormalInvokers

MockInvokersSelector.getNormalInvokers方法

    private <T> List<Invoker<T>> getNormalInvokers(final List<Invoker<T>> invokers) {
       //判断是否存在mock://协议的提供者  如果不存在 直接返回原提供者 
        if (!hasMockProviders(invokers)) {
            return invokers;
        } else {//存在mock://协议的提供者  去掉mock://的提供者过滤后返回
            List<Invoker<T>> sInvokers = new ArrayList<Invoker<T>>(invokers.size());
            for (Invoker<T> invoker : invokers) {
                if (!invoker.getUrl().getProtocol().equals(Constants.MOCK_PROTOCOL)) {
                    sInvokers.add(invoker);
                }
            }
            return sInvokers;
        }
    }

MockInvokersSelector.getMockedInvokers方法

    private <T> List<Invoker<T>> getMockedInvokers(final List<Invoker<T>> invokers) {
        //判断是否存在mock://协议的提供者  如果不存在 直接返回null
        if (!hasMockProviders(invokers)) {
            return null;
        }

        List<Invoker<T>> sInvokers = new ArrayList<Invoker<T>>(1);
        for (Invoker<T> invoker : invokers) {

            // 过滤mock://的提供者 
            if (invoker.getUrl().getProtocol().equals(Constants.MOCK_PROTOCOL)) {
                sInvokers.add(invoker);
            }
        }
        return sInvokers;
    }

过滤完之后就又回到了 MockClusterInvoker.doMockInvoke 方法,然后进入 MockInvoker.invoke 方法了

MockInvoker.invoke

 public Result invoke(Invocation invocation) throws RpcException {
        //该值即我们在dubbo-admin设置的  可能是 force:return null
        String mock = getUrl().getParameter(invocation.getMethodName() + "." + Constants.MOCK_KEY);
        if (invocation instanceof RpcInvocation) {
            //设置invoke 为当前实例
            ((RpcInvocation) invocation).setInvoker(this);
        }
        if (StringUtils.isBlank(mock)) {
            mock = getUrl().getParameter(Constants.MOCK_KEY);
        }

        if (StringUtils.isBlank(mock)) {
            throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url));
        }

       //首先进行URL解码获得mock的具体要执行的结果 比如force:return null 执行normallizeMock后结果为return null
        mock = normallizeMock(URL.decode(mock));

        //配置 “return” 执行该语句
        if (Constants.RETURN_PREFIX.trim().equalsIgnoreCase(mock.trim())) {
            RpcResult result = new RpcResult();
            result.setValue(null);
            return result;

         //配置 “return *” *表示任务内容执行该语句
        } else if (mock.startsWith(Constants.RETURN_PREFIX)) { 
            mock = mock.substring(Constants.RETURN_PREFIX.length()).trim();
            mock = mock.replace('`', '"');
            try {
                Type[] returnTypes = RpcUtils.getReturnTypes(invocation);
                Object value = parseMockValue(mock, returnTypes);
                return new RpcResult(value);
            } catch (Exception ew) {
                throw new RpcException("mock return invoke error. method :" + invocation.getMethodName() + ", mock:" + mock + ", url: " + url, ew);
            }

        //配置 “throw *” 执行该语句
        } else if (mock.startsWith(Constants.THROW_PREFIX)) { 
            mock = mock.substring(Constants.THROW_PREFIX.length()).trim();
            mock = mock.replace('`', '"');
            if (StringUtils.isBlank(mock)) {
                throw new RpcException(" mocked exception for Service degradation. ");

            //用户自定义类
            } else { // user customized class
                Throwable t = getThrowable(mock);
                throw new RpcException(RpcException.BIZ_EXCEPTION, t);
            }

        //自定以mock实现执行该语句
        } else { //impl mock
            try {
                Invoker<T> invoker = getInvoker(mock);
                return invoker.invoke(invocation);
            } catch (Throwable t) {
                throw new RpcException("Failed to create mock implemention class " + mock, t);
            }
        }
    }

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值