Nginx 是一个高性能的 HTTP 服务器和反向代理服务器,广泛应用于负载均衡中。它的负载均衡功能支持多种策略,可以有效分配流量到后端服务器,提升系统的可靠性和可用性。
负载均衡
首先,Nginx 负载均衡配置是通过在 Nginx 配置文件中定义 upstream 块和对应的 server 块来实现的。Nginx 安装过程就不多说了,如果没有安装,可以参考安装链接:Nginx 安装和访问-CSDN博客
负载均衡配置
我这儿以我的示例进行的配置。先在 192.168.128.139 机器上修改 nginx.conf (记得先将原 nginx.conf 备份)。
events {
worker_connections 1024;
}
http {
upstream backend {
server 192.168.128.139:5001 weight=1;
server 192.168.128.138:5001 weight=2;
}
server {
listen 5000;
location / {
proxy_pass http://backend;
}
}
}
在上面的例子中,upstream backend 定义了一个名为 backend 的服务器组,包含了两个后端服务器,并设置权重分别为 1 和 2。
server 监听指定端口 5000(监听并转发来自 5000 端口的请求), proxy_pass 指令则将请求代理到定义的服务器组 backend 中。
配置好以后,执行 sbin/nginx 启动服务(后续修改 nginx.conf 配置以后,可使用 sbin/nginx -s reload 命令重新加载配置即可)。
root@master /u/l/nginx# pwd
/usr/local/nginx
root@master /u/l/nginx# sbin/nginx
为了配合验证 nginx 的负载均衡,我分别在两个虚拟机 192.168.128.139 和 192.168.128.138 上分别搭建了一个 flask 服务。
服务示例
如果仅仅是测试,为了避免访问被挡住的问题,可以将测试虚拟机的防火墙关闭。
[root@master ~]# service iptables stop
Redirecting to /bin/systemctl stop iptables.service
root@slave ~# service firewalld stop
Redirecting to /bin/systemctl stop firewalld.service
192.168.128.139
from flask import Flask
app = Flask(__name__)
@app.route('/test', methods=['POST', 'GET'])
def test():
result = {
"result": "from 192.168.128.139: I am backend1"
}
return result
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001)
192.168.128.138
from flask import Flask
app = Flask(__name__)
@app.route('/test', methods=['POST', 'GET'])
def test():
result = {
"result": "from 192.168.128.138: I am backend2"
}
return result
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001)
请求示例
可以看到,我模拟请求的是 192.168.128.139 的 5000 端口,但请求最后还是可以被转发到 flask 服务的 5001 端口。
模拟请求 10 次,可以看到 192.168.128.139 和 192.168.128.138 的请求比 是 3:7,和我们配置的权重 1: 2 很接近(多次请求基本上就和配置权重一致了)。
import requests
def test_request(url):
res = requests.get(url)
print(res.json())
if __name__ == '__main__':
url = "http://192.168.128.139:5000/test"
for i in range(10):
test_request(url)
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
指定路由转发
上面的示例是统一将 根路径 下的所有请求进行转发。虽然我的请求使用的 /test 路径,但也会命中基础匹配,因此也会被转发。
但是,有时候我可能希望特定路由的请求被转发到特定的服务器。
如果涉及到指定多个 URL 路径(如 根路径 / 和路径 /test
),则可以配置多个 location 分配不同路径到不同的后端服务器组。
events {
worker_connections 1024;
}
http {
upstream backend {
server 192.168.128.139:5001 weight=1;
server 192.168.128.138:5001 weight=2;
}
upstream backend_test {
server 192.168.128.138:5001;
}
server {
listen 5000;
location / {
proxy_pass http://backend;
}
location /test {
proxy_pass http://backend_test;
}
}
}
这样,满足 /test 前缀的会被转发到 backend_test 服务器组,其它不满足的则命中基础匹配被转发到 backend 服务器组。
匹配规则
基础匹配
location / { ... }:这是最基本的匹配,匹配所有请求。因为它没有指定具体的文件或目录,所以通常作为后备选项出现。
精确匹配
location = /exact/path { ... }:精确匹配这个路径。如果请求的 URI 完全等于 /exact/path,则使用这个 location 块的配置处理此请求。这具有最高的优先级。
前缀匹配
location /prefix/ { ... }:前缀匹配请求的 URI 的开始部分。如果请求的 URI 以 /prefix/ 开始,这个 location 块将被用来处理请求。
location ^~ /prefix/ { ... }:前缀匹配,但它会停止正则表达式匹配,即使正则表达式可能会更具体匹配。如果该 location 匹配,Nginx 不会考虑之后的正则 location 块。
正则表达式匹配
location ~ /regex/ { ... }:大小写敏感的正则匹配。
location ~* /regex/ { ... }:大小写不敏感的正则匹配。
location / { ... } 正则表达式匹配会在普通字符串前缀匹配后进行。如果有多个正则表达式 location 都匹配请求,则使用第一个匹配的 location 块。
匹配优先级
Nginx 处理请求时 location 匹配的优先级顺序如下:
- 首先进行精确匹配 location =
- 其次按文件中出现顺序匹配所有正则表达式 location ~ 和 location ~*
- 然后进行最长的前缀匹配 location ^~
- 最后是通常的前缀匹配 location /prefix/
- 如果前面的匹配都没有找到,就使用默认的 location /
负载均衡算法
Nginx 支持多种负载均衡算法,常见的包括:
- 轮询(Round Robin):默认算法,按照顺序将请求依次分发到后端服务器。
- 权重(Weighted Round Robin):为每个后端服务器设置权重,权重高的服务器分配的请求会更多。
- IP 哈希(IP Hash):根据客户端 IP 的哈希值决定分发到哪台后端服务器,适用于需要保持会话一致性的场景。
- 最少连接(Least Connections):将请求分配给当前连接数最少的后端服务器。
- Hash(指定字段哈希):基于指定的请求字段(如 URL、Cookie 等)来分配请求。
轮询配置
默认的配置,按顺序把请求依次发到列表的服务器。
events {
worker_connections 1024;
}
http {
upstream backend {
server 192.168.128.139:5001;
server 192.168.128.138:5001;
}
server {
listen 5000;
location / {
proxy_pass http://backend;
}
}
}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
权重配置
为每个后端服务器设置权重,会尽可能将请求按照设定的权重比例进行转发。
upstream backend {
server 192.168.128.139:5001 weight=1;
server 192.168.128.138:5001 weight=2;
}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
IP哈希配置
根据客户端 IP 的哈希值决定分发到哪台后端服务器,适用于需要保持会话一致性的场景。
简单理解,也即客户端的请求第一次被转发到的那台服务器,那么后续这个客户端的请求,都会被转发到之前的同一台服务器。
events {
worker_connections 1024;
}
http {
upstream backend {
ip_hash;
server 192.168.128.139:5001;
server 192.168.128.138:5001;
}
server {
listen 5000;
location / {
proxy_pass http://backend;
}
}
}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
最少连接配置
将请求分配给当前连接数最少的后端服务器,也即哪台请求服务器的压力比较小,就将请求转发到哪台服务器。
events {
worker_connections 1024;
}
http {
upstream backend {
least_conn;
server 192.168.128.139:5001;
server 192.168.128.138:5001;
}
server {
listen 5000;
location / {
proxy_pass http://backend;
}
}
}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.139: I am backend1'}
{'result': 'from 192.168.128.138: I am backend2'}
健康检查配置
手动将某个服务器标记为不可用,那么负载均衡进行请求转发的时候,就会忽略掉 "不可用" 的服务器,从而不会将请求转发到这个失效的服务器上。
events {
worker_connections 1024;
}
http {
upstream backend {
server 192.168.128.139:5001 down;
server 192.168.128.138:5001;
}
server {
listen 5000;
location / {
proxy_pass http://backend;
}
}
}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
反向代理
简单理解,反向代理就是服务端的代理,上面一节的 负载均衡 也用到了反向代理。
upstream
我们把 192.168.128.139 上的 nginx.conf 配置从负载均衡稍微改一下。
events {
worker_connections 1024;
}
http {
upstream backend {
server 192.168.128.138:5001;
}
server {
listen 5000;
location / {
proxy_pass http://backend;
}
}
}
这样的话,请求到 192.168.128.139:5000 的请求,都被转发到了 192.168.128.138:5001 上去了。
再次用相同的请求运行,可以看到,请求都被转发到另外一个机器的另外一个端口上去了。
import requests
def test_request(url):
res = requests.get(url)
print(res.json())
if __name__ == '__main__':
url = "http://192.168.128.139:5000/test"
for i in range(10):
test_request(url)
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
{'result': 'from 192.168.128.138: I am backend2'}
proxy_pass
当然,如果只是想做单一的反向代理,并不考虑负载均衡的话,我们也可以不用 upstream 模块。
events {
worker_connections 1024;
}
http {
server {
listen 5000;
location / {
proxy_pass http://192.168.128.138:5001;
}
}
}
上面这种,也可以达到反向代理的作用。但这种情况具有局限性,并不能做到负载均衡,只限于一个被代理服务器的情况下。
还有个小的知识点。proxy_pass 结尾带 / 和不带 / 是有区别的。
我们可以测试一下。在 192.168.128.138 上部署如下服务。
192.168.128.138
from flask import Flask
app = Flask(__name__)
@app.route('/test', methods=['POST', 'GET'])
def test():
result = {
"result": "from 192.168.128.138: I am backend2"
}
return result
@app.route('/image', methods=['POST', 'GET'])
def test2():
result = {
"result": "from 192.168.128.138: I am backend2"
}
return result
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001)
配置 nginx 为 :
events {
worker_connections 1024;
}
http {
server {
listen 5000;
location /test {
proxy_pass http://192.168.128.138:5001;
}
}
}
请求 http://192.168.128.139:5000/test 可以正常调通。
import requests
if __name__ == '__main__':
url = "http://192.168.128.139:5000/test"
res = requests.get(url)
print(res.json()) # {'result': 'from 192.168.128.138: I am backend2'}
我们修改 nginx 为(proxy_pass 中的 url 末尾多了一个斜杠 /):
events {
worker_connections 1024;
}
http {
server {
listen 5000;
location /test {
proxy_pass http://192.168.128.138:5001/;
}
}
}
此时,请求 http://192.168.128.139:5000/test 无法正常调通。但是请求 http://192.168.128.139:5000/test/image 却是可以正常调通的。
import requests
if __name__ == '__main__':
url = "http://192.168.128.139:5000/test/image"
res = requests.get(url)
print(res.json()) # {'result': 'from 192.168.128.138: I am backend2'}
这是因为, nginx 匹配到路由中的 /test 并将其阶段,最终转发的 url 是 http://192.168.128.139:5000/image,因此能正常调通。
下面直接给出结论:
proxy_pass 结尾带/
的场景中,会截断匹配成功的 location 规则,转发 proxy_pass + 剩余的 URI。
如果是下面这种 nginx 配置(localtion 和 proxy_pass 后缀一致):
events {
worker_connections 1024;
}
http {
server {
listen 5000;
location /test {
proxy_pass http://192.168.128.138:5001/test/;
}
}
}
假设请求 url 是 http://192.168.128.139:5000/test/test_image,则虽然 /test/test_image 被截断 /test 后是 /test_image,但是转发时拼接上 proxy_pass 的 http://192.168.128.138:5001/test ,最后真正转发的仍然是 http://192.168.128.138:5001/test/test_image 。