20210103 Dubbo - 拉勾教育
环境信息
- 教学版本:
2.7.5
- 最新版本:
2.7.8
- 笔记版本:
2.7.5
根据 Maven 依赖分析,Dubbo 2.7.5
依赖于
依赖 | 版本 |
---|---|
Spring | 4.3.16.RELEASE |
ZooKeeper | 3.4.13 |
Netty | 4.1.25.Final |
Gson | 2.8.5 |
Jackson | 2.9.9 |
Nacos | 1.1.1 |
Dubbo 是一款 高性能 的 Java RPC 框架
项目架构演变过程
随着互联网的发展,用户群体逐渐壮大,网站的流量成倍增长,常规的单体架构已无法满足请求压力暴增和业务的快速迭代,架构的变化势在必行。
1、单体架构
单体架构所有模块和功能都集中在一个项目中 ,部署时也是将项目所有功能部整体署到服务器中。
- 优点
- 小项目上,开发快,成本低
- 架构简单
- 易于测试
- 易于部署
- 缺点
- 大项目上,模块耦合严重,不易开发、维护,沟通成本高
- 新增业务困难
- 核心业务与边缘业务混合在一块,出现问题互相影响
2、垂直架构
根据业务把项目垂直切割成多个项目,因此这种架构称之为垂直架构。
为了避免上面提到的那些问题,我们开始做模块的垂直划分,做垂直划分的原则是基于拉勾的业务特性,核心目标,第一个是为了业务之间互不影响,第二个是在研发团队的壮大后为了提高效率,减少之间的依赖。
- 优点
- 系统拆分实现了流量分担,解决了并发问题
- 可以针对不同系统进行优化
- 方便水平扩展,负载均衡,容错率提高
- 系统间相互独立,互不影响,新的业务迭代时更加高效
- 缺点
- 服务系统之间接口调用硬编码
- 搭建集群之后,实现负载均衡比较复杂
- 服务系统接口调用监控不到位 调用方式不统一
- 服务监控不到位
- 数据库资源浪费,充斥慢查询,主从同步延迟大
3、分布式服务架构(SOA )
SOA 全称为 Service Oriented Architecture ,即面向服务的架构 。它是在垂直划分的基础上,将每个项目拆分出多个具备松耦合的服务,一个服务通常以独立的形式存在于操作系统进程中。各个服务之间通过网络调用,这使得构建在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。
我们在做了垂直划分以后,模块随之增多,系统之间的 RPC 逐渐增多,维护的成本也越来越高,一些通用的业务和模块重复的也越来越多,这个时候上面提到的接口协议不统一、服务无法监控、服务的负载均衡等问题更加突出,为了解决上面的这些问题,我们将通用的业务逻辑下沉到服务层,通过接口暴露,供其他业务场景调用。
同时引入了阿里巴巴开源的 Dubbo ,一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
对上图的解释说明:
-
分层:按照业务性质分层 ,每一层要求简单 和 容易维护
- 应用层:距离用户最近的一层,也称之为接入层,使用 Tomcat 作为 web 容器接收用户请求,使用下游的 Dubbo 提供的接口来返回数据 并且该层禁止访问数据库
- 业务服务层:根据具体的业务场景演变而来的模块,比如:简历投递、职位搜索、职位推荐等
- 基础业务层:拉勾网招聘业务的核心:账号、简历、公司、职位
- 基础服务层:这一层是与业务无关的模块,是一些通用的服务
- 这类服务的特点:请求量大、逻辑简单、特性明显、功能独立
- 消息服务(发邮件、短信、微信)
- 附件解析 50% 自己上传附件简历,需要解析成 pdf 格式
- 存储层:不同的存储类型:MySQL、MongoDB、ES 、FastDFS
-
分级:按照业务性质分层,同一层的业务也要做好分级,依据业务的重要性进行分级,按照二八定律网站 80% 的流量,都在核心功能上面 要优先保证核心业务的稳定。
-
隔离:不同性质、不同重要性的业务做好隔离,包括 业务、缓存、DB、中间件 都要做好隔离,比如 核心业务的数据库 要和活动相关的数据库隔离
-
调用:总体上调用要单向,可以跨层调用,但不能出现逆向调用
-
优点
- 服务以接口为粒度,为开发者屏蔽远程调用底层细节,使用 Dubbo 面向接口远程方法调用屏蔽了底层调用细节
- 业务分层以后架构更加清晰,并且每个业务模块职责单一,扩展性更强
- 数据隔离,权限回收,数据访问都通过接口,让系统更加稳定、安全
- 服务应用本身无状态化,这里的无状态化指的是应用本身不做内存级缓存,而是把数据存入 db,服务责任易确定,每个服务可以确定责任人,这样更容易保证服务质量和稳定
-
缺点
- 粒度控制复杂,如果没有控制好服务的粒度,服务的模块就会越来越多,就会引发 超时、分布式事务等问题
- 服务接口数量不宜控制,容易引发接口爆炸,所以服务接口建议以业务场景进行单位划分,并对相近的业务做抽象,防止接口爆炸
- 版本升级兼容困难,尽量不要删除方法、字段,枚举类型的新增字段也可能不兼容
- 调用链路长,服务质量不可监控,调用链路变长,下游抖动可能会影响到上游业务,最终形成连锁反应 服务质量不稳定,同时链路的变长使得服务质量的监控变得困难
4、微服务架构
微服务架构是一种将单个应用程序 作为一套小型服务开发的方法,每种应用程序都在其自己的进程中独立运行,并使用轻量级机制(通常是 HTTP 资源的 API )进行通信。这些服务是围绕业务功能构建的,可以通过全自动部署机制进行独立部署。这些服务的集中化管理非常少,它们可以用不同的编程语言编写,并使用不同的数据存储技术。
微服务是在SOA上做的升华 , 粒度更加细致,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”。
Dubbo 架构与实战
Dubbo 架构概述
Apache Dubbo 是一款高性能的 Java RPC 框架。其前身是阿里巴巴公司开源的一个高性能、轻量级的开源 Java RPC 框架,可以和 Spring 框架无缝集成。
Apache Dubbo |ˈdʌbəʊ|
提供了六大核心能力:
- 面向接口代理的高性能RPC调用
- 智能容错和负载均衡
- 服务自动注册和发现
- 高度可扩展能力
- 运行期流量调度
- 可视化的服务治理与运维。
Dubbo 的服务治理
服务治理( SOA governance ),企业为了确保项目顺利完成而实施的过程,包括最佳实践、架构原则、治理规程、规律以及其他决定性的因素。服务治理指的是用来管理 SOA 的采用和实现的过程。
Dubbo 要解决的需求:
- 当服务越来越多时,服务 URL 配置管理变得非常困难,F5 硬件负载均衡器的单点压力也越来越大。
- 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。
- 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器?
Dubbo 处理流程
服务注册中心 ZooKeeper
Dubbo 推荐使用 ZooKeeper 注册中心
Dubbo 管理控制台 dubbo-admin
这个项目貌似已经不在维护了,拉出来的默认分支的代码启动都会有问题,不建议在生产上使用
- master 分支可用,但是最新一次代码提交是 2018 年
- develop 分支,是默认分支,启动都会报错,可以回退几次提交达到可以启动的效果,前端使用 vue 进行了升级,界面比 master 分支好看,但是用起来很多地方很难用,还不如用 master 分支
Dubbo 配置项说明
注意:文档上的配置并不全,具体还是看代码中相关标签对应的配置类
标签 | 配置类 |
---|---|
dubbo:application | org.apache.dubbo.config.ApplicationConfig |
dubbo:argument | org.apache.dubbo.config.ArgumentConfig |
dubbo:config-center | org.apache.dubbo.config.ConfigCenterConfig |
dubbo:consumer | org.apache.dubbo.config.ConsumerConfig |
dubbo:method | org.apache.dubbo.config.MethodConfig |
dubbo:module | org.apache.dubbo.config.ModuleConfig |
dubbo:monitor | org.apache.dubbo.config.MonitorConfig |
dubbo:parameter | java.util.Map |
dubbo:protocol | org.apache.dubbo.config.ProtocolConfig |
dubbo:provider | org.apache.dubbo.config.ProviderConfig |
dubbo:reference | org.apache.dubbo.config.ReferenceConfig |
dubbo:registry | org.apache.dubbo.config.RegistryConfig |
dubbo:service | org.apache.dubbo.config.ServiceConfig |
Dubbo 高级实战
Dubbo 中的 SPI
SPI
SPI 全称为 ( Service Provider Interface ) ,是 JDK 内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现,简单来说,它就是一种动态替换发现的机制。使用 SPI 机制的优势是实现解耦,使得第三方服务模块的装配控制逻辑与调用者的业务代码分离。
Java 中如果想要使用 SPI 功能,先提供标准服务接口,然后再提供相关接口实现和调用者。这样就可以通过 SPI 机制中约定好的信息进行查询相应的接口实现。
SPI 遵循如下约定:
- 当服务提供者提供了接口的一种具体实现后,在
META-INF/services
目录下创建一个以 “接口全限定名” 为命名的文件,内容为实现类的全限定名; - 接口实现类所在的 jar 包放在主程序的 classpath 中;
- 主程序通过
java.util.ServiceLoader
动态装载实现模块,它通过扫描META-INF/services
目录下的配置文件找到实现类的全限定名,把类加载到JVM; - SPI 的实现类必须携带一个无参构造方法;
代码示例
-
新建项目 java_spi_demo
-
新建 Module,java_spi_demo_api、java_spi_demo_impl、java_spi_demo_main
-
在 java_spi_demo_api 中定义服务接口,
com.lagou.service.HelloService
public interface HelloService { String sayHello(); }
-
在 java_spi_demo_impl 中添加对 api 模块的依赖
<dependency> <groupId>com.lagou</groupId> <artifactId>java_spi_demo_api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
在 java_spi_demo_impl 中新建接口实现,
com.lagou.service.impl.DogHelloService
public class DogHelloService implements HelloService { @Override public String sayHello() { return "wang wang"; } }
-
在 java_spi_demo_impl 中新建接口实现,
com.lagou.service.impl.HumanHelloService
public class HumanHelloService implements HelloService { @Override public String sayHello() { return "hello 你好"; } }
-
在 java_spi_demo_impl 中,新建资源文件,
META-INF/services/com.lagou.service.HelloService
,内容为:com.lagou.service.impl.DogHelloService com.lagou.service.impl.HumanHelloService
-
在 java_spi_demo_main 中添加对 api、impl 模块的依赖
<dependency> <groupId>com.lagou</groupId> <artifactId>java_spi_demo_api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.lagou</groupId> <artifactId>java_spi_demo_impl</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
在 java_spi_demo_main 中测试功能,
com.lagou.test.JavaSpiMain
public class JavaSpiMain { public static void main(String[] args) { final ServiceLoader<HelloService> helloServices = ServiceLoader.load(HelloService.class); for (HelloService helloService : helloServices) { System.out.println(helloService.getClass().getName() + ":" + helloService.sayHello()); } } }
结果为:
com.lagou.service.impl.DogHelloService:wang wang com.lagou.service.impl.HumanHelloService:hello 你好
Dubbo 中的 SPI
Dubbo 中大量的使用了 SPI 来作为扩展点,通过实现同一接口的前提下,可以进行定制自己的实现类。比如比较常见的协议,负载均衡,都可以通过 SPI 的方式进行定制化,自己扩展。 Dubbo 中已经存在的所有已经实现好的扩展点。
dubbo-xxx.jar/META-INF/dubbo.internal
下有大量的 SPI 扩展点
Dubbo 自己做 SPI 的目的:
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源
- 如果有扩展点加载失败,则所有扩展点无法使用
- 提供了对扩展点包装的功能( Adaptive ),并且还支持通过 set 的方式对其他的扩展点进行注入
代码示例 - 使用 Dubbo 的 SPI 实现
使用 Dubbo 的 SPI 对上面的 JDK 的 SPI 进行改造
-
新建项目 dubbo_spi_demo
-
新建 Module,dubbo_spi_demo_api、dubbo_spi_demo_impl、dubbo_spi_demo_main
-
在 dubbo_spi_demo_api 增加依赖
<dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>2.7.5</version> </dependency>
-
在 dubbo_spi_demo_api 中定义服务接口,
com.lagou.service.HelloService
@SPI("human") // @SPI public interface HelloService { String sayHello(); @Adaptive("xxh") // @Adaptive String sayHello(URL url); }
@SPI
value
属性指定接口默认的扩展- 当
@Adaptive
的value
属性为空时,使用@SPI
的value
属性指定的默认扩展
@Adaptive
- 需要配合 URL 参数使用
value
属性对应参数 url 参数中 key 的值,如果value
属性为空,则以如下规则寻找对应参数org.apache.dubbo.xxx.YyyInvokerWrapper
对应yyy.invoker.wrapper
-
在 dubbo_spi_demo_impl 中添加对 api 模块的依赖
<dependency> <groupId>com.lagou</groupId> <artifactId>dubbo_spi_demo_api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
在 dubbo_spi_demo_impl 中新建接口实现,
com.lagou.service.impl.DogHelloService
public class DogHelloService implements HelloService { @Override public String sayHello() { return "wang wang"; } @Override public String sayHello(URL url) { return "wang url"; } }
-
在 dubbo_spi_demo_impl 中新建接口实现,
com.lagou.service.impl.HumanHelloService
public class DogHelloService implements HelloService { @Override public String sayHello() { return "wang wang"; } @Override public String sayHello(URL url) { return "wang url"; } }
-
在 dubbo_spi_demo_impl 中,新建资源文件,
META-INF/dubbo/com.lagou.service.HelloService
,内容为:human=com.lagou.service.impl.HumanHelloService dog=com.lagou.service.impl.DogHelloService
-
在 dubbo_spi_demo_main 中添加对 api、impl 模块的依赖
<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>
-
在 dubbo_spi_demo_main 中测试 SPI 功能,
com.lagou.DubboSpiMain
public class DubboSpiMain { public static void main(String[] args) { // 获取扩展加载器 ExtensionLoader<HelloService> extensionLoader = ExtensionLoader.getExtensionLoader(HelloService.class); // 遍历所有的支持的扩展点 META-INF.dubbo Set<String> extensions = extensionLoader.getSupportedExtensions(); for (String extension : extensions) { String result = extensionLoader.getExtension(extension).sayHello(); System.out.println(extension + " ============> " + result); } System.out.println("=========================="); // extensionLoader.getExtension() HelloService defaultExtension = extensionLoader.getDefaultExtension(); System.out.println(defaultExtension.sayHello()); } }
结果为:
dog ============> wang wang human ============> hello 你好 ========================== hello 你好
-
在 dubbo_spi_demo_main 中测试 Adaptive 功能,
com.lagou.DubboAdaptiveMain
public class DubboAdaptiveMain { public static void main(String[] args) { URL url = URL.valueOf("test://localhost/hello"); // URL url = URL.valueOf("test://localhost/hello?xxh=dog"); // 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); } }
结果为:
hello url
代码示例 - 使用 Dubbo 的 SPI 实现扩展 Filter 接口
与很多框架一样, Dubbo 也存在拦截(过滤)机制,可以通过该机制在执行目标程序前后执行我们指定的代码。
Dubbo 的 Filter 机制,是专门为服务提供方和服务消费方调用过程进行拦截设计的,每次远程方法执行,该拦截都会被执行。这样就为开发者提供了非常方便的扩展性,比如为 Dubbo 接口实现 ip 白名单功能、监控功能 、日志记录等。
-
新建 Module,dubbo_spi_filter
-
实现
org.apache.dubbo.rpc.Filter
接口,com.lagou.filter.DubboInvokeFilter
@Activate(group = {CommonConstants.CONSUMER/*, CommonConstants.PROVIDER*/}) public class DubboInvokeFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { long startTime = System.currentTimeMillis(); // 执行方法 Result result = invoker.invoke(invocation); long endTime = System.currentTimeMillis(); System.out.println("invoke time:" + (endTime - startTime) + "毫秒"); return result; } }
-
新建资源文件,
META-INF/dubbo/org.apache.dubbo.rpc.Filter
,内容为timeFilter=com.lagou.filter.DubboInvokeFilter
-
引入此 Module 作为依赖,在调用 Dubbo 接口时,便会自动拦截
<dependency> <groupId>com.lagou</groupId> <artifactId>dubbo_spi_filter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
负载均衡策略
- 在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为
random
随机调用。 - 配置负载均衡策略,既可以在服务提供者一方配置,也可以在服务消费者一方配置
XML 配置如下,也可以使用对应的 属性配置 或 注解配置等:
- 服务端服务级别
<dubbo:service interface="..." loadbalance="roundrobin" />
- 客户端服务级别
<dubbo:reference interface="..." loadbalance="roundrobin" />
- 服务端方法级别
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
- 客户端方法级别
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>
自定义负载均衡器
-
新建 Module,dubbo-spi-loadbalance
-
实现
org.apache.dubbo.rpc.cluster.LoadBalance
接口,com.laogu.loadbalance.OnlyFirstLoadbalancer
public class OnlyFirstLoadbalancer implements LoadBalance { @Override public <T> Invoker<T> select(List<Invoker<T>> list, URL url, Invocation invocation) throws RpcException { // 所有的服务提供者 按照IP + 端口排序 选择第一个 return list.stream().sorted((i1, i2) -> { final int ipCompare = i1.getUrl().getIp().compareTo(i2.getUrl().getIp()); if (ipCompare == 0) { return Integer.compare(i1.getUrl().getPort(), i2.getUrl().getPort()); } return ipCompare; }).findFirst().get(); } }
-
新建资源文件,
META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance
,内容为onlyFirst=com.laogu.loadbalance.OnlyFirstLoadbalancer
-
引入此 Module 作为依赖
<dependency> <groupId>com.lagou</groupId> <artifactId>dubbo-spi-loadbalance</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
异步调用
- Dubbo 不只提供了堵塞式的的同步调用,同时提供了异步调用的方式。这种方式主要应用于提供者接口响应耗时明显,消费者端可以利用调用接口的时间去做一些其他的接口调用,利用 Future 模式来异步等待和获取结果即可。这种方式可以大大的提升消费者端的利用率。
- 目前这种方式可以通过 XML 的方式进行引入。
- 需要特别说明的是,该方式的使用,请确保 Dubbo 的版本在 2.5.4 及以后的版本使用。 原因在于在 2.5.3 及之前的版本使用的时候,会出现异步状态传递问题。
- 比如我们的服务调用关系是
A - > B - > C
, 这时候如果 A 向 B 发起了异步请求,在错误的版本时, B 向 C 发起的请求也会连带的产生异步请求。这是因为在底层实现层面,他是通过RPCContext
中的attachment
实现的。在 A 向 B 发起异步请求时,会在attachment
中增加一个异步标示字段来表明异步等待结果。 B 在接受到 A 中的请求时,会通过该字段来判断是否是异步处理。但是由于值传递问题, B 向 C 发起时同样会将该值进行传递,导致 C 误以为需要异步结果,导致返回空。这个问题在 2.5.4 及以后的版本进行了修正。
代码示例
-
使用 xml 配置消费端,
src/main/resources/consumer.xml
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <dubbo:application name="service-consumer"/> <dubbo:registry address="zookeeper://127.0.0.1:2181"/> <dubbo:reference id="helloService" interface="com.lagou.service.HelloService" timeout="3000"> <dubbo:method name="sayHello" async="true"></dubbo:method> </dubbo:reference> <context:component-scan base-package="com.lagou.bean"></context:component-scan> </beans>
-
测试异步功能,
com.lagou.XMLConsumerMain
public class XMLConsumerMain { public static void main(String[] args) throws IOException { ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("consumer.xml"); HelloService service = app.getBean(HelloService.class); while (true) { System.in.read(); try { String hello = service.sayHello("world", 2000); // 利用Future 模式来获取 Future<Object> future = RpcContext.getContext().getFuture(); System.out.println("result :" + hello); System.out.println("future result:" + future.get()); } catch (Exception e) { e.printStackTrace(); } } } }
结果为:
result :null future result:hello:world
线程池
默认使用 fixed
fixed=org.apache.dubbo.common.threadpool.support.fixed.FixedThreadPool
路由规则
路由是决定一次请求中需要发往目标机器的重要判断,通过对其控制可以决定请求的目标机器。我们可以通过创建这样的规则来决定一个请求会交给哪些服务器去处理。
服务动态降级
服务降级,当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略的降低服务级别,以释放服务器资源,保证核心任务的正常运行。
为什么要使用服务降级,这是防止分布式服务发生雪崩效应,什么是雪崩?就是蝴蝶效应,当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机,这样下去将导致整个分布式服务都瘫痪,这就是雪崩。
实现方式
-
在 dubbo admin 管理控制台配置服务降级
屏蔽和容错
-
屏蔽:
mock=force:return+null
表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可
用时对调用方的影响。 -
容错:
mock=fail:return+null
表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳
定时对调用方的影响。
-
-
指定返回简单值或者 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" />
或使用对应的其他方式的配置