简介
Apache Dubbo
Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
1 PRC RMI HESSION DUBBO
2 DUBBO的其他功能
3 DUBBO源码解析(SERVICE,)
首先了解什么叫RPC,为什么要RPC,RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
首先,要解决通讯的问题,主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
第二,要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么,这样才能完成调用。比如基于Web服务协议栈的RPC,就要提供一个endpoint URI,或者是从UDDI服务上查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。
第三,当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给B服务器。
第四,B服务器收到请求后,需要对参数进行反序列化(序列化的逆操作),恢复为内存中的表达方式,然后找到对应的方法(寻址的一部分)进行本地调用,然后得到返回值。
第五,返回值还要发送回服务器A上的应用,也要经过序列化的方式发送,服务器A接到后,再反序列化,恢复为内存中的表达方式,交给A服务器上的应用
RPC的协议有很多,比如最早的CORBA,Java RMI,Web Service的RPC风格,Hessian,Thrift,甚至Rest API。
RPC DEMO
接口:
public interface ProviderService {
String SayHello(String word);
}
实现类
public class ProviderServiceImpl implements ProviderService {
public String SayHello(String word) {
return "服务方被调用,传入的参数为:"+word;
}
}
服务方
public class rpcProbiderMian {
public static void main(String[] args) {
ServerSocket server = null;
ObjectOutputStream out = null;
try {
server = new ServerSocket(8080);
Socket socket =null;
while(true){
System.out.println("---开始监听---");
socket = server.accept();
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
String interfaceName = input.readUTF(); //接口名称
String methodName = input.readUTF(); //方法名称
Class<?>[] parameterType = (Class<?>[]) input.readObject(); //方法类型
Object[] arguments = (Object[]) input.readObject(); //参数列表
System.out.println("接收到的参数:"+Arrays.toString(arguments));
//根据接口名称获取class
Class<?> serviceInterfaceClass = Class.forName(interfaceName);
//根据方法名称和参数类型反射得到方法
Method method = serviceInterfaceClass.getMethod(methodName, parameterType);
//服务实例化(这里做简单处理,正常应该根据得到的接口名称serviceInterfaceClass获取对应的service,但本demo只提供一个服务)
ProviderService service = new ProviderServiceImpl();
//反射执行这个方法
Object result = method.invoke(service, arguments);
//写会处理结果
out = new ObjectOutputStream(socket.getOutputStream());
out.writeObject(result);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
if(server!=null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
server = null;
}
}
}
}
消费方
public class prcConsumerMain {
public static void main(String[] args) {
try {
//接口名称
String interfaceName = ProviderService.class.getName();
//接口方法
Method method = ProviderService.class.getMethod("SayHello", java.lang.String.class);
//参数
Object[] arguments = {"hello"};
Socket socket = new Socket("127.0.0.1",8080);
//发送请求
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.writeUTF(interfaceName);
out.writeUTF(method.getName());
out.writeObject(method.getParameterTypes());
out.writeObject(arguments);
//获取结果
ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
Object returnObject = inputStream.readObject();
System.out.println(returnObject.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
RMI demo
接口
/**
*
* 定义一个远程接口,必须继承Remote接口,其中需要远程调用的方法必须抛出RemoteException异常
*/
public interface RmiHelloService extends Remote {
//远程调用的方法必须有返回值,不能为void
public String sayHello() throws RemoteException;
//必须抛出RemoteException异常,否认则Client运行时会抛出RemoteException异常
public String sayHelloTo(String somebody) throws RemoteException;
}
实现类
/**
*
* 继承UnicastRemoteObject 父类并实现自定义的远程接口
* 因为UnicastRemoteObject的构造方法抛出了RemoteException异常,因此这里默认的构造方法必须写,必须声明抛出RemoteException异常
*/
public class HelloImpl extends UnicastRemoteObject implements IHello {
public HelloImpl() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
//System.out.println("hello!");
return "hello";
}
@Override
public String sayHelloTo(String somebody) throws RemoteException {
//System.out.println("hello" + somebody);
return "hello" + somebody;
}
}
服务方方法
public class rmiProbiderMian {
public static void main(String[] args) {
try {
//创建一个远程对象
RmiHelloService rmiHello = new RmiHelloServiceImpl();
//远程主机远程对象注册表Registry的实例,并指定端口为8888,这一步必不可少(Java默认端口是1099),
// 必不可缺的一步,缺少注册表创建,则无法绑定对象到远程注册表上
LocateRegistry.createRegistry(8888);
//把远程对象注册到RMI注册服务器上,并命名为RHello
//绑定的URL标准格式为:rmi://host:port/name(其中协议名可以省略,下面两种写法都是正确的)
Naming.bind("rmi://localhost:8888/RmiHello",rmiHello);
//Naming.bind("//localhost:8888/RmiHello",rmiHello);
//必须捕获这三个异常,否则需要在main方法中抛出
} catch (RemoteException e) {
System.out.println("创建远程对象发生异常");
e.printStackTrace();
} catch (MalformedURLException e) {
System.out.println("URL畸形异常");
e.printStackTrace();
} catch (AlreadyBoundException e) {
System.out.println("重复绑定对象异常");
e.printStackTrace();
}
}
}
消费方代码
public class rmiConsumerMain {
public static void main(String[] args) {
try {
//在RMI服务注册表中查找名称为RHello的对象,并调用其上的方法
RmiHelloService localHello = (RmiHelloService)Naming.lookup("rmi://localhost:8888/RmiHello");
System.out.println(localHello.sayHello());
System.out.println(localHello.sayHelloTo("大商"));
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
DUBBO DEMO
consumer配置
<?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://code.alibabatech.com/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.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<dubbo:application name="consumer" owner="sihai"/>
<dubbo:registry address="zookeeper://10.201.1.222:2181"></dubbo:registry>
<!-- timeout="0" 默认是1000ms-->
<!-- retries="":重试次数,不包含第一次调用,0代表不重试-->
<!-- 幂等(设置重试次数)【查询、删除、修改】、非幂等(不能设置重试次数)【新增】 -->
<!--在user-service-provider中暴露的接口服务 -->
<!-- <dubbo:method name="getUserAddressList" timeout="1000"></dubbo:method> -->
<dubbo:reference interface="service.provider.api.ProviderService"
id="providerService" timeout="5000" retries="3" >
</dubbo:reference>
</beans>
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://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 1、指定当前服务/应用的名字(同样的服务名字相同,不要和别的服务同名) -->
<dubbo:application name="user-service-provider01"></dubbo:application>
<!-- 2、指定注册中心的位置 -->
<!-- <dubbo:registry address="zookeeper://127.0.0.1:2181"></dubbo:registry> -->
<dubbo:registry protocol="zookeeper" address="10.201.1.222:2181"></dubbo:registry>
<!-- 3、指定通信规则(通信协议?通信端口) -->
<dubbo:protocol name="dubbo" port="20882"></dubbo:protocol>
<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service interface="service.provider.api.ProviderService"
ref="providerService" />
<!--Bean bean定义-->
<bean id="providerService" class="service.provider.impl.ProviderServiceImpl"/>
</beans>
public class dubboConsumerMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext c = new ClassPathXmlApplicationContext("consumer.xml");
c.start();
ProviderService providerService = (ProviderService) c.getBean(ProviderService.class);
String dubbo = providerService.SayHello("dubbo");
System.out.println(dubbo);
System.out.println("调用完成....");
try {
System.in.read();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class dubboProviderMain {
public static void main( String[] args ) throws IOException {
//加载xml配置文件启动
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("provider.xml");
context.start();
System.in.read(); // 按任意键退出
}
}
HESSIAN DEMO
web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>hessian-showcase</display-name>
<servlet>
<servlet-name>hessian-service</servlet-name>
<servlet-class>
com.caucho.hessian.server.HessianServlet
</servlet-class>
<init-param>
<param-name>home-class</param-name>
<param-value>
<!-- 服务实现类 -->
session.service.impl.HelloServiceImpl
</param-value>
</init-param>
<init-param>
<param-name>home-api</param-name>
<!-- 服务接口 -->
<param-value>hession.service.api.HelloService</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>hessian-service</servlet-name>
<url-pattern>/hessian</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
public interface HelloService {
public String helloWorld(String message);
}
public class HelloServiceImpl implements HelloService {
@Override
public String helloWorld(String message) {
return "hello," + message;
}
}
public class hessionConsumerMain {
public static void main(String[] args) throws MalformedURLException {
String url = "http://localhost:8080/hessian-showcase/hessian";
System.out.println(url);
HessianProxyFactory factory = new HessianProxyFactory();
ProviderService helloService = (ProviderService) factory.create(ProviderService.class, url);
System.out.println(helloService.SayHello("jimmy"));
}
}
RMI使用
前台客户端调用
List<FuncDto> funcList = (List<FuncDto>) ApplicationContext.getInstance().getClient()
.remoteInvoke(ModuleConstants.MOUDLE_NAME_CMS,"FuncRecordService", "findAllFunc");
dubbo的一些应用
1 超时
超时(timeout)
在接口调用过程中,consumer调用provider的时候,provider在响应的时候,有可能会慢,如果provider 10s响应,那么consumer也会至少10s才响应。如果这种情况频度很高,那么就会整体降低consumer端服务的性能。
这种响应时间慢的症状,就会像一层一层波浪一样,从底层系统一直涌到最上层,造成整个链路的超时。
所以,consumer不可能无限制地等待provider接口的返回,会设置一个时间阈值,如果超过了这个时间阈值,就不继续等待。
2 重试 超时时间的配置是为了保护服务,避免consumer服务因为provider 响应慢而也变得响应很慢,这样consumer可以尽量保持原有的性能。
但是也有可能provider只是偶尔抖动,那么超时后直接放弃,不做后续处理,就会导致当前请求错误,也会带来业务方面的损失。
那么,对于这种偶尔抖动,可以在超时后重试一下,重试如果正常返回了,那么这次请求就被挽救了,能够正常给前端返回数据,只不过比原来响应慢一点
查询可以重试,增删改不建议重试(如果允许重试,那么应该需要做幂等,不能因为多次处理同一种请求使业务数据造成异常,比如新增2次)
3 熔断
Dubbo + Hystrix 实现服务熔断
Hystrix [hɪst’rɪks],中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力。本文所说的Hystrix是Netflix开源的一款容错框架,同样具有自我保护能力。为了实现容错和自我保护,下面我们看看Hystrix如何设计和实现的。
根据业务来拆分成一个个的服务,服务与服务之间可以通过RPC相互调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证 100% 可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩”效应。
当对特定的服务的调用的不可用达到一个阀值(Hystrix 是 5 秒 20 次) 熔断器将会被打开
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
@EnableHystrix
@SpringBootApplication
public class HelloDubboServiceUserProviderApplication {
public static void main(String[] args) {
SpringApplication.run(HelloDubboServiceUserProviderApplication.class, args);
Main.main(args);
}
}
@Service(version = "${user.service.version}")
public class UserServiceImpl implements UserService {
@Value("${dubbo.protocol.port}")
private String port;
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
})
@Override
public String sayHi() {
// return "Hello Dubbo, i am from port:" + port;
throw new RuntimeException("Exception to show hystrix enabled.");
}
}
@EnableHystrix
@SpringBootApplication
public class HelloDubboServiceUserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(HelloDubboServiceUserConsumerApplication.class, args);
}
}
@RestController
public class UserController {
@Reference(version = "${user.service.version}")
private UserService userService;
@HystrixCommand(fallbackMethod = "hiError")
@RequestMapping(value = "hi")
public String sayHi() {
return userService.sayHi();
}
public String hiError() {
return "Hystrix fallback";
}
}
过滤器黑白名单:
public class ValidationFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
Properties prop = new Properties();
InputStream in = ValidationFilter.class.getResourceAsStream("/ipwhitelist.properties");
String clientIp = RpcContext.getContext().getRemoteHost();//客户端ip
try {
prop.load(in);
String ipwhitelist = prop.getProperty("ipwhitelist");//ip白名单
if (ipwhitelist.contains(clientIp)) {
return invoker.invoke(invocation);
} else {
return new RpcResult(new Exception("ip地址:"
+ clientIp + "没有访问权限"));
}
} catch (IOException e) {
e.printStackTrace();
} catch (RpcException e) {
throw e;
} catch (Throwable t) {
throw new RpcException(t.getMessage(), t);
}
return invoker.invoke(invocation);
}
}
<?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://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
"
>
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="common-service" />
<!-- 使用zookeeper注册中心暴露服务地址 -->
<dubbo:registry protocol="zookeeper" address="193.112.76.194:2181" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:provider filter="validation" delay="-1" timeout="6000" retries="0"/>
<!-- 用户服务接口 -->
<dubbo:service interface="com.service.UserService" ref="userService" />
<bean id="userService" class="com.service.UserServiceImpl"></bean>
</beans>