微服务理论
Dubbo的好处
- .透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何
API
侵入。 - 软负载均衡及容错机制,可在内网替代
F5
等硬件负载均衡器,降低成本,减少单点。 - 服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的
IP
地址,并且能够平滑添加或删除服务提供者。 Dubbo
采用全Spring
配置方式,透明化接入应用,对应用没有任何API
侵入,只需用Spring
加载Dubbo
的配置即可,Dubbo
基于Spring
的Schema
扩展进行加载。
服务调用的全过程
-
服务容器负责启动,加载,运行服务提供者。
-
服务提供者(生产者)在启动时,向注册中心注册自己提供的服务。
-
服务消费者在启动时,向注册中心订阅自己所需的服务。
-
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
-
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
-
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心.
Dubbo的通信原理
通信原理
- 客户端一个线程调用远程接口,生成一个唯一的
ID
。dubbo
使用AtomicLong
从0开始计数。 - 将打包的方法调用信息(调用接口的接口名称、方法名称、参数值列表)以及处理结构的回调对象
callback
,全部封装在一起,组成一个对象Object
- 向专门存放调用信息的全局
ConCurrentHashMap
中put(ID,Obejct)
- 将
ID
和打包的方法调用信息封装成一对象connRequest
,使用IoSession.write(connRequest)
异步发送出去 - 当前线程再使用
callback
的get()
方法试图获取远程返回的结果。在get()
内部使用synchronized
获取回调对象的callback
锁,再检测是否已经获得结果。如果没有,然后调用callback
的wait()
方法。释放callback
上的锁。让当前线程处于等待状态。 - 服务端接收到请求并处理后。将结果发送给客户端。客户端上socket连接上专门监听消息的线程监听到消息。分析结果,取得
ID
,用ConCurrentHashMap.get(ID)
。从而找到callback
。将调用结果设置到callback
对象中 - 监听线程接着使用
synchronized
获取回调对象的锁(callback
的锁已经被释放掉了),再notifyAll()
,唤醒前面处于等待状态的线程继续执行。
并发处理
Dubbo缺省协议采用单一长连接和NIO异步通讯 , 适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。
Dubbo通信默认采用的是Netty框架。Netty实质就是通过Socket进行通信,Socket(TCP)通信是全双工的方式。 因为采用单一长连接,所以如果消费者多线程请求,服务端处理完消息后返回,就会造成消息错乱的问题。dubbo解决上述问题的方法:就是给包头中添加一个全局唯一标识id,服务器端响应请求时也要携带这个id,供客户端多线程领取对应的响应数据提供线索。
Zookeeper用作注册中心的原理
原理示意图
注册中心工作流程
-
服务提供方启动
服务提供者在启动的时候,会在
ZooKeeper
上注册服务。所谓注册服务,其实就是在ZooKeeper
的/dubbo/com.foo.BarService/providers
节点下创建一个子节点,并写入自己的URL地址(包括ip地址和端口号),这就代表了com.foo.BarService
这个服务的一个提供者。 -
服务消费者启动
服务消费者在启动的时候,会向
ZooKeeper
注册中心订阅自己的服务。其实,就是读取并订阅ZooKeeper
上/dubbo/com.foo.BarService/providers
节点下的所有子节点,并解析出所有提供者的URL地址来作为该服务地址列表。 同时,服务消费者还会在
ZooKeeper
的/dubbo/com.foo.BarService/consumers
节点下创建一个临时节点,并写入自己的URL
地址,这就代表了com.foo.BarService
这个服务的一个消费者。 -
消费者远程调用提供者
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一个提供者进行调用,如果调用失败,再选另一个提供者调用。
-
增加服务提供者
增加提供者,也就是在providers下面新建子节点。一旦服务提供方有变动,
zookeeper
就会把最新的服务列表推送给消费者。 -
减少服务提供者
所有提供者在
ZooKeeper
上创建的节点都是临时节点,利用的是临时节点的生命周期和客户端会话相关的特性,因此一旦提供者所在的机器出现故障导致该提供者无法对外提供服务时,该临时节点就会自动从ZooKeeper
上删除,同样,zookeeper
会把最新的服务列表推送给消费者。 -
ZooKeeper
宕机之后 消费者每次调用服务提供方是不经过
ZooKeeper
的,消费者只是从zookeeper
那里获取服务提供方地址列表。所以当zookeeper
宕机之后,不会影响消费者调用服务提供者,影响的是zookeeper
宕机之后如果提供者有变动,增加或者减少,无法把最新的服务提供者地址列表推送给消费者,所以消费者感知不到。
Zookeeper中的两个重要机制
-
session
每个
zk
客户端与zk
连接时会创建一个session,在设置的sessionTimeOut
内,客户端会与zk
进行心跳包的定时发送,从而感知每个客户端是否宕机,如果创建某个临时Znode
的对应session
销毁时,相应的临时节点也会被zk
删除。 -
watcher
监听机制,监听某个Znode 当该znode发生变化时,会回调该watcher,但是这个watcher是一次性的,下次需要监听时还得再注册一次。
dubbo与Springboot
提供者模块
-
Springboot
中application.properties
文件的配置信息# Spring boot application spring.application.name = dubbo-provider-demo server.port = 9090 # DemoService service version demo.service.version = 1.0.0 # Base packages to scan Dubbo Component: @com.alibaba.dubbo.config.annotation.Service dubbo.scan.basePackages = cn.edu.xjtu.provider.serviceImpl # Dubbo Config properties ## ApplicationConfig Bean dubbo.application.id = dubbo-provider-demo dubbo.application.name = dubbo-provider-demo ## ProtocolConfig Bean dubbo.protocol.id = dubbo dubbo.protocol.name = dubbo dubbo.protocol.port = 12345 dubbo.protocol.status = server dubbo.protocol.payload=335544320 ## RegistryConfig Bean dubbo.registry.id = my-registry dubbo.registry.address = zookeeper://192.168.1.115:2181
-
具体服务类的声明标识
@Service( version = "${demo.service.version}", application = "${dubbo.application.id}", protocol = "${dubbo.protocol.id}", registry = "${dubbo.registry.id}", timeout=600000 )
消费者模块
-
application.properties
文件的配置信息# Dubbo Config properties ## ApplicationConfig Bean dubbo.application.id = dubbo-consumer-demo dubbo.application.name = dubbo-consumer-demo ## ProtocolConfig Bean dubbo.protocol.id = dubbo dubbo.protocol.name = dubbo dubbo.protocol.port = 12345 dubbo.protocol.payload=335544320 ## RegistryConfig Bean dubbo.registry.id = my-registry dubbo.registry.address = zookeeper://192.168.1.115:2181
这里同样需要配置将要获取服务的端口号,以及注册中心的ip地址以及端口号
-
具体调用代码实现
@Reference(version = "1.0.0") private DataProcessService dataProcessService;
因为配置文件中已经指定配置信息,此处直接创建相应版本的服务实例即可使用。
Dubbo_admin界面
-
所有可用服务列表
这里显示所有可用服务名称、版本号以及每个服务的状态。 -
服务提供者具体信息
查看每个服务提供者的具体信息,包括每个服务所提供的具体方法、版本号、状态以及延迟时间等。其中包含了两个端口号,一个是主机名(ip+端口号),这个标识用于服务消费者找到相应主机上相应版本及名称的服务,另外一个是个进程号(端口号),这个是指用于注册发布此服务进程的端口号。 -
服务消费者具体信息
-
添加服务的两种方式
服务类型分为两类,一种是动态服务,一种是静态服务。
一般情况下,服务总是在启动的时候向Registry注册,并且双方之间建立长连接,Registry通过心跳检测服务是否在线,如果连接断或者心跳检测不到,那么就相当于解除注册了,此方式注册的称之为动态服务。
有时候需要手动控制服务的上线与下线,这个时候就可以使用静态服务了,它也是围绕着Registry展开的。服务正常启动,但它不自动向Registry注册,同样,如果服务关停,它也不会自动告诉Registry解除注册。静态服务与Registry这间应该不会建立长连接,双方不会维持心跳。总之,静态服务就是通过人式的形式完成服务的注册与解除注册。
-
负载均衡
Dubbo中的四种负载均衡的方法:随机算法、权重轮询算法、一致性Hash算法、最少活跃数算法
-
随机算法(RandomLoadBalance)
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int totalWeight = 0; // 总权重 boolean sameWeight = true; // 权重是否都一样 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); totalWeight += weight; // 累计总权重 if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) { sameWeight = false; // 计算所有权重是否一样 } } if (totalWeight > 0 && ! sameWeight) { // 如果权重不相同且权重大于0则按总权重数随机 int offset = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < length; i++) { offset -= getWeight(invokers.get(i), invocation); if (offset < 0) { return invokers.get(i); } } } // 如果权重相同或权重为0则均等随机 return invokers.get(random.nextInt(length)); }
-
权重轮询算法(RoundRobinLoadBalance)
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName(); int length = invokers.size(); // 总个数 int maxWeight = 0; // 最大权重 int minWeight = Integer.MAX_VALUE; // 最小权重 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); maxWeight = Math.max(maxWeight, weight); // 累计最大权重 minWeight = Math.min(minWeight, weight); // 累计最小权重 } if (maxWeight > 0 && minWeight < maxWeight) { // 权重不一样 AtomicPositiveInteger weightSequence = weightSequences.get(key); if (weightSequence == null) { weightSequences.putIfAbsent(key, new AtomicPositiveInteger()); weightSequence = weightSequences.get(key); } int currentWeight = weightSequence.getAndIncrement() % maxWeight; List<Invoker<T>> weightInvokers = new ArrayList<Invoker<T>>(); for (Invoker<T> invoker : invokers) { // 筛选权重大于当前权重基数的Invoker if (getWeight(invoker, invocation) > currentWeight) { weightInvokers.add(invoker); } } int weightLength = weightInvokers.size(); if (weightLength == 1) { return weightInvokers.get(0); } else if (weightLength > 1) { invokers = weightInvokers; length = invokers.size(); } } AtomicPositiveInteger sequence = sequences.get(key); if (sequence == null) { sequences.putIfAbsent(key, new AtomicPositiveInteger()); sequence = sequences.get(key); } // 取模轮循 return invokers.get(sequence.getAndIncrement() % length); }
-
一致性Hash算法(ConsistentHashLoadBalance)
-
最小活跃数算法
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int leastActive = -1; // 最小的活跃数 int leastCount = 0; // 相同最小活跃数的个数 int[] leastIndexs = new int[length]; // 相同最小活跃数的下标 int totalWeight = 0; // 总权重 int firstWeight = 0; // 第一个权重,用于于计算是否相同 boolean sameWeight = true; // 是否所有权重相同 for (int i = 0; i < length; i++) { Invoker<T> invoker = invokers.get(i); int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // 活跃数 int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 权重 if (leastActive == -1 || active < leastActive) { // 发现更小的活跃数,重新开始 leastActive = active; // 记录最小活跃数 leastCount = 1; // 重新统计相同最小活跃数的个数 leastIndexs[0] = i; // 重新记录最小活跃数下标 totalWeight = weight; // 重新累计总权重 firstWeight = weight; // 记录第一个权重 sameWeight = true; // 还原权重相同标识 } else if (active == leastActive) { // 累计相同最小的活跃数 leastIndexs[leastCount ++] = i; // 累计相同最小活跃数下标 totalWeight += weight; // 累计总权重 // 判断所有权重是否一样 if (sameWeight && i > 0 && weight != firstWeight) { sameWeight = false; } } } // assert(leastCount > 0) if (leastCount == 1) { // 如果只有一个最小则直接返回 return invokers.get(leastIndexs[0]); } if (! sameWeight && totalWeight > 0) { // 如果权重不相同且权重大于0则按总权重数随机 int offsetWeight = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < leastCount; i++) { int leastIndex = leastIndexs[i]; offsetWeight -= getWeight(invokers.get(leastIndex), invocation); if (offsetWeight <= 0) return invokers.get(leastIndex); } } // 如果权重相同或权重为0则均等随机 return invokers.get(leastIndexs[random.nextInt(leastCount)]); }
主要思想是记录一个活跃数,这个值保存在
RpcStatus
里。找到最少活跃数的结点集合后,在这些集合中根据权重随机选择结合(这里的随机选择算法和RandomLoadBalance一样)。
-
看了很多篇集中整理出来的内容,原文链接已经找不到。