教程 · 2021年3月21日 0

使用 nginx 为后端应用服务器搭建负载均衡

内容目录

本文不会详细介绍 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 配置文件

  1. 在 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;
    }
}
  1. nginx.conf 文件的 http 块最后加一行 include loadbalance.conf (注意相对路径)(如果你是用命令安装的且 /etc/nginx/conf.d 目录存在,那么直接在那里创建 loadbalance.conf 配置文件即可,默认配置会自动引入)

像这样:

http {
    #其他配置省略
    include loadbalance.conf
}
  1. 为了支持 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),那么我将无法取得到正确的资源响应。解决方案如下。

  1. 采用无状态的会话令牌,如 JWT
  2. 使用集中式会话管理,比如所有 session 保存到数据库或 redis 中
  3. 相同的客户端分配到相同的节点处理

前两种办法在应用中实现,第 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 的文档。