Dubbo框架原理剖析

Dubbo框架原理剖析






背景

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。

image

单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。

分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。

流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。




简单介绍

Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,SOA服务治理方案。

简单的说,dubbo就是个服务框架,如果没有分布式的需求,其实是不需要用的,只有在分布式的时候,才有dubbo这样的分布式服务框架的需求,并且本质上还是服务调用,远程服务调用的分布式框架。

核心功能

  • 远程通讯: 提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
    RPC的亮点在于将远程调用的细节隐藏起来,使得调用远程服务就像调用本地服务一样简单,而实现的方式就是代理

  • 集群容错:提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。

  • 自动发现:基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。







架构原理

dubbo-architucture

节点角色说明

节点角色说明
Provider暴露服务的服务提供方
Consumer调用远程服务的服务消费方
Registry服务注册与发现的注册中心
Monitor统计服务的调用次数和调用时间的监控中心
Container服务运行容器

调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。




服务发布与引用

spring配置声明服务

dubbo-provider.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
	http://code.alibabatech.com/schema/dubbo
	http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

	<!-- 提供方应用信息,用于计算依赖关系-->
	<dubbo:application name="demo-provider"  />
	<!-- 注册中心,register是否向此注册中心注册服务 -->
	<dubbo:registry address="zookeeper://127.0.0.1:2181" register="false"/>
	<!-- 用dubbo协议在20880端口暴露服务,用于通讯-->
	<dubbo:protocol name="dubbo" port="20880" />
	<!-- 申明本地服务-->
	<bean id="demoService" class="cn.demo.dubbo.demo.provider.DemoServiceImpl" />
	<!-- 申明需要暴露服务的接口-->
	<dubbo:service interface="cn.demo.dubbo.demo.api.DemoService" ref="demoService" />
	
</beans>

dubbo-consumer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
	http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

	<dubbo:application name="demo-consumer"  />
	<dubbo:registry address="zookeeper://127.0.0.1:2181" />
	<dubbo:protocol name="dubbo" port="20880" />
	<dubbo:reference id="demoService" interface="cn.demo.dubbo.demo.api.DemoService" check="false"/>
</beans>

spring解析配置

spring.handlers文件中

http://code.alibabatech.com/schema/dubbo=
com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler

DefaultBeanDefinitionDocumentReader

	protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
		if (delegate.isDefaultNamespace(root)) {
			NodeList nl = root.getChildNodes();
			for (int i = 0; i < nl.getLength(); i++) {
				Node node = nl.item(i);
				if (node instanceof Element) {
					Element ele = (Element) node;
					if (delegate.isDefaultNamespace(ele)) {
						parseDefaultElement(ele, delegate);
					}else {
						delegate.parseCustomElement(ele);
					}
				}
			}
		}else {
			delegate.parseCustomElement(root);
		}
	}

DubboNamespaceHandler

public class DubboNamespaceHandler extends NamespaceHandlerSupport {
    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }
    public void init() {
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
        registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
        registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        registerBeanDefinitionParser("annotation", new AnnotationBeanDefinitionParser());
    }
}

如何暴露服务

在这里插入图片描述

ServiceBean继承了ServiceConfig,在ServiceConfig方法里有段代码, 找到所有的注册中心,再遍历所有的协议注册

private void doExportUrls() {
        List<URL> registryURLs = loadRegistries(true);
        for (ProtocolConfig protocolConfig : protocols) {
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&check=false&dubbo=2.0.1&pid=10772&registry=zookeeper&timestamp=1546226393659

registry://127.0.0.1:6379/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&check=false&dubbo=2.0.1&pid=10772&registry=redis&timestamp=1546226408125

Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = protocol.export(wrapperInvoker);

1、所有的服务都封装成Invoker模型,在服务提供端,以及服务引用端均会出现 Invoker。Dubbo 官方文档中对 Invoker 进行了说明,这里引用一下。

Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。

public interface Node {
    URL getUrl(); 
    boolean isAvailable();
    void destroy();
}

public interface Invoker<T> extends Node {
    Class<T> getInterface();
    /**
     * invoke.
     * @param invocation
     * @return result
     * @throws RpcException
     */
    Result invoke(Invocation invocation) throws RpcException;
}

2、RegistryProtocol暴露服务,开启Netty链接,保持对20881端口的通信,
同时向zk上注册临时节点,所谓的服务注册,本质上是将服务配置数据写入到 Zookeeper 的某个路径的节点下。
节点名:
/dubbo/cn.demo.dubbo.demo.api.DemoService/providers/dubbo://192.168.2.150:20881/cn.demo.dubbo.demo.api.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.1&generic=false&interface=cn.demo.dubbo.demo.api.DemoService&methods=sayHello,login&pid=10128&revision=0.0.1-SNAPSHOT&side=provider&timestamp=1546236231866

在这里插入图片描述

服务引用

服务引用是服务的消费方向注册中心订阅服务提供方提供的服务地址后向服务提供方引用服务的过程。

<dubbo:reference id="demoService" interface="cn.demo.dubbo.demo.api.DemoService" check="false"/>

spring在容器启动的时候会解析自定义的schema元素<dubbo: reference/>转换成dubbo内部数据结构ReferenceBean
在这里插入图片描述
竟然实现了FactoryBean接口,
在这里插入图片描述
为什么要实现FactoryBean,因为我们是要引用远程的服务,而并不是要ReferenceBean本身这个对象,get()方法里面肯定封装了返回接口的实际代理
ReferenceConfig.java

 private T createProxy(Map<String, String> map) {
 	....
 	// create service proxy
    return (T) proxyFactory.getProxy(invoker);
 }

JavassistProxyFactory.java javassist生成代理的工厂类

public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        // 生成 Proxy 子类(Proxy 是抽象类)。并调用 Proxy 子类的 newInstance 方法创建 Proxy 实例
        return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }

InvokerInvocationHandler.java 拦截方法

public class InvokerInvocationHandler implements InvocationHandler {
    private final Invoker<?> invoker;
    public InvokerInvocationHandler(Invoker<?> handler) {
        this.invoker = handler;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(invoker, args);
        }
        .....
        return invoker.invoke(new RpcInvocation(method, args)).recreate();
    }
}

最终调用DubboInvoker.java中doInvoke方法与提供方通信并等待返回执行结果

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





注册中心

注册中心承担着服务的注册与发现,服务的提供者将服务发布到注册中心,服务的使用者到注册中引用服务。
配置样例如下

<!--dubbo推荐使用zk,register是否向此注册中心注册服务,如果设为false,将只订阅,不注册-->
<dubbo:registry address="zookeeper://10.20.153.10:2181" register="true" />
<!--Multicast 注册中心不需要启动任何中心节点,只要广播地址一样,就可以互相发现。-->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!--使用 Redis 的 Key/Map 结构存储数据结构,使用 Redis 的 Publish/Subscribe 事件通知数据变更-->
<dubbo:registry address="redis://10.20.153.10:6379" />

简单介绍一下zk注册中心

Zookeeper 是 Apacahe Hadoop 的子项目,是一个树型的目录服务,支持变更推送,适合作为 Dubbo 服务的注册中心,工业强度较高,可用于生产环境,并推荐使用 。

/user-guide/images/zookeeper.jpg

流程说明:

  • 服务提供者启动时: 向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址
  • 服务消费者启动时: 订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向 /dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址
  • 监控中心启动时: 订阅 /dubbo/com.foo.BarService 目录下的所有提供者和消费者 URL 地址。

支持以下功能:

  • 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息
  • 当注册中心重启时,能自动恢复注册数据,以及订阅请求
  • 当会话过期时,能自动恢复注册数据,以及订阅请求
  • 当设置 <dubbo:registry check="false" /> 时,记录失败注册和订阅请求,后台定时重试
  • 可通过 <dubbo:registry username="admin" password="1234" /> 设置 zookeeper 登录信息
  • 可通过 <dubbo:registry group="dubbo" /> 设置 zookeeper 的根节点,不设置将使用无根树
  • 支持 * 号通配符 <dubbo:reference group="*" version="*" />,可订阅服务的所有分组和所有版本的提供者





集群容错

作为一个分布式的框架,同一个服务肯定存在于多个提供方,当一个服务调用失败之后,就涉及到容错
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。

cluster

将Directory中的多个Invoker伪装成一个Invoker, 对上层透明,包含集群的容错机制
Cluster 接口定义

public interface Cluster {
    <T> Invoker<T> join(Directory<T> directory) throws RpcException;
}

下图只列出了部分容错实现
在这里插入图片描述

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

重试次数配置如下:

<dubbo:reference retries="2" />

AvailableCluster
获取可用的调用。遍历所有Invokers判断Invoker.isAvalible,只要一个有为true直接调用返回,不管成不成功

Failfast Cluster

快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

Failsafe Cluster

失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

Failback Cluster

失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

Forking Cluster

并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。

Broadcast Cluster

广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。





负载均衡策略

负载均衡负责从多个 Invokers中选出具体的一个Invoker用于本次调用,调用过程中包含了负载均衡的算法,调用失败后需要重新选择

LoadBalance接口定义

public interface LoadBalance {
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}

在这里插入图片描述

在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。

Random LoadBalance

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

RoundRobin LoadBalance

  • 轮询,按公约后的权重设置轮询比率。
  • 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

LeastActive LoadBalance

  • 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
  • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。

ConsistentHash LoadBalance

  • 一致性 Hash,相同参数的请求总是发到同一提供者。
  • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
  • 缺省只对第一个参数 Hash,如果要修改,请配置
    <dubbo:parameter key="hash.arguments" value="0,1" />
  • 缺省用 160 份虚拟节点,如果要修改,请配置
    <dubbo:parameter key="hash.nodes" value="320" />
    在这里插入图片描述



管理控制台

dubbo自带了一个简单的监控管理后台, dubbo-admin web 应用配置 应用配置 属性文件路径在dubbo-admin\WEB-INF\dubbo.properties中

dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.admin.root.password=123456
dubbo.admin.guest.password=123456

这里需要指定注册中心地址, 因为对于服务的治理从获取相关信息这里需要指定注册中心地址, 因为对于服务的治理从获取相关信息如提供者、消费路由信息权重等

管理控制台演示地址 http://192.168.2.150:8090/dubbo-admin/



引入dubbo带来的一些问题

1、提供方和调用方接口依赖严重,大型工程很难协同
2、需要解决分布式事务

服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持。

3、支持的功能多,但也有很多鸡肋,比如支持的协议有:dubbo,rmi,http,webservice,redis,memcache,rest
4、太专注于服务治理,如果我们需要使用配置中心、分布式跟踪这些内容都需要自己去集成,这样无形中增加了使用 Dubbo 的难度,也减少了选择。
5、只支持JAVA
6、、、大家一起想想



dubbo/springcloud比较

1、背景 http://dubbo.apache.org/zh-cn/index.html
2、社区活跃度
3、功能支持
在这里插入图片描述
4、学习成本,吃透难度
5、springcloud来搭建微服务体系对研发成员的要求更高
6、、、、

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值