Dubbo 配置实战
建议看这篇文章是在学习了快速入门 dubbo 那篇文章的基础上来学习
配置说明
文档地址 https://dubbo.apache.org/zh/index.html
关于 dubbo 的配置说明 在文档中都有比较详细的说明,下面举例的都是较为常用的
1 启动时检查
- 启动时会在注册中心检查依赖的服务是否可用,不可用时会抛出异常
- 在消费方编写初始化容器的 main 方法启动(tomcat 启动方式,必须访问一次 action 才能初始化
spring)- 想想为什么要有这个配置呢?
- 可以提前发现服务提供方是否可用
示例代码
直接启动这个测试类,注意 spring 配置文件的位置
- 我这里测试,现在是没有启动提供者
- 因为我们测试的目的就是让他没有提供者,会不会有报错提示
/** * @author : look-word * 2022-07-19 09:44 **/ public class TestCheckException { public static void main(String[] args) throws IOException { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring.xml"); // 让程序一直读取, 目的是不让他停止 System.in.read(); } }
当我们启动后会发现,诶,怎么没有错误呢,是下面 log4j 的提示呢?
- 这里没有错误提示的原因呢,就是说我们没有正确的去配置 log4j,的确我们也没有去配置
- 系统级别日志,需要配合 log4j 才输出,在 resources 下添加 log4j.properties,内容如下:
log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file.File=dubbo.log log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l %m%n log4j.rootLogger=error, stdout,file
再次启动,会发现。如我们所愿它出错了。
错误信息
- 翻译的意思:说在 zookeeper 中没有找到可用的服务
java.lang.IllegalStateException: Failed to check the status of the service service.HelloService. No provider available for the service service.HelloService from the url zookeeper:
关闭检查
在 spring.xml 配置文件中加上就不会有异常提示了
- 可以看到,我这里的这个配置是注释掉的,在实际开发中我们是需要这个异常提示的,不推荐关闭
<!--默认是true:抛异常;false:不抛异常--> <dubbo:consumer check="false" />
然后启动测试文件即可,这里不做演示了
2 超时时间
- 由于网络或服务端不可靠,会导致调用过程中出现不确定的阻塞状态(超时)
- 为了避免超时导致客户端资源(线程)挂起耗尽,必须设置超时时间
- 在服务提供者添加如下配置:
<!--设置超时时间为2秒,默认为1秒--> <dubbo:provider timeout="2000"/>
- 可以将服务实现 HelloServiceImpl.java 中加入模拟的网络延迟进行测试:
@com.alibaba.dubbo.config.annotation.Service public class HelloServiceImpl implements HelloService { @Override public String sayHello(String name) { try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } return "Hello," + name + "!!!"; } }
- 超时设置 2 秒,而模拟的网络延迟有 3 秒,超出时限,报错!
错误代码: com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout.
- 说服务器响应超时。
配置原则:
dubbo 推荐在Provider上尽量多配置Consumer端属性
:
- 作
服务的提供者
,比服务使用方更清楚服务性能参数
,如调用的超时时间
,合理的重试
次数,等等 在Provider配置后
,Consumer不配置
则会使用 Provider 的配置值,即 Provider 配置可
以作消费者的缺省值
3 重试次数
- 当出现失败,自动切换并重试其它服务器,dubbo 重试的缺省值是 2 次,我们可以自行设置
- 在 provider 提供方配置:
<!-- 消费方连接第1次不算,再来重试3次,总共重试4次 --> <dubbo:provider timeout="2000" retries="3"/>
修改实现类代码: 增加次数
@com.alibaba.dubbo.config.annotation.Service public class HelloServiceImpl implements HelloService { int a; @Override public String sayHello(String name) { System.out.println("被调用第"+(++a)+"次"); try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } return "Hello," + name + "!!!"; } }
可以看到 重试了 3 次 第一次不算
引入问题
并不是所有的方法都适合设置重试次数
- 幂等方法:适合(当参数一样,无论执行多少次,结果是一样的,例如:查询,修改)
- 非幂等方法:不适合(当参数一样,执行结果不一样,例如:删除,添加)
我们需要单独为某个方法设置重试次数
- 需要再添加一个方法,作对比
- 提供方接口添加 sayNo()方法并实现
public interface HelloService { String sayHello(String name); String no(); }
@com.alibaba.dubbo.config.annotation.Service public class HelloServiceImpl implements HelloService { int a,b; @Override public String sayHello(String name) { System.out.println("sayHello被调用第"+(++a)+"次"); try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } return "Hello," + name + "!!!"; } @Override public String no() { System.out.println("no被调用第"+(++b)+"次"); return "no"; } }
- 消费方接口添加 sayNo()方法声明
public interface HelloService { String sayHello(String name); String no(); }
- 消费方 controller
@RestController public class HelloAction { // Resource 注解 指定名称注入 @Resource(name = "helloService") private HelloService hs; @RequestMapping("hello/{name}") @ResponseBody public String hello(@PathVariable String name) { return hs.sayHello(name); } @RequestMapping("no") @ResponseBody public String no() { return hs.no(); } }
- 消费方配置方法重试次数
<dubbo:reference interface="service.HelloService" id="helloService"> <dubbo:method name="sayHello" retries="3"/> <dubbo:method name="no" retries="0"/> </dubbo:reference>
启动项目,访问
可以看到,我们为每种方法配置的重试次数成功了
4 多版本
- 一个接口,多个(版本的)实现类,可以使用定义版本的方式引入
- 为 HelloService 接口定义两个实现类,提供者修改配置:
配置文件
为 HelloService 定义了两个版本
<dubbo:service interface="service.HelloService" class="service.impl.HelloServiceImpl1" version="1.0.0"> </dubbo:service> <dubbo:service interface="service.HelloService" class="service.impl.HelloServiceImpl2" version="2.0.0"> </dubbo:service>
修改实现类
- 复制 HelloServiceImpl 重命名为 1 和 2
- 分别为每个实现类标识版本信息
- 因为提供者定义了版本所以消费者就可以根据 version 的版本,选择具体的服务版本 这里是消费者配置文件
注意
:消费者的控制层要改为自动注入,因为@Reference 注解和 dubbo:reference在这里冲突
- Resource 注解默认是根据变量名去 spring 容器中找对应的 bean 的
- 需要在直接参数中配置 bean 的名称 和 上面图中 id 对应
启动测试
注意 每次修改配置文件 都需要重启项目
-
当消费者的版本修改为 version="*",那么就会随机调用服务提供者的版本
这是访问多次 http://localhost:8002/no 控制台输出的信息
5 本地存根
为什么要有本地存根?
- 目前我们的分布式架构搭建起来有一个严重的问题,就是所有的操作全都是 消费者发起,由服务
提供者执行- 消费者动动嘴皮子却什么活都不干,这样会让提供者很累,例如简单的参数验证,消费者完全能够
胜任,把合法的参数再发送给提供者执行,效率高了,提供者也没那么累了- 例如:去房产局办理房屋过户,请带好自己的证件和资料,如果什么都不带,那么办理过户手续会
很麻烦,得先调查你有什么贷款,有没有抵押,不动产证是不是你本人,复印资料等操作。一天肯
定办不完。明天还要来。如果你能提前将这些东西准备好,办理过户,1 个小时足矣,这就是“房产
中介办事效率高的原因”- 话不多说,
先在消费者处理一些业务逻辑,再调用提供者的过程,就是“本地存根”
示例代码
代码实现肯定在 消费者,创建一个 HelloServiceStub 类并且实现 HelloService 接口
注意:必须使用构造方法的方式注入
public class HelloServiceStub implements HelloService { private HelloService helloService; // 注入HelloService public HelloServiceStub(HelloService helloService) { this.helloService = helloService; } @Override public String sayHello(String name) { System.out.println("本地存根数据验证。。。"); if(!StringUtils.isEmpty(name)){ return helloService.sayHello(name); } return "i am sorry!"; } @Override public String no() { return helloService.no(); } }
修改消费者配置文件
- 添加的是红框位置的参数
<dubbo:reference interface="service.HelloService" id="helloService" version="*" stub="service.impl.HelloServiceStub"> <dubbo:method name="sayHello" retries="3"/> <dubbo:method name="no" retries="0"/> </dubbo:reference>
老样子,clean项目 然后打包启动
- 因为我们只对 sayHello 方法进行了存根校验,所以访问
- http://localhost:8002/hello/zhangsan
负载均衡策略
- 负载均衡(Load Balance), 其实就是将请求分摊到多个操作单元上进行执行,从而共同完成工作
任务。- 简单的说,好多台服务器,不能总是让一台服务器干活,应该“雨露均沾”
- dubbo 一共提供 4 种策略,缺省为 random 随机分配调用
示例代码
- 修改提供者配置并启动 3 个提供者,让消费者对其进行访问
- tomcat 端口 8001,8002,8003
- provider 端口 20881,20882,20883
<dubbo:provider timeout="2000" retries="3" port="20881"/>
HelloServiceImpl2 类,服务器 1,服务器 2,服务器 3
- 在每次修改 tomcat 端口号 和 provider 端口是 修改 HelloServiceImpl2 的内容
- 因为我这里用的是 2.0.0 的版本,所以修改的是 HelloServiceImpl2 的内容
启动 consumer 进行测试
启动一个消费者,三个提供者
- 底下我已经访问了一次,当我们访问多次,去控制台查看输出信息时,会发现他是随机的去调用提供者
消费方修改权重
<dubbo:reference loadbalance="roundrobin" interface="service.HelloService" id="helloService" version="2.0.0" stub="service.impl.HelloServiceStub"> <dubbo:method name="sayHello" retries="3"/> <dubbo:method name="no" retries="0"/> </dubbo:reference>
- 最好使用管理端修改权重
然后启动测试即可
高可用
1 zookeeper 宕机
- zookeeper 注册中心宕机,还可以消费 dubbo 暴露的服务
- 监控中心宕掉不影响使用,只是丢失部分采样数据
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复- 测试:
- 正常发出请求
- 关闭 zookeeper:./zkServer.sh stop
- 消费者仍然可以正常消费
服务降级
- 壁虎遇到危险会自动脱落尾巴,目的是损失不重要的东西,保住重要的
- 服务降级,就是根据实际的情况和流量,对一些服务有策略的停止或换种简单的方式处理,从而释
放服务器的资源来保证核心业务的正常运行
1 为什么要服务降级
- 而为什么要使用服务降级,这是防止分布式服务发生雪崩效应
- 什么是雪崩?就是蝴蝶效应,当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,
很多请求都是因为这样一直等着响应,直到服务资源耗尽产生宕机,而宕机之后会导致分布式其他
服务调用该宕机的服务也会出现资源耗尽宕机,这样下去将导致整个分布式服务都瘫痪,这就是雪
崩。
2 服务降级实现方式
- 在 管理控制台配置服务降级:屏蔽和容错
- 屏蔽:mock=force:return+null 表示消费方对该服务的方法调用都 直接返回 null 值,不发起远程
调用。用来屏蔽不重要服务不可用时对调用方的影响。- 容错:mock=fail:return+null 表示消费方对该服务的方法调用在 失败后,再返回 null 值,不抛异
常。用来容忍不重要服务不稳定时对调用方的影响。