基础知识
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。
分布式系统(distributed system)是建立在网络之上的软件系统。
《分布式系统原理与范型》定义:
“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”
发展演变:
RPC
RPC(Remote Procedure Call)是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
RPC基本原理图:
在各个服务器组件建立连接的速度和系列化/反系列化的速度决定了RPC框架的性能好坏。
RPC的两大模块:通讯和系列化。
dubbo
Apache Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
官方网站:http://dubbo.apache.org/zh-cn/
dubbo的设计架构:
dubbo的特性:
注册中心
dubbo可以使用以下的支持中心,官方推荐使用zookeeper注册中心。
下载zookeeper
zookeeper官网:https://zookeeper.apache.org/
下载后解压:
新建data文件夹:
进入conf目录:
然后打开zoo.cfg文件,修改配置:
把里面文件的存放文件路径改为刚刚建的data文件夹里:
dataDir=../data
打开终端,输入cd空格,然后把解压的目录拉到终端里,回车进入zookeeper目录,然后进入bin目录,输入./zkServer.sh start
启动服务器
./zkCli.sh
启动客户端
测试:
中途跑去补了一下zookeeper的理论,笔记写得不怎么样,有兴趣就看看。(Zookeeper入门学习笔记)
zookeeper的基本命令
ls /
:查看根目录
quit
:退出客户端
./zkServer.sh stop
:停止服务器
监控中心
dubbo本身并不是一个服务软件。它其实就是一个jar包能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。所以你不用在Linux上启动什么dubbo服务。
但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序,不过这个监控即使不装也不影响使用。
下载dubbo-admin
图形化的服务管理页面;安装时需要指定注册中心地址,即可从注册中心中获取到所有的提供者/消费者进行配置管理。
dubbo在github的网址:https://github.com/apache/dubbo
点进Dubbo-Admin后,默认是develop
分支,我们需要切换到master分支。
下载后解压
查看/dubbo-admin-master/dubbo-admin-master/dubbo-admin/src/main/resources目录下的application.properties文件。
从终端进入到dubbo-admin目录,输入mvn clean package
清理后在打包。(前提需要安装配置好maven,安装maven的步骤)
然后在dubbo-admin目录下就会生成一个target目录。
dubbo-admin-0.0.1-SNAPSHOT.jar
就是刚刚打包生成的jar包。
终端进入target目录,输入java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
运行。
启动成功,默认是7001端口。(可以在application.properties修改)
然后在浏览器输入http://localhost:7001/
,账号密码默认都是root。
在这里就可以看到开启的zk的服务器和客户端。
按control+c停止项目。
可以把jar放到桌面或任意点击到的地方,方便启动。
编写服务提供者和消费者
新建项目
创建一个空项目
新建模块
-
创建服务提供者模块:
创建一个空的maven项目
-
再创建一个服务消费者模块:
同样创建一个空的maven项目:
-
再创建一个公共的API模块(用于存放bean、接口、异常等公共类)
同样创建一个空的maven项目:
-
gmall-interface公共的API模块
在main/java包下创建com/angenin/bean/UserAddress
/**
* 用户地址
*/
public class UserAddress implements Serializable {
private Integer id;
private String userAddress; //用户地址
private String userId; //用户id
private String consignee; //收货人
private String phoneNum; //电话号码
private String isDefault; //是否为默认地址 Y-是 N-否
//有参无参构造器,get/set方法
在angenin包下新建service/UserService接口
/**
* 用户服务
*/
public interface UserService {
/**
* 按照用户id返回所有的收货地址
* @param userId
* @return
*/
public List<UserAddress> getUserAddressList(String userId);
}
在service包下再创建OrderService接口
public interface OrderService {
/**
* 初始化订单
* @param userId
*/
public List<UserAddress> initOrder(String userId);
}
复制这个公共API的依赖坐标
- user-service-provider服务提供者
在pom.xml中引入上面公共API的依赖,上面复制的依赖要放到< dependency >标签里。
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>gmall-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
在main/java包下新建com/angenin/service/impl/UserServiceImpl
public class UserServiceImpl implements UserService {
@Override
public List<UserAddress> getUserAddressList(String userId) {
UserAddress address1 = new UserAddress(1, "北京市昌平区宏福科技园综合楼3层", "1", "李老师", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市宝安区西部硅谷大厦B座3层(深圳分校)", "1", "王老师", "010-56253825", "N");
return Arrays.asList(address1,address2);
}
}
引入dubbo
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.7</version>
</dependency>
因为我们使用的注册中心是zookeeper,所以还需要导入操作zookeeper的客户端。(dubbo2.0以后使用的是curator)
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
然后在resources下新建一个名为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://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo
http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 1. 指定当前服务/应用的名字(同样的服务名字相同,不要和别的服务同名,一般都是项目名) -->
<dubbo:application name="user-com.angenin.service-provider" />
<!-- 2. 指定注册中心的位置 -->
<!-- <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181"/>-->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!-- 3. 指定通信规则(通信协议?通信端口)服务提供者和消费者之间的通信 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 4. 暴露服务(interface暴露的是服务的接口,ref是指向服务真正的实现对象) -->
<dubbo:service interface="com.angenin.service.UserService" ref="userServiceImpl" />
<!-- 服务的实现 -->
<bean id="userServiceImpl" class="com.angenin.service.impl.UserServiceImpl"/>
</beans>
然后在angenin包下新建MainApplication.java(进行测试)
public class MainApplication {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("provider.xml");
context.start();
System.in.read(); // 按任意键退出
}
}
运行main方法
出现错误:
引入netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.48.Final</version>
</dependency>
然后重新运行main方法,到管理页面就可以看到提供者提供的服务。(保存此main方法一直运行着,为消费者提供服务)
- order-service-consumer服务消费者
在pom.xml中引入上面公共API的依赖和dubbo、zookeeper客户端还有netty的依赖。
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>gmall-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.48.Final</version>
</dependency>
</dependencies>
在main/java包下新建com/angenin/service/impl/OrderServiceImpl
@Service //是spring的注解,不是dubbo的
public class OrderServiceImpl implements OrderService {
@Autowired
UserService userService;
@Override
public List<UserAddress> initOrder(String userId) {
System.out.println("用户id:" + userId);
//1、查询用户的收货地址
List<UserAddress> addressList = userService.getUserAddressList(userId);
for (UserAddress userAddress : addressList) {
System.out.println(userAddress.getUserAddress());
}
return addressList;
}
}
在resources目录下新建名为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://dubbo.apache.org/schema/dubbo"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.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">
<context:component-scan base-package="com.angenin.service.impl"/>
<dubbo:application name="order-service-consumer" />
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!-- 声明需要调用的远程服务的接口(生成远程服务代理)interface引用提供者暴露的接口 -->
<dubbo:reference id="userService" interface="com.angenin.service.UserService" />
</beans>
在angenin包下新建MainApplication.java(进行测试)
public class MainApplication {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("consumer.xml");
OrderService bean = context.getBean(OrderService.class);
bean.initOrder("1");
System.out.println("调用完成");
System.in.read();
}
}
运行main方法
在管理页面可以看到服务的提供者和消费者。
dubbo-monitor-simple
简单的监控中心。
使用终端进入dubbo-monitor-simple目录
用mvn package
命令进行打包
打包完成后,生成target目录,因为我们需要修改配置,所以不能直接使用java -jar运行生成的jar包,需要解压jar包对应的.tar.gz压缩包。
进入解压后目录的conf目录,确保dubbo.properties配置文件的注册中心地址正确即可。(8080端口容易被占用,建议修改,我改成了8090)
然后用终端进入assembly.bin目录,输入./start.sh
启动。
如果用http://localhost:8080
访问不了,就用本机的ip加端口号访问。
在管理页面可以看到启动的simple-monitor。
如果想让其能监控到服务的调用信息,需要在项目中进行一些配置。
到order-service-consumer项目的consumer.xml文件中添加:
<!--表示从注册中心发现监控中心地址-->
<dubbo:monitor protocol="registry"/>
<!-- 直连监控中心 -->
<!-- <dubbo:monitor address="192.168.0.106:7070"/>-->
在user-service-provider项目的provider.xml中也要添加:
<!--连接监控中心-->
<dubbo:monitor protocol="registry"/>
然后重新启动提供者和消费者的main方法,然后在监控中心的管理页面就可以看到了。(需要先启动提供者)
与springboot进行整合
新建模块
不选模块,直接创建,然后再新建一个模块
选择web模块
- boot-user-server-provider的provider
把user-service-provider的service包复制到boot-user-server-provider的provider包下
然后在pom.xml中引入公共API(gmall-interface)的依赖坐标
<dependency>
<groupId>org.example</groupId>
<artifactId>gmall-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
导入dubbo-starter依赖和zookeeper的客户端依赖
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.6</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
在resources目录下的application.properties对dubbo进行配置
#根据之前的xml配置的内容进行相应的配置
dubbo.application.name=boot-user-server-provider
dubbo.registry.address=127.0.0.1:2181
dubbo.registry.protocol=zookeeper
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
dubbo.monitor.protocol=registry
#暴露服务不需要在这里配置,只需要在要暴露的类上加上dubbo的@Service注解
在UserServiceImpl类上加上两个@Service注解
@org.apache.dubbo.config.annotation.Service //暴露服务
@Service //添加到容器中
在主配置类上加上@EnableDubbo
,开启基于注解的dubbo。
启动项目后,管理页面出现服务提供者
- boot-order-service-consumer
在pom.xml中引入依赖(同上)
<dependency>
<groupId>org.example</groupId>
<artifactId>gmall-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.6</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
在resources目录下的application.properties对dubbo进行配置
server.port=8081 #把项目的端口号改为8081,不改的话会和其他别的冲突
dubbo.application.name=boot-order-service-consumer
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.monitor.protocol=registry
#声明需要调用的远程服务的接口可以不需要在这里配置了,可以直接使用@Reference远程调用
在主配置类上加上@EnableDubbo
注解。
把order-service-consumer的server包放到boot-order-service-consumer的consumer包下。
然后在consumer包下新建controller/OrderController,用于查询。
@RestController
public class OrderController {
@Autowired
@Reference //使用dubbo的@Reference注解远程调用
OrderService orderService;
@GetMapping("/initOrder")
public List<UserAddress> initOrder(@RequestParam("userId") String userId){
return orderService.initOrder(userId);
}
}
启动项目
dubbo配置
dubbo的配置参数
官方文档:http://dubbo.apache.org/zh-cn/docs/user/references/xml/introduction.html
配置来源
首先,从Dubbo支持的配置来源说起,默认有四种配置来源:
- JVM System Properties,-D参数
- Externalized Configuration,外部化配置
- ServiceConfig、ReferenceConfig等编程接口采集的配置
- 本地配置文件dubbo.properties
覆盖关系
下图展示了配置覆盖关系的优先级,从上到下优先级依次降低:
配置种类
在官方文档的实例里列举了所有配置种类:启动时检查、集群容错、负载均衡、线程模型、直连提供者、只订阅、只注册、静态服务、多协议等等。
启动时检查
消费者启动后,会到注册中心寻找需要用到的服务(即使还没使用,但远程调用了),没找到就会报错(默认 check=“true”),把check改为false就可以跳过这个检查。
不启动user-service-provider提供者,直接启动order-service-consumer消费者后,报错:
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'orderServiceImpl': Unsatisfied dependency expressed through field 'userService'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: Failed to check the status of the service com.angenin.service.UserService. No provider available for the service com.angenin.service.UserService from the url zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=order-service-consumer&dubbo=2.0.2&interface=com.angenin.service.UserService&methods=getUserAddressList&pid=39633®ister.ip=192.168.0.106&side=consumer×tamp=1590999460690 to the consumer 192.168.0.106 use dubbo version 2.6.7
注释掉消费者中使用服务的代码,然后在consumer.xml配置文件的dubbo:reference标签中加入check="false"
。(@Reference中也有check属性可以设置)
再次运行消费者的main方法,没有报错:
加上check=false后,启动后不会去注册中心检查,只有使用到服务并且没找到才会报错。
也可以使用
<!-- 配置当前消费者的统一规则:所有的服务都不检查 -->
<dubbo:consumer check="false"/>
关闭注册中心启动时检查 (注册订阅失败时报错):
<dubbo:registry check="false" />
springboot中的dubbo.properties(application.properties)
dubbo.reference.com.foo.BarService.check=false
dubbo.reference.check=false
dubbo.consumer.check=false
dubbo.registry.check=false
timeout属性
timeout=“xxxx”(返回时间,单位毫秒,默认1000毫秒),超过这个时间还没返回就会立即中止连接,防止阻塞,造成性能下降。
我们给order-service-consumer的配置文件中dubbo:reference加上timeout="3000"
并且把main方法中调用服务的注释去掉。
然后给user-service-provider的UserServiceImpl类的getUserAddressList方法加上Thread.sleep(4000);
。
启动提供者,在启动消费者。
Exception in thread "main" com.alibaba.dubbo.rpc.RpcException: Failed to invoke the method getUserAddressList in the service com.angenin.service.UserService. Tried 3 times of the providers [192.168.0.106:20880] (1/1) from the registry 127.0.0.1:2181 on the consumer 192.168.0.106 using the dubbo version 2.6.7. Last error is: Invoke remote method timeout. method: getUserAddressList, provider: dubbo://192.168.0.106:20880/com.angenin.service.UserService?anyhost=true&application=order-service-consumer&bean.name=com.angenin.service.UserService&check=false&default.check=false&dubbo=2.0.2&generic=false&interface=com.angenin.service.UserService&methods=getUserAddressList&monitor=dubbo%3A%2F%2F127.0.0.1%3A2181%2Fcom.alibaba.dubbo.registry.RegistryService%3Fapplication%3Dorder-service-consumer%26dubbo%3D2.0.2%26pid%3D40284%26protocol%3Dregistry%26refer%3Dapplication%253Dorder-service-consumer%2526dubbo%253D2.0.2%2526interface%253Dcom.alibaba.dubbo.monitor.MonitorService%2526pid%253D40284%2526register.ip%253D192.168.0.106%2526timestamp%253D1591000941631%26registry%3Dzookeeper%26timestamp%3D1591000941614&pid=40284®ister.ip=192.168.0.106&remote.timestamp=1591000753808&side=consumer&timeout=3000×tamp=1591000936534, cause: Waiting server-side response timeout. start time: 2020-06-01 16:42:45.373, end time: 2020-06-01 16:42:48.378, client elapsed: 1 ms, server elapsed: 3004 ms, timeout: 3000 ms, request: Request [id=2, version=2.0.2, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=getUserAddressList, parameterTypes=[class java.lang.String], arguments=[1], attachments={path=com.angenin.service.UserService, interface=com.angenin.service.UserService, version=0.0.0, timeout=3000}]], channel: /192.168.0.106:58227 -> /192.168.0.106:20880
把timeout="3000
改为5000,再启动,成功查出数据。
dubbo:method给指定方法的设置属性。(优先于在其他标签配置)
<dubbo:method name="getUserAddressList" timeout="2000"/>
需要加到dubbo:reference标签中:
再次重新启动,因为我们在这里给getUserAddressList方法的返回时间设置为2秒,所以保错。
Exception in thread "main" com.alibaba.dubbo.rpc.RpcException: Failed to invoke the method getUserAddressList in the service com.angenin.service.UserService. Tried 3 times of the providers [192.168.0.106:20880] (1/1) from the registry 127.0.0.1:2181 on the consumer 192.168.0.106 using the dubbo version 2.6.7. Last error is: Invoke remote method timeout. method: getUserAddressList, provider: dubbo://192.168.0.106:20880/com.angenin.service.UserService?anyhost=true&application=order-service-consumer&bean.name=com.angenin.service.UserService&check=false&default.check=false&dubbo=2.0.2&generic=false&getUserAddressList.timeout=2000&interface=com.angenin.service.UserService&methods=getUserAddressList&monitor=dubbo%3A%2F%2F127.0.0.1%3A2181%2Fcom.alibaba.dubbo.registry.RegistryService%3Fapplication%3Dorder-service-consumer%26dubbo%3D2.0.2%26pid%3D40647%26protocol%3Dregistry%26refer%3Dapplication%253Dorder-service-consumer%2526dubbo%253D2.0.2%2526interface%253Dcom.alibaba.dubbo.monitor.MonitorService%2526pid%253D40647%2526register.ip%253D192.168.0.106%2526timestamp%253D1591001688537%26registry%3Dzookeeper%26timestamp%3D1591001688528&pid=40647®ister.ip=192.168.0.106&remote.timestamp=1591000753808&side=consumer&timeout=5000×tamp=1591001683452, cause: Waiting server-side response timeout. start time: 2020-06-01 16:55:10.215, end time: 2020-06-01 16:55:12.220, client elapsed: 3 ms, server elapsed: 2002 ms, timeout: 2000 ms, request: Request [id=2, version=2.0.2, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=getUserAddressList, parameterTypes=[class java.lang.String], arguments=[1], attachments={path=com.angenin.service.UserService, interface=com.angenin.service.UserService, version=0.0.0, timeout=5000}]], channel: /192.168.0.106:58357 -> /192.168.0.106:20880
xml中各个标签的覆盖关系:(其他属性遵循这个优先级)
- 方法级优先,接口级次之,全局配置再次之。(精确优先)
- 如果级别一样,则消费方优先,提供方次之。(同级别时消费者优先,如果提供者级别高(方法级别),消费者级别低(全局级别),那么提供者优先)
retries属性
retries=“xx”(重试次数,默认2次(不包含第一次调用,即第一次调用失败后的重试次数)),设置连接失败后给予重试的次数,如果是0表示不重试。
幂等:设置重试次数。(查询、删除、修改(重复多次效果都一样的操作))
非幂等:不能设置重试次数。(新增(每次都会做改变的操作))
order-service-consumer的dubbo:reference标签中加入retries="3"
。
然后在user-service-provider的UserServiceImpl类getUserAddressList方法里加上System.out.println("请求次数");
。(此时UserServiceImpl每次会睡3秒,而消费者只给2秒的返回时间)
重启提供者,再启动消费者。
消费者方因为返回时间超时报错
提供者方打印了四次(也说明了,retries不包含第一次连接)
多版本
灰度发布:发布新版本时,先让一部分用户使用新版本,其他用户进行使用旧版本,等新版本用了一段时间后,没出现问题,再全部改为使用新版本。
在user-service-provider中复制一份UserServiceImpl,让旧版本打印old,新版本打印new。
在配置文件修改和添加新旧版本的服务
<dubbo:service interface="com.angenin.service.UserService" ref="userServiceImpl01" version="1.0.0" />
<bean id="userServiceImpl01" class="com.angenin.service.impl.UserServiceImpl"/>
<!-- 新版本-->
<dubbo:service interface="com.angenin.service.UserService" ref="userServiceImpl02" version="2.0.0" />
<bean id="userServiceImpl02" class="com.angenin.service.impl.UserServiceImpl2"/>
然后就可以在order-service-consumer配置文件中的dubbo:reference标签中用version="2.0.0"
指定使用新版本。
启动提供者,再启动消费者。
用version="*"
随机使用。
本地存根
消费者远程调用时,本地存根先对调用的方法或参数进行判断,也可以增加一些功能,然后再由本地存根决定是否要进行远程调用。
创建本地存根:
在gmall-interface的server包下新建impl/UserServiceStub的UserService的实现类(一般都是放在公共的API中)
public class UserServiceStub implements UserService {
//新建调用的接口对象
private final UserService userService;
//必须要有有参构造器,由dubbo传入userService远程的代理对象
public UserServiceStub(UserService userService) {
this.userService = userService;
}
@Override
public List<UserAddress> getUserAddressList(String userId) throws Exception {
System.out.println("UserServiceStub");
if(!"".equals(userId)){
return userService.getUserAddressList(userId);
}
return null;
}
}
然后在order-service-consumer的consumer.xml配置文件中的dubbo:reference标签里添加stub="com.angenin.service.impl.UserServiceStub"
(UserServiceStub的全类名)
启动提供者,启动消费者,本地存根有被调用:
springboot与dubbo的三种整合方式
-
导入dubbo-starter和zookeeper客户端,给主配置类加上
@EnableDubbo
注解,在application.properties中配置属性,使用@Service
暴露服务,使用@Reference
引用服务。 -
导入dubbo-starter和zookeeper客户端,使用dubbo的xml配置文件,在主配置类上加上
@ImportResource(locations = "classpath:provider.xml")
注解,把provider.xml配置文件导入。(主配置类就不需要加@EnableDubbo注解,也不用使用@Service和@Reference,因为已经在xml中配置了) -
使用配置类,把每个组件手动创建到容器中,在主配置类上添加
@EnableDubbo(scanBasePackages = "com.example.provider.config")
注解,开启基于注解的Dubbo并扫描dubbo的配置类,使用@Service
暴露服务,使用@Reference
引用服务。
dubbo的配置类
@Configuration //往容器中注册 xxxConfig 代替 dubbo:xxx 标签
public class MyDubboConfig {
@Bean //替代dubbo:application标签
public ApplicationConfig applicationConfig(){
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("boot-user-server-provider");
return applicationConfig;
}
@Bean //替代dubbo:registry
public RegistryConfig registryConfig(){
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
//也可以
// registryConfig.setProtocol("zookeeper");
// registryConfig.setAddress("127.0.0.1:2181");
return registryConfig;
}
@Bean //替代dubbo:protocol
public ProtocolConfig protocolConfig(){
ProtocolConfig protocolConfig = new ProtocolConfig();
protocolConfig.setName("dubbo");
protocolConfig.setPort(20880);
return protocolConfig;
}
@Bean //替代dubbo:service 泛型为暴露的接口
public ServiceConfig<UserService> userServiceConfig(UserService userService){
ServiceConfig<UserService> userServiceConfig = new ServiceConfig<>();
userServiceConfig.setInterface(UserService.class);
//需要将提供者UserService的实现类添加到容器中(这个组件在创建时就会从容器中寻找并传入)
userServiceConfig.setRef(userService);
userServiceConfig.setVersion("1.0.0");
//配置每一个method的信息(dubbo:method标签)
MethodConfig methodConfig = new MethodConfig();
methodConfig.setName("getUserAddressList");
methodConfig.setTimeout(2000);
//将method的设置关联到service配置中
List<MethodConfig> methods = new ArrayList<>();
methods.add(methodConfig);
userServiceConfig.setMethods(methods);
return userServiceConfig;
}
@Bean
public MonitorConfig monitorConfig(){
MonitorConfig monitorConfig = new MonitorConfig();
monitorConfig.setProtocol("registry");
return monitorConfig;
}
}
高可用
zookeeper宕机与dubbo直连
现象:zookeeper注册中心宕机,还可以消费dubbo暴露的服务。
原因:
健壮性
- 监控中心宕掉不影响使用,只是丢失部分采样数据
- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
- 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
- 服务提供者无状态,任意一台宕掉后,不影响使用
- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
高可用:通过设计,减少系统不能提供服务的时间。
集群下dubbo负载均衡配置
在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。
修改负载均衡策略:官方示例
使用loadbalance属性可以修改负载均衡策略。
有四种可以选择:random(默认)、roundrobin、leastactive、consistenthash
在dubbo的@service注解里可以使用weight属性设置权重。(不过这样是固定的)
可以在管理页面动态修改提供者的权重值。
负载均衡策略
1. Random LoadBalance
随机,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。(随机访问,设置权重可以增加或减小概率)
2. RoundRobin LoadBalance
轮循,按公约后的权重设置轮循比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。(按照顺序访问,可以设置权重增加或减少被轮循到的次数)
3. LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。(访问前先查询每一个上一次响应的速度,然后访问上一次响应速度最快的)
4. ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。算法参见:http://en.wikipedia.org/wiki/Consistent_hashing
缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key=“hash.arguments” value=“0,1” />
缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key=“hash.nodes” value=“320” />
服务降级
当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。
可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。
其中:
- mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。(调用时直接返回null,不处理)
在管理页面点击指定消费者的屏蔽即可:
- 还可以改为 mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。(调用失败后返回null,不重试)
在管理页面点击指定消费者的容错即可:
集群容错
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。
集群容错模式
-
Failover Cluster(默认)
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2"来设置重试次数(不含第一次)。重试次数配置如下: <dubbo:service retries="2" /> 或 <dubbo:reference retries="2" /> 或 <dubbo:reference> <dubbo:method name="findFoo" retries="2" /> </dubbo:reference>
-
Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 -
Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 -
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 -
Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2"来设置最大并行数。 -
Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
集群模式配置
按照以下示例在服务提供方和消费方配置集群模式
<dubbo:service cluster="failsafe" />
或
<dubbo:reference cluster="failsafe" />
整合hystrix
Hystrix 旨在通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备拥有回退机制和断路器功能的线程和信号隔离,请求缓存和请求打包,以及监控和配置等功能。
(Hystrix在springcloud的视频里会详细讲)
- 导入hystrix-starter依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
- 在主配置类上加上
@EnableHystrix
注解,开启访服务容错。(提供者和消费者都有导入依赖,加注解开启服务容错) - 在提供者暴露服务的方法上加上
@HystrixCommand
注解把方法交给Hystrix进行代理,然后出现异常,就会进行容错。 - 在消费者进行远程调用的方法上加上
@HystrixCommand(fallbackMethod = "xxx")
注解,当远程调用的方法出现异常时,回调xxx方法(xxx方法在消费者远程调用方法同一个类中)。
原理
RBC原理
一次完整的RPC调用流程(同步调用,异步另说)如下:
- 服务消费方(client)调用以本地调用方式调用服务;
- client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
- client stub找到服务地址,并将消息发送到服务端;
- server stub收到消息后进行解码;
- server stub根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给server stub;
- server stub将返回结果打包成消息并发送至消费方;
- client stub接收到消息,并进行解码;
- 服务消费方得到最终结果。
RPC框架的目标就是要2~8这些步骤都封装起来,这些细节对用户来说是透明的,不可见的。
netty通信原理
Netty是一个异步事件驱动的网络应用程序框架, 用于快速开发可维护的高性能协议服务器和客户端。它极大地简化并简化了TCP和UDP套接字服务器等网络编程。
BIO:(Blocking IO(阻塞IO))
NIO (Non-Blocking IO(非阻塞IO))
Selector一般称为选择器 ,也可以翻译为多路复用器,Selector监听事件(通道的姿态),连接成功会做什么,就绪了做什么,读就绪和写就绪了做什么,都由Selector来监听并处理。
通道的状态有:Connect(连接就绪)、Accept(接受就绪)、Read(读就绪)、Write(写就绪)。
netty的基本原理
netty就是基于NIO多路复用模型。
dubbo原理
框架设计
官方文档:http://dubbo.apache.org/zh-cn/docs/dev/design.html
启动解析、加载配置信息
在spring中由DubboBeanDefinitionParser(bean标签解析器)的parse方法对dubbo的xml配置文件的标签进行解析,解析完后放到对应的xxxConfig里(ServiceBean和ReferenceBean不同)。
在DubboNamespaceHandler(Dubbo的名称空间处理器)的init方法中注册了很多的标签解析器。
public void init() {
this.registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
this.registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
this.registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
this.registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
this.registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
this.registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
this.registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
this.registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
this.registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
this.registerBeanDefinitionParser("annotation", new AnnotationBeanDefinitionParser());
}
ServiceBean的解析牵扯到服务暴露的过程,如何把服务注册到注册中心,让别人来引用。
ReferenceBean的解析牵扯到服务引用的过程。
服务暴露
上面是从文档里拿的完整的图,不太清晰;下面是视频中截取的。
ServiceBean实现了InitializingBean和ApplicationListener< ContextRefreshedEvent >。
当组件创建完后会来调用InitializingBean里面的afterPropertiesSet方法。
当IOC容器刷新完成,IOC容器里所有对象都创建完后,会回调ApplicationListener的onApplicationEvent方法。
- 即:ServiceBean会在容器创建完对象后,调用afterPropertiesSet方法,还会在IOC容器准备完成以后调用onApplicationEvent方法。
- 在afterPropertiesSet方法中,把之前配置的xxxConfig信息都保存到ServiceBean中。
- 在onApplicationEvent方法中,同过如果通过
if(this.isDelay() && !this.isExported() && !this.isUnexported())
判断后,会调用export方法来暴露服务。- 由doExportUrls方法来暴露url地址。
- Protocol的export来暴露执行器,主要是DubboExporter和RegistryExporter。
- DubboExporter的openServer方法开启服务器。
- RegistryExporter会把每一个服务以及它的url地址对应信息保存到ProviderConsumerRegTable注册表里,以后远程调用url地址,按照注册表里的url地址拿到对应的执行器,实现远程调用。
- Protocol的export来暴露执行器,主要是DubboExporter和RegistryExporter。
- 由doExportUrls方法来暴露url地址。
服务引用
文档里的图不清晰,下面从视频中截取的图比较清晰。
服务引用主要是dubbo:reference标签,而这个标签对应ReferenceBean(是一个FactoryBean)。
-
ReferenceBean通过getObject调用get方法,获取对象。
- get在init方法中调用createProxy方法,创建代理对象。
- createProxy方法中,refprotocol.refer方法从注册中心获取想远程引用的接口。(refprotocol还是DubboExporter和RegistryExporter这两个)
-
RegistryExporter中的refer根据url得到注册中心的信息,给doRefer方法传入注册中心的信息,引用的接口和注册中心的地址。
- doRefer中的directory.subscribe订阅注册中心服务提供者提供的服务。
-
然后来到DubboExporter中的refer方法执行远程引用,通过getClients根据url获取客户端,新建一个invoker。
- getClients方法里根据连接数新建客户端,然后initClient方法获取url地址然后进行初始化。
- initClient方法中通过层层的connect获取到一个NettyClient来监听url。
订阅完成后,获取到注册中心返回的invoker,然后把这个invoker和其他信息(如url)一起保存到ProviderConsumerRegTable注册表里,然后把invoker代理对象层层返回。
ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
- getClients方法里根据连接数新建客户端,然后initClient方法获取url地址然后进行初始化。
-
- createProxy方法中,refprotocol.refer方法从注册中心获取想远程引用的接口。(refprotocol还是DubboExporter和RegistryExporter这两个)
ReferenceBean通过getObject调用get方法,获取invoker代理对象。
- get在init方法中调用createProxy方法,创建代理对象。
服务调用
这个代理对象进行方法调用的过程:
- 调用时首先来到InvokerInvocationHandler的invoke方法获取到要执行的方法信息和参数信息,然后把要执行的方法信息和参数信息封装到一个RpcInvocation对象中传给invoke方法(MockClusterInvoker类的invoke)。
- 然后在这个invoke方法里在调用一个invoke方法。(AbstractClusterInvoker的invoke)
- 在这个invoke里有一个list(invocation)会在注册中心找有几个invoke可以执行(不同版本),这里有找到了两个可以执行的方法(1版本和2版本)。
- 然后把获取到负载均衡机制(random)放到loadbalance对象中,然后调用doInvoke方法。
- doInvoke方法中把负载均衡机制和可执行的invoke传给select,选出要执行的invoke。
- 然后继续调用invoke进入层层filter中的invoke,最后从AbstractInvoker(抽象的invoker)的doInvoke方法进入DubboInvoker的doInvoke。
- doInvoke方法先获取方法的信息,然后会拿到客户端(客户端在服务引用时暴露过一次),这个客户端保存了我们要连上哪个服务,然后获取到的这个客户端会通过request方法发起请求并get获取到返回的结果并层层返回(如果超时就会报错然后进行重试从select方法拿重新选出要执行的invoke再进行以上步骤,也会换客户端)。
- 然后继续调用invoke进入层层filter中的invoke,最后从AbstractInvoker(抽象的invoker)的doInvoke方法进入DubboInvoker的doInvoke。
- doInvoke方法中把负载均衡机制和可执行的invoke传给select,选出要执行的invoke。
- 在这个invoke里有一个list(invocation)会在注册中心找有几个invoke可以执行(不同版本),这里有找到了两个可以执行的方法(1版本和2版本)。
- 然后在这个invoke方法里在调用一个invoke方法。(AbstractClusterInvoker的invoke)
总结:层层包装,底层用netty进行通信。
番外(自己点进去看的,不一定写得对):
客户端的request->ReferenceCountExchangeClient的request->HeaderExchangeChannel的request
HeaderExchangeChannel的request会创建一个Request对象,把信息传入到这个对象后,channel.send(req)发送出去。
能设计出这样的框架真厉害,世界上牛人真的太多了,每次接触源码都感觉自己稍微前进了一点,变强了一点。
雷老师:学无止境,希望大家不要停止探索的步伐。
就算我们不能成为很厉害的人,但我们也不应停下脚步,一点一点的拉近与牛人间的差距。学无止境,祝各位学业有成,互利共勉。