随机算法
既然稍微了解了一下spring-cloud IRule的算法,那我就去找那些大佬稍微学习了一下基础的几个负载均衡算法,现在来做一个记录,可能写的不是很好,毕竟算法这个东西有一种只可意会不可言传的感觉。
先来一个入门级算法吧
普通随机算法
这个没啥好说的就只是简单随机一下就可以了。我们先编写一个ServerIps类来存放ip集合:
public class ServerIps {
public static final List<String> IP_LIST = Arrays.asList(
"192.168.0.1",
"192.168.0.2",
"192.168.0.3",
"192.168.0.4",
"192.168.0.5",
"192.168.0.6",
"192.168.0.7",
"192.168.0.8",
"192.168.0.9",
"192.168.0.10"
);
/**
* key = ip
* value = 权重占比
*/
public static final Map<String,Integer> IP_MAP = new HashMap<String,Integer>();
static {
IP_MAP.put("192.168.0.1",1);
IP_MAP.put("192.168.0.2",8);
IP_MAP.put("192.168.0.3",3);
IP_MAP.put("192.168.0.4",6);
IP_MAP.put("192.168.0.5",5);
IP_MAP.put("192.168.0.6",5);
IP_MAP.put("192.168.0.7",6);
IP_MAP.put("192.168.0.8",7);
IP_MAP.put("192.168.0.9",2);
IP_MAP.put("192.168.0.10",9);
}
}
新建一个RandomTest类
public class RandomTest {
/**
* 随机算法
* @return
*/
public static String getRandomIp(){
int ipAddres = new Random().nextInt(ServerIps.IP_LIST.size());
return ServerIps.IP_LIST.get(ipAddres);
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
System.out.println(getRandomIp());
}
}
运行一下main方法。简单是随机算法就完成了,是不是太简单了。不过呢,不要慌,这个只是最最最最简单的一个了,感觉都算不上算法,接下来我们稍微加一点难度吧。
权重随机算法
我们正式环境的服务器可不一定是每一台都一致的,总有性能好的和性能稍微差一点,所以这就会涉及到一个服务器权重,所以接下来我们做一下权重随机算法:
先讲一下这里的思路,如图:
画图这个真不会,所以就简单的意思意思了一下,我们还是继续来写代码吧。看一下代码就知道咋回事儿了:
public class RandomTest {
/**
* 随机算法
* @return
*/
public static String getRandomIp(){
int ipAddres = new Random().nextInt(ServerIps.IP_LIST.size());
return ServerIps.IP_LIST.get(ipAddres);
}
/**
* 加权随机算法
* @return
*/
public static String getWeightId(){
//将ip占比转换为对象数组,用于循环计算权重总数
Object[] ips = ServerIps.IP_MAP.values().toArray();
int ipTotal = 0;//权重总数
boolean flag=true;//用于判断权重是不是一样的
for(int i=0;i<ips.length;i++){
//计算权重总数
Integer ipWeight = (Integer) ips[i];
ipTotal += ipWeight;
//判断所有ip权重是否一致
if(flag && i>0 && !ipWeight.equals(ips[i-1])){
flag = false;
}
}
//权重不一致,就随机一个数来判断一下这个树落在哪里
if(!flag){
//产生一个随机数
int ipAddres = new Random().nextInt(ipTotal);
for(String ipKey : ServerIps.IP_MAP.keySet()){
Integer ipValue = ServerIps.IP_MAP.get(ipKey);
if(ipAddres<=ipValue){
return ipKey;
}
ipAddres = ipAddres - ipValue;
}
}
//所有权重都一致,那就随机好啦
return getRandomIp();
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
System.out.println(getWeightId());
}
}
运行一下,其实少量数据也看不出啥结果,这个还是一个概率问题,需要大量数据来统计分析的,但你数据越大就越会发现某个Ip出现的概率绝对跟他的权重占比相近。
轮询算法
随机轮询算法
一样的,我们先来一个简单的轮询算法,怎么理解这个轮询呢,就是一个接着一个,跟学生在食堂打饭一样,一个接一个(千万别说插队,那个你可以假装不知道)。还是来撸代码吧
public class PollingAlgorithm {
//定义一个全局参数,轮询依据
private static Integer address = 0;
/**
* 简单轮询算法
* @return
*/
public static String getPollingIp(){
String ip = null;
//这里加一个锁,防止并发影响轮询结果
synchronized (address){
if(address>= ServerIps.IP_LIST.size()){
address = 0;
}
ip = ServerIps.IP_LIST.get(address);
address++;
}
return ip;
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
System.out.println(getPollingIp());
}
}
}
运行结果,不用猜也知道了…
是吧,一个接着一个,故意加了一个锁,就是怕你们来插队。
加权轮询算法
好了,我们接下来在来看一下权重轮询算法吧,这里大概讲一下意思就不画图了,跟上面个随机权重有点像,不过一个是随机一个是轮询而已。
我们需要一个计数器,每次使用后就加1,然后我们根据计数器来除以权重总数得到余数,然后判断余数落在哪个点上就返回这个点上对应的IP就好。代码如下:
新建一个Sequence类来当计数器:
public class Sequence {
public static Integer num = 0;
public static Integer getIncrement(){
return ++num;
}
}
改一下 PollingAlgorithm类:
public class PollingAlgorithm {
//定义一个全局参数,轮询依据
private static Integer address = 0;
/**
* 简单轮询算法
* @return
*/
public static String getPollingIp(){
String ip = null;
//这里加一个锁,防止并发影响轮询结果
synchronized (address){
if(address>= ServerIps.IP_LIST.size()){
address = 0;
}
ip = ServerIps.IP_LIST.get(address);
address++;
}
return ip;
}
public static String getWeightPollingIp(){
//将ip占比转换为对象数组,用于循环计算权重总数
Object[] ips = ServerIps.IP_MAP.values().toArray();
int ipTotal = 0;//权重总数
boolean flag=true;//用于判断权重是不是一样的
for(int i=0;i<ips.length;i++){
//计算权重总数
Integer ipWeight = (Integer) ips[i];
ipTotal += ipWeight;
//判断所有ip权重是否一致
if(flag && i>0 && !ipWeight.equals(ips[i-1])){
flag = false;
}
}
//取得计数器
Integer sequenceNum = Sequence.getIncrement();
//取余数
Integer resultNum = sequenceNum%ipTotal;
if(resultNum == 0){
resultNum = ipTotal;
}
if(!flag){
//循环判断当前这个点落在哪里
for(String ipKey : ServerIps.IP_MAP.keySet()){
Integer ipValue = ServerIps.IP_MAP.get(ipKey);
if(resultNum <= ipValue){
return ipKey;
}
resultNum = resultNum - ipValue;
}
}
//如果权重都相同,那就简单的轮询就行
return getPollingIp();
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
System.out.println(getWeightPollingIp());
}
}
}
运行一下,这个就会根据你定义的权重来一个一个的走了。举一个例子,你定义了ABC三个IP地址,然后A占5,B占1,C占1那么这里打印的就会是
这个算是完成了吧,是不是很开心哈哈哈哈。但是呢,你有没有想过,一周七天,作为老大哥的A也不能连续上七天的班呀,那样超级累的,那我们能不能做成AABACAA,这样老大哥A也能中途出去玩一玩放松一下,毕竟劳逸结合才能做出更好的事情嘛。好吧,还能真能做出这种效果。接着看吧!
平滑加权轮询
首先我们来看一下大概的代码思想
好吧,不知道为啥这样做?或则不懂上面图片的意思?那就看这个吧:
现在是不是大概知道咋回事儿了?但是你不要问我为什么要这么操作。这个就像1+1为什么等于2不等于3。这个具体谁做出来的算法我也还没有找到根源,但是nginx的负载均衡算法好像就是这个,当然啦,比这个应该要复杂很多的。如果你还是不太懂呢,那就把下面代码多写几遍,这样你就知道是啥意思了。
新建一个IpWeight类
/**
* ip就是保存ip咯
* weight 保存 初始权重(固定不变的)
* crrentWeight 当前权重,随时改变
*/
public class IpWeight {
private String ip;
private Integer weight;
private Integer currentWeight;
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
public Integer getCurrentWeight() {
return currentWeight;
}
public void setCurrentWeight(Integer currentWeight) {
this.currentWeight = currentWeight;
}
public IpWeight(String ip, Integer weight, Integer currentWeight) {
this.ip = ip;
this.weight = weight;
this.currentWeight = currentWeight;
}
}
再新建一个SmoothPollingAlgorithm类
public class SmoothPollingAlgorithm {
private static Map<String,IpWeight> weightMap = new HashMap<String,IpWeight>();
public static String getIp(){
//将ip占比转换为对象数组,用于循环计算权重总数
Object[] ips = ServerIps.IP_MAP.values().toArray();
int ipTotal = 0;//权重总数
for(int i=0;i<ips.length;i++){
//计算权重总数
Integer ipWeight = (Integer) ips[i];
ipTotal += ipWeight;
}
//初始化 weightMap
if(weightMap.isEmpty()){
for(String ip:ServerIps.IP_MAP.keySet()){
weightMap.put(ip,new IpWeight(ip,ServerIps.IP_MAP.get(ip),ServerIps.IP_MAP.get(ip)));
}
}
//找出最大的IpWeight对象 根据currentWeight寻找
IpWeight maxWeight = null;
for (IpWeight weight:weightMap.values()){
if(maxWeight == null || weight.getCurrentWeight()>maxWeight.getCurrentWeight()){
maxWeight = weight;
}
}
//将最大的IpWeight对象减去总权重
maxWeight.setCurrentWeight(maxWeight.getCurrentWeight()- ipTotal);
for (IpWeight weight:weightMap.values()){
weight.setCurrentWeight(weight.getCurrentWeight()+weight.getWeight());
}
return maxWeight.getIp();
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
System.out.println(getIp());
}
}
}
运行一下,结果如下:
这个呢就叫做平滑加权轮询算法。是不是有点意思。
一致性Hash算法
一致性hash算法通过一个叫作一致性hash环的数据结构实现。这个环的起点是0,终点是2^32 - 1,并且起点与终点连接,环的中间的整数按逆时针分布,故这个环的整数分布范围是[0, 2^32-1]。
我们用这个能干啥?
服务器集群接收到一次请求调用时,可以根据请求的信息,比如客户端Ip地址,或则请求路径以及请求参数等信息进行哈希,可以得出一个哈希值,特点是对于相同的ip地址,或请求路径和参数哈希出来的值是一样的。只要再增加一个算法,能够把这个哈希值映射成一个服务端ip地址,就可以相同的请求(相同的ip地址,或请求路径和请求参数)落到同一台服务器上。
因为客户端的请求情况是无穷尽的(ip不同,地址不同,参数不同),所以对于哈希值也是无穷尽的,所以我们不可能把所有的哈希值都进行映射到服务器ip上,所以这里需要一个哈希环,如下:
但是这种情况要是挂掉一个就不公平了,比如ip2服务器挂了,然后ip3的压力就会增大,所以我们需要加入虚拟节点。大概如下:
就是添加很多虚拟节点,然后大家均分,这样就比较平均了,既重男也重女是吧!
那我们要来看一下代码了:
public class HashAlgorithm {
//定义一个TreeMap,基于红黑树的一个map
private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();
//每个真实节点对应的虚拟节点
private static final int VIRTUAL_NODES = 160;
//给每个真实节点添加虚拟节点
static {
for(String ip: ServerIps.IP_LIST){
for(int i=0;i<VIRTUAL_NODES;i++){
int hash = getHash(ip+"TE"+i);
virtualNodes.put(hash,ip);
}
}
}
public static String getIp(String clint){
//获取hash值
int hash = getHash(clint);
//得到大于这个hash值的map
SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
//大于该hash值的第一个元素位置
Integer resultIndex = subMap.firstKey();
//这是一个hash环,最后一个值就不能获取到了,需要给根节点
if(resultIndex == null){
resultIndex = virtualNodes.firstKey();
}
return virtualNodes.get(resultIndex);
}
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
//取出来的值为负数就取绝对值
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(getIp("client" + i));
}
}
}
这里可能有一个TreeMap是一个比较生疏的技术点,这个还请大家各自自己去了解一下这里我就不讲解了,他是基于红黑树实现的。
好了 运行一下,其实你就会发现分散还比较平均(我前面拿了好多数据来测做了统计看的)。
前面几种方法主要目的是使服务器分配到的调用次数尽量均衡。但实际情况呢,还需要根据调用活跃度来进行计算的。
最小活跃数算法
这里呢稍微讲一下,就是首先获取服务器活跃度,拿到最小的活跃度的ip。如果只有一个,那么就直接调用这个ip。但是有多个咋办?那我们就来判断权重啦。权重大的优先,权重相同的就随机咯。哎,实在是不知道怎么说了,还是来看代码吧:
public class ServerIps {
public static final List<String> IP_LIST = Arrays.asList(
"192.168.0.1",
"192.168.0.2",
"192.168.0.3",
"192.168.0.4",
"192.168.0.5",
"192.168.0.6",
"192.168.0.7",
"192.168.0.8",
"192.168.0.9",
"192.168.0.10"
);
/**
* key = ip
* value = 权重占比
*/
public static final Map<String,Integer> IP_MAP = new HashMap<String,Integer>();
static {
IP_MAP.put("192.168.0.1",1);
IP_MAP.put("192.168.0.2",8);
IP_MAP.put("192.168.0.3",3);
IP_MAP.put("192.168.0.4",6);
IP_MAP.put("192.168.0.5",5);
IP_MAP.put("192.168.0.6",5);
IP_MAP.put("192.168.0.7",6);
IP_MAP.put("192.168.0.8",7);
IP_MAP.put("192.168.0.9",2);
IP_MAP.put("192.168.0.10",9);
/* IP_MAP.put("A",5);
IP_MAP.put("B",1);
IP_MAP.put("C",1);*/
}
public static final Map<String,Integer>IP_ACTIVE = new HashMap<String,Integer>();
static{
IP_ACTIVE.put("192.168.0.1",2);
IP_ACTIVE.put("192.168.0.2",0);
IP_ACTIVE.put("192.168.0.3",1);
IP_ACTIVE.put("192.168.0.4",3);
IP_ACTIVE.put("192.168.0.5",0);
IP_ACTIVE.put("192.168.0.6",1);
IP_ACTIVE.put("192.168.0.7",4);
IP_ACTIVE.put("192.168.0.8",2);
IP_ACTIVE.put("192.168.0.9",7);
IP_ACTIVE.put("192.168.0.10",0);
}
}
public class MinActive {
private static String getServer() {
//找出数最小的服务器
Optional<Integer> minValue =
ServerIps.IP_ACTIVE.values().stream().min(Comparator.naturalOrder());
if (minValue.isPresent()) {
List<String> minActivityIps = new ArrayList<>();
ServerIps.IP_ACTIVE.forEach((ip, activity) -> {
if (activity.equals(minValue.get())) {
minActivityIps.add(ip);
}
});
//最下活跃数有多个的时候,根据权重来选择,权重大的优先
if (minActivityIps.size() > 1) {Map<String, Integer> weightList = new LinkedHashMap<String, Integer>
();
//过滤出对应ip和权重
ServerIps.IP_MAP.forEach((ip, weight) -> {
if (minActivityIps.contains(ip)) {
weightList.put(ip, ServerIps.IP_MAP.get(ip));
}
});
int totalWeight = 0;//权重总数
boolean sameWeight = true;//用于判断权重是不是一样的
//将ip占比转换为对象数组,用于循环计算权重总数
Object[] weights = weightList.values().toArray();
for (int i = 0; i < weights.length; i++) {
Integer weight = (Integer) weights[i];
totalWeight += weight;
//判断所有ip权重是否一致
if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) {
sameWeight = false;
}
}
java.util.Random random = new java.util.Random();
int randomPos = random.nextInt(totalWeight);
if (!sameWeight) {
//循环判断当前这个点落在哪里
for (String ip : weightList.keySet()) {
Integer value = weightList.get(ip);
if (randomPos < value) {
return ip;
}
randomPos = randomPos - value;
}
}
return (String) weightList.keySet().toArray()[new
java.util.Random().nextInt(weightList.size())];
} else {
return minActivityIps.get(0);
}
} else {
return (String) ServerIps.IP_MAP.keySet().toArray()[new
java.util.Random().nextInt(ServerIps.IP_MAP.size())];
}
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
System.out.println(getServer());
}
}
好吧,这里是有问题的,就是正常情况呢是在调用的时候将活跃度加1,返回成功后再减1的。但是今天实在是太累了,就忘记写了。不过只要知道有这个就行了,如果后面我还记得我一定来改掉。
运行结果就会发现192.168.0.10这个调用的次数稍微多一点咯。
好了,到此结束了。以上就是一些简单的负载均衡算法。希望能给你一丝帮助!