目录
1、背景
目前应用程序构架一般都是客户端/服务端的模式,而服务端在单机作为服务器时,是有性能瓶颈的,单机上cpu、磁盘IO、网络IO、内存等资源上是会达到一个瓶颈上,所以垂直扩容肯定会达到一个阈值;
而水平扩容则可以突破这个瓶颈,由多台服务器组成一个逻辑上的集群,共同对外提供服务,理论上性能变为之前的N倍(N为集群节点个数),本文针对集群中的负载均衡进行研究,提高理论素养。
2、负载均衡方案
2.1 基于客户端的负载均衡
在基于客户端的负载均衡实现方案中,负载均衡器是在客户端这端实现,并与客户端一起使用的,如下图所示:
在SpringCloud项目中,RestTemplate、Feign底层都是依赖了Ribbon这种基于客户端的负载均衡实现。
2.2 基于服务端的负载均衡
在基于服务端的负载均衡实现方案中,负载均衡器是在服务端这端实现,客户端所有的请求都是发向服务端负载均衡器,由负载均衡器根据配置的负载均衡策略,将
请求最终转发到命中的真实的服务器,如下图所示:
以Nginx为例,常常作为负载均衡器与反向代理来使用,下面截取部分配置:
http {
upstream {服务集群自定义名称}{
server 192.168.1.101:8080;
server 192.168.1.102:8080;
}
server {
listen 8080;
server_name localhost;
location / {
proxy_pass http://{服务集群自定义名称};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
3 常见的均衡均衡算法
负载均衡算法既可以用于客户端实现,也可以用于服务端实现。下面算法代码引自:Java代码实现负载均衡五种算法。
从持续性这个维度可以给负载均衡算法分成两大类:持续性算法、非持续性算法。
持续性负载均衡算法:从一个特定的客户端/会话发出的请示总是被分配到服务集群中特定的一台真实服务器进行处理,请示具有一定的黏性。
非持续性负载均衡算法:与持续性负载均衡算法正好相反,请示会依赖于负载均衡策略的配置,随机将请示发给任意一台真实服务器。
3.1 非持续性负载均衡算法
3.1.1 随机
package com.autocoding.lb;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class RandomAlgorithmTest {
private static List<String> IP_LIST = new ArrayList<>();
static {
IP_LIST.add("192.168.13.1");
IP_LIST.add("192.168.13.2");
IP_LIST.add("192.168.13.3");
}
public static String selectServer() {
Random random = new Random();
int index = random.nextInt(IP_LIST.size());
String selectedServer = IP_LIST.get(index);
return selectedServer;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
String server = RandomAlgorithmTest.selectServer();
System.out.println(server);
}
}
}
3.1.2 轮询(RoundRobin)
package com.autocoding.lb;
import java.util.ArrayList;
import java.util.List;
public class RoundRobinAlgorithmTest {
private static List<String> IP_LIST = new ArrayList<>();
private int currentIndex = 0;
static {
IP_LIST.add("192.168.13.1");
IP_LIST.add("192.168.13.2");
IP_LIST.add("192.168.13.3");
}
public synchronized String selectServer() {
if (currentIndex >= IP_LIST.size()) {
currentIndex = 0;
}
String selectedServer = IP_LIST.get(currentIndex);
currentIndex++;
return selectedServer;
}
public static void main(String[] args) {
RoundRobinAlgorithmTest roundRobinAlgorithmTest = new RoundRobinAlgorithmTest();
for (int i = 0; i < 10; i++) {
String server = roundRobinAlgorithmTest.selectServer();
System.out.println(server);
}
}
}
3.1.3 加权随机
package com.autocoding.lb;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
/**
*
* @ClassName: WeightRandomAlgorithmTest
* @Description: 加权随机算法
* @author: QiaoLi
* @date: Jan 16, 2021 1:41:21 PM
*/
public class WeightRandomAlgorithmTest {
private static Map<String, Integer> IP_WEIGHT_MAP = new HashMap();
private static List<String> IP_LIST = new ArrayList<>();
static {
IP_WEIGHT_MAP.put("192.168.13.1", 10);
IP_WEIGHT_MAP.put("192.168.13.2", 20);
IP_WEIGHT_MAP.put("192.168.13.3", 30);
for (Entry<String, Integer> e : IP_WEIGHT_MAP.entrySet()) {
for (int i = 0; i < e.getValue(); i++) {
IP_LIST.add(e.getKey());
}
}
}
public static String selectServer() {
Random random = new Random();
int index = random.nextInt(IP_LIST.size());
String selectedServer = IP_LIST.get(index);
return selectedServer;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
String server = WeightRandomAlgorithmTest.selectServer();
System.out.println(server);
}
}
}
3.1.4 基于响应时间
在一个固定时间窗口内(当前统计当前5分钟之内的响应时间),负载均衡器周期性的给集群中每一台服务器发送发送探测请示PING请示,来统计固定时间窗口内,每台真实服务器的平均响应时间,
当负载均衡器需要选择真实服务器时,根据最短的响应时间来选择真实服务器。这种基于响应时间的实现有一个问题,统计的响应时间,是负载均衡器与真实服务器之间的响应时间,而不能真实地的
反映到客户端与真实服务器的响应时间,不能体现真实服务器对请求的处理能力。
3.1.5 基于最少连接数
客户端的每一次请求服务在服务器停留的时间都可能会有较大的差异,随着工作时间的加长,如果采用简单的轮循或随机均衡算法,每一台服务器上的连接进程可能会产生极大的不同,这样的结果并不会达到真正的负载均衡。
最少连接数均衡算法对内部中有负载的每一台服务器都有一个数据记录,记录的内容是当前该服务器正在处理的连接数量,当有新的服务连接请求时,将把当前请求分配给连接数最少的服务器,使均衡更加符合实际情况,负载更加均衡。
此种负载均衡算法适合长时间处理的请求服务。
3.2 持续性负载均衡算法
3.2.1 基于IP的Hash函数
package com.autocoding.lb;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
*
* @ClassName: HashAlgorithmTest
* @Description: 基于哈希函数的负载均衡算法
* @author: QiaoLi
* @date: Jan 16, 2021 1:46:44 PM
*/
public class HashAlgorithmTest {
private static List<String> IP_LIST = new ArrayList<>();
static {
IP_LIST.add("192.168.13.1");
IP_LIST.add("192.168.13.2");
IP_LIST.add("192.168.13.3");
}
public static String selectServer(String clientIp) {
int index = Math.abs(clientIp.hashCode()) % IP_LIST.size();
String selectedServer = IP_LIST.get(index);
return selectedServer;
}
public static void main(String[] args) {
List<String> clientIpList = Arrays.asList("192.168.1.101", "192.168.1.102", "192.168.1.103");
for (String clientIp : clientIpList) {
String server = HashAlgorithmTest.selectServer(clientIp);
System.out.println(server);
}
}
}
3.2.2 基于请求的Hash函数
基于请示头、URL、SessionId、AccessToken进行哈希来选择固定的一台真实服务器
3.2.3 基于Cookie的Hash函数
基于请示中的Cookie进行哈希来选择固定的一台真实服务器
4 负载均衡组件
4.1 Ribbon组件
Ribbon本身提供了下面几种负载均衡策略:
RoundRobinRule: 轮询策略,Ribbon以轮询的方式选择服务器,这个是默认值。所以示例中所启动的两个服务会被循环访问;
RandomRule: 随机策略,也就是说Ribbon会随机从服务器列表中选择一个进行访问;
BestAvailableRule: 最大可用策略,即先过滤出故障服务器后,选择一个当前并发请求数最小的;
WeightedResponseTimeRule: 带有加权的轮询策略,对各个服务器响应时间进行加权处理,然后在采用轮询的方式来获取相应的服务器;
AvailabilityFilteringRule: 可用过滤策略,先过滤出故障的或并发请求大于阈值的一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个;
ZoneAvoidanceRule: 区域感知策略,先使用主过滤条件(区域负载器,选择最优区域)对所有实例过滤并返回过滤后的实例清单,依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤,判断最小过滤数(默认1)和最小过滤百分比(默认0),最后对满足条件的服务器则使用RoundRobinRule(轮询方式)选择一个服务器实例。
我们可以将上例中的message-service的负载均衡策略设置为随机访问RandomRule,application.yml 配置如下:
message-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
4.2 Nginx中间件
1. 在http节点下,添加upstream节点。
upstream linuxidc {
server 10.0.6.108:7080;
server 10.0.0.85:8980;
}
2. 将server节点下的location节点中的proxy_pass配置为:http:// + upstream名称,即“http://linuxidc”.
location / {
root html;
index index.html index.htm;
proxy_pass http://linuxidc;
}
3. 现在负载均衡初步完成了。upstream按照轮询(默认)方式进行负载,每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。虽然这种方式简便、成本低廉。
但缺点是:可靠性低和负载分配不均衡。适用于图片服务器集群和纯静态页面服务器集群。 除此之外,upstream还有其它的分配策略,分别如下:
weight(权重)
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。如下所示,10.0.0.88的访问比率要比10.0.0.77的访问比率高一倍。
upstream linuxidc{
server 10.0.0.77 weight=5;
server 10.0.0.88 weight=10;
}
ip_hash(访问ip)
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
upstream favresin{
ip_hash;
server 10.0.0.10:8080;
server 10.0.0.11:8080;
}
fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。与weight分配策略类似。
upstream favresin{
server 10.0.0.10:8080;
server 10.0.0.11:8080;
fair;
}
url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
注意:在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法。
upstream resinserver{
server 10.0.0.10:7777;
server 10.0.0.11:8888;
hash $request_uri;
hash_method crc32;
}
总结
upstream还可以为每个设备设置状态值,这些状态值的含义分别如下:
down 表示单前的server暂时不参与负载.
weight 默认为1.weight越大,负载的权重就越大。
max_fails :允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream 模块定义的错误.
fail_timeout : max_fails次失败后,暂停的时间。
backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
upstream bakend{ #定义负载均衡设备的Ip及设备状态
ip_hash;
server 10.0.0.11:9090 down;
server 10.0.0.11:8080 weight=2;
server 10.0.0.11:6060;
server 10.0.0.11:7070 backup;
}