目录
一. SPI
(一)SPI简介
SPI
全称为
(Service Provider Interface)
,是
JDK
内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现,简单来说,它就是一种动态替换发现的机制。使用
SPI
机制的优势是实现解耦,
使得第三方服务模块的装配控制逻辑与调用者的业务代码分离。
(二)JDK中的SPI
Java
中如果想要使用
SPI
功能,先提供标准服务接口,然后再提供相关接口实现和调用者。这样就可以通过
SPI
机制中约定好的信息进行查询相应的接口实现。
SPI
遵循如下约定:
1 、当服务提供者提供了接口的一种具体实现后,在 META-INF/services 目录下创建一个以 “ 接口全限定名 ” 为命名的文件,内容为实现类的全限定名;2 、接口实现类所在的 jar 包放在主程序的 classpath 中;也就是必须将实现类加载到调用者模块中3 、主程序通过 java.util.ServiceLoader 动态装载实现模块,它通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把类加载到 JVM ;4 、 SPI 的实现类必须携带一个无参构造方法;
(三)JDK-SPI代码示例
(四)Dubbo中的SPI
dubbo
中大量的使用了
SPI
来作为扩展点,通过实现同一接口的前提下,可以进行定制自己的实现类。比如比较常见的协议,负载均衡,都可以通过
SPI
的方式进行定制化,自己扩展。
Dubbo
中已经存在的
所有已经实现好的扩展点。
下图中则是
Dubbo
中默认提供的负载均衡策略。
(五)Dubbo中扩展点使用方式
这里使用三个模块来演示
Dubbo
中扩展点的使用方式,一个主模块
main
,一个服务接口模块
api
,一个
服务实现模块
impl
。
1. api模块创建
(1)导入坐标 dubbo
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo_spi_demo</artifactId>
<groupId>com.lagou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo_spi_demo_api</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.5</version>
</dependency>
</dependencies>
</project>
(
2)创建接口,在接口上
使用
@SPI
package com.lagou.service;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
@SPI
public interface HelloService {
String sayHello();
}
2. impl模块创建
(
1
)导入
api
项目 的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo_spi_demo</artifactId>
<groupId>com.lagou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo_spi_demo_impl</artifactId>
<dependencies>
<dependency>
<groupId>com.lagou</groupId>
<artifactId>dubbo_spi_demo_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(
2
)建立实现类,为了表达支持多个实现的目的,这里分别创建两个实现。分别为
HumanHelloService
和
DogHelloService
。
public class HumanHelloService implements HelloService{
public String sayHello() {
return "hello 你好";
}
}
public class DogHelloService implements HelloService{
public String sayHello() {
return "wang wang";
}
}
(
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
3. main主模块创建
(
1
)导入坐标
接口项目
和 实现类项目
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo_spi_demo</artifactId>
<groupId>com.lagou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dubbo_spi_demo_main</artifactId>
<dependencies>
<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>
</dependencies>
</project>
(
2
)创建
DubboSpiMain
和原先调用的方式不太相同,
dubbo
有对其进行自我重新实现 需要借助
ExtensionLoader
,创建新的运行项目。这里
demo
中的示例和
java
中的功能相同,查询出所有的已知实现,并且调用
package com.lagou;
import com.lagou.service.HelloService;
import org.apache.dubbo.common.extension.ExtensionLoader;
import java.util.Set;
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){
//调用所遍历的实现类的.sayHello方法
String result = extensionLoader.getExtension(extension).sayHello();
System.out.println(result);
}
}
}
(
3
)
dubbo
自己做
SPI
的目的
1. JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源2. 如果有扩展点加载失败,则所有扩展点无法使用3. 提供了对扩展点包装的功能 (Adaptive) ,并且还支持通过 set 的方式对其他的扩展点进行注入
(六)Dubbo SPI中的Adaptive功能
Dubbo
中的
Adaptive
功能,主要解决的问题是如何动态的选择具体的扩展点。通过
getAdaptiveExtension
统一对指定接口对应的所有扩展点进行封装,通过
URL
的方式对扩展点来进行动态选择。
(dubbo
中所有的注册信息都是通过
URL
的形式进行处理的。
)
这里同样采用相同的方式进行
实现。
(
1
)创建接口
api
中的
HelloService
扩展如下方法
,
与原先类似,在
sayHello
中增加
Adaptive
注解,并且在参数中提供
URL
参数
.
注意这里的
URL
参数的类为
org.apache.dubbo.common.URL
其中
@SPI
可以指定一个字符串参数,用于指明该
SPI
的默认实现。
package com.lagou.service;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
//@SPI注解后面加上key值后,主类加载时哪怕URL不选定KEY值,也能根绝此注解标注的进行选择
//但是主类中URL参数指定的优先级是高于此处的
@SPI("human")
public interface HelloService {
String sayHello();
@Adaptive
String sayHello(URL url);
}
(
2
)创建实现类
与上面
Service
实现类代码相似,只需增加
URL
形参即可
package com.lagou.service.impl;
import com.lagou.service.HelloService;
import org.apache.dubbo.common.URL;
public class DogHelloService implements HelloService{
public String sayHello() {
return "wang wang";
}
public String sayHello(URL url) {
return "wang url";
}
}
package com.lagou.service.impl;
import com.lagou.service.HelloService;
import org.apache.dubbo.common.URL;
public class HumanHelloService implements HelloService{
public String sayHello() {
return "hello 你好";
}
public String sayHello(URL url) {
return "hello url";
}
}
(
3
)编写
DubboAdaptiveMain
最后在获取的时候方式有所改变,需要传入
URL
参数,并且在参数中指定具体的实现类参数
package com.lagou;
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中test://localhost/hello问号之前的部分因为是测试可以随便写
// hello.service=dog问号之后的部分,hello.service 代表接口HelloService,Dubbo会自动识别,
// dog 为META-INF.dubbo目录下配置文件中的key值来选择接口的实现类
URL url = URL.valueOf("test://localhost/hello?hello.service=human");
//获得接口
HelloService adaptiveExtension =ExtensionLoader.getExtensionLoader(HelloService.class).getAdaptiveExtension();
//通过URL参数调用具体的实现类
String msg = adaptiveExtension.sayHello(url);
System.out.println(msg);
}
}
注意:
- 因为在这里只是临时测试,所以为了保证URL规范,前面的信息均为测试值即可,关键的点在于hello.service 参数,这个参数的值指定的就是具体的实现方式。关于为什么叫 hello.service 是因为这个接口的名称,其中后面的大写部分被dubbo自动转码为 . 分割。
- 通过 getAdaptiveExtension 来提供一个统一的类来对所有的扩展点提供支持(底层对所有的扩展点进行封装)。
- 调用时通过参数中增加 URL 对象来实现动态的扩展点使用。
- 如果URL没有提供该参数,则该方法会使用默认在 SPI 注解中声明的实现。
(七)Dubbo-SPI完整示例代码
二. Dubbo调用时拦截操作
与很多框架一样,
Dubbo
也存在拦截(过滤)机制,可以通过该机制在执行目标程序前后执行我们指定的代码。
Dubbo
的
Filter
机制,是专门为服务提供方和服务消费方调用过程进行拦截设计的,每次远程方法执行,该拦截都会被执行。这样就为开发者提供了非常方便的扩展性,比如为
dubbo
接口实现
ip
白名单功
能、监控功能 、日志记录等。
步骤如下:
(
1
)实现
org.apache.dubbo.rpc.Filter
接口
(
2
)使用
org.apache.dubbo.common.extension.Activate
接口进行对类进行注册 通过
group
可以指定生产端 消费端
如
:
@Activate ( group = { CommonConstants . CONSUMER )
(
3
)计算方法运行时间的代码实现
package com.lagou.filter;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
@Activate(group = {CommonConstants.CONSUMER,CommonConstants.PROVIDER})
public class DubboInvokeFilter implements Filter {
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
long startTime = System.currentTimeMillis();
try {
// 执行方法
return invoker.invoke(invocation);
} finally {
System.out.println("invoke time:"+(System.currentTimeMillis()-startTime) + "毫秒");
}
}
}
(
4)在DubboInvokeFilter 类所在项目的resources目录下创建
META-
INF.dubbo并
新建
org.apache.dubbo.rpc.Filter
文件,并将当前类的全名写入
timeFilter=com.lagou.filter.DubboInvokeFilter
注意:一般类似于这样的功能都是单独开发依赖的,所以再使用方的项目中只需要引入依赖,在调用接口时,该方法便会自动拦截。
Dubbo调用时拦截操作完整代码示例
三. 负载均衡策略
(一)负载均衡基本配置
负载均衡(
Load Balance
)
,
其实就是将请求分摊到多个操作单元上进行执行,从而共同完成工作任务。
负载均衡策略主要用于客户端存在多个提供者时进行选择某个提供者。
在集群负载均衡时,
Dubbo
提供了多种均衡策略(包括随机、轮询、最少活跃调用数、一致性
Hash
),缺省为
random
随机调用。
配置负载均衡策略,既可以在服务提供者一方配置,也可以在服务消费者一方配置,如下:
// 在服务消费者一方配置负载均衡策略@Reference ( check = false , loadbalance = "random" )
// 在服务提供者一方配置负载均衡@Service ( loadbalance = "random" )public class HelloServiceImpl implements HelloService {public String sayHello ( String name ) {return "hello " + name ;}}
1. 客户端配置示例:
package com.lagou.bean;
import com.lagou.service.HelloService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Component;
@Component
public class ConsumerComponent {
@Reference(loadbalance = "onlyFirst")
private HelloService helloService;
public String sayHello(String name, int timeToWait) {
return helloService.sayHello(name, timeToWait);
}
}
2. 服务端配置示例
package com.lagou.service.impl;
import com.lagou.service.HelloService;
import org.apache.dubbo.config.annotation.Service;
@Service
//在服务提供者一方配置负载均衡
@Service(loadbalance = "random")
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name, int timeToWait) {
return "hello3:"+name;
}
}
(二)自定义负载均衡器
负载均衡器在
Dubbo
中的
SPI
接口是
org.apache.dubbo.rpc.cluster.LoadBalance
,
可以通过实现这
个接口来实现自定义的负载均衡规则。
(
1
)自定义负载均衡器
在上一节的案例基础上创建名称为
dubbo-spi-loadbalance
的
Maven
模块,并创建负载均衡器
OnlyFirstLoadbalancer
。这里功能只是简单的选取所有服务提供者中的第一个
(
按照字母排序
+
端口排序
)
。
package com.laogu.loadbalance;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.cluster.LoadBalance;
import java.util.List;
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)->{
//先比较IP
final int ipCompare = i1.getUrl().getIp().compareTo(i2.getUrl().getIp());
//如果IP地址相等
if(ipCompare == 0){
//再比较端口号
return Integer.compare(i1.getUrl().getPort(),i2.getUrl().getPort());
}
return ipCompare;
//拿出第一个
}).findFirst().get();
}
}
(2)配置负载均衡器
在
dubbo-spi-loadbalance
工程的
META
-
INF/dubbo
目录下新建
org.apache.dubbo.rpc.cluster.LoadBalance
文件,并将当前类的全名写入
onlyFirst=com.laogu.loadbalance.OnlyFirstLoadbalancer
(
3
)在服务提供者工程实现类中编写用于测试负载均衡效果的方法
启动不同端口时
方法返回的信息不同
(
4
)启动多个服务
要求他们使用同一个接口注册到同一个注册中心
但是他们的
dubbo
通信端口不同
(
5
)在服务消费方指定自定义负载均衡器
onlyFirst
package com.lagou.bean;
import com.lagou.service.HelloService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Component;
@Component
public class ConsumerComponent {
@Reference(loadbalance = "onlyFirst")
private HelloService helloService;
public String sayHello(String name, int timeToWait) {
return helloService.sayHello(name, timeToWait);
}
}
(三)Dubbo负载均衡简单示例完整代码
四. 异步调用
Dubbo
不只提供了堵塞式的的同步调用,同时提供了异步调用的方式。这种方式主要应用于提供者接口响应耗时明显,消费者端可以利用调用接口的时间去做一些其他的接口调用
,
利用
Future
模式来异步等
待和获取结果即可。这种方式可以大大的提升消费者端的利用率。 目前这种方式可以通过
XML
的方式进
行引入。
(一)异步调用实现
(1)为了能够模拟等待,接口通过 int timeToWait参数,标明需要休眠多少毫秒后才会进行返回。
package com.lagou.service;
public interface HelloService {
public String sayHello(String name, int timeToWait);
}
(
2
)接口实现
为了模拟调用耗时 可以让线程等待一段时间
package com.lagou.service.impl;
import com.lagou.service.HelloService;
import org.apache.dubbo.config.annotation.Service;
@Service
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name, int timeToWait) {
try {
Thread.sleep(timeToWait);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello:"+name;
}
}
(
3
)在消费者端,通过XML方式配置异步调用 注意消费端默认超时时间
1000
毫秒
如果提供端耗时大于
1000
毫秒会出现超时
<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:config-center/>
<dubbo:application name="service-consumer"/>
<dubbo:registry address="zookeeper://192.168.200.128:2181"/>
<!--xml配置异步模式-->
<dubbo:reference id="helloService" interface="com.lagou.service.HelloService">
<dubbo:method name="sayHello" async="true"></dubbo:method>
</dubbo:reference>
<context:component-scan base-package="com.lagou.bean"></context:component-scan>
</beans>
可以通过改变消费端的超时时间 通过timeout属性设置即可单位毫秒
(
4
)测试,我们休眠
100
毫秒,然后再去进行获取结果。方法在同步调用时的返回值是空,我们可以通过
RpcContext.getContext().getFuture()
来进行获取
Future
对象来进行后续的结果等待操作。
package com.lagou;
import com.lagou.bean.ConsumerComponent;
import com.lagou.service.HelloService;
import com.sun.org.apache.xpath.internal.functions.FuncTrue;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.io.IOException;
import java.util.concurrent.Future;
public class XMLConsumerMain {
public static void main(String[] args) throws IOException, InterruptedException {
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("consumer.xml");
HelloService service = app.getBean(HelloService.class);
while (true) {
System.in.read();
try {
String hello = service.sayHello("world", 100);
// 利用Future 模式来获取异步调用结果
Future<Object> future = RpcContext.getContext().getFuture();
//同步调用结果
System.out.println("result :" + hello);
//异步调用结果
System.out.println("future result:"+future.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
(二)异步调用特殊说明
需要特别说明的是,该方式的使用,请确保
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
及以后的
版本
进行了修正
。
(三)Dubbo异步调用完整代码示例
五. 线程池
(一)Dubbo已有线程池
dubbo
在使用时,都是通过创建真实的业务线程池进行操作的。目前已知的线程池模型有两个和
java
中的相互对应
:
- fifix: 表示创建固定大小的线程池。也是Dubbo默认的使用方式,默认创建的执行线程数为200,并且是没有任何等待队列的。所以再极端的情况下可能会存在问题,比如某个操作大量执行时,可能
- 存在堵塞的情况。后面也会讲相关的处理办法。
- cache: 创建非固定大小的线程池,当线程不足时,会自动创建新的线程。但是使用这种的时候需
- 要注意,如果突然有高TPS的请求过来,方法没有及时完成,则会造成大量的线程创建,对系统的
- CPU和负载都是压力,执行越多反而会拖慢整个系统。
(二)自定义线程池
在真实的使用过程中可能会因为使用
fifix
模式的线程池,导致具体某些业务场景因为线程池中的线程数量不足而产生错误,而很多业务研发是对这些无感知的,只有当出现错误的时候才会去查看告警或者通过
客户反馈出现严重的问题才去查看,结果发现是线程池满了。所以可以在创建线程池的时,通过某些手
段对这个线程池进行监控,这样就可以进行及时的扩缩容机器或者告警。下面的这个程序就是这样子
的,会在创建线程池后进行对其监控,并且及时作出相应处理。
(
1
)线程池实现
,
这里主要是基于对
FixedThreadPool
中的实现做扩展出线程监控的部分
package com.lagou.threadpool;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.threadpool.support.fixed.FixedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.*;
public class WachingThreadPool extends FixedThreadPool implements Runnable{
private static final Logger LOGGER = LoggerFactory.getLogger(WachingThreadPool.class);
// 定义线程池使用的阀值
private static final double ALARM_PERCENT = 0.90;
//存储URL与执行器对应关系的对象
private final Map<URL, ThreadPoolExecutor> THREAD_POOLS = new ConcurrentHashMap<>();
public WachingThreadPool(){
// 每隔3秒打印线程使用情况,每隔三秒执行当前类
Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(this,1,3, TimeUnit.SECONDS);
}
// 通过父类创建线程池
@Override
public Executor getExecutor(URL url) {
//这个executor执行器,我们根据父类的getExecutor方法从URL中拿一个
final Executor executor = super.getExecutor(url);
//把拿到的这个执行器的类型属于ThreadPoolExecutor,就把它加到THREAD_POOLS这个Map中
if(executor instanceof ThreadPoolExecutor){
THREAD_POOLS.put(url,(ThreadPoolExecutor)executor);
}
return executor;
}
@Override
public void run() {
// 遍历线程池
for (Map.Entry<URL,ThreadPoolExecutor> entry: THREAD_POOLS.entrySet()){
final URL url = entry.getKey();
final ThreadPoolExecutor executor = entry.getValue();
// 计算相关指标
final int activeCount = executor.getActiveCount();//获取活动的线程数量
final int poolSize = executor.getCorePoolSize();//获取总线程数量
double usedPercent = activeCount / (poolSize*1.0);//获取线程使用率
LOGGER.info("线程池执行状态:[{}/{}:{}%]",activeCount,poolSize,usedPercent*100);
if (usedPercent > ALARM_PERCENT){
LOGGER.error("超出警戒线! host:{} 当前使用率是:{},URL:{}",url.getIp(),usedPercent*100,url);
}
}
}
}
(2)SPI声明,创建文件 META-INF/dubbo/org.apache.dubbo.common.threadpool.ThreadPool
watching=com.lagou.threadpool.WachingThreadPool
(
3
)在服务提供方项目引入该依赖
(
4
)在服务提供方项目中设置使用该线程池生成器
dubbo.application.name=dubbo-demo-annotation-provider
dubbo.protocol.name=dubbo
dubbo.protocol.port=20885
#dubbo.protocol.host=192.168.1.109
dubbo.provider.threadpool=watching
#dubbo.protocol.telnet=clear,exit,help,status,log,ls,ps,cd,pwd,invoke,trace,count,select,shutdown
(
5
)接下来需要做的就是模拟整个流程,因为该线程当前是每
1
秒抓一次数据,所以我们需要对该方法的提供者超过
1
秒的时间
(
比如这里用休眠
Thread.sleep
)
,消费者则需要启动多个线程来并行执行,来
模拟整个并发情况。
(
6
)在调用方则尝试简单通过
for
循环启动多个线程来执行
查看服务提供方的监控情况
(三)Dubbo自定义线程池完整代码示例
六. 路由规则
路由是决定一次请求中需要发往目标机器的重要判断,通过对其控制可以决定请求的目标机器。我们可以通过创建这样的规则来决定一个请求会交给哪些服务器去处理。
(一)路由规则快速入门
(
1
)提供两个提供者
(
一台本机作为提供者,一台为其他的服务器
)
,每个提供者会在调用时可以返回不同的信息 以区分提供者。
(
2
)针对于消费者,我们这里通过一个死循环,每次等待用户输入,再进行调用,来模拟真实的请求情况。
通过调用的返回值
确认具体的提供者。
package com.lagou;
import com.lagou.bean.ConsumerComponent;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.IOException;
public class AnnotationConsumerMain {
public static void main(String[] args) throws IOException, InterruptedException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConsumerConfiguration.class);
context.start();
ConsumerComponent service = context.getBean(ConsumerComponent.class);
while (true) {
System.in.read();
try {
String hello = service.sayHello("world", 0);
System.out.println("result :" + hello);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Configuration
@PropertySource("classpath:/dubbo-consumer.properties")
//@EnableDubbo(scanBasePackages = "com.lagou.bean")
@ComponentScan("com.lagou.bean")
@EnableDubbo
static class ConsumerConfiguration {
}
}
(
3
)我们通过
ipconfifig
来查询到我们的
IP
地址,并且单独启动一个客户端,来进行如下配置
(
这里假设我们希望隔离掉本机的请求,都发送到另外一台机器上
)
。
package com.lagou.router;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.registry.Registry;
import org.apache.dubbo.registry.RegistryFactory;
public class DubboRouterMain {
public static void main(String[] args) {
//通过注册工厂拿到注册中心
//注册中心的工厂对象
RegistryFactory
registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
//获取注册中心
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://127.0.0.1:2181"));
//通过注册中心修改路由规则
registry.register(URL.valueOf("condition://0.0.0.0/com.lagou.service.HelloService?category=routers&force=true&dynamic=true&rule="
//=>前面代表消费者的IP限制,后面代表服务提供者者的IP地址
+ URL.encode("=> host != 192.168.20.1")));
}
}
(
4
)通过这个程序执行后,我们就通过消费端不停的发起请求,看到真实的请求都发到了除去本机以外的另外一台机器上。
(二)路由规则详解
通过上面的程序,我们实际本质上就是通过在
zookeeper
中保存一个节点数据,来记录路由规则。消费者会通过监听这个服务的路径,来感知整个服务的路由规则配置,然后进行适配。这里主要介绍路由配
置的参数。具体请
参考文档
,
这里只对关键的参数做说明。
- route:// 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。
- 0.0.0.0 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填。
- com.lagou.service.HelloService 表示只对指定服务生效,必填。
- category=routers 表示该数据为动态配置类型,必填。
- dynamic : 是否为持久数据,当指定服务重启时是否继续生效。必填。
- runtime : 是否在设置规则时自动缓存规则,如果设置为true则会影响部分性能。
- rule : 是整个路由最关键的配置,用于配置路由规则。 ... => ... 在这里 => 前面的就是表示消费者方的匹配规则,可以不填(代表全部)。 => 后方则必 须填写,表示当请求过来时,如果选择提供者的配置。官方这块儿也给出了详细的示例,可以按照 那里来讲。 其中使用最多的便是 host 参数。 必填。
(三)路由入门示例完整代码
七. 路由与上线系统结合简单案例
当公司到了一定的规模之后,一般都会有自己的上线系统,专门用于服务上线。方便后期进行维护和记录的追查。我们去想象这样的一个场景,一个
dubbo
的提供者要准备进行上线,一般都提供多台提供者
来同时在线上提供服务。这时候一个请求刚到达一个提供者,提供者却进行了关闭操作。那么此次请求
就应该认定为失败了。所以基于这样的场景,我们可以通过路由的规则,把预发布
(
灰度
)
的机器进行从
机器列表中移除。并且等待一定的时间,让其把现有的请求处理完成之后再进行关闭服务。同时,在启
动时,同样需要等待一定的时间,以免因为尚未重启结束,就已经注册上去。等启动到达一定时间之
后,再进行开启流量操作。
(一)实现主体思路
1. 利用 zookeeper 的路径感知能力,在服务准备进行重启之前将当前机器的 IP 地址和应用名写入zookeeper 。2. 服务消费者监听该目录,读取其中需要进行关闭的应用名和机器 IP 列表并且保存到内存中。3. 当前请求过来时,判断是否是请求该应用,如果是请求重启应用,则将该提供者从服务列表中移除。
(
1
)引入
Curator
框架,用于方便操作
Zookeeper
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
(
2
)编写
Zookeeper
的操作类,用于方便进行
zookeeper
处理
package com.lagou.router;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class ZookeeperClients {
private final CuratorFramework client;
private static ZookeeperClients INSTANCE;
static {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.200.128:2181",retryPolicy);
INSTANCE = new ZookeeperClients(client);
client.start();
}
private ZookeeperClients(CuratorFramework client) {
this.client = client;
}
public static CuratorFramework client(){
return INSTANCE.client;
}
}
(
3
)编写需要进行预发布的路径管理器,用于缓存和监听所有的待灰度机器信息列表。
package com.lagou.router;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ReadyRestartInstances implements PathChildrenCacheListener {
private static final Logger LOGGER = LoggerFactory.getLogger( ReadyRestartInstances.class);
private static final String LISTEN_PATHS = "/lagou/dubbo/restart/instances";
private final CuratorFramework zkClient;
// 当节点变化时 给这个集合赋值 重启机器的信息列表
private volatile Set<String> restartInstances = new HashSet<>();
private ReadyRestartInstances(CuratorFramework zkClient) {
this.zkClient = zkClient;
}
public static ReadyRestartInstances create(){
final CuratorFramework zookeeperClient = ZookeeperClients.client();
try {
// 检查监听路径是否存在
final Stat stat = zookeeperClient.checkExists().forPath(LISTEN_PATHS);
// 如果监听路径不存在 则创建
if (stat == null){
zookeeperClient.create().creatingParentsIfNeeded().forPath(LISTEN_PATHS);
}
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("确保基础路径存在");
}
final ReadyRestartInstances instances = new ReadyRestartInstances(zookeeperClient);
// 创建一个NodeCache
PathChildrenCache nodeCache = new PathChildrenCache(zookeeperClient,LISTEN_PATHS,false);
// 给节点缓存对象 加入监听
nodeCache.getListenable().addListener(instances);
try {
nodeCache.start();
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("启动路径监听失败");
}
return instances;
}
/** 返回应用名 和 主机拼接后的字符串 */
private String buildApplicationAndInstanceString(String applicationName,String host){
return applicationName + "_" + host;
}
/** 增加重启实例的配置信息方法 */
public void addRestartingInstance(String applicationName,String host) throws Exception{
zkClient.create().creatingParentsIfNeeded().forPath(LISTEN_PATHS + "/" + buildApplicationAndInstanceString(applicationName,host));
}
/** 删除重启实例的配置信息方法 */
public void removeRestartingInstance(String applicationName,String host) throws Exception{
zkClient.delete().forPath(LISTEN_PATHS + "/" + buildApplicationAndInstanceString(applicationName,host));
}
/** 判断节点信息 是否存在于 restartInstances */
public boolean hasRestartingInstance(String applicationName,String host){
return restartInstances.contains(buildApplicationAndInstanceString(applicationName,host));
}
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
// 查询出监听路径下 所有的目录配置信息
final List<String> restartingInstances = zkClient.getChildren().forPath(LISTEN_PATHS);
// 给 restartInstances
if(CollectionUtils.isEmpty(restartingInstances)){
//如果监听的zookeeper节点的变更列表为空,则给restartInstances赋值个空Set
this.restartInstances = Collections.emptySet();
}else{
//将zookeeper变更的集合列表restartingInstances通过HashSet构造赋值给restartInstances
this.restartInstances = new HashSet<>(restartingInstances);
}
}
}
(
4
)编写路由类
(
实现
org.apache.dubbo.rpc.cluster.Router
),主要目的在于对ReadyRestartInstances
中的数据进行处理,并且移除路由调用列表中正在重启中的服务。
package com.lagou.router;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.cluster.Router;
import java.util.List;
import java.util.stream.Collectors;
public class RestartingInstanceRouter implements Router {
private final ReadyRestartInstances instances;
private final URL url;
public RestartingInstanceRouter(URL url) {
this.url = url;
this.instances = ReadyRestartInstances.create();
}
@Override
public URL getUrl() {
return url;
}
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
// 如果没有在重启列表中 才会加入到后续调用列表
//返回没有在重启列表中过滤以后的list的
return invokers.stream().filter(i->!instances.hasRestartingInstance(i.getUrl().getParameter("remote.application"),i.getUrl().getIp()))
.collect(Collectors.toList());
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean isForce() {
return true;
}
@Override
public int getPriority() {
return 0;
}
}
(
5
)由于
Router
机制比较特殊,所以需要利用一个专门的
RouterFactory
来生成,原因在于并不是所有的都需要添加路由,所以需要利用
@Activate
来锁定具体哪些服务才需要生成使用。
package com.lagou.router;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.cluster.Router;
import org.apache.dubbo.rpc.cluster.RouterFactory;
@Activate
public class RestartingInstanceRouterFactory implements RouterFactory {
@Override
public Router getRouter(URL url) {
return new RestartingInstanceRouter(url);
}
}
(
6
)对
RouterFactory
进行注册,同样放入到
META
-
INF/dubbo/org.apache.dubbo.rpc.cluster.RouterFactory
文件中。
restartInstances = com.lagou.router.RestartingInstanceRouterFactory
(
7
)将
dubbo-spi-router
项目引入至
consumer
项目的依赖中。
(
8
)这时直接启动程序,还是利用上面中所写好的
consumer
程序进行执行,确认各个
provider
可以正常执行。
(
9
)单独写一个
main
函数来进行将某台实例设置为启动中的状态,比如这里我们认定为当前这台机器中的
service
-
provider
这个提供者需要进行重启操作。
(
10
)执行完成后,再次进行尝试通过
consumer
进行调用,即可看到当前这台机器没有再发送任何请求
(
11
)一般情况下,当机器重启到一定时间后,我们可以再通过
removeRestartingInstance
方法对这个机器设定为既可以继续执行。
(
12
)调用完成后,我们再次通过
consumer
去调用,即可看到已经再次恢当前机器的请求参数。
package com.lagou;
import com.lagou.router.ReadyRestartInstances;
public class ServerRestartMain {
public static void main(String[] args) throws Exception {
//ReadyRestartInstances.create().addRestartingInstance("service-provider","192.168.20.1");
ReadyRestartInstances.create().removeRestartingInstance("service-provider","192.168.20.1");
}
}
(二)案例完整示例代码
八. 服务动态降级
(一)什么是服务降级
服务降级,当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略的降低服务级别,以释放服务器资源,保证核心任务的正常运行。
(二)为什么要服务降级
而为什么要使用服务降级,这是防止分布式服务发生雪崩效应,什么是雪崩?就是蝴蝶效应,当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到
服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机,
这样下去将导致整个分布式服务都瘫痪,这就是雪崩。
(三)dubbo 服务降级实现方式
第一种 在 dubbo 管理控制台配置服务降级
屏蔽和容错
- 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" />
如果是标注 则使用
@Reference(mock="return null") @Reference(mock="return
简单值
")
也支持
@Reference(mock="force:return null")
第三种
使用
java
代码 动态写入配置中心
RegistryFactory registryFactory =ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://IP: 端口 "));registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));
第四种
整合整合
hystrix
会在后期
SpringCloud
课程中详细讲解
以上为拉钩教育课堂笔记整理