网站创立初期,一般都是单台服务器对外提供集中服务。随着业务量的增大 ,一台服务器不够用,就会把多台服务器组成一个集群对外提供服务。但是,网站对外提供的访问入口通常只有一个,比如www.baidu.com。那么当用户在浏览器中输入www.baidu.com进行访问的时候,如何将用户的请求分发到集群中不同的机器上呢?这就是负载均衡需要做的事情。
负载均衡通常是指将请求“均匀”分摊到集群中多个服务器节点上执行,这里的均匀是指在一个比较大的统计范围内是基本均匀的,并不是完全均匀的。
准备web项目
只要有一个欢迎页面就可以,主要是为了展示结果,所以就用springboot快速写一个就ok
@Controller
public class MyController {
@RequestMapping("/")
public String toIndex() {
return "index";
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h3 th:text="${#request.getServerName()}"></h3>
<h3 th:text="${#request.getServerPort()}"></h3>
<h3>192.168.3.28中的项目</h3>
</body>
</html>
后续要把这个项目部署在两个虚拟机上,所以为了体现出差距,区分nginx到底代理到哪台虚拟机了,这里的html一个写“192.168.3.28”一个写“192.168.3.30”,分别对应虚拟机的ip地址。
然后配置文件配一下上下文根:
server.servlet.context-path=/bootweb
spring.mvc.servlet.load-on-startup=1
准备工作完成后就可以分别打包了,注意html里的ip地址要区分一下,打成两个jar包,然后传到虚拟机上运行。
配置nginx
这里记住upstream不要配在server里面了,不然nginx启动会报错。
upstream www.bootweb.com {
# 2台服务器上放的都有web项目
server 192.168.3.28:8080;
server 192.168.3.30:8080;
}
server {
...
location /bootweb {
# www.myweb.com是任意的,随便写,只要和upstream的对应上就行
proxy_pass http://www.bootweb.com;
}
}
配好后reload一下nginx,这里还是强烈建议把nginx加到系统服务里,这样reload的时候systemctl reload nginx就可以,不用专门跑到nginx的目录下。
reload后,就可以在浏览器下访问了,这里肯定还是访问nginx,访问8080就没有负载均衡的效果了。http是80端口,所以80写不写都无所谓,192.168.3.28/bootweb就对应了刚刚配置的路径,
第一次访问发现分配到的是192.168.3.28这台虚拟机;再刷新一下,发现url路径没有变,但是分配到了192.168.3.30这台虚拟机;再刷新又变28这台虚拟机;再刷新变30…这和负载均衡的策略有关,下面会讲几种负载均衡策略,这个就是轮询的策略。但是比较有意思的是serverName居然是www.bootweb.com,我以为会是nginx服务器的名字,这个后续再研究一下到底什么原理。
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210131222937.png)
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210131222915.png)
常用负载均衡策略
轮询(默认)
upstream默认的就是这个策略,每个请求都会逐一分配到不同的服务器,如果某个服务器down掉,就会被自动剔除。这种适合所有服务器性能差不多的,如果差距太大,性能差的服务器堆积的请求就越来越多然后down掉。
权重
在upstream模块中,每个server后面加一个weight=x,比如刚刚的配置就可以改一下:
upstream www.bootweb.com {
server 192.168.3.28:8080 weight=5;
server 192.168.3.30:8080 weight=7;
}
因为192.168.3.28上面还跑着nginx,希望它分到的请求少一点,让30这台机器多分点请求,就给30的权重大一点。这里的意思就是28这台机器处理5个请求的时间和30这台机器处理7个请求的时间一样,但绝不是给28发5个请求之后再给30发7个,这样就没有任何意义了,而是28分一点,30分一点,保证他们最后的处理时间一样。适用于服务器性能不一致的情况下。
最少连接数
web请求会被转发到连接数最少的服务器上,配置文件中加一个least_conn就可以:
upstream www.bootweb.com {
least_conn;
server 192.168.3.28:8080 weight=5;
server 192.168.3.30:8080 weight=7;
}
ip_hash
前三种都存在跨服务器的问题,导致session丢失(后续有办法解决)。ip_hash可以把每个请求按访问ip的hash值分配,这样每个访问客户端会固定访问一个后端服务器,解决session丢失的问题。但是这样可能会导致某台服务器的压力过大。
upstream www.bootweb.com {
ip_hash
server 192.168.3.28:8080 weight=5;
server 192.168.3.30:8080 weight=7;
}
其余配置
- backup:当所有非backup都down掉的时候backup才能被访问,项目更新的时候可能只有很短暂的时间会出现所有服务器都关掉的时候,这个时候backup的代码已经是新的,可以短暂的挡一会儿,只要有服务器启动了backup就不会被访问到了。如果是因为访问压力过大导致非backup都down掉了,那backup也顶不住,所有访问都过来backup也跟着一块down了。
- down:表示当前的server是down状态,不参与负载均衡。
解决request.getServerName()
把proxy_pass和upstream中的www.bootweb.com都给改成tmp,reload一下nginx再访问页面,发现页面中也变成了tmp:
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210201170631.png)
那就说明这里的proxy_pass声明的是一个“假”的url,相当于以这个url的名义对upstream中配置的几个服务器进行请求。所以在html中的request对象得到的服务器名称和proxy_pass中声明的一样。
server_name详解
server_name默认的是localhost,我一直以为这个是和ip地址挂钩的,但是参考这篇博客,发现并不是这回事。下面做一些对比实验。
首先server_name并不是必须存在的,可以没有,即使在配置文件中把这个注释,也能正常运行,但是这个时候只监听了端口,怎么处理请求的?其实listen是可以同时监听ip地址和端口的,只要把listen改成192.168.3.28:80,就可以了。这个时候访问项目还是正常的。
但是突然突然异想天开了一下,如果这里把ip变了呢?不监听本机ip了,随便写个乱七八糟的ip,按道理这个时候应该无法访问了,但是改了之后并且清了一下浏览器缓存,发现页面还是可以正常访问:
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210201173107.png)
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210201173143.png)
那这个时候用191.1.1.1/bootweb去访问呢?想都不用想,这个ip地址肯定访问不到的啊,又不是自己的虚拟机。那么这样看下来,监听错误的ip应该导致这个server直接没用了,因为监听了一个根本不可能到达的请求。但是上面又是怎么访问到的呢?也是看提到的那篇博客才知道,没有匹配的server就默认用第一个server,那现在就验证一下,在这个server上面再写个server直接返回403:
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210201174956.png)
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210201175110.png)
说了半天好像还是和server_name没太大关系。上面只是解释了没server_name的情况下,有server_name的情况下,就不要监听ip了,因为监听ip的时候默认不对server_name进行匹配,那篇博客里也验证得非常清楚。
第一个返回403的server还是不变,让它呆在第一个,把server_name xxxx的注释打开,再在C:\Windows\System32\drivers\etc\hosts文件中添加一个映射 192.168.3.28 xxxx。主要是为了让windows可以把xxxx域名解析为192.168.3.28,这样才能访问到虚拟机,而这个时候第二个server的server_name又恰好是xxxx,就可以访问bootweb。结果确实可以访问:
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210201181142.png)
由域名解析可以得到192.168.3.28保证可以访问到虚拟机,然后请求头里带了域名xxxx,到nginx的时候就可以根据请求头中的域名匹配server_name,然后就可以访问bootweb了。
可能会有人有疑惑了,你哪知道是根据xxxx匹配的server,还是根据解析出的ip匹配的server呢?这个时候就要注意了,上面已经提了配server_name的时候不要监听ip,所以我的配置文件里是没监听192.168.3.28的,然后也可以验证一下直接通过http://192.168.3.28/bootweb能不能访问:
![](https://cdn.jsdelivr.net/gh//Fushier/ImgCloud@main/data/20210201181810.png)
被默认的第一个server处理了,因为根本没有server在监听192.168.3.28。
所以就可以看出server_name到底有什么用了,一般都是和域名保持一致,这样当域名通过DNS解析(这里用hosts假装进行了DNS解析)得到nginx服务器的ip的时候,访问到服务器就可以根据不同的域名(因为有多级域名),代理到不同的服务器,所以不要配监听ip。
也有意外的收获,就是第一个server一定要什么都不监听,直接把错误请求直接打回去。