Dubbo的基本应用与高级应用
负载均衡
负载均衡策略
- Random LoadBalance(默认)
随机,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 - RoundRobin LoadBalance
轮询,按公约后的权重设置轮询比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 - LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 - ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
案例
轮询
- 服务提供者
package com.tuling.provider.service;
import com.tuling.DemoService;
import com.tuling.DemoServiceListener;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.RpcContext;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service(version = "default")
public class DefaultDemoService implements DemoService {
@Override
public String sayHello(String name) {
System.out.println("执行了服务" + name);
URL url = RpcContext.getContext().getUrl();
return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问
}
}
- 消费端
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@EnableAutoConfiguration
public class LoadBalanceDubboConsumerDemo {
@Reference(version = "default", loadbalance = "roundrobin")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(LoadBalanceDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
// 用来负载均衡
for (int i = 0; i < 1000; i++) {
System.out.println((demoService.sayHello("周瑜")));
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 运行结果
一致性Hash
-
服务提供者不变
-
消费端
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@EnableAutoConfiguration
public class LoadBalanceDubboConsumerDemo {
@Reference(version = "default", loadbalance = "consistenthash")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(LoadBalanceDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
// 一致性hash算法测试
for (int i = 0; i < 1000; i++) {
System.out.println((demoService.sayHello(i%5+"周瑜")));
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 运行结果
小结
如果在消费端和服务端都配置了负载均衡策略,以消费端为准。
这其中比较难理解的就是最少活跃调用数是如何进行统计的?
讲道理,最少活跃数应该是在服务提供者端进行统计的,服务提供者统计有多少个请求正在执行中。
但在Dubbo中,就是不讲道理,它是在消费端进行统计的,为什么能在消费端进行统计?
逻辑是这样的:
- 消费者会缓存所调用服务的所有提供者,比如记为p1、p2、p3三个服务提供者,每个提供者内都有一个属性记为active,默认位0
- 消费者在调用次服务时,如果负载均衡策略是leastactive
- 消费者端会判断缓存的所有服务提供者的active,选择最小的,如果都相同,则随机
- 选出某一个服务提供者后,假设为p2,Dubbo就会对p2.active+1
- 然后真正发出请求调用该服务
- 消费端收到响应结果后,对p2.active-1
- 这样就完成了对某个服务提供者当前活跃调用数进行了统计,并且并不影响服务调用的性能
服务超时
在服务提供者和服务消费者上都可以配置服务超时时间,这两者是不一样的。
消费者调用一个服务,分为三步:
- 消费者发送请求(网络传输)
- 服务端执行服务
- 服务端返回响应(网络传输)
如果在服务端和消费端只在其中一方配置了timeout,那么没有歧义,表示消费端调用服务的超时时间,消费端如果超过时间还没有收到响应结果,则消费端会抛超时异常,但,服务端不会抛异常,服务端在执行服务后,会检查执行该服务的时间,如果超过timeout,则会打印一个超时日志。服务会正常的执行完。
如果在服务端和消费端各配了一个timeout,那就比较复杂了,假设
- 服务执行为5s
- 消费端timeout=3s
- 服务端timeout=6s
那么消费端调用服务时,消费端会收到超时异常(因为消费端超时了),服务端一切正常(服务端没有超时)。
案例
- 服务提供者
package com.tuling.provider.service;
import com.tuling.DemoService;
import com.tuling.DemoServiceListener;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.RpcContext;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Service(version = "timeout", timeout = 6000)
public class TimeoutDemoService implements DemoService {
@Override
public String sayHello(String name) {
System.out.println("执行了timeout服务" + name);
// 服务执行5秒
// 服务超时时间为3秒,但是执行了5秒,服务端会把任务执行完的
// 服务的超时时间,是指如果服务执行时间超过了指定的超时时间则会抛一个warn
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行结束" + name);
URL url = RpcContext.getContext().getUrl();
return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问
}
}
- 消费者
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@EnableAutoConfiguration
public class TimeoutDubboConsumerDemo {
@Reference(version = "timeout", timeout = 3000)
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(TimeoutDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
// 服务调用超时时间为1秒,默认为3秒
// 如果这1秒内没有收到服务结果,则会报错
System.out.println((demoService.sayHello("周瑜"))); //xxservestub
}
}
- 运行结果
集群容错
集群容错表示:服务消费者在调用某个服务时,这个服务有多个服务提供者,在经过负载均衡后选出其中一个服务提供者之后进行调用,但调用报错后,Dubbo所采取的后续处理策略。
集群容错模式
- Failover Cluster
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。该配置为缺省配置 - Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 - Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 - Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 - Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数。 - Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
案例
Failover Cluster
上述服务超时的案例服务提供者的运行结果
Failfast Cluster
- 服务提供者不变
- 消费者
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@EnableAutoConfiguration
public class ClusterDubboConsumerDemo {
@Reference(version = "timeout",timeout = 1000, cluster = "failfast")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(ClusterDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
System.out.println((demoService.sayHello("周瑜")));
}
}
- 运行结果
服务降级
服务降级表示:服务消费者在调用某个服务提供者时,如果该服务提供者报错了,所采取的措施。
集群容错和服务降级的区别在于:
- 集群容错是整个集群范围内的容错
- 服务降级是单个服务提供者的自身容错
案例
- 服务提供者不变
- 消费者
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@EnableAutoConfiguration
public class MockDubboConsumerDemo {
@Reference(version = "timeout", timeout = 1000, mock = "force: return 123")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(MockDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
System.out.println((demoService.sayHello("周瑜")));
}
}
- 运行结果
本地存根
本地存根,名字很抽象,但实际上不难理解,本地存根就是一段逻辑,这段逻辑是在服务消费端执行的,这段逻辑一般都是由服务提供者提供,服务提供者可以利用这种机制在服务消费者远程调用服务提供者之前或之后再做一些其他事情,比如结果缓存,请求参数验证等等。
案例
-
服务提供者不变
-
消费者
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@EnableAutoConfiguration
public class StubDubboConsumerDemo {
// @Reference(version = "timeout", timeout = 1000, stub = "com.tuling.DemoServiceStub")
@Reference(version = "timeout", timeout = 1000, stub = "true")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(StubDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
System.out.println((demoService.sayHello("周瑜")));
}
}
package com.tuling;
public class DemoServiceStub implements DemoService {
private final DemoService demoService;
// 构造函数传入真正的远程代理对象
public DemoServiceStub(DemoService demoService){
this.demoService = demoService;
}
@Override
public String sayHello(String name) {
// 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
try {
return demoService.sayHello(name); // safe null
} catch (Exception e) {
// 你可以容错,可以做任何AOP拦截事项
return "容错数据";
}
}
}
- 运行结果
本地伪装
本地伪装就是Mock,Dubbo中Mock的功能相对于本地存根更简单一点,Mock其实就是Dubbo中的服务容错的解决方案。
参数回调
首先,如果当前服务支持参数回调,意思就是:对于某个服务接口中的某个方法,如果想支持消费者在调用这个方法时能设置回调逻辑,那么该方法就需要提供一个入参用来表示回调逻辑。
因为Dubbo协议是基于长连接的,所以消费端在两次调用同一个方法时想指定不同的回调逻辑,那么就需要在调用时在指定一定key进行区分。
案例
- 服务提供者
package com.tuling.provider.service;
import com.tuling.DemoService;
import com.tuling.DemoServiceListener;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.config.annotation.Argument;
import org.apache.dubbo.config.annotation.Method;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.RpcContext;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// DemoService的sayHello方法的index=1的参数是回调对象,服务消费者可以调用addListener方法来添加回调对象,服务提供者一旦执行回调对象的方法就会通知给服务消费者
@Service(version = "callback", methods = {@Method(name = "sayHello", arguments = {@Argument(index = 2, callback = true)})}, callbacks = 3)
public class CallBackDemoService implements DemoService {
private final Map<String, DemoServiceListener> listeners = new ConcurrentHashMap<String, DemoServiceListener>();
public CallBackDemoService() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
for (Map.Entry<String, DemoServiceListener> entry : listeners.entrySet()) {
entry.getValue().changed(getChanged(entry.getKey()));
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
}
private String getChanged(String key) {
return "Changed: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
@Override
public String sayHello(String name) {
return null;
}
@Override
public String sayHello(String name, String key, DemoServiceListener callback) {
System.out.println("执行了回调服务" + name);
callback.changed("xxxx");
listeners.put(key, callback);
URL url = RpcContext.getContext().getUrl();
return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问
}
}
- 消费者
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@EnableAutoConfiguration
public class CallbackDubboConsumerDemo {
@Reference(version = "callback")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(CallbackDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
// 用来进行callback
System.out.println(demoService.sayHello("周瑜", "d1", new DemoServiceListenerImpl()));
System.out.println(demoService.sayHello("周瑜", "d2", new DemoServiceListenerImpl()));
System.out.println(demoService.sayHello("周瑜", "d3", new DemoServiceListenerImpl()));
}
}
package com.tuling;
public interface DemoServiceListener {
void changed(String msg);
}
package com.tuling.consumer;
import com.tuling.DemoServiceListener;
public class DemoServiceListenerImpl implements DemoServiceListener {
@Override
public void changed(String msg) {
System.out.println("被回调了:"+msg);
}
}
- 运行结果
异步调用
我们平常大部分都是使用 Dubbo 的同步调用,即调用 Dubbo 请求之后,调用线程将会阻塞,直到服务提供者返回结果。
那相反,Dubbo 异步调用就不会阻塞调用线程,那么在服务提供者返回结果这段时间,我们就可以执行其他业务逻辑。
Dubbo 异步调用是针对方法级别
主要要理解CompletableFuture
案例
- 服务提供者
package com.tuling.provider.service;
import com.tuling.DemoService;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.RpcContext;
import java.util.concurrent.CompletableFuture;
@Service(version = "async", protocol = "p1")
public class AsyncDemoService implements DemoService {
@Override
public String sayHello(String name) {
System.out.println("执行了同步服务" + name);
return "执行了同步服务";
}
@Override
public CompletableFuture<String> sayHelloAsync(String name) {
System.out.println("执行了异步服务" + name);
return CompletableFuture.supplyAsync(() -> {
return sayHello(name);
});
}
}
- 消费者
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
@EnableAutoConfiguration
public class AsyncDubboConsumerDemo {
@Reference(version = "async")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(AsyncDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
// 调用直接返回CompletableFuture
CompletableFuture<String> future = demoService.sayHelloAsync("异步调用"); // 5
future.whenComplete((v, t) -> {
if (t != null) {
t.printStackTrace();
} else {
System.out.println("Response: " + v);
}
});
System.out.println("结束了");
}
}
package com.tuling;
import java.util.concurrent.CompletableFuture;
public interface DemoService {
// 同步调用方法
String sayHello(String name);
// 异步调用方法
default CompletableFuture<String> sayHelloAsync(String name) {
return null;
};
// 添加回调
default String sayHello(String name, String key, DemoServiceListener listener) {
return null;
};
}
- 运行结果
泛化调用
泛化调用可以用来做服务测试。
在Dubbo中,如果某个服务想要支持泛化调用,就可以将该服务的generic属性设置为true,那对于服务消费者来说,就可以不用依赖该服务的接口,直接利用GenericService接口来进行服务调用。
案例
- 服务调用者不变
- 消费者
package com.tuling.consumer;
import org.apache.dubbo.config.annotation.Reference;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@EnableAutoConfiguration
public class GenericDubboConsumerDemo {
@Reference(id = "demoService", version = "default", interfaceName = "com.tuling.DemoService", generic = true)
private GenericService genericService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(GenericDubboConsumerDemo.class);
GenericService genericService = (GenericService) context.getBean("demoService");
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"周瑜"});
System.out.println(result);
}
}
- 运行结果
泛化服务
实现了GenericService接口的就是泛化服务
案例
- 服务提供者
package com.tuling.provider.service;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.service.GenericException;
import org.apache.dubbo.rpc.service.GenericService;
@Service(interfaceName = "com.tuling.DemoService", version = "generic")
public class GenericDemoService implements GenericService {
@Override
public Object $invoke(String s, String[] strings, Object[] objects) throws GenericException {
System.out.println("执行了generic服务");
return "执行的方法是" + s;
}
}
- 消费者
package com.tuling.consumer;
import org.apache.dubbo.config.annotation.Reference;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@EnableAutoConfiguration
public class GenericDubboConsumerDemo {
@Reference(id = "demoService", version = "generic", interfaceName = "com.tuling.DemoService", generic = true)
private GenericService genericService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(GenericDubboConsumerDemo.class);
GenericService genericService = (GenericService) context.getBean("demoService");
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"周瑜"});
System.out.println(result);
}
}
- 运行结果
Dubbo中的REST
注意Dubbo的REST也是Dubbo所⽀持的⼀种协议。
当我们⽤Dubbo提供了⼀个服务后,如果消费者没有使⽤Dubbo也想调⽤服务,那么这个时候我们就可以 让我们的服务⽀持REST协议,这样消费者就可以通过REST形式调⽤我们的服务了
案例
- 配置文件
# Spring boot application
spring.application.name=dubbo-provider-demo
server.port=8081
# Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service
dubbo.scan.base-packages=com.tuling.provider.service
dubbo.application.name=${spring.application.name}
## Dubbo Registry
dubbo.registry.address=zookeeper://192.168.1.104:2181
# Dubbo Protocol
#dubbo.protocol.name=dubbo
#dubbo.protocol.port=20880
#dubbo.protocol.name=rest
#dubbo.protocol.port=8083
dubbo.protocols.p1.id=dubbo1
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20881
dubbo.protocols.p1.host=0.0.0.0
dubbo.protocols.p2.id=rest
dubbo.protocols.p2.name=rest
dubbo.protocols.p2.port=8083
dubbo.protocols.p2.host=0.0.0.0
#dubbo.protocols.p2.id=dubbo2
#dubbo.protocols.p2.name=dubbo
#dubbo.protocols.p2.port=20882
#dubbo.protocols.p2.host=0.0.0.0
dubbo.protocols.p3.id=dubbo3
dubbo.protocols.p3.name=dubbo
dubbo.protocols.p3.port=20883
dubbo.protocols.p3.host=0.0.0.0
- 服务提供者
package com.tuling.provider.service;
import com.tuling.DemoService;
import com.tuling.DemoServiceListener;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.protocol.rest.support.ContentType;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service(version = "rest", protocol = "p2")
//@Service(version = "rest")
@Path("demo")
public class RestDemoService implements DemoService {
@GET
@Path("say")
@Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})
@Override
public String sayHello(@QueryParam("name") String name) {
System.out.println("执行了rest服务" + name);
URL url = RpcContext.getContext().getUrl();
return String.format("%s: %s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问
}
}
- 运行结果