记得,我刚工做的时候,同事说了一个故事:在他刚工做的时候,他同事有一天兴冲冲的跑到公司说,大家知道吗,公司请了个大牛。大牛?对,那人会写AJAX!哇,真是大牛啊,跟着他,能够学很多东西啊。我听了笑了,但有点难以理解,由于如今几乎只要是一个开发,都会写AJAX,怎么写个AJAX就算大牛呢?后来我明白了,三年前高深莫测的技术到如今变得普普统统,不足为奇,就像咱们今天要讲的负载均衡,在几什么时候,负载均衡只有大牛才能玩转起来,可是到今天,一个小开发均可以聊上几句。如今,就让咱们简单的看看负载均衡把。node
从负载均衡设备的角度来看,分为硬件负载均衡和软件负载均衡:程序员
从负载均衡的技术来看,分为服务端负载均衡和客户端负载均衡:算法
从负载均衡的算法来看,又分为 随机,轮询,哈希,最小压力,固然可能还会加上权重的概念,负载均衡的算法就是本文的重点了。bash
随机就是没有规律的,随便从负载中得到一台,又分为彻底随机和加权随机:服务器
public class Servers {
public List<String> list = new ArrayList<>() {
{
add("192.168.1.1");
add("192.168.1.2");
add("192.168.1.3");
}
};
}
复制代码
public class FullRandom {
static Servers servers = new Servers();
static Random random = new Random();
public static String go() {
var number = random.nextInt(servers.list.size());
return servers.list.get(number);
}
public static void main(String[] args) {
for (var i = 0; i < 15; i++) {
System.out.println(go());
}
}
}
复制代码
运行结果: 负载均衡
彻底随机是最简单的负载均衡算法了,缺点比较明显,由于服务器有好有坏,处理能力是不一样的,咱们但愿性能好的服务器多处理些请求,性能差的服务器少处理一些请求,因此就有了加权随机。dom
加权随机,虽然仍是采用的随机算法,可是为每台服务器设置了权重,权重大的服务器得到的几率大一些,权重小的服务器得到的几率小一些。性能
关于加权随机的算法,有两种实现方式:ui
一种是网上流传的,代码比较简单:构建一个服务器的List,若是A服务器的权重是2,那么往List里面Add两次A服务器,若是B服务器的权重是7,那么我往List里面Add7次B服务器,以此类推,而后我再生成一个随机数,随机数的上限就是权重的总和,也就是List的Size。这样权重越大的,被选中的几率固然越高,代码以下:this
public class Servers {
public HashMap<String, Integer> map = new HashMap<>() {
{
put("192.168.1.1", 2);
put("192.168.1.2", 7);
put("192.168.1.3", 1);
}
};
}
复制代码
public class WeightRandom {
static Servers servers = new Servers();
static Random random = new Random();
public static String go() {
var ipList = new ArrayList<String>();
for (var item : servers.map.entrySet()) {
for (var i = 0; i < item.getValue(); i++) {
ipList.add(item.getKey());
}
}
int allWeight = servers.map.values().stream().mapToInt(a -> a).sum();
var number = random.nextInt(allWeight);
return ipList.get(number);
}
public static void main(String[] args) {
for (var i = 0; i < 15; i++) {
System.out.println(go());
}
}
}
复制代码
运行结果:
能够很清楚的看到,权重小的服务器被选中的几率相对是比较低的。
固然我在这里仅仅是为了演示,通常来讲,能够把构建服务器List的代码移动到静态代码块中,不用每次都构建。
这种实现方式相对比较简单,很容易就能想到,可是也有缺点,若是我几台服务器权重设置的都很大,好比上千,上万,那么服务器List也有上万条数据,这不是白白占用内存吗?
因此聪明的程序员想到了第二种方式:
为了方便解释,仍是就拿上面的例子来讲吧:
若是A服务器的权重是2,B服务器的权重是7,C服务器的权重是1:
不知道博客对于大于小于符号,会不会有特殊处理,因此我再截个图:
也许,光看文字描述仍是不够清楚,能够结合下面丑到爆炸的图片来理解下:
代码以下:
public class WeightRandom {
static Servers servers = new Servers();
static Random random = new Random();
public static String go() {
int allWeight = servers.map.values().stream().mapToInt(a -> a).sum();
var number = random.nextInt(allWeight);
for (var item : servers.map.entrySet()) {
if (item.getValue() >= number) {
return item.getKey();
}
number -= item.getValue();
}
return "";
}
public static void main(String[] args) {
for (var i = 0; i < 15; i++) {
System.out.println(go());
}
}
}
复制代码
运行结果:
这种实现方式虽然相对第一种实现方式比较“绕”,但倒是一种比较好的实现方式, 对内存没有浪费,权重大小和服务器List的Size也没有关系。
轮询又分为三种,1.彻底轮询 2.加权轮询 3.平滑加权轮询
public class FullRound {
static Servers servers = new Servers();
static int index;
public static String go() {
if (index == servers.list.size()) {
index = 0;
}
return servers.list.get(index++);
}
public static void main(String[] args) {
for (var i = 0; i < 15; i++) {
System.out.println(go());
}
}
}
复制代码
运行结果:
彻底轮询,也是比较简单的,可是问题和彻底随机是同样的,因此出现了加权轮询。
加权轮询仍是有两种经常使用的实现方式,和加权随机是同样的,在这里,我就演示我认为比较好的一种:
public class WeightRound {
static Servers servers = new Servers();
static int index;
public static String go() {
int allWeight = servers.map.values().stream().mapToInt(a -> a).sum();
int number = (index++) % allWeight;
for (var item : servers.map.entrySet()) {
if (item.getValue() > number) {
return item.getKey();
}
number -= item.getValue();
}
return "";
}
public static void main(String[] args) {
for (var i = 0; i < 15; i++) {
System.out.println(go());
}
}
}
复制代码
运行结果:
加权轮询,看起来并没什么问题,可是仍是有一点瑕疵,其中一台服务器的压力可能会忽然上升,而另外的服务器却很“清闲,喝着咖啡,看着新闻”。咱们但愿虽然是按照轮询,可是中间最好能够有交叉,因此出现了第三种轮询算法:平滑加权轮询。
平滑加权是一个算法,很神奇的算法,咱们有必要先对这个算法进行讲解。 好比A服务器的权重是5,B服务器的权重是1,C服务器的权重是1。 这个权重,咱们称之为“固定权重”,既然这个叫“固定权重”,那么确定还有叫“非固定权重的”,没错,“非固定权重”每次都会根据必定的规则变更。
请求 | 得到服务器前的非固定权重 | 选中的服务器 | 得到服务器后的非固定权重 |
---|---|---|---|
1 | {5, 1, 1} | A | {-2, 1, 1} |
2 | {3, 2, 2} | A | {-4, 2, 2} |
3 | {1, 3, 3} | B | {1, -4, 3} |
4 | {6, -3, 4} | A | {-1, -3, 4} |
5 | {4, -2, 5} | C | {4, -2, -2} |
6 | {9, -1, -1} | A | {2, -1, -1} |
7 | {7, 0, 0} | A | {0, 0, 0} |
8 | {5, 1, 1} | A | {-2, 1, 1} |
当第8次的时候,“非固定权重“又回到了初始的5 1 1,是否是很神奇,也许算法仍是比较绕的,可是代码却简单多了:
public class Server {
public Server(int weight, int currentWeight, String ip) {
this.weight = weight;
this.currentWeight = currentWeight;
this.ip = ip;
}
private int weight;
private int currentWeight;
private String ip;
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public int getCurrentWeight() {
return currentWeight;
}
public void setCurrentWeight(int currentWeight) {
this.currentWeight = currentWeight;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
}
复制代码
public class Servers {
public HashMap<String, Server> serverMap = new HashMap<>() {
{
put("192.168.1.1", new Server(5,5,"192.168.1.1"));
put("192.168.1.2", new Server(1,1,"192.168.1.2"));
put("192.168.1.3", new Server(1,1,"192.168.1.3"));
}
};
}
复制代码
public class SmoothWeightRound {
private static Servers servers = new Servers();
public static String go() {
Server maxWeightServer = null;
int allWeight = servers.serverMap.values().stream().mapToInt(Server::getWeight).sum();
for (Map.Entry<String, Server> item : servers.serverMap.entrySet()) {
var currentServer = item.getValue();
if (maxWeightServer == null || currentServer.getCurrentWeight() > maxWeightServer.getCurrentWeight()) {
maxWeightServer = currentServer;
}
}
assert maxWeightServer != null;
maxWeightServer.setCurrentWeight(maxWeightServer.getCurrentWeight() - allWeight);
for (Map.Entry<String, Server> item : servers.serverMap.entrySet()) {
var currentServer = item.getValue();
currentServer.setCurrentWeight(currentServer.getCurrentWeight() + currentServer.getWeight());
}
return maxWeightServer.getIp();
}
public static void main(String[] args) {
for (var i = 0; i < 15; i++) {
System.out.println(go());
}
}
}
复制代码
运行结果:
这就是平滑加权轮询,巧妙的利用了巧妙算法,既有轮询的效果,又避免了某台服务器压力忽然升高,不可谓不妙。
负载均衡算法中的哈希算法,就是根据某个值生成一个哈希值,而后对应到某台服务器上去,固然能够根据用户,也能够根据请求参数,或者根据其余,想怎么来就怎么来。若是根据用户,就比较巧妙的解决了负载均衡下Session共享的问题,用户小明走的永远是A服务器,用户小笨永远走的是B服务器。
那么如何用代码实现呢,这里又须要引出一个新的概念:哈希环。
什么?我只听过奥运五环,还有“啊 五环 你比四环多一环,啊 五环 你比六环少一环”,这个哈希环又是什么鬼?容我慢慢道来。
哈希环,就是一个环!这...好像...有点难解释呀,咱们仍是画图来讲明把。
一个圆是由无数个点组成的,这是最简单的数学知识,相信你们均可以理解吧,哈希环也同样,哈希环也是有无数个“哈希点”构成的,固然并无“哈希点”这样的说法,只是为了便于你们理解。
咱们先计算出服务器的哈希值,好比根据IP,而后把这个哈希值放到环里,如上图所示。
来了一个请求,咱们再根据某个值进行哈希,若是计算出来的哈希值落到了A和B的中间,那么按照顺时针算法,这个请求给B服务器。
理想很丰满,现实很孤单,可能三台服务器掌管的“区域”大小相差很大很大,或者干脆其中一台服务器坏了,会出现以下的状况:
能够看出,A掌管的“区域”实在是太大,B能够说是“很清闲,喝着咖啡,看着电影”,像这种状况,就叫“哈希倾斜”。
那么怎么避免这种状况呢?虚拟节点。
什么是虚拟节点呢,说白了,就是虚拟的节点...好像..没解释啊...仍是上一张丑到爆炸的图吧:
要实现此种负载均衡算法,须要用到一个平时不怎么经常使用的Map:TreeMap,对TreeMap不了解的朋友能够先去了解下TreeMap,下面放出代码:
private static String go(String client) {
int nodeCount = 20;
TreeMap<Integer, String> treeMap = new TreeMap();
for (String s : new Servers().list) {
for (int i = 0; i < nodeCount; i++)
treeMap.put((s + "--服务器---" + i).hashCode(), s);
}
int clientHash = client.hashCode();
SortedMap<Integer, String> subMap = treeMap.tailMap(clientHash);
Integer firstHash;
if (subMap.size() > 0) {
firstHash = subMap.firstKey();
} else {
firstHash = treeMap.firstKey();
}
String s = treeMap.get(firstHash);
return s;
}
public static void main(String[] args) {
System.out.println(go("今每天气不错啊"));
System.out.println(go("192.168.5.258"));
System.out.println(go("0"));
System.out.println(go("-110000"));
System.out.println(go("风雨交加"));
}
复制代码
运行结果:
哈希负载均衡算法到这里就结束了。
因此的最小压力负载均衡算法就是 选择一台当前最“清闲”的服务器,若是A服务器有100个请求,B服务器有5个请求,而C服务器只有3个请求,那么毫无疑问会选择C服务器,这种负载均衡算法是比较科学的。可是遗憾的在当前的场景下没法模拟出来“原汁原味”的最小压力负载均衡算法的。
固然在实际的负载均衡下,可能会将多个负载均衡算法合在一块儿实现,好比先根据最小压力算法,当有几台服务器的压力同样小的时候,再根据权重取出一台服务器,若是权重也同样,再随机取一台,等等。
今天的内容到这里就结束了,谢谢你们。