白话Dubbo(三):走向集群

2020-02-21  本文已影响0人  空挡

服务集群

一个系统从本地调用转成远程调用,原因有很多种。最主要的两个,一个是功能解耦,另外一个就是高可用。为了解决高可用问题,通常再服务部署的时候,都会部署多套,以防止单个服务压力过大或者网络分区等单点故障导致服务不可用,也就是我们常说的集群。

路由

当服务提供方部署成集群的时候,问题就来了,原来只有一个机器提供服务的时候,调用方只需要知道ip+port都可以把请求发过去了。现在,提供方变成了一个ip+port的列表,每次调用前都需要选择其中一个,显然这不是用户想要关心的。这时候一个新的路由模块会被加到中间,调用方把请求发给路由模块,路由模块决定这个请求应该发给后端的那个提供方。像下图这样,调用方不需要知道具体请求发给谁了。


服务路由

当然,这个路由模块可以是一个单独的Proxy,也可以是一个jar包,集成到Consumer的程序里面。

服务发现

对于路由模块来说,后端的集群配置是不断变化的,比如节点的上下线,ip和端口的变化等。因为服务节点不会知道路由模块的存在,所以双方要有个公共的地方,服务节点变化时更新数据,路由模块通过通知或者轮询拉取变化。这个模块就是注册中心了,常见的作为注册中心的有Eureka、zookeeper等。有了注册中心,路由模块就有了服务发现的能力,上面的图就变成这样:


注册中心

Dubbo集群解决方案

Dubbo集群的特点

跟现在主流的微服务框架的集群概念稍有不同。Dubbo的集群就是指接口提供者的集合,而不是节点或者机器的集合。当然这不妨碍对它的理解:


Dubbo集群

先来回顾下Dubbo Consumer一侧的模块调用链:


Dubbo Consumer

上图中,Proxy通过url获取到一个远程调用的Invoker,Dubbo要做的,就是Invoker提供路由功能,通过注册中心来获取真实的url列表。而做为一个开放的rpc框架,显然要能够很容易的集成现有的注册中心。
这里Dubbo的逻辑是,如果url是具体服务节点的url,比如dubbo://127.0.0.1:12345,那就正常走建立连接,发起调用。如果url是注册中心url,Dubbo通过替换成集群的Invoker来发起调用,下面来看下集群部分的接口抽象。

Cluster

上面提到需要将Invoker替换成一个支持集群调用的Invoker,这个Invoker就是从Cluster中获取的。

@SPI(FailoverCluster.NAME)
public interface Cluster {
    /**
     * Merge 从directory获取的invoker列表
     */
    @Adaptive
    <T> Invoker<T> join(Directory<T> directory) throws RpcException;
}

Cluster接口通过将从directory获取的Invoker列表合并成一个集群ClusterInvoker。Directory是Dubbo对服务列表提供者的抽象,显然注册中心就是一种Directory的实现,更准确得说是一种Directory的数据来源,注册中心的部分后面讲。
Cluster实现
Dubbo提供了很多Cluster实现,默认的是FailoverCluster:

public class FailoverCluster extends AbstractCluster {
    public final static String NAME = "failover";
    @Override
    public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
        return new FailoverClusterInvoker<>(directory);
    }
}

可以看到这里的实现就是创建一个具体的ClusterInvoker,将directory传给它。所有的ClusterInvoker实现都继承了AbstractClusterInvoker
ClusterInvoker实现
AbstractClusterInvoker实现了Invoker接口,所以对Consumer端的Proxy的是透明的。这个类代码比较多,最主要的就是实现接口的invoke() 方法。

public abstract class AbstractClusterInvoker<T> implements Invoker<T> {
        ...
       //从directory获取invoker列表
        List<Invoker<T>> invokers = list(invocation);
       //初始化负载均衡
        LoadBalance loadbalance = initLoadBalance(invokers, invocation);
        //子类实现调用
        return doInvoke(invocation, invokers, loadbalance);
    }
}

这里面除了从directory获取invoker列表外,最主要的就是初始化LoadBalance,然后发起调用。Dubbo中默认的ClusterInvoker实现就是前面FailoverCluster返回的FailoverClusterInvoker,这个实现包含重试逻辑,即调用一个节点失败的情况下会重试其它的。加了ClusterInvoker之后,调用如下:


Consumer Cluster

负载均衡

当ClusterInvoker从Directory处获取到多个后端服务的Invoker后,选择调用哪个Invoker是有负载均衡策略决定的。就是上面提到的LoadBalance接口。
Dubbo实现了4种策略,分别是加权随机、加权轮询、最小活跃调用、一致性Hash,默认采用加权随机。

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

    /**
     * select one invoker in list.
     */
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}

接口比较简单,根据要调用参数信息,从invokers中选择一个。这个方法由ClusterInvoker负责调用。

注册中心

Dubbo为了对接不同的注册中心,抽象出了Registry接口,定义如下:

public interface Registry extends Node, RegistryService {
}

这个接口只是简单组合了Node和RegistryService,组合Node的原因是Dubbo配置中将注册中心也是用url的形式配置的,比如zookeeper://127.0.0.1:2081。主要的接口定义都在RegistryService中:

public interface RegistryService {
    /**
     *  注册
     */
    void register(URL url);
    /**
     * 注销
     */
    void unregister(URL url);
    /**
     * 订阅
     */
    void subscribe(URL url, NotifyListener listener);
    /**
     * 取消订阅
     */
    void unsubscribe(URL url, NotifyListener listener);
    /**
     * 主动查询
     */
    List<URL> lookup(URL url);
}

注册中心包含两类接口,注册和注销主要是给服务提供方使用,订阅和取消订阅主要是给服务消费方使用。Dubbo已经默认实现了主流注册中心的对接,比如zookeeper、eureka等。
RegistryDirectory
上面讲到的Cluster接口,当需要一个ClusterInvoker的时候,需要提供一个Directory参数。RegistryDirectory相当于Directory的注册中心实现,它包含了一个注册中心,当注册中心的数据发生变化后,刷新自身的Invoker缓存。

public class RegistryDirectory<T> extends AbstractDirectory<T> implements NotifyListener {
    ...
    // 初始化时注入
    private Registry registry;
    ...
}

路由策略

服务路由功能中,除了可以通过负载均衡策略来干涉具体调用的服务之外,通常需要一些更加个性化的设置。比如,部分新上线的功能只想让符合一定条件的用户使用,就可以设置根据用户id或者标签来决定请求发送到后端那个提供方。
Dubbo中针对路由策略的接口是Router:

public interface Router extends Comparable<Router> {
    int DEFAULT_PRIORITY = Integer.MAX_VALUE;
    /**
     * Get the router url.
     */
    URL getUrl();
    /**
     * 对于传入的invokers做Rute规则匹配,返回匹配上的invoker列表
     */
    <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
    /**
     * 接收invoker list变化通知
     */
    default <T> void notify(List<Invoker<T>> invokers) {

    }
    /**
     * Router 的优先级
     */
    int getPriority();
    ... 
}

接口中最主要的就是route() 方法,在Cluster从Directory获取到Invoker列表后,首先查询可用的Route规则,并逐个匹配,只有符合条件的invoker才会交给LoadBalance再选择。
Dubbo默认提供两种实现,一种是基于条件表达式的路由规则设置,一种式基于脚本的路由规则设置。

总结

上面分别从集群支持、负载均衡、注册中心、路由策略等方面分解了Dubbo对集群调用的支持,下面还是通过一张图来看下各个模块的关系。


Consumer集群调用

当服务变成一个集群之后,情况复杂了很多,要让用户无感知的调用集群,需要将集群调用做抽象,并对接注册中心。这也是Dubbo在Proxy之后又抽象出Invoker的原因。针对集群调用,内部实现了不同的容错策略,同时围绕Invoker,Dubbo还扩容了其它功能,这个以后再讲。

上一篇下一篇

猜你喜欢

热点阅读