分布式中间件之Dubbo详解

文章内容输出来源:拉勾教育Java高薪训练营

心得体会: 在拉勾教育高薪训练营里经过三个月多的学习,很辛苦但也收获很多,主要体现在:1.以前是工作遇到了哪方面的问题才去学习并解决,很难形成知识体系,在拉勾高薪训练营里课程内容很丰富,基础回顾加进阶讲解,完美的打造了个人的知识体系,为成为一名合格的架构师奠定了基础;2.值得一说的是“闯关式”的学习模式,若上一个模块的作业未完成就无法学习下一个模块,但有班主任和导师对个人的学习进行督促,可以让我们学员必须牢靠的掌握技术知识点;3.在知识点技术落地方面,以前只是听说过或停留在概念层面,在拉勾教育的课程中,不同的老师会从不同的角度对知识点进行讲解,有些比较难理解的知识点(例如Dubbo的SPI机制),老师一般会从基础简单的案例入手,让我们轻松理解并掌握,然后在扩展延伸其重点。特别是源码分析方面,彻底发散了个人思维,理解过后可以写出优雅的代码,比如之前是知道有反射这个概念,也知道有责任链这种设计模式,但不会用也不知道在什么场景下用,经过这次对Dubbo源码的分析理解,成功的使用反射和责任链模式解决了工作上一个不好处理的业务逻辑问题。当然在这里还有一群爱学习的小伙伴,可以一起讨论问题,一起分享技术。是的,如果你现在的能力支撑不了你的梦想,那就让我们站在拉勾教育这个巨人的肩膀上去实现自己的梦想吧!

本文概述: 本文将主要讲解分布式系统中Dubbo框架,主要内容包括Dubbo相关概念、架构设计的演变、注册中心Zookeeper、Dubbo的高级特性、Dubbo源码分析、Dubbo中的设计模式和Dubbo项目实战。
Dubbo知识点思维导图:
在这里插入图片描述

一、Dubbo相关概念

1. 分布式

分布式是指把一个系统拆分为多个子系统,每个子系统负责各自的那部分功能,独立部署,各司其职。简单来说,分布式就是多个系统在一起作不同的事。

2. 集群

集群是指多个实例共同工作,最简单或最常见的集群是把一个应用复制多份部署。简单来说,集群就是多个系统在一起作同样的事 。
常见的集群模式有:
主从集群:一主多从,主从各司其职,主节点负责资源分配和任务调度,从节点负责具体的执行;
主备集群:一主一备,主要是为了解决单点故障问题,便于备份恢复;
注意:分布式的核心体现是拆分;集群的核心体现是复制;所以分布式一定是集群,但集群不一定是分布式。

3. RPC

RPC(remote procedure call)即远程过程调用,比如发送了一个查询用户明细的请求get/userInfo?userId=1234,实际上是调用了服务端的一个方法userInfo(Integer userId)。即在本地电脑上可以调用一个远程服务端的方法,所以叫远程过程调用。这里的"远"也不一定是跨越网络的,也可以是同一台主机的两个进程之间的相互交流。RPC并不是一个具体的技术,它指定了一种通信方式,是一种服务间通信的理念。在java中RPC的落地框架比较多,常见的有Hessian、gRPC、Thrift、HSF (High-Speed Service Framework)、Dubbo等。
【Hessian】
Hessian是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。基于HTTP协议,采用二进制编解码。
【gRPC】
gRPC是Google 公布的开源软件,基于最新的HTTP 2.0协议,并支持常见的众多编程语言。
【Thrift】
Thrift是Facebook的开源 RPC 框架,主要是一个跨语言的服务开发框架。它拥有功能强大的代码生成引擎,无缝地支持C++,C#,Java,Python,PHP和Ruby。Thrift允许你定义一个描述文件,描述数据类型和服务接口。依据该文件,编译器方便地生成RPC客户端和服务器通信代码。
Thrift最初由Facebook开发用做系统内部语言之间的RPC通信,2007年由Facebook贡献到apache组织,现在是apache下的opensource之一 。支持多种语言之间的RPC方式的通信,比如PHP语言client可以构造一个对象,调用相应的服务方法来调用Java语言的服务,跨越语言的C/S RPC调用。底层通讯基于SOCKET。
【HSF】
HSF全称为High-Speed Service Framework,是一个分布式的服务框架,HSF从分布式应用层面以及统一的发布/调用方式层面提供服务支持,从而可以很容易的开发分布式的应用以及提供或使用公用功能模块,而不用考虑分布式领域中的各种细节技术,例如远程通讯、性能损耗、调用的透明化、同步/异步调用方式的实现等问题。
【Dubbo】
Dubbo框架是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的RPC实现服务的输出和输入功能,可以和Spring框架无缝集成。

4. Restful API

RESTful是一种架构风格,其最大的几个特点为:资源、统一接口、URI 和无状态。
资源: 所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,就是一个具体的实体。
统一接口: RESTful架构风格规定,数据的元操作,即 CRUD(Create,Read,Update和Delete,即数据的增查改删)操作,分别对应于HTTP方法:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,这样就统一了数据操作的接口,仅通过 HTTP方法,就可以完成对数据的所有增删查改工作。
URL: 可以用一个URI(统一资源定位符)指向资源,即每个URI都对应一个特定的资源,要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或识别符。
无状态: 所谓无状态的,即所有的资源,都可以通过URI定位,而且这个定位与其他资源无关,也不会因为其他资源的变化而改变。有状态和无状态的区别,举例如下:
比如查询员工的工资,如果查询工资是需要登录系统,进入查询工资的页面,执行相关操作后,获取工资的多少,则这种情况是有状态的。因为查询工资的每一步操作都依赖于前一步操作,只要前置操作不成功,后续操作就无法执行。如果输入一个URI即可得到指定员工的工资,则这种情况是无状态的,因为获取工资不依赖于其他资源或状态。且这种情况下,员工工资是一个资源,由一个URI与之对应,可以通过HTTP中的GET方法得到资源,这是典型的RESTful风格。
注意: RPC侧重于动作,即RPC可以通过动作去操作资源;RESTful侧重于资源,是面向资源的设计架构;在传输效率方面,RPC效率更高,RPC使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率;但RPC实现复杂,流程繁琐;REST调用及测试都很方便。

5. 长连接和短连接

短连接: HTTP1.0中每一个请求和响应都会触发一次TCP连接的建立和断开,此为短连接。
长连接: 在HTTP1.1中增加了keep-alive机制,允许在一个TCP连接上发送多个请求和响应,此为长连接。通俗点说,就是建立连接过后可以持续发送请求,无须再建立连接。HTTP请求头部中的Connection标识是否使用长连接,当为Connection: keep-alive时,表明使用长连接(通常无需设置,默认);当不希望使用长连接,则设为Connection: close。
在这里插入图片描述
Dubbo服务调用连接是长连接,Dubbo服务调用是小数据量的通信,针对每一次RPC通信,都会生成一个唯一的id来标识,这样就能区分出一次RPC请求对应的RPC响应了。
长连接和短连接的性能比较:
如果二者的限制条件一样(比如都是同步方式或IO复用方式、同等数据量通信、连接个数相同)情况下,长连接性能理论上要优于短连接,因为前者省去了三次握手和四次挥手过程。
如果二者限制条件不同,绝大多数场景的限制条件都是不同的,如果通信较频繁,使用长连接性能较好;如果通信次数较少,使用短连接更合适,毕竟不用维护长连接。

6. Dubbo框架

Dubbo官网:http://dubbo.apache.org/
官网上说,Apache Dubbo是一款高性能Java RPC框架。所以这里必须指出,我们经常所说的Dubbo不是指Dubbo协议,而是指RPC框架即远程服务调用的分布式框架,具体的说Dubbo框架是RPC理念的落地产品。而本文着重介绍的是Dubbo框架。
在分布式应用场景有高并发,高可扩展和高性能的要求。还涉及到,序列化/反序列化,网络,多线程以及设计模式的问题,而Dubbo框架对这些进行了封装。
Dubbo底层是使用Netty这样的NIO框架,是基于TCP协议传输的,配合以Hession序列化完成RPC通信。

7. Dubbo协议

Dubbo协议是阿里巴巴自己实现的一种网络应用层协议,传输层还是TCP,Dubbo协议与HTTP协议、FTP协议,SMTP协议等这些应用层协议是并列的概念。

8. Dubbox框架

Dubbox是一个分布式服务框架,其前身是阿里巴巴开源项目Dubbo ,被国内电商及互联网项目中使用,后期阿里巴巴停止了该项目的维护,当当网便在Dubbo基础上进行优化,并继续维护,为了与原有的Dubbo区分,故将其命名为Dubbox。Dubbox弥补了Dubbo中没有REST框架的不足,即Dubbo服务端服务可以通过Dubbox快速包装为REST风格的服务。

9. Zookeeper

ZooKeeper是一个开源的分布式协调服务,它是服务集群的管理者,监视着集群中各个服务节点的状态,并根据节点提交的反馈进行合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
分布式应用程序基于Zookeeper可以实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理(注册中心)、Master选举、分布式锁和分布式队列等功能。
ZooKeeper将作为注册中心与Dubbo结合使用。

10. Dubbo和Zookeeper的关系

Dubbo为服务间提供了调用,主要用于管理分布式系统,从后面的Dubbo架构中我们可以看到Dubbo的工作需要一个非常重要的组件——注册中心(Register),而Zookeeper是分布式协调的治理工具,它能很好的提供服务注册和发现的功能,这样Dubbo和Zookeeper就完美的相遇了,当然Dubbo官网也推荐注册中心使用Zookeeper。简单来说,打个比方:每个dubbo服务就是动物园的动物,zookeeper是动物园。如果游客想看动物的话那么就去动物园看。比如你要看老虎,那么动物园有老虎你才能看到。换句话说我们把很多不同的dubbo服务(动物)放到zookeeper(动物园中)提供给我们游客进行观赏。这个过程中三个关键:场所、提供者、消费者。
再说一个分布式的项目,提供者与消费者被拆分了开来,部署在不同的tomcat中,在消费者中需要调用提供者的接口,但是两个运行在不同tomcat下的服务无法直接互调接口,那么就可以通过zookeeper和dubbo实现。就好比把动物放到动物园,我们要看了直接去动物园就行。而不能直接去动物生活的地方去看,会有性命安全之忧(比如你去看老虎)。
我们通过Dubbo建立提供者服务,并且到Zookeeper上面注册,即在Zookeeper上填写对应的Dubbo服务的IP及端口号。Zokeeper (注册中心)主要功能是服务的注册与发现,Dubbo(远程服务调用的分布式框架)主要实现应用服务与Zokeeper注册中心的链接调用(类似JDBC工具类)。

二、架构设计的演变

1. 单体架构

单体架构所有模块和功能都集中在一个项目中 ,部署时也是将项目所有功能整体部署到服务器中。

优点:
(1)小项目开发快,成本低
(2)架构简单
(3)易于测试
(4)易于部署
缺点:
(1)大项目模块耦合严重,不易开发维护,沟通成本高
(2)新增业务困难
(3)核心业务与边缘业务混合在一块,出现问题互相影响

2. 垂直架构

根据业务把项目垂直切割成多个项目,因此这种架构称之为垂直架构。

优点:
(1)系统拆分实现了流量分担,解决了并发问题
(2)可以针对不同系统进行优化
(3)方便水平扩展,负载均衡,容错率提高
(4)系统间相互独立,互不影响,新的业务迭代时更加高效
缺点:
(1)服务系统之间接口调用硬编码
(2)搭建集群之后,实现负载均衡比较复杂
(3)服务系统接口调用监控不到位,调用方式不统一
(4)数据库资源浪费,充斥慢查询,主从同步延迟大

3. 分布式架构(SOA )

SOA全称为Service Oriented Architecture,即面向服务的架构 。它是在垂直划分的基础上,将每个项目拆分出多个具备松耦合的服务,一个服务通常以独立的形式存在于操作系统进程中。各个服务之间通过网络调用,这使得构建在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。

优点:
(1)服务以接口为粒度,为开发者屏蔽远程调用底层细节,使用Dubbo面向接口远程方法调用屏蔽了底层调用细节
(2)业务分层以后架构更加清晰,并且每个业务模块职责单一,扩展性更强
(3)数据隔离,权限回收,数据访问都通过接口,让系统更加稳定安全
(4)服务应用本身无状态化,这里的无状态化指的是应用本身不做内存级缓存,而是把数据存入数据库
(5)服务责任易确定,每个服务可以确定责任人,这样更容易保证服务质量和稳定
缺点:
(1)粒度控制复杂,如果没有控制好服务的粒度,服务的模块就会越来越多,就会引发超时、分布式事务等问题
(2)服务接口数量不宜控制,容易引发接口爆炸,所以服务接口建议以业务场景进行单位划分,并对相近的业务做抽象,防止接口爆炸
(3)版本升级兼容困难,尽量不要删除方法、字段,枚举类型的新增字段也可能不兼容等
(4)调用链路长,服务质量不可监控,调用链路变长,下游抖动可能会影响到上游业务,最终形成连锁反应,服务质量不稳定,同时链路的变长使得服务质量的监控变得困难

4. 微服务架构

微服务架构是SOA架构的一种扩展,这种架构模式下它拆分粒度更小、服务更独立。把应用拆分成为一个个微小的服务,不同的服务可以使用不同的开发语言和存储,服务之间往往通过Restful等轻量级通信。微服务架构关键在于微小、独立、轻量级通信。
微服务是在SOA上做的升华 , 粒度更加细致,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”。

优点:
(1)微服务很小,便于特定业务功能的聚焦
(2)微服务很小,每个微服务都可以被一个小团队单独实施(开发、测试、部署上线、运维),团队合作一定程度解耦,便于实施敏捷开发
(3)微服务很小,便于重用和模块之间的组装
(4)微服务很独立,不同的微服务可以使用不同的语言开发,松耦合
(5)微服务架构下,我们更容易引入新技术
(6)微服务架构下,我们可以更好的实现DevOps开发运维一体化

缺点:
(1)微服务架构下,分布式复杂难以管理,当服务数量增加,管理将越加复杂
(2)微服务架构下,分布式链路追踪难等

三、Dubbo架构概述

1. Dubbo基本架构

在这里插入图片描述

节点 角色名称
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心
Container 服务运行容器,负责启动、加载、运行服务提供者

调用关系说明:
虚线:代表异步调用
实线:代表同步访问
蓝色虚线:代表启动时完成的功能
蓝绿色虚线:代表程序运行中执行的功能
调用流程:
0:服务运行容器运行容器初始化,启动、加载、运行服务提供者
1:服务提供者在服务容器启动时,向注册中心注册自己提供的服务
2:服务消费者在启动时,向注册中心订阅自己所需的服务
3:注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心会基于长连接推送变更数据给消费者
4:服务消费者从提供者地址列表中基于软负载均衡算法选一台提供者进行调用,如果调用失败,则重新选择一台
5:服务提供者和消费者在内存中的调用次数和调用时间会定时每分钟发送给监控中心

2. Dubbo的服务治理

在这里插入图片描述
在大规模服务化之前,应用可能只是通过RMI或Hessian等工具,简单的暴露和引用远程服务,通过配置服务的URL地址进行调用,通过F5等硬件进行负载均衡。

2.1 服务注册与发现

问题: 当服务越来越多时,服务URL配置管理变得非常困难,F5硬件负载均衡器的单点压力也越来越大。
解决: 此时需要一个服务注册中心,动态的注册和发现服务,使服务的位置透明。并通过在消费方获取服务提供方地址列表,实现软负载均衡和Failover,降低对F5硬件负载均衡器的依赖,也能减少部分成本。

2.2 服务依赖关系

问题: 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。
解决: 这时,需要自动画出应用间的依赖关系图,以帮助架构师理清理关系。

2.3 服务监控与统计

问题: 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器?
解决: 为了解决这些问题,首先,要将服务现在每天的调用量,响应时间,都统计出来,作为容量规划的参考指标。其次,要可以动态调整权重,在线上,将某台机器的权重一直加大,并在加大的过程中记录响应时间的变化,直到响应时间到达阀值,记录此时的访问量,再以此访问量乘以机器数反推总容量。

2.4 服务分层架构

问题: 规模继续扩大,应用之间不再是扁平的对应关系,开始分层,比如核心数据层,业务集成层等,就算没有出现循环依赖,也不允许从低层向高层依赖,以免后续被逼循环依赖。
解决: 这时,需要在注册中心定义架构体系,列明有哪些层的定义,每个服务暴露或引用时,都必须声明自己应用属于哪一层,这样注册中心能更快的发现架构的腐化现象。

2.5 服务负责人和服务文档

问题: 服务多了,沟通成本也开始上升,调某个服务失败该找谁?服务的参数都有什么约定?
解决: 这时就需要登记每个服务都是谁负责的,并建立一个服务的文档库,方便检索。

2.6 服务权限控制

问题: 慢慢一些敏感数据也都服务化了,安全问题开始变得重要,谁能调该服务?如何授权?
解决: 这样的服务可能需要一个密码,访问时需带着此密码,但如果用密码,要改密码时,就会很不方便,所有的消费方都要改,所以动态生成令牌(Token)可能会更好,提供方将令牌告之注册中心,由注册中心决定是否告之消费方,这样就能在注册中心页面上做复杂的授权模型。

2.7 服务等级协定

问题: 就算是不敏感的服务,也不是能任意调用,比如某服务突然多了一个消费者,这个消费者的请求量直接把服务给拖跨了,其它消费者跟着一起故障。
解决: 首先服务提供方需要流控,当流程超标时,能拒绝部分请求,进行自我保护。其次,消费者上线前和提供者约定《服务质量等级协定(SLA)》,SLA包括消费者承诺每天调用量,请求数据量,提供方承诺响应时间,出错率等,将SLA记录在监控中心,定时与监控数据对比,超标则报警。

2.8 服务路由和服务负载均衡

问题: 虽然有SLA约定,如果不能控制,就只是君子协定,如何确保服务质量?
解决: 比如:一个应用很重要,一个不那么重要,它们调用同一个服务,这个服务就应该向重要应用倾斜,而不是一视同仁,当支撑不住时,应限制不重要应用的访问,保障重要应用的可用,如何做到这一点呢。这时,就需要服务路由,控制不同应用访问不同机器,比如:
应用分离:
consumer.application = foo ==> provider.host = 1,2,3
consumer.application != foo ==> provider.host = 5,6
读写分离:
method.name = find*,get* ==> provider.host = 1,2,3
method.name != find*,get* ==> provider.host = 5,6

2.9 服务自动测试

问题: 服务上线后,需要验证服务是否可用,但因防火墙的限制,线下是不能访问线上服务的,不得不先写好一个测试Main,然后放到线上去执行,非常麻烦,并且容易忘记验证。
解决: 所以线上需要有一个自动运行的验证程序,用户只需在界面上填上要验证的服务方法,以及参数值和期望的返回值,当有一个服务提供者上线时,将自动运行该用例,并将运行结果发邮件通知负责人。

2.10 服务模板工程

问题: 服务应用和Web应用是有区别的,它是一个后台Daemon程序,不需要Tomcat之类的Web容器。但因公司之前以Web应用为主,规范都是按Web应用的,所以不得不把服务跑在一个根本用不上的Web容器里,而搭一个这样的Web工程也非常费事。
解决: 所以需要实现一个非Web的容器,只需简单的Main加载spring配置即可,并提供Maven模板工程,只需mvn dubbo:generate即可创建一个五脏俱全的服务应用。

2.11 服务开发IDE

问题: 开发服务的人越来越多,更注重开发效率,IDE的集成支持必不可少。
解决: 通过插件,可以在IDEA中直接运行服务,提供方可以直接填入测试数据测试服务,消费方可以直接Mock服务不依赖提供方开发。

2.12 服务上线审批和下线通知

问题: 因为暴露服务很简单,服务的上线越来越随意,有时候负责服务化的架构师都不知道有人上线了某个服务,使得线上服务鱼龙混杂,甚至出现重复的服务,而服务下线比上线还困难。
解决: 需要一个新服务上线审批流程,必须经过服务化的架构师审批过了,才可以上线。而服务下线时,应先标识为过时,然后通知调用方尽快修改调用,直到没有人调此服务,才能下线。

2.13 服务兼容性检测

问题: 因服务接口设计的经验一直在慢慢的积累过程中,很多接口并不能一促而蹴,在修改的过程中,如何保证兼容性,怎么判断是否兼容?另外,更深层次的,业务行为兼容吗?
解决: 可以根据使用的协议类型,分析接口及领域模型的变更是否兼容,比如:对比加减字段,方法签名等。而业务上,可能需要基于自动回归测试用例,形成Technology Compatibility Kit (TCK),确保兼容升级。

2.14 服务容错和降级

问题: 随着服务的不停升级,总有些意想不到的事发生,比如cache写错了导致内存溢出,故障不可避免,每次核心服务一挂,影响一大片,人心慌慌,如何控制故障的影响面?服务是否可以功能降级?或者资源劣化?
解决: 应用间声明依赖强度,哪些功能强依赖,哪些弱依赖,然后基于依赖强度,计算出影响面,并定期测试复查,加强关键路径上的服务的优化和容错,清理不该在关键路径上的服务。提供容错Mock数据,Mock数据也应可以在注册中心在运行时动态下发,当某服务不可用时,用Mock数据代替,可以减少故障的发生,比如某验权服务,当验权服务全部挂掉后,直接返回false表示没有权限,并打印Error日志报警。另外,前端的页面也应采用Portal进行降级,当该Portal获取不到数据时,直接隐藏,或替换为其它模块展示,并提供功能开关,可人工干预是否展示,或限制多少流量可以展示。

2.15 服务编排

问题: 当已有很多小服务,可能就需要组合多个小服务的大服务,为此,不得不增加一个中间层,暴露一个新服务,里面分别调其它小服务,这样的新服务业务逻辑少,却带来很多开发工作量。
解决: 此时,需要一个服务编排引擎,内置简单的流程引擎,只需用XML或DSL声明如何聚合服务,注册中心可以直接下发给消费者执行聚合逻辑,或者部署通用的编排服务器,所有请求有编排服务器转发。

2.16 服务使用情况报告

问题: 并不是所有服务的访问量都大,很多的服务都只有一丁点访问量,却需要部署两台提供服务的机器,进行HA互备,如何减少浪费的机器。
解决: 此时可能需要让服务容器支持在一台机器上部署多个应用,可以用多JVM隔离,也可以用ClassLoader隔离。

2.17 服务自动部署

问题: 多个应用如果不是一个团队开发的,部署在一台机器上,很有可以误操作,停掉了别人的服务。
解决: 需要实现自动部署,所有的部署都无需人工干扰,最好是一键式部署。

2.18 服务权重动态调整

问题: 机器总是的闲时和忙时,或者冗余机器防灾,如何提高机器的利用率?
解决: 即然已经可以自动部署了,那根据监控数据,就可以实现资源调度,根据应用的压力情况,自动添加机器并部署。如果你的应用是国际化的,有中文站,美国站之类,因为时差,美国站的机器晚上闲的时候,可能正是中文站的白天忙时,可以通过资源调度,分时段自动调配和部署双方应用。

四、Dubbo的注册中心Zookeeper

官方推荐Dubbo的注册中心使用Zookeeper,本部分将对Zookeeper的相关知识进行讲解,包括Zookeeper基本概念、基本使用、工作原理、集群相关和主要应用场景。

1. Zookeeper基本概念

1.1 会话 Session

Session指客户端会话,一个客户端连接是指客户端和服务端之间的一个TCP长连接,Zookeeper对外的服务端口默认为2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来自服务器的Watch事件通知。

1.2 数据节点 Znode

Zookeeper提供一个多层级的节点命名空间即节点称为Znode,在Zookeeper中,节点可以分为两类,第一类指构成集群的机器,称为机器节点;第二类指数据模型中的数据单元即数据节点。Zookeeper中数据节点分为以下四种:
(1)PERSISTENT-持久节点
除非手动删除,否则节点一直存在于 Zookeeper 上。
(2)EPHEMERAL-临时节点
临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与Zookeeper连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。
(3)PERSISTENT_SEQUENTIAL-持久顺序节点
基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
(4)EPHEMERAL_SEQUENTIAL-临时顺序节点
基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

1.3 版本 Version

由上可知,Zookeeper的每个Znode上都会存储数据,对于每个Znode,Zookeeper都会为其维护一个叫作Stat的数据结构,Stat记录了这个Znode的三个数据版本,分别是version(当前Znode的版本)、cversion(当前Znode子节点的版本)、aversion(当前Znode的ACL版本)。

1.4 事件监听器 Watcher

事件监听器(Watcher)是Zookeeper中一个很重要的特性,Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端,该机制是Zookeeper实现分布式协调服务的重要特性。

1.5 权限控制策略 ACL

Zookeeper采用ACL(Access Control Lists)策略来进行权限控制,其定义了如下五种权限:
(1)CREATE:创建子节点的权限;
(2)READ:获取节点数据和子节点列表的权限;
(3)WRITE:更新节点数据的权限;
(4)DELETE:删除子节点的权限;
(5)ADMIN:设置节点ACL的权限。
其中需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。

2. Zookeeper基本使用

2.1 Zookeeper的部署模式

部署模式:单机模式、伪集群模式、集群模式。

2.2 Zookeeper基本使用方式

在客户端如何对Zookeeper的节点数据进行操作,Zookeeper为我们提供了以下四种方式:
(1)命令行操作
(2)API操作
(3)开源客户端ZkClient操作
(4)开源客户端Curator操作

3. Zookeeper工作原理

3.1 Zookeeper Watcher 机制(数据变更通知)

Zookeeper允许客户端向服务端的某个Znode注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher监听,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据Watcher通知状态和事件类型做出业务上的改变。
Watcher特性:
(1)一次性
无论是服务端还是客户端,一旦一个Watcher被 触 发 ,Zookeeper都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
(2)客户端串行执行
客户端Watcher回调的过程是一个串行同步的过程。
(3)轻量
Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容,客户端向服务端注册Watcher的时候,并不会把客户端真实的Watcher对象实体传递到服务端,仅仅是在客户端请求中使用Boolean类型属性进行了标记。
(4)Watcher event异步发送Watcher的通知事件从server发送到client是异步的,这就存在一个问题,不同的客户端和服务器之间通过socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于Zookeeper本身提供了ordering guarantee,即客户端监听事件后,才会感知它所监视的Znode发生了变化。所以我们使用Zookeeper不能期望能够监控到节点每次的变化。Zookeeper只能保证最终的一致性,而无法保证强一致性。
(5)注册Watcher getData、exists、getChildren
(6)触发Watcher create、delete、setData
(7)当一个客户端连接到一个新的服务器上时,Watcher将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到Watcher的。而当client重新连接时,如果需要的话,所有先前注册过的Watcher,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,Watcher可能会丢失:对于一个未创建的Znode的exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个Watcher事件可能会被丢失。

工作机制:
(1)客户端注册Watcher
(2)服务端处理Watcher
(3)客户端回调Watcher
客户端注册Watcher实现说明:
(1)调用getData()/getChildren()/exist()三个API,传入Watcher对象
(2)标记请求request,封装Watcher到WatchRegistration
(3)封装成Packet对象,服务端发送request
(4)收到服务端响应后,将Watcher注册到ZKWatcherManager中进行管理
(5)请求返回,完成注册。
服务端处理Watcher实现:
(1)服务端接收Watcher并存储
接收到客户端请求,处理请求判断是否需要注册Watcher,需要的话将数据节点的节点路径和ServerCnxn(ServerCnxn 代表一个客户端和服务端的连接,实现了Watcher的process接口,此时可以看成一个Watcher对象)存储在WatcherManager的WatchTable和watch2Paths 中去。
(2)Watcher触发
以服务端接收到setData() 事务请求触发NodeDataChanged事件为例:
先封装WatchedEvent,将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个WatchedEvent对象;后查询Watcher,即从WatchTable中根据节点路径查找 Watcher,若没找到说明没有客户端在该数据节点上注册过Watcher,若找到则提取并从WatchTable 和Watch2Paths中删除对应Watcher(从这里可以看出Watcher在服务端是一次性的,触发一次就失效了);最后调用process方法来触发Watcher,即通过ServerCnxn对应的TCP连接发送Watcher事件通知。
客户端回调 Watcher:
客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher。客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了。

3.2 ZAB协议(保证主从节点的状态同步)

ZAB协议是为分布式协调服务(保证各个服务之间的数据状态同步)Zookeeper专门设计的一种支持崩溃恢复的原子广播协议。
ZAB 协议包括两种基本的模式:崩溃恢复和消息广播。
当整个Zookeeper集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的Leader 服务器,然后集群中Follower服务器开始与新的Leader服务器进行数据同步,当集群中超过半数机器与该Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
恢复模式:
当服务启动或者在领导者崩溃后,Zookeeper集群服务就进入了恢复模式,当领导者被选举出来,且大多数服务Server完成了和Leader的状态同步以后,恢复模式就结束了。状态同步保证了Leader和 Server具有相同的系统数据状态。
广播模式:
一旦Leader已经和多数的Follower进行了状态同步后,它就可以开始广播消息了,即进入广播模式。这时候当一个Server加入到ZooKeeper集群服务中,它会在恢复模式下启动,发现Leader,并和 Leader进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper服务一直维持在Broadcast状态,直到Leader崩溃了或者Leader失去了大部分的Followers支持。

4. Zookeeper集群相关

4.1 Zookeeper集群规则

如果保证Zookeeper集群可用,必须满足过半原则;
集群规则为 2N+1 台,N>0,即Zookeeper集群数量为奇数,最少为3台。
官方推荐集群数必须为奇数的原因:
如果Zookeeper集群总数为5的话,能够保证Zookeeper可用性,最多只能宕机几台?
因为可用必须满足5-2>5/2,所以最多只能宕机2台;
如果Zookeeper集群总数为6的话,能够保证Zookeeper可用性,最多只能宕机几台?
因为可用必须满足6-2>6/2,所以最多只能宕机2台;
也就是说,如果有2个Zookeeper节点,那么只要有1个Zookeeper节点死了,那么Zookeeper服务就不能用了,因为1没有过半,所以2个Zookeeper的死亡容忍度为0;同理,要是有3个Zookeeper,一个死了,还剩下2个正常的,过半了,所以3个Zookeeper的容忍度为1;同理也可以多列举几个:2->0; 3->1; 4->1; 5->2; 6->2 就会发现一个规律,2n和2n-1的容忍度是一样的,都是n-1,所以为了更加高效,不必增加那一个不必要的Zookeeper节点,所以从资源节省的角度来考虑,Zookeeper集群的节点最好要部署成奇数个。

4.2 集群角色

通常在分布式系统中,构成集群的每一台机器(服务)都有自己的角色,Zookeeper中服务角色分为Leader、Follower、Observer,主要功能如下:
Leader:
(1)事务请求的唯一调度和处理者,保证集群事务处理的顺序性
(2)集群内部各服务的调度者
Follower:
(1)处理客户端的非事务请求,转发事务请求给Leader服务器
(2)参与事务请求Proposal的投票
(3)参与Leader选举投票
Observer:
(1)3.0 版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力
(2)处理客户端的非事务请求,转发事务请求给Leader服务器
(3)不参与任何形式的投票

4.3 集群服务的工作状态

集群中服务器具有四种状态,分别是 LOOKING、FOLLOWING、LEADING、OBSERVING。
(1)LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。
(2)FOLLOWING:跟随者状态。表明当前服务器角色是Follower。
(3)LEADING:领导者状态。表明当前服务器角色是Leader。
(4)OBSERVING:观察者状态。表明当前服务器角色是Observer。

4.4 Zookeeper集群的脑裂问题

(1)什么是脑裂
脑裂(split-brain)就是“大脑分裂”,也就是本来一个“大脑”被拆分了两个或多个“大脑”,对于人来说,如果一个人有多个大脑,并且相互独立的话,那么会导致人体“手舞足蹈”,“不听使唤”。
在"双机热备"高可用(HA)系统中,当联系两个节点的"心跳线"断开时(即两个节点断开联系时),本来为一个整体、动作协调的HA系统,就分裂成为两个独立的节点(即两个独立的个体)。由于相互失去了联系,都以为是对方出了故障,两个节点上的HA软件像"裂脑人"一样,“本能"地争抢"共享资源”、争起"应用服务"。
对于Zookeeper集群来说,在Zookeeper集群中有Leader节点,Zookeeper集群的脑裂就是存在多个Leader节点。
(2)Zookeeper集群的脑裂问题
对于一个集群,想要提高这个集群的可用性,通常会采用多机房部署,比如现在有一个由6台zkServer所组成的一个集群,部署在了两个机房,每个机房有3台zkServer。正常情况下,此集群只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer还是可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个Leader。这就相当于原本一个集群,被分成了两个集群,出现了两个“大脑”,这就是脑裂。对于这种情况,原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么解决等等问题。
(3)Zookeeper集群脑裂问题的解决
Zookeeper集群的脑裂问题在Zookeeper中已经通过过半选举机制解决了。所谓过半选举机制,即在领导者选举的过程中,如果某台zkServer获得了超过半数的选票,则此zkServer就可以成为Leader了。
接下来,先看下过半选举机制的源码:

public class QuorumMaj implements QuorumVerifier {
   
    private static final Logger LOG = LoggerFactory.getLogger(QuorumMaj.class);
    
    int half;
    
    // n表示集群中zkServer的个数(准确的说是参与者的个数,参与者不包括观察者节点)
    public QuorumMaj(int n){
   
        this.half = n/2;
    }

    // 验证是否符合过半机制
    public boolean containsQuorum(Set<Long> set){
   
        // half是在构造方法里赋值的
        // set.size()表示某台zkServer获得的票数
        return (set.size() > half);
    }
    
}

可以看出代码中过半选举出Leader的条件是,获得投票数必须大于服务个数的一半,注意不是大于等于!
解决过程分析:
情况1:如果现在集群中有5台zkServer,机房1有3台,机房2有2台,如下:
在这里插入图片描述
那么half=5/2=2,此时过半机制的条件是set.size() > 2,也就是说,领导者选举的过程中至少要有三台zkServer投了同一个zkServer,才会符合过半机制,才能选出来一个Leader。此时机房间的网络断开了,对于机房1来说是没有影响的,Leader依然还是Leader,对于机房2来说是选不出来Leader的,此时整个集群中只有一个Leader。

情况2:如果现在集群中有6台zkServer,机房1有3台,机房2有3台,如下:
在这里插入图片描述
当机房间的网络断掉之后,机房1内的三台服务器会进行领导者选举,但是此时过半机制的条件是set.size() > 3,也就是说至少要4台zkServer才能选出来一个Leader,所以对于机房1来说它不能选出一个Leader,同样机房2也不能选出一个Leader,这种情况下整个集群当机房间的网络断掉后,整个集群将没有Leader。
而如果过半机制的条件是set.size() >= 3,那么机房1和机房2都会选出一个Leader,这样就出现了脑裂。所以我们就知道了,为什么过半机制中是大于,而不是大于等于。就是为了防止脑裂。

综上所述,可以得出结论,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要没只有1个Leader,这样就避免了脑裂问题。

5. Zookeeper主要应用场景

5.1 数据发布/订阅(配置中心)

原理解说:
数据发布/订阅,即配置中心管理,就是发布者发布数据供订阅者进行数据订阅,实现动态获取配置数据。常见的设计模式有Push模式和Pull模式。
Zookeeper作为配置中心适合以下数据(配置信息)特性:
(1)数据量通常比较小
(2)数据内容在运行时会发生动态更新
(3)集群中各机器共享,配置一致
比如机器列表信息、运行时开关配置、数据库配置信息等
案例:基于Zookeeper实现动态获取数据源配置信息,即切换数据源时,不需要重启应用服务即可获得最新的数据源信息。
实现图解:
在这里插入图片描述
代码地址:https://gitee.com/tudedong/zookeeper/tree/master/zk-config-management

5.2 命名服务

命名服务简单理解为提供名字的服务,Zookeeper提供的命名服务功能能够帮助应用系统通过一个资源引用的方式来实现对资源的定位和使用。
Zookeeper的命名服务,主要有两个应用方向:
(1)提供类似JNDI的功能,即利用Zookeeper中的树形分层结构,可以把系统中的各种服务的名称,地址以及目录信息存放在Zookeeper中,需要的时候去Zookeeper中去读取;
(2)生成分布式全局唯一ID,即利用Zookeeper中的顺序节点的特性,来生成分布式全局唯一ID,我们知道,在数据库中插入数据时,通常是要有一个ID号,在单机环境下,可以利用数据库的主键自动生成ID号,但在分布式环境下就无法使用了,可以使用UUID,但是UUID有一个缺点,就是没有什么规律很难理解。使用Zookeeper的命名服务可以生成有顺序的序列号,支持分布式的编号。
案例:基于Zookeeper实现分布式的ID生成器
代码实现图解:
在这里插入图片描述
代码地址:https://gitee.com/tudedong/zookeeper/tree/master/zk-name-service

5.3 分布式锁

在分布式环境中,经常需要对某些操作进行加锁,这时就需要分布式锁来完成。
实现分布式锁的方案:
(1)基于数据库实现分布式锁
(2)基于Redis实现分布式锁
(3)基于Zookeeper实现分布式锁
(4)基于Redission框架实现分布式锁
基于Zookeeper实现分布式锁实现思路:
这里是基于模板模式来实现分布式锁,主要步骤如下:
(1)定义模板,即锁的实现骨架,包括:定义锁、等待锁、释放锁;
(2)实现模块,即Zookeeper定义锁实现模板,包括创建临时节点来定义锁,通过事件监听来等待锁,关闭客户端连接(Session会话)来释放锁;
(3)测试:通过多个线程来创建订单序号模拟在分布式环境下独占锁使用情况。

代码地址:https://gitee.com/tudedong/zookeeper/tree/master/zk-lock

5.4 集群管理(注册中心)

Zookeeper在集群管理方面作为注册中心的应用非常广泛,即Zookeeper利用其数据节点模型和Watcher机制提供了服务的注册和发现功能,这里主要通过Zookeeper自身实现服务的注册为例来熟悉Zookeeper作为注册中心的工作逻辑。特别Zookeeper作为Dubbo框架的注册中心将在后面学习到。(具体可以阅读下面的Dubbo中的注册中心)
基于Zookeeper实现简单的服务注册中心思路:
主要步骤如下:
(1)启动服务端时,将服务端注册到Zookeeper中,以节点形式进行数据存储,节点为/zookeeper/server;
(2)客户端启动时,从Zookeeper中获取节点信息,并进行连接;
(3)下线一个服务端,利用Zookeeper的监听机制,客户端与它断开连接;
(4)某服务端上线后,利用Zookeeper的监听机制,客户端感知与它重新建立连接;
(5)测试
先分别启动两个服务端,然后启动客户端,查看连接情况;再停掉一个服务端,查看连接情况;最后再启动停掉的服务端,查看连接情况;

代码地址:https://gitee.com/tudedong/zk-register.git

五、Dubbo的高级特性

这部分根据官网的介绍,知道Dubbo提供的三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。除此之外,还有高度可扩展能力(Dubbo的SPI机制),运行期流量调度(路由规则),Dubbo服务的监控和管理(Dubbo管理控制台 dubbo-admin)等,这些实现基于它的一些高级特性。

1. Dubbo的SPI机制

1.1 SPI介绍

SPI(Service Provider Interface),即服务提供者接口的意思。简单说SPI就是一种扩展机制,我们在相应配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实现类并实例化。即用户调用接口时按照SPI的机制可以调用到想要的实现类。
常见的SPI机制有:
(1)Java的SPI机制:例如JDBC驱动加载案例,即利用Java的SPI机制,可以根据不同的数据库厂商来引入不同的JDBC驱动包;
(2)SpringBoot的SPI机制:例如自动装载配置类案例,我们可以在spring.factories中加上我们自定义的自动配置类,事件监听器或初始化器等;
(3)Dubbo的SPI机制:Dubbo基本上自身的每个功能点都提供了扩展点,从官网上可以看到,比如提供了集群扩展,路由扩展和负载均衡扩展等差不多接近30个扩展点。如果Dubbo的某个内置实现不符合我们的需求,那么我们只要利用其SPI机制将我们的实现类替换掉Dubbo的实现类即可,即通过实现同一接口的前提下,可以进行定制自己的实现类。

1.2 Dubbo中SPI机制的使用

下面使用三个项目来演示Dubbo中SPI机制的使用,一个主项目main,一个服务接口项目api,一个
服务实现项目impl。
api项目创建:
(1)导入Dubbo依赖

<dependency>
	<groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.7.5</version>
</dependency>

(2)创建接口
在接口上使用@SPI,也可以使用@SPI(“xxx”),添加默认的实现

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;

@SPI("human")
public interface HelloService {
   
    String  sayHello();
    @Adaptive
    String  sayHello(URL  url);
}

impl项目创建:
(1)导入api项目的依赖

<dependency>
	<groupId>com.lagou</groupId>
	<artifactId>dubbo_spi_demo_api</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>

(2)建立实现类,为了表达支持多个实现的目的,这里分别创建两个实现。分别为HumanHelloService和DogHelloService

import com.lagou.service.HelloService;
import org.apache.dubbo.common.URL;

public class HumanHelloService implements HelloService{
   
    @Override
    public String sayHello() {
   
        return "hello 你好";
    }

    @Override
    public String sayHello(URL url) {
   
        return  "hello url";
    }
}
public class DogHelloService implements HelloService{
   
    @Override
    public String sayHello() {
   
        return "wang wang";
    }

    @Override
    public String sayHello(URL url) {
   
        return "wang url";
    }
}

(3)SPI进行声明操作,在resources目录下创建目录META-INF/dubbo目录,在目录下创建名称为
com.lagou.dubbo.study.spi.demo.api.HelloService的文件,文件内部配置两个实现类名称和对应的全限定名:

human=com.lagou.service.impl.HumanHelloService
dog=com.lagou.service.impl.DogHelloService

main项目创建:
(1)导入接口项目依赖和实现类项目依赖

<dependency>
	<groupId>com.lagou</groupId>
	<artifactId>dubbo_spi_demo_api</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
	<groupId>com.lagou</groupId>
	<artifactId>dubbo_spi_demo_impl</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>

(2)创建DubboSpiMain
测试时,Dubbo有对其进行自我重新实现,借助ExtensionLoader查询出所有的已知实现,并且调用。

import com.lagou.service.HelloService;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;

public class DubboAdaptiveMain {
   
    public static void main(String[] args) {
   
        URL   url  = URL.valueOf("test://localhost/hello?hello.service=dog");
        HelloService  adaptiveExtension = ExtensionLoader.getExtensionLoader(HelloService.class).getAdaptiveExtension();
        String  msg = adaptiveExtension.sayHello(url);
        System.out.println(msg);
    }
}

1.3 Dubbo中SPI机制的Activate和Adaptive

Dubbo中的Activate功能,主要表示了一个扩展类被获取到的的条件,符合条件就被获取,不符合条件就不获取 ,@Activate注解在类型和方法上,根据 @Activate中的 group 、value属性来过滤 。
Dubbo中的Adaptive功能,主要解决的问题是如何动态的选择具体的扩展点。通过
getAdaptiveExtension统一对指定接口对应的所有扩展点进行封装,通过URL的方式对扩展点来进行
动态选择。 (Dubbo中所有的注册信息都是通过URL的形式进行处理的)这里同样采用相同的方式进行
实现。具体实现见上面代码。

2. Dubbo的注册中心

2.1 Dubbo的注册中心介绍

Dubbo通过注册中心实现了分布式环境中服务的注册和发现。
Dubbo 有五种注册中心的实现,分别是Multicast,Zookeeper,Redis,Simple和Nacos 。
Dubbo的注册中心实现了如下的功能:
(1)动态注册服务。服务提供者通过注册中心,把自己暴露给消费者,无须消费者逐个更新配置文件。
(2)动态发现服务。消费者动态感知新的配置,路由规则和新的服务提供者。
(3)参数动态调整。支持参数的动态调整,新参数自动更新到所有服务节点。
(4)服务统一配置。统一连接到注册中心的服务配置。

2.2 Dubbo注册中心的工作流程

Dubbo注册中心的工作流程如下:
(1)提供者(Provider)启动时,会向注册中心写入自己的元数据信息(调用方式)。
(2)消费者(Consumer)启动时,也会在注册中心写入自己的元数据信息,并且订阅服务提供者,路由和配置元数据的信息。
(3)服务治理中心(Duubo-admin)启动时,会同时订阅所有消费者,提供者,路由和配置元数据的信息。
(4)当提供者离开或者新提供者加入时,注册中心发现变化会通知消费者和服务治理中心。

2.3 Dubbo注册中心Zookeeper

在这里插入图片描述
注册中心Zookeeper工作流程:
(1)服务提供者启动时,向/dubbo/com.foo.BarService/providers目录下写入自己的URL地址;
(2)服务消费者启动时,订阅/dubbo/com.foo.BarService/providers目录下的提供者URL地址。并向/dubbo/com.foo.BarService/consumers目录下写入自己的URL地址;
(3)监控中心启动时,订阅/dubbo/com.foo.BarService目录下的所有提供者和消费者URL地址;
注册中心Zookeeper工作原理:
Zookeeper通过树形文件存储的数据节点ZNode在/dubbo/Service目录下面建立了四个目录,分别是:
(1)Providers目录下面,存放服务提供者URL和元数据。
(2)Consumers目录下面,存放消费者的URL和元数据。
(3)Routers目录下面,存放消费者的路由策略。
(4)Configurators目录下面,存放多个用于服务提供者动态配置URL元数据信息。
客户端第一次连接注册中心的时候,会获取全量的服务元数据,包括服务提供者和服务消费者以及路由和配置的信息。根据ZooKeeper客户端的特性,会在对应ZNode的目录上注册一个Watcher,同时让客户端和注册中心保持TCP长连接。如果服务的元数据信息发生变化,客户端会接受到变更通知,然后去注册中心更新元数据信息。变更时根据ZNode节点中版本变化进行。

3. Dubbo的容错策略

Dubbo的消费者在提供者数据的时候,它timeout=0代表永不超时,这样就很容易阻塞过多,为了防止这种服务雪崩的情况,Dubbo提供了一些容错处理策略。
Dubbo 主要提供了这样几种容错方式:
Failover,失败自动切换,失败时会重试其它服务器,可以设置重试次数。
Failfast, 快速失败,请求失败后快速返回异常结果,不进行重试。
Failsafe,失败安全,出现异常,直接忽略,会对请求做负载均衡。
Failback, 失败自动恢复, 请求失败后,会自动记录请求到失败队列中,通过定时线程扫描该队列,并定时重试。
Forking, 并行调用多个服务提供者,其中有一个返回,则立即返回结果。
Broadcast,广播调用所有可以连接的服务,任意一个服务返回错误,就任务调用失败。
Mock,响应失败时返回伪造的响应结果。
Available,通过遍历的方式查找所有服务列表,找到第一个可以返回结果的节点,并且返回结果。
Mergable,将多个节点请求合并进行返回。

策略名称 优点 缺点 主要应用场景
Failover 对调用者屏蔽调用失败的信息 增加RT,额外资源开销,资源浪费 对调用RT不敏感的场景
Failfast 业务快速感知失败状态进行自主决策 产生较多报错的信息 调用非幂等性接口,需要快速感知失败的场景
Failsafe 即使失败了也不会影响核心流程 对于失败的信息不敏感,需要额外的监控 旁路系统,失败不影响核心流程正确性的场景
Failback 失败自动异步重试 重试任务可能堆积 对于实时性要求不高,且不需要返回值的一些异步操作
Forking 并行发起多个调用,降低失败概率 消耗额外的机器资源,需要确保操作幂等性 资源充足,且对于失败的容忍度较低,实时性要求高的场景
Broadcast 支持对所有的服务提供者进行操作 资源消耗很大 通知所有提供者更新缓存或日志等本地资源信息

4. Dubbo的服务降级

4.1 什么是服务降级

服务降级,当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略的降低服务级别,以释放服务器资源,保证核心任务的正常运行。

4.2 为什么要服务降级

防止分布式服务发生雪崩效应,即当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机,这样下去将导致整个分布式服务都瘫痪,这就是雪崩。

4.3 Dubbo服务降级实现方式

(1)在Dubbo管理控制台配置服务降级(即屏蔽和容错)
mock=force:return+null 表示消费方对该服务的方法调用都直接返回null值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回null值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。在这里插入图片描述
(2)指定返回简单值或者null

<dubbo:reference id="xxService" check="false" interface="com.xx.XxService"
timeout="3000" mock="return null" />
<dubbo:reference id="xxService2" check="false" interface="com.xx.XxService2" 
timeout="3000" mock="return 1234" />

如果是标注,则使用@Reference(mock=“return null”) @Reference(mock=“return 简单值”),也支持 @Reference(mock=“force:return null”)

(3)使用java代码动态写入配置中心

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

(4)整合hystrix实现服务降级,见第八部分项目实战。
Dubbo服务降级的真实含义:并不是对provider进行操作,而是告诉consumer,调用服务时要做哪些动作。

5. Dubbo的负载均衡

Dubbo中为了消费者能很好的调用到对应的服务提供者,也为了保证服务调用的高可用提供了多种负载均衡策略。
Dubbo的负载均衡策略有四种(缺省为Random随机调用):
Random, 随机策略,按照权重设置随机概率做负载均衡。
RoundRobin, 轮询策略,按照公约后的权重设置轮询比例。
LeastActive, 按照活跃数调用策略,活跃度差的被调用的次数多。活跃度相同的Invoker进行随机调用。
ConsistentHash, 一致性Hash策略,相同参数的请求总是发到同一个提供者。

6. Dubbo的过滤器

6.1 Dubbo的Filter介绍

在服务的调用过程中,在服务消费者调用服务提供者的前后,都会调用Filter(过滤器)。可以针对消费者和提供者配置对应的过滤器,由于过滤器在RPC执行过程中都会被调用,所以为了提高性能需要根据具体情况配置。Dubbo框架中有自带的系统过滤器,服务提供者有11个,服务消费者有5个。

6.2 Dubbo的Filter使用

Dubbo中过滤器的使用可以通过@Activate的注释,或者配置文件实现。
举例:配置文件实现过滤器

<!-- 消费者过滤器配置 -->
<dubbo:reference filter="filter01,filter02"/>
<!-- 消费者默认过滤器配置,拦截reference过滤器 -->
<dubbo:consumer filter="filter03,filter04"/>
<!-- 提供者过滤器配置 -->
<dubbo:service filter="filter05"/>
<!-- 提供者默认过滤器配置,拦截service过滤器 -->
<dubbo:provider filter="filter06,filter07"/>

过滤器的使用遵循以下几个规则:
(1)过滤器顺序,过滤器执行是有顺序的。例如,用户定义的过滤器的过滤顺序默认会在系统过滤器之后。又例如,上图中 filter=“filter01, filter02”,filter01 过滤器执行就在filter02之前。
(2)过滤器失效,如果针对某些服务或者方法不希望使用某些过滤器,可以通过“-”(减号)的方式使该过滤器失效。例如,filter=“-filter01”。
(3)过滤器叠加,如果服务提供者和服务消费者都配置了过滤器,那么两个过滤器会被叠加生效。
由于,每个服务都支持多个过滤器,而且过滤器之间有先后顺序。因此在设计上Dubbo采用了装饰器模式,将Invoker进行层层包装,每包装一层就加入一层过滤条件。在执行过滤器的时候就好像拆开一个一个包装一样。

7. Dubbo的协议

7.1 Dubbo 支持的通信协议

(1)dubbo协议
Dubbo默认的通信协议就是dubbo协议,单一长连接,进行的是NIO异步通信,基于hessian作为序列化协议。适用场景:传输数据量小(每次请求在100kb以内),但是并发量很高。

(2)rmi协议
支持java二进制序列化,多个短连接。适用场景:消费者和提供者数量差不多的情况,适用于文件的传输,一般较少用。

(3)hessian协议
支持hessian序列化协议,多个短连接。适用场景:提供者数量比消费者数量多的情况,适用于文件的传输,跨语言传输,一般较少用。

(4)http协议
支持json序列化协议,多个短连接,采用同步传输。适用场景:提供者数量比消费者数量多的情况,数据包混合。

(5)webservice协议
支持SOAP文本序列化协议,多个短连接,采用同步传输。适用场景:系统集成和跨语言调用。

7.2 Dubbo支持的序列化协议

Dubbo 支持hession、java二进制序列化、json序列化、SOAP文本序列化多种序列化协议。但是 hessian是其默认的序列化协议。

7.3 Dubbo协议详解

Dubbo的缺省协议,使用基于mina 1.1.7和hessian 3.2.1的tbremoting交互。

  • 连接个数:单连接
  • 连接方式:长连接
  • 传输协议:TCP协议
  • 传输方式:NIO异步传输
  • 序列化协议:Hessian二进制序列化协议
  • 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo协议传输大文件或超大字符串。
  • 适用场景:常规远程服务方法调用
    Dubbo协议采用固定长度的消息头(16字节)和不定长度的消息体来进行数据传输,消息头定义了底层框架(netty)在IO线程处理时需要的信息,协议的报文格式如下:
    在这里插入图片描述
    协议体包含了传输的主要内容,它是由16字节长的报文组成,每个字节包括8个二进制位。协议详情如下:
Magic - Magic High & Magic Low (16 bits)
标识协议版本号,Dubbo 协议:0xdabb
Serialization ID (5 bit)
标识序列化类型:比如 fastjson 的值为6。
Event (1 bit)
标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。
Two Way (1 bit)
仅在 Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器
的返回值,则设置为1。
Req/Res (1 bit)
标识是请求或响应。请求: 1; 响应: 0。
Status (8 bits)
仅在 Req/Res 为0(响应)时有用,用于标识响应的状态。
	20 - OK
	30 - CLIENT_TIMEOUT
	31 - SERVER_TIMEOUT
	40 - BAD_REQUEST
	50 - BAD_RESPONSE
	60 - SERVICE_NOT_FOUND
	70 - SERVICE_ERROR
	80 - SERVER_ERROR
	90 - CLIENT_ERROR
	100 - SERVER_THREADPOOL_EXHAUSTED_ERROR
Request ID (64 bits)
标识唯一请求。类型为long。
Data Length (32 bits)
序列化后的内容长度(可变部分),按字节计数。int类型。
Variable Part
被特定的序列化类型(由序列化 ID 标识)序列化后,每个部分都是一个 byte [] 或者 byte
	如果是请求包 ( Req/Res = 1),则每个部分依次为:
		Dubbo version
		Service name
		Service version
		Method name
		Method parameter types
		Method arguments
		Attachments
	如果是响应包(Req/Res = 0),则每个部分依次为:
		返回值类型(byte),标识从服务器端返回的值类型:
		返回空值:RESPONSE_NULL_VALUE 2
		正常响应值: RESPONSE_VALUE 1
		异常:RESPONSE_WITH_EXCEPTION 0
		返回值:从服务端返回的响应bytes

注意:对于(Variable Part)变长部分,当前版本的Dubbo框架使用json序列化时,在每部分内容间额外增加了换行符作为分隔,请在Variable Part的每个part后额外增加换行符, 如:

Dubbo version bytes (换行符)
Service name bytes (换行符)
...

优点:
协议设计上很紧凑,能用1个bit表示的,不会用一个byte来表示,比如boolean类型的标识。请求、响应的header 一致,通过序列化器对content组装特定的内容,代码实现起来简单。
可以改进的点:
(1)类似于http请求,通过header就可以确定要访问的资源,而Dubbo需要涉及到用特定序列化协议才可以将服务名、方法、方法签名解析出来,并且这些资源定位符是string类型或者string数组,很容易转成bytes,因此可以组装到header中。类似于http2的header压缩,对于RPC调用的资源也可以协商出来一个int来标识,从而提升性能,如果在header上组装资源定位符的话,该功能则更易实现。
(2)通过req/res是否是请求后,可以精细定制协议,去掉一些不需要的标识和添加一些特定的标识。比如status , twoWay标识可以严格定制,去掉冗余标识。还有超时时间是作为Dubbo的attachment进行传输的,理论上应该放到请求协议的header中,因为超时是网络请求中必不可少的。提到 attachment ,通过实现可以看到attachment中有一些是跟协议content中已有的字段是重复的,比如 path和version 等字段,这些会增大协尺寸。
(3)Dubbo会将服务名com.alibaba.middleware.hsf.guide.api.param.ModifyOrderPriceParam ,转换为com/alibaba/middleware/hsf/guide/api/param/ModifyOrderPriceParam; ,理论上是不必要的,最后追加一个 ; 即可。
(4)Dubbo协议没有预留扩展字段,没法新增标识,扩展性不太好,比如新增响应上下文的功能,只有改协议版本号的方式,但是这样要求客户端和服务端的版本都进行升级,对于分布式场景很不友好。

8. Dubbo的线程池

8.1 Dubbo线程池的使用

DUBBO提供多种线程池策略,选择线程池策略并在配置文件指定threadpool属性,如下:

<dubbo:protocol name="dubbo" threadpool="fixed" threads="100" />
<dubbo:protocol name="dubbo" threadpool="cached" threads="100" />
<dubbo:protocol name="dubbo" threadpool="limited" threads="100" />
<dubbo:protocol name="dubbo" threadpool="eager" threads="100" />

8.2 Dubbo提供的4种线程池策略

这里的线程池ThreadPool也是一个扩展接口SPI,Dubbo提供了该扩展接口的一些实现,具体实现如下:
CachedThreadPool:创建一个自适应线程池,当线程处于空闲1分钟时候,线程会被回收,当有新请求到来时候会创建新线程

public class CachedThreadPool implements ThreadPool {
   

    @Override
    public Executor getExecutor(URL url) {
   
        //线程名,默认为Dubbo
        String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
        //核心线程数,默认为0
        int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);
        //可使用的最大线程数,默认为Integer.MAX_VALUE
        int threads = url.getParameter(THREADS_KEY, Integer.MAX_VALUE);
        //队列数,默认为0
        int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
        //线程存活时间,默认60s
        int alive = url.getParameter(ALIVE_KEY, DEFAULT_ALIVE);
        //根据queue决定是SynchronousQueue还是LinkedBlockingQueue,默认queue=0,所以是SynchronousQueue
        return new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS,
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)),
                //NamedInternalThreadFactory主要用于修改线程名,方便我们排查问题。
                // AbortPolicyWithReport对拒绝的任务打印日志,也是方便排查问题。
                new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }
}

LimitedThreadPool:创建一个线程池,这个线程池中线程个数随着需要量动态增加,但是数量不超过配置的阈值的个数,另外空闲线程不会被回收,会一直存在

public class LimitedThreadPool implements ThreadPool {
   

    @Override
    public Executor getExecutor(URL url) {
   
        //线程名,默认为Dubbo
        String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
        //核心线程数,默认为0
        int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);
        //可使用的最大线程数,默认为200
        int threads = url.getParameter(THREADS_KEY, DEFAULT_THREADS);
        //队列数,默认为0
        int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
        //线程存活时间,默认Long.MAX_VALUE,说明线程数只会增加不会减少
        return new ThreadPoolExecutor(cores, threads, Long.MAX_VALUE, TimeUnit.MILLISECONDS,
                //根据queue决定是SynchronousQueue还是LinkedBlockingQueue,默认queue=0,所以是SynchronousQueue
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)),
                //NamedInternalThreadFactory主要用于修改线程名,方便我们排查问题。
                // AbortPolicyWithReport对拒绝的任务打印日志,也是方便排查问题。
                new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

}

FixedThreadPool:创建一个复用固定个数线程的线程池

public class FixedThreadPool implements ThreadPool {
   

    @Override
    public Executor getExecutor(URL url) {
   
        //线程名,默认为Dubbo
        String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
        //可使用的最大线程数,默认为200
        int threads = url.getParameter(THREADS_KEY, DEFAULT_THREADS);
        //队列数,默认为0
        int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
        //线程存活时间,默认0s
        return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS,
                //根据queue决定是SynchronousQueue还是LinkedBlockingQueue,默认queue=0,所以是SynchronousQueue
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)),
                //NamedInternalThreadFactory主要用于修改线程名,方便我们排查问题。
                // AbortPolicyWithReport对拒绝的任务打印日志,也是方便排查问题。
                new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

}

EagerThreadPool:创建一个线程池,这个线程池当所有核心线程都处于忙碌状态时候,创建新的线程来执行新任务,而不是把任务放入线程池阻塞队列

public class EagerThreadPool implements ThreadPool {
   

    @Override
    public Executor getExecutor(URL url) {
   
        //线程名,默认为Dubbo
        String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
        //核心线程数,默认为0
        int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);
        //可使用的最大线程数,默认为Integer.MAX_VALUE
        int threads = url.getParameter(THREADS_KEY, Integer.MAX_VALUE);
        //队列数,默认为0
        int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
        //线程存活时间,默认60s
        int alive = url.getParameter(ALIVE_KEY
  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值