唠唠闲话

Nginx 不仅仅是一个高效的反向代理服务器,它也是搭建负载均衡的利器,为应用提供了稳定的访问保障。而 Shell 脚本则作为 Linux 环境中强大的自动化工具,在日常运维中不可或缺。

本篇介绍 Nginx 的负载均衡功能,并通过 Shell 自动化脚本的结合,帮助开发者实现高效的服务管理。

废话不多说,直接看实战。

Nginx 负载均衡

假设我们在 localhost 上运行了两个服务实例,分别监听 1972920729 端口。我们希望将请求分发给这两个实例。

Nginx 配置策略

使用 upstream 指令来定义一组后端服务器。在具体的负载均衡配置中,还可以利用一些高级选项来实现更加细致的流量管控,比如权重、最大连接数等。以下是几个常用的高级负载均衡配置示例:

权重用于控制请求分配到每个服务器的比例,让性能更好或资源更多的服务器承担更多的请求:

1
2
3
4
upstream myapp {
server localhost:19729 weight=3;
server localhost:20729 weight=1;
}

这里,localhost:19729 拥有比 localhost:20729 更高的权重设置,意味着三倍于另一个服务器的请求将被分配给它。

设置每个服务器的最大并发连接数,旨在防止过载。

1
2
3
4
upstream myapp {
server localhost:19729 max_conns=100;
server localhost:20729 max_conns=100;
}

其他选项:

  • backup:指定备用服务器,只有当所有非备用服务器都不可用时,才会被选用。
  • down:将服务器标记为不可用。
  • fail_timeoutmax_fails:配置失败响应的条件和时间限制。

除了 轮询(默认),Nginx 还支持其它负载均衡策略,如 ip_hashleast_conn

1
2
3
4
5
upstream myapp {
least_conn;
server localhost:19729;
server localhost:20729;
}

least_conn 策略将请求分发给连接数最少的服务器,这在请求处理时间差异较大的情况下能提供更平衡的负载。

完整配置示例

整合上述选项,我们可以得到一个更复杂的 Nginx 配置例子:

1
2
3
4
5
6
7
8
9
10
11
12
upstream myapp {
least_conn; # 使用最少连接策略
server localhost:19729 weight=2 max_conns=100; # 设置权重和最大连接数
server localhost:20729 max_conns=100 backup; # 设为备用服务器
}

server {
listen 80;
location / {
proxy_pass http://myapp;
}
}

Shell 自动化脚本

先编写一个脚本函数 start_instances,用于批量启动服务实例。预期的函数输入如下,完整代码参见后面:

1
2
3
4
5
6
7
# 写入文件 common.sh
start_instances() {
local description=$1
local log_dir=$2
local command=$3
shift 3
local json_env_configurations="$(echo "$@" | jq -c '.[]')"

参数说明:

  • description:用于标识任务的描述性字符串,这将帮助日志文件的分辨与管理。
  • log_dir:日志存储目录,方便集中管理各个服务实例的输出日志。
  • command:启动服务实例所需的实际命令,这里面可能包含 Shell 任何可执行程序。
  • json_env_configurations:以 JSON 格式给出的一系列环境变量配置,便利于为每个实例配备不同的启动参数。

现在,假设我们有一个 Python 服务 myapp.py,通过环境变量 CUDA_VISIBLE_DEVICESSERVER_PORT 来配置 GPU 和端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# 1. 导入函数 start_instances
source common.sh

# 2. 定义服务启动命令
CMD="nohup python myapp.py"

# 3. 定义 JSON 格式的配置文件
configs='
[
{"CUDA_VISIBLE_DEVICES":"6", "SERVER_PORT":"19729"},
{"CUDA_VISIBLE_DEVICES":"7", "SERVER_PORT":"20729"}
]'

# 批量启动服务实例
start_instances "示例服务" "logs" "$CMD" $configs

演示了启动两个服务实例,configs 的每一项代表一个独立的实例启动配置,其绑定不同的 GPU 和端口,并将日志输出到 logs 目录下。

下面逐步解析脚本的实现逻辑和过程。

1. 解析与读取输入参数

1
2
3
4
5
6
start_instances() {
local description=$1
local log_dir=$2
local command=$3
shift 3
local json_env_configurations="$(echo "$@" | jq -c '.[]')"

将需要的描述性字符串、日志目录、启动命令等输入信息传递给 start_instances 函数。使用 shift 将注意力集中在约定的 JSON 配置输入上。随后,用 jq 打开 JSON 列表。

2. 解析 JSON 环境配置

使用 jq 来解析每个 JSON 字符串,将其转换为 Bash 对能用的键值对:

1
2
3
4
5
6
7
8
for json_config in ${json_env_configurations[@]}
do
local keys=()
local values=()
while IFS= read -r key && IFS= read -r value; do
keys+=("$key")
values+=("$value")
done < <(echo $json_config | jq -r 'to_entries[] | "\(.key)\n\(.value)"')

通过 jqto_entries[] 方法,将 JSON 字符串解析为键值对,以便后续过程使用。

3. 日志文件路径

根据每个实例的配置构建独特的日志文件名,以便于区分不同实例的日志:

1
2
3
4
5
6
7
8
9
local log_file="$log_dir/$description"
declare -A env_vars
for ((i=0; i<${#keys[@]}; i++)); do
key="${keys[$i]}"
value="${values[$i]}"
env_vars["$key"]="$value"
log_file+="_${value}"
done
log_file+=".log"

4. 检查并关闭已有进程

确定进程占用的端口并关闭,以便为新实例的正常启动腾出资源:

1
2
3
4
5
6
7
8
9
10
for key in "${!env_vars[@]}"; do
if [[ "$key" == *_PORT ]]; then
port=${env_vars[$key]}
pid=$(lsof -i:$port -t)
if [ -n "$pid" ]; then
kill -9 $pid
echo "$description instance on port $port is down"
fi
fi
done

通过lsof命令,可以识别出程序占用的端口并终止相关进程。

5. 启动服务实例

最后,针对每个环境变量启动服务实例:

1
2
3
4
5
6
7
8
9
    for key in "${!env_vars[@]}"; do
export "$key"="${env_vars[$key]}"
done

# Start the instance
eval $command > $log_file 2>&1 &

echo "$description with config $json_config is up"
done

执行命令,将输出重定向到构建的日志文件中。这里有个踩坑点,最后要用 eval 运行而不能直接用 $command 运行,否则对于多指令代码会有问题。

举个例子,假设 command="echo 1 && pwd",使用 $command 会输出字符串 1 && pwd,而只有用 eval $command 才能把后边的 pwd 也执行。

完整脚本

汇总上边内容,编写文件 common.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/bin/bash

start_instances() {
local description=$1
local log_dir=$2
local command=$3
shift 3
local json_env_configurations="$(echo "$@" | jq -c '.[]')"

# Process each JSON configuration
for json_config in ${json_env_configurations[@]}
do
# Use jq to parse the JSON string into key-value pairs
local keys=()
local values=()
while IFS= read -r key && IFS= read -r value; do
keys+=("$key")
values+=("$value")
done < <(echo $json_config | jq -r 'to_entries[] | "\(.key)\n\(.value)"')

# Prepare environment variables and log file name
local log_file="$log_dir/$description"
declare -A env_vars
for ((i=0; i<${#keys[@]}; i++)); do
key="${keys[$i]}"
value="${values[$i]}"
env_vars["$key"]="$value"
log_file+="_${value}"
done
log_file+=".log"

# Identify and kill process based on ports
for key in "${!env_vars[@]}"; do
if [[ "$key" == *_PORT ]]; then
port=${env_vars[$key]}
pid=$(lsof -i:$port -t)
if [ -n "$pid" ]; then
kill -9 $pid
echo "$description instance on port $port is down"
fi
fi
done

# Export environment variables
for key in "${!env_vars[@]}"; do
export "$key"="${env_vars[$key]}"
done

# Start the instance
eval $command > $log_file 2>&1 &

echo "$description with config $json_config is up"
done
}

总结

以上,我们介绍了 Nginx 负载均衡的基本配置和 Shell 脚本的实际应用。通过结合两者,我们可以实现服务的自动化管理与负载均衡,提高服务的稳定性和可靠性。