Dubbo学习笔记11:使用Dubbo中需要注意的一些事情

指定方法异步调用

前面我们讲解了通过设置ReferenceConfig的setAsync()方法来让整个接口里的所有方法变为异步调用,那么如何指定某些方法为异步调用呢?下面讲解下如何正确地设置默写方法为异步调用。

假如你只需要设置接口里的方法sayHello为异步调用,那么可以使用下面方式:

final List<MethodConfig> asyncMethodList = new ArrayList<MethodConfig>();
MethodConfig methodConfig = new MethodConfig();
methodConfig.setAsync(true);
methodConfig.setName("sayHello");
asyncMethodList.add(methodConfig);

然后调用ReferenceConfig的setMethods(asyncMethodList)即可。另外如果异步调用的方法没有返回值,则可以再调用methodConfig.setReturn(false); ,以便减少Dubbo内部Future对象的创建和管理成本。

关闭启动检查

正常情况下,在服务消费端启动时会检查依赖的服务是否存在,如果不存在则会抛出 throw new IllegalStateException("Failed to check the status of the service" + interfaceName + ".No provider available for the service ")异常阻止Spring初始化完成,以便上线前能及早发现问题。

可以通过调用ReferenceConfig.setCheck(false)关闭检查,设置check为true有助于及时发现问题,那么什么时候需要设置false呢?

比如测试时,有些无关的服务启动不了,导致整个应用都启动不了,这时候你可以把那些无关服务的check设置为false。再比如出现了循环依赖,必须有一方先启动,比如你给服务使用方提供了一个SDK,SDK里面使用Dubbo API远程消费服务器提供方的服务,如果你在服务提供方的服务器里面也引入这个SDK,在服务提供方启动时候就会抛出 No Provider available for the service异常,具体原因是服务提供方启动时候会初始化SDK,而SDK里面初始化时候需要检查服务提供方是否存在,而服务提供方的服务还没提供出来。

另外需要注意的是check设置为false,总是会返回调用,当服务提供方恢复服务时,能自动连上。

如何设置均衡策略

由于Dubbo提供的一致性Hash负载均衡策略,可以允许你自定义虚拟节点个数和指定某些方法需要使用一致性Hash策略,下面具体讲下如何设置:

// 虚拟节点设置为512
Map<String,String> parameters = new HashMap<String,String>();
parameters.put("hash.nodes","512");
ReferenceConfig<T> reference = new ReferenceConfig<T>();
// 设置负载均衡为一致性Hash
reference.setLoadbalance(consistenthash);
// 设置参数
reference.setParameters(parameters);

如下代码设置接口的sayHello方法为一致性Hash负载均衡策略,设置saySomething方法为随机负载均衡策略:

ReferenceConfig reference = new ReferenceConfig();
final List<MethodConfig> methodList = new ArrayList<MethodConfig>();
// 设置sayHello方法为一致性Hash负载均衡策略
MethodConfig methodConfig = new MethodConfig();
methodConfig.setName("sayHello");
methodConfig.setLoadbalance("consistenthash");
// 虚拟节点设置为512
Map<String,String> parameters = new HashMap<String,String>();
parameters.put("hash.nodes","512");
methodConfig.setParameters(parameters);
methodList.add(methodConfig);
// 设置saySomething方法为随机负载均衡策略
methodConfig = new MethodConfig();
methodConfig.setName("saySomething");
methodConfig.setLoadbalance("random");
methodList.add(methodConfig);
reference.setMethods(methodList);

另外,默认情况下一致性hash使用第一个参数值计算hash值,如果你需要自定义可以通过以下设置:

Map<String,String> parameters = new HashMap<String,String>();
parameters.put("hash.nodes","512");
parameters.put("hash.arguments","0,1");    // 使用第一个和第二个参数值计算hash值
methodConfig.setParameters(parameters);
    

注意"0,1"是一个字符串,里面使用英文","分隔。

服务消费端ReferenceConfig需要自行缓存

ReferenceConfig实例是个很重的实例,每个ReferenceConfig实例里都维护了与服务注册中心的一个长链,并且维护了与所有服务提供者的长链。假设有一个服务注册中心和N个服务提供者,那么每个ReferenceConfig实例里面维护了N+1个长链,如果频繁地生成ReferenceConfig实例,可能会造成性能问题,甚至产生内存或者连接泄露的风险。特别是使用Dubbo API编程时候容易忽略这个问题。

为了解决这个问题,之前都是自行缓存,但自从发布Dubbo 2.4.0版本后,Dubbo提供了简单的工具类ReferenceConfigCache用于缓存ReferenceConfig实例。使用如下:

// 创建服务消费实例
ReferenceConfig<XxxService> reference = new ReferenceConfig<XxxService>();
reference.setInterface(XxxService.class);
reference.setVersion("1.0.0");
......
// 获取Dubbo提供的缓存
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
// cache.get方法中会缓存reference对象,并且调用reference.get方法启动ReferenceConfig,并返回经过代理后的服务接口的对象
XxxService xxxService = cache.get(reference);
// 使用xxxService对象
xxxService.sayHello();     

需要注意的是Cache内持有ReferenceConfig对象的引用,不要在外部再调用ReferenceConfig的destroy方法了,这会导致Cache内的ReferenceConfig失效!

如果要销毁Cache中的ReferenceConfig,将销毁ReferenceConfig并释放对应的资源,具体使用下面方法来销毁:

ReferenceConfigCache cache = ReferenceConfigCache.getCache();
cache.destroy(reference);

另外在Dubbo中唯一确定一个服务是通过 接口+分组+版本,所以默认情况下cache内是通过服务Group/接口/版本三个属性来标示一个ReferenceConfig实例的。即以服务Group/接口/版本为缓存的key,ReferenceConfig实例为对应的value。如果你需要使用自定义的key,可以在创建cache时候调用 ReferenceConfigCache cache = ReferenceConfigCache.getCache(keyGenerator); 方法传递自定义的keyGenerator。

并发控制

服务消费方并发控制

在服务消费方进行并发控制,需要设置actives参数,如下:

<dubbo:reference id="userService" interface="com.test.UserServiceBo" group="dubbo" version="1.0.0" timeout="3000" actives="10" />

设置com.test.UserServiceBo接口中的所有方法,每个方法最多同时并发请求10个请求。

也可以使用下面方法设置接口中单个方法的并发请求个数,如下:

<dubbo:reference id="userService" interface="com.test.UserServiceBo" group="dubbo" version="1.0.0" timeout="3000">
    <dubbo:method name="sayHello" actives="10" />
</dubbo:reference>

如上设置sayHello方法的并发请求数量最大为10,如果客户端请求该方法并发超过了10则客户端会被阻塞,等客户端并发请求数量少于10的时候,该请求才会被发送到服务提供方服务器。在Dubbo中客户端并发控制使用ActiveLimitFilter过滤器来控制,代码如下:

public class ActiveLimitFilter implements Filter{
    public Result invoke(Invoker<?> invoker , Invocation invocation) throws RpcException{
        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();
        // 获取设置的active值,默认为0
        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){
                sychronized(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参数,如下:

<dubbo:service interface="com.test.UserServiceBo" ref="userService" group="dubbo" version="1.0.0" timeout="3000" executes="10" />

设置com.test.UserServiceBo 接口中所有方法,每个方法最多同时并发处理10个请求,这里并发是指同时在处理10个请求。

也可以使用下面方法设置接口中单个方法的并发处理个数,如下:

<dubbo:service interface="com.test.UserServiceBo" ref="userService" group="dubbo" version="1.0.0" timeout="3000">
    <dubbo:method name="sayHello" executes="10" />
</dubbo:service>

如上设置sayHello方法的并发处理数量为10 。

需要注意的是,服务提供方设置并发数量后,如果同时请求数量大于了设置的executes的值,则会抛出异常,而不是像服务端设置actives时候,会等待。服务提供方并发控制是使用ExecuteLimitFilter过滤器实现的,ExecuteLimitFilter代码如下:

public class ExecuteLimitFilter implements Filter{
    public Result invoke(Invoker<?> invoker , Invocation invocation) throws RpcException{
        URL url = invoker.getUrl();
        // 默认不设置executes时候,其值为0
        int max = url.getMethodParameter(methodName , Constants.EXECUTES_KEY , 0);
        if(max > 0){    // max>0说明设置了executes值
            RpcStatus count = RpcStatus.getStatus(url , invocation.getMethodName());
            // 可知如果并发处理数量大于设置的值,会抛出异常
            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 execute=\"" + max + "\"> limited.");
            }
        }
        ...
        try{    // 没有限流的时候,激活filter链
            Result result = invoker.invoke(invocation);
            return result;
        }catch(){
            ...
        }finally{
            ...
        }

    }
}

所以当使用executes参数时候要注意,当并发量过大时侯,多余的请求会失败。

改进的广播策略

前面我们讲解集群容错时谈到广播策略,该策略主要用于对所有服务提供者广播消息,那么有个问题需要思考,广播是说你在客户端调用接口一次,内部就是轮询调用所有服务提供者的机器的服务,那么你调用一次该接口,返回值是什么呢?比如内部轮询了10台机器,每个机器应该都有一个返回值,那么你调用的这一次返回值是10个返回值的组成?其实不是,返回的轮询调用的最后一个机器结果,我们可以看下BroadcastClusterInvoker的主干代码:

public class BroadcastClusterInvoker<T> extends AbstractClusterInvoker<T>{
    private static final Logger logger = LoggerFactory.getLogger(BroadcastClusterInvoker.class);

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

    @SuppressWarnings({"unchecked","rawtypes"})
    public Result doInvoke(final Invocation invocation , List<Invoker<T>> invokers , LoadBalance loadbalance){
        ...
        // 使用循环,轮询每个机器进行调用,其中result为最后一个机器的结果
        for(Invoker<T> invoker : invokers){
            try{
                result = invoker.invoke(invocation);
            }catch(RpcException e){
                exception = e;
                logger.warn(e.getMessage() , e);
            }catch(Throwable e){
                exception = new RpcException(e.getMessage(),e);
                logger.warn(e.getMessage() , e);
            }
        }
        if(exception != null){
            throw exception;
        }
        return result;
    }
}        

如上代码,可知使用循环轮询调用每个机器,其中result为调用最后一个机器的结果。

如果我想获取所有服务提供者的结果,该怎么办呢?其实我们可以自定义一个SPI扩展,并且规定我们的服务接口的返回结果为一个map,代码如下:

public Result doInvokePara(final Invocation invocation , List<Invoker<T>> invokers , LoadBalance loadbalance) throws RpcException{
    // 用来保存所有服务提供者返回的结果
    Map allResult = new ConcurrentHashMap<String , Result>();
    // 保存异步调用返回的Future对象
    List<Future<Result>> futureList = new ArrayList<Future<Result>>();
    // 所有服务提供者的机器个数
    int machineNum = invokers.size();
    for(Invoker<T> invoker : invokers){
        try{
            // 异步调用服务提供者
            Future<Result> future = paramCallPool.submit(new Callable<Result>(){
                @Override
                public Result call() throws Exception{
                    try{
                        // 具体调用服务提供者
                        Result result = invoker.invoke(invocation);
                        // 服务提供者 ip:port
                        String url = invoker.getUrl().getAddress();
                        // 保存结果到map,key为服务提供者的地址,value为返回结果
                        allResult.put(url , result.getResult());
                        return result;
                    }catch(RpcException e){
                        logger.warn(e.getMessage(),e);
                    }catch(Throwable e){
                        logger.warn(e.getMessage(),e);
                    }
                    return null;
                }
            });
            futureList.add(future);
        }catch(Exception e){
            logger.warn(e.getMessage() , e);
        }
    }
    // 等所有调用完成
    for(Future<Result> future : futureList){
        try{
            future.get();
        }catch(InterruptedException | ExecutionException e){
            e. printStackTrace();   
        }
    }

    // 假设服务接口返回中类型为这个
    ACCSResult<Map> resultTemp = new ActionResult<Map>(true,null,null,null);
    // 自定义返回结果
    Map finalResult = new HashMap<String,Result>();
    finalResult.put("machineNum",machineNum);
    finalResult.put("result",result);
    resultTemp.setData(finalResult);
    // 重新设置返回值
    Result result = new RpcResult(resultTemp);
    return result;    
}

 

  

posted @ 2018-06-02 17:38  徐浩进  阅读(3216)  评论(0编辑  收藏  举报