Dubbo分布式服务治理框架(二)负载均衡原理分析

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服务器

过了一会客户端才显示服务端发生变化

原因:这种强制性的关闭服务端,注册中心不会立刻就会发生改变,需要等一段时间才会发生改变,并且发送通知到客户端。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值