本文不会详细介绍 nginx 的安装与配置,本文只是介绍负载均衡这一个内容
相关术语
仅仅是对本文而言,哈哈
- 节点:指一个应用服务器
- 应用服务器:后端 spring 应用、tomcat、nodejs server 等等
- 网关:如果你的服务器直接有公网 IP,那么不需要网关,但是通常情况下都是机房通过 NAT 地址转换转发流量到内网机器的,一般家用路由器也可称为网关。另外如果使用端口映射,那么端口映射的服务器就充当网关。
结构分析
通常情况下我们部署应用后,会是下图这样的结构。
但是这样的结构缺点很明显:
- 重启时整个业务不可用
- 网关与应用任意环节出现问题也会导致业务不可用
- 所有任务只有一个节点处理,服务器负载高
因此,可以采用 nginx 负载均衡的模式,将任务分配到多个节点处理。
这样的结构可以解决上面的问题 1、3,同时只要网关和所有的节点不同时出问题,业务就能正常访问。
准备工作
首先我们先创建一个测试应用服务器,随便什么都行,只要访问后显示节点的名字即可,为了方便,下面以 nodejs 为例。
创建一个新的 server.js
文件,输入下面的代码即可。
const http = require("http");
if (process.argv.length < 3) {
console.error("请输入节点名字");
console.error("用法:node index.js [节点名] [端口号]");
process.exit(1);
}
const name = process.argv[2];
const port = Number(process.argv[3] || 8848);
let server = http.createServer(function (req, res) {
res.writeHead(200, {
"Content-Type": "application/json"
});
res.end(JSON.stringify({
"name": name,
"port": port
}));
});
server.listen(port);
server.addListener('listening', () => {
console.log("测试服务器运行在 http://localhost:" + port);
});
然后让我们启动一些 node 服务当作应用服务器节点。
node server.js 节点1 8001
node server.js 节点2 8002
node server.js 节点3 8003
最后按照好 nginx 等待进一步配置。(下文使用版本 1.18.0)
创建 nginx 配置文件
- 在 nginx 的配置文件目录新建一个
loadbalance.conf
文件内容如下,下文解释含义。
# 基础版负载均衡配置
upstream backend {
# 填写后端节点的地址
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
}
server {
listen 80;
server_name localhost; # 如果提示冲突,先禁用旧的或者重命名这个
location / {
proxy_pass http://backend;
proxy_connect_timeout 5; # 连接超时
proxy_send_timeout 3600; # 发送超时
proxy_read_timeout 3600; # 读取超时
proxy_http_version 1.1; # 使用 HTTP/1.1 (支持长连接)
proxy_redirect default; # 自动重写跳转地址
proxy_set_header Host $host; # 重写 host (一般不需要)
# 识别真实地址(按需配置)
proxy_set_header Forwarded ''; # 禁用 Forwarded (目前 nginx 不支持这个,防止伪造代理所以禁用)
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 支持 websocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
- 在
nginx.conf
文件的 http 块最后加一行include loadbalance.conf
(注意相对路径)(如果你是用命令安装的且/etc/nginx/conf.d
目录存在,那么直接在那里创建loadbalance.conf
配置文件即可,默认配置会自动引入)
像这样:
http {
#其他配置省略
include loadbalance.conf
}
- 为了支持 websocket,必须在
nginx.conf
文件的 http 块加上如下代码
http {
#其他配置省略
# 支持 websocket
map $http_upgrade $connection_upgrade {
default 'keep-alive,Upgrade';
'' keep-alive;
}
include loadbalance.conf
}
重启 nginx 测试
重启 nginx,假定一切正常,打开浏览器访问 http://localhost
不断刷新,你会发现访问到的是不同的应用服务器。即每次刷新,显示的节点名随机。
更高级的玩法
如果使用默认的 upstream 配置,则会随机分配节点处理请求,如果某个节点不能用了或者超时了,nginx 会自动将其删除,待其恢复后自动重新加入队列。但是有时候这不能满足我们的需求。
设置 plan b、plan c ……
如果我有多台服务器,它们的处理能力不尽相同,其中有一台配置很高,能同时处理很多请求,但是其他的配置一般,那么这时候就可以配置权重 weight
,示例如下。
upstream backend {
# 填写后端节点的地址
server 127.0.0.1:8001 weight=5; # 配置权重,权重越大,被调用的几率就越高,默认权重为 1
server 127.0.0.1:8002 weight=3; # 这是我的 plan B
server 127.0.0.1:8003 weight=2; # 这是我的 plan C
}
保留备胎节点
如果我的业务在某个时间段比较集中,平时访问量不高,我想让一些节点空闲下来节约计算资源,那么就可以使用 backup
参数。
upstream backend {
# 填写后端节点的地址
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003 backup; # 仅当其他的节点都不可用时才会使用 backup 节点
server 127.0.0.1:8004 backup;
}
不回消息?拉黑!
当某个节点不可用时,浏览器访问负载均衡,负载均衡需要一个一个尝试可用节点,遇到不可用节点很长时间才会超时的情况时,客户端会卡顿几秒钟才会得到响应。这时负载均衡会缓存该节点的状态,下次直接认为改节点不可用,但是缓存到期后会再次尝试,客户端会再次面临卡顿。方案如下。
就相当于我是负载均衡,你节点不回我消息,我就长时间拉黑你,一段时间后我心情好了再去尝试。
upstream backend {
# 填写后端节点的地址
server 127.0.0.1:8001 fail_timeout=600s; # 设置错误状态的缓存时间,越长用户遇到卡顿的几率就越小,发现节点恢复的时间也就越长。默认 10 秒
server 127.0.0.1:8002 fail_timeout=600s;
server 127.0.0.1:8003 fail_timeout=600s;
}
server {
proxy_connect_timeout 2s; # 设置反向代理连接超时,越短就越快发现节点问题,但是延迟大时就容易误判
# 省略其他配置
}
专人专制,避免尴尬
如果我的应用服务器使用 cookie 或者请求头的方式识别用户状态,使用负载均衡后,用户每次请求使用的是不同的节点,如果各个节点状态独立(指各自维持登录等状态会话),那么不同的节点将无法识别客户端为相同的状态。
比如我在节点 1 登录后去访问节点 2 的资源,但是节点 2 不认识我的会话 ID (SESSION ID),那么我将无法取得到正确的资源响应。解决方案如下。
- 采用无状态的会话令牌,如 JWT
- 使用集中式会话管理,比如所有 session 保存到数据库或 redis 中
- 相同的客户端分配到相同的节点处理
前两种办法在应用中实现,第 3 中办法可以使用 nginx 配置。
使用下面三种方式后将无法使用权重分配
方式 1:根据 IP 地址分配节点。
upstream backend {
ip_hash;
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
}
这种方式会根据用户 IP 地址的前24位(IPv4)或者全部(IPv6)来计算 hash 值,相同 hash 值的客户端会分配到相同的节点。当某个节点不可用时,该节点的客户端会被暂时映射到另一个可用节点,当该节点恢复时会重新使用该节点提供服务。
方式 2:根据某个变量的 hash 分配节点(1.7.2 以后的版本才支持)
upstream backend {
hash $http_authorization; # 使用 "Authorization" 请求头 作为 hash
#hash $cookie_session_id; # 使用 "session-id" Cookie 作为 hash
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
}
这种方式会根据请求中的某个变量来计算 hash,hash 结果相同的请求会被分配到同一个节点。
方式 3:让 nginx 为我们设置一个 cookie 来标识客户端
upstream backend {
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
sticky cookie server_id expires=2h domain=localhost path=/;
}
这样 nginx 会在客户端第一次请求的时候设置一个 “server_id” Cookie 发送给客户端,下次就会用客户端 Cookie 中指定的节点处理请求。
不过我使用的 nginx 版本貌似没有把 sticky 模块编译进去,所以只能读者自己去尝试了。官方文档
下面介绍一个办法不用重新编译源码达到相同的效果。
upstream backend {
hash $cookie_server_id; # 使用 "server-id" Cookie 作为 hash
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
}
server {
# 省略其他配置
location / {
if ($cookie_server_id = '') {
# 将当前时间戳作为 server id
add_header Set-Cookie "server_id=$msec; Max-Age=86400; Path=/" always;
return 307 $request_uri; # 让客户端重新发送请求
}
# 省略其他配置
}
}
总结
上面介绍了集中常用的负载均衡配置,使用时按照实际需要配置即可,还有其他参数就不在这里说了,可以翻一翻 nginx 的文档。
近期评论