1.Dubbo原理分析图:
2.Dubbo服务信息存放方式
Dubbo服务信息以持久+临时混合进行存储在注册中心zookeeper中。
服务基本信息以持久进行存储,服务接口信息一般不会发生改变,采用持久节点进行存储。
服务接口地址以临时节点进行存储,因为地址是动态,所以采用临时存放。
1.准备工作
以上一篇博客的Maven项目代码继续演示:
项目结构,分为三个项目:
itmayiedu-dubbo-provider-api 服务提供者对外接口
itmayiedu-dubbo-provider 服务提供者接口实现
itmayiedu-dubbo-consumer 服务消费者
2.启动zookeeper
3.启动服务提供者项目 itmayiedu-dubbo-provider
4.启动服务消费者项目 itmayiedu-dubbo-consumer
5.查看服务提供者项目,调用成功
6.打开zookeeper图形化的客户端工具ZooInspector,查看zookeeper信息
客户端工具课参考文档:https://blog.csdn.net/xuruanshun/article/details/102733259
打开可以查看到,服务提供者将服务信息列表以key,value的形式注册到注册中心上。
key是服务接口的全路径
注:此节点是持久节点,只要曾经注册到zookeeper中,这个key就永远保存。
com.itmayiedu.member.service.MemberService
value有子节点providers存放多个服务接口的实际地址
注:当服务提供者项目是一个集群的时候,providers是列表。
且是一个临时节点,一旦服务提供者项目停止,则此节点数据消失。
dubbo%3A%2F%2F192.168.1.4%3A20880%2Fcom.itmayiedu.member.service.MemberService%3Fanyhost%3Dtrue%26application%3Dmember-provider%26dubbo%3D2.5.3%26interface%3Dcom.itmayiedu.member.service.MemberService%26methods%3DgetUser%26pid%3D52644%26revision%3D1.0-SNAPSHOT%26side%3Dprovider%26timestamp%3D1572269472070
演示一下,持久节点和临时节点效果:
1.把服务提供者项目停止掉。
2.再打开ZooInspector查看zookeeper信息
key依然存在,value却消失了。
3.服务提供者做集群
1.第一台服务提供者的端口号20880
2.编辑启动类MemberApp,MemberServiceImpl,都加上端口号
3.启动端口号20880的项目
4.修改dubbo-provider.xml的端口号为20881,第二台服务提供者的端口号20881
5.编辑启动类MemberApp,MemberServiceImpl,都加上端口号
6.再次启动端口号20881的项目
此时服务提供者已经运行了两台服务器,相当于做了集群。
注:在Allow paraller run勾上对勾,则可以多项目并行。
7.打开ZooInspector,查看zookeeper信息,有了两个服务接口列表。
3.Dubbo的一些重点知识(重点)
参考文档:https://my.oschina.net/jiagouzhan/blog/3005582
1)dubbo官方推荐使用的是dubbo协议,dubbo协议是单一长连接 和NIO异步通讯。
2)dubbo默认采用的是zookeeper注册中心。
3)注册中心集群挂断后,发布者和订阅者短时间内依然可以通信,采用的是本地缓存机制。
4)dubbo提供了四种负载均衡策略,默认的是第一种Random LoadBalance。
讨论:之前我一直以为dubbo的负载均衡策略是第二种采用轮循策略,启动多台服务器,客户端会依次调用几台服务器,但后来发现并不是这样,不是平均分布的,而是采用随机提供者策略,调用次数越多,分布越均匀。
通过注解方式可以更改负载均衡策略,在@Service注解上添加属性
@Service(loadbalance=“random”,retries=2)
@Service(loadbalance=“roundrobin”,retries=2)
@Service(loadbalance=“leastactive”,retries=2)
@Service(loadbalance=“consistenthash”,retries=2)
5)dubbo默认的容错机制是第一种Failover Cluster,失败自动切换到别的服务器。
讨论:我启动两台服务器,一台客户端,访问时候两台都是可以访问到,然后关掉一台服务器,客户就只能访问到没宕机的那台,根本不会访问宕机的那台,这就是采用的是dubbo的默认容错机制。
zookeeper采用的是心跳机制,默认是30s,每间隔30s就会检查注册到zookeeper中的服务器是否宕机,dubbo的客户端采用订阅的方式去监听zookeeper,一旦zookeeper发现有服务器宕机,则会通过事件通知的方式告诉dubbo客户端,然后dubbo客户端重新从zookeeper中获取服务接口列表,远程调用服务接口。
上面的情况,强制宕机一台服务器,zookeeper没有到30s就不会发现有服务器宕机,此时dubbo客户端采用的是本地缓存的接口服务列表,所以此时的服务列表还是两台服务器,但是调用宕机的那一台时发现没法调通,此时采用dubbo默认的容错机制,失败后自动切换到别的服务器,所以只会访问没有宕机的服务器,不会访问宕机的服务器。当心跳时间到了,zookeeper发现有服务器宕机,就会刷新zookeeper的服务接口列表,客户端监控到之后,就会刷新本地服务接口列表,调用服务。
6)注册中心存在心跳机制,可以在zoo.cfg中更改心跳间隔时间。
7)在rpc远程调用技术中,一般是用的都是客户端本地负载均衡,原因如下:
8)当我宕掉一台服务器时,使用zookeeper客户端工具时,会立刻发现这台服务器宕掉了,按道理来说zookeeper的心态时间是30秒,不会立刻发现这台机器宕掉了,其实zookeeper中还是有两台机器的,只是zookeeper客户端工具会坐一个甄别,他从zookeeper的缓存中找到两台服务器,但是会去看机器是否宕掉,发现有一台宕掉,所以只剩下一台了。也就是说zookeeper是有心跳时间,缓存机制的,但是zookeeper的客户端工具是实时的
4.软负载和硬负载:
无论是本地负载均衡(ribbon),还是服务端负载均衡(nginx),都是通过软件实现的负载均衡,属于软负载。
5.基于Zookeeper实现Dubbo动态负载均衡
就是我们自己根据socket协议写一个类似于dubbo的项目,实现服务端注册接口信息到zookeeper,服务端从zookeeper中获取到服务接口信息列表的功能。
1)原理图:
2.创建项目
3.pom依赖
<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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itmayiedu</groupId>
<artifactId>itmayiedu_dubbo_load</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.8</version>
</dependency>
</dependencies>
</project>
4.ServerHandler
package com.itmayiedu.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerHandler implements Runnable {
private Socket socket;
public ServerHandler(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while (true) {
body = in.readLine();
if (body == null)
break;
System.out.println("Receive : " + body);
out.println("Hello, " + body);
}
} catch (Exception e) {
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (out != null) {
out.close();
}
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
this.socket = null;
}
}
}
}
5.ServerScoekt服务端
package com.itmayiedu.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import org.I0Itec.zkclient.ZkClient;
//##ServerScoekt服务端
public class ZkServerScoekt implements Runnable {
private static int port = 8081;
private String parentService = "/service";
/**
* 服务器端:<br>
* 1.服务端启动的时候,会将当前服务信息注册到注册中心。首先先创建一个父节点为service,在父节点下面在创建一个子节点,
* 每个子节点都存放当前服务接口地址。<br>
* ##节点结构 <br>
* /service 持久节点<br>
* #####/8080 value 127.0.0.1:8080 临时节点<br>
* #####/8081 value 127.0.0.1:8081 临时节点<br>
*
*/
private ZkClient zkClient = new ZkClient("127.0.0.1:2181");
public static void main(String[] args) throws IOException {
ZkServerScoekt server = new ZkServerScoekt(port);
Thread thread = new Thread(server);
thread.start();
}
public ZkServerScoekt(int port) {
this.port = port;
}
private void regServer() {
// 1.先创建父节点service 为持久节点
if (!zkClient.exists(parentService)) {
// 2.创建父节点
zkClient.createPersistent(parentService);
}
String serverKey = parentService + "/server_" + port;
if (zkClient.exists(serverKey)) {
zkClient.delete(serverKey);
}
// 3.创建子节点 value为服务接口地址
zkClient.createEphemeral(serverKey, "127.0.0.1:" + port);
}
public void run() {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(port);
System.out.println("Server start port:" + port);
regServer();
Socket socket = null;
while (true) {
socket = serverSocket.accept();
new Thread(new ServerHandler(socket)).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (serverSocket != null) {
serverSocket.close();
}
} catch (Exception e2) {
}
}
}
}
6.ServerScoekt客户端
package com.itmayiedu.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;
//##ServerScoekt客户端
public class ZkServerClient {
// 存放服务列表信息
public static List<String> listServer = new ArrayList<String>();
// 客户端:读取service节点,获取下面的子节点value值 本地实现远程调用。
private static ZkClient zkClient = new ZkClient("127.0.0.1:2181");
private static String parentService = "/service";
public static void main(String[] args) {
initServer();
ZkServerClient client = new ZkServerClient();
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String name;
try {
name = console.readLine();
if ("exit".equals(name)) {
System.exit(0);
}
client.send(name);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 注册所有server
public static void initServer() {
// listServer.clear();
// listServer.add("127.0.0.1:8080");
// 从Zookeeper上获取服务列表信息
List<String> children = zkClient.getChildren(parentService);
getChildData(zkClient, children);
// 使用Zk事件通知获取最新服务列表信息
zkClient.subscribeChildChanges(parentService, new IZkChildListener() {
public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
System.out.println("注册中心服务里列表信息发生变化..");
getChildData(zkClient, currentChilds);
}
});
}
public static void getChildData(ZkClient zkClient, List<String> children) {
listServer.clear();
for (String p : children) {
String serverAddres = zkClient.readData(parentService + "/" + p);
listServer.add(serverAddres);
}
System.out.println("服务接口地址:" + listServer.toString());
}
// 请求总数
private static int reqCount = 1;
// 获取当前server信息
public static String getServer() {
int index = reqCount % listServer.size();
String addres = listServer.get(index);
System.out.println("客户端请求服务端:" + addres);
reqCount++;
return addres;
}
public void send(String name) {
String server = ZkServerClient.getServer();
String[] cfg = server.split(":");
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket(cfg[0], Integer.parseInt(cfg[1]));
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
out.println(name);
while (true) {
String resp = in.readLine();
if (resp == null)
break;
else if (resp.length() > 0) {
System.out.println("Receive : " + resp);
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
7.启动服务端
8.启动客户端
9.修改服务端端口为8081,再启动一台服务端
客户端立即显示服务端发生变化
10.再强制关掉8081服务器
过了一会客户端才显示服务端发生变化
原因:这种强制性的关闭服务端,注册中心不会立刻就会发生改变,需要等一段时间才会发生改变,并且发送通知到客户端。