目录
一. 为什么要对 nginx 平滑升级
随着 nginx
越来越流行,并且 nginx
的优势也越来越明显,nginx
的版本迭代也来时加速模式,1.9.0版本的nginx更新了许多新功能,例如 stream
四层代理功能,伴随着 nginx
的广泛应用,版本升级必然越来越快,线上业务不能停,此时 nginx
的升级就是运维的工作了
nginx 方便地帮助我们实现了平滑升级。其原理简单概括,就是: (1)在不停掉老进程的情况下,启动新进程。 (2)老进程负责处理仍然没有处理完的请求,但不再接受处理请求。 (3)新进程接受新请求。 (4)老进程处理完所有请求,关闭所有连接后,停止。 这样就很方便地实现了平滑升级。一般有两种情况下需要升级 nginx,一种是确实要升级 nginx 的版本,另一种是要为 nginx 添加新的模块。
二. Nginx 平滑升级原理
多进程模式下的请求分配方式
nginx 默认工作在多进程模式下,即主进程(master process)启动后完成配置加载和端口绑定等动作,fork
出指定数量的工作进程(worker process),这些子进程会持有监听端口的文件描述符(fd),并通过在该描述符上添加监听事件来接受连接(accept)。
信号的接收和处理
nginx 主进程在启动完成后会进入等待状态,负责响应各类系统消息,如SIGCHLD、SIGHUP、SIGUSR2等。
Nginx信号简介
主进程支持的信号
-
TERM
,INT
: 立刻退出 -
QUIT
: 等待工作进程结束后再退出 -
KILL
: 强制终止进程 -
HUP
: 重新加载配置文件,使用新的配置启动工作进程,并逐步关闭旧进程。 -
USR1
: 重新打开日志文件 -
USR2
: 启动新的主进程,实现热升级 -
WINCH
: 逐步关闭工作进程
工作进程支持的信号
-
TERM
,INT
: 立刻退出 -
QUIT
: 等待请求处理结束后再退出 -
USR1
: 重新打开日志文件
三. 前提准备
准备一台初始化的虚拟机
localhost | Rocky_linux9.4 | 192.168.226.20 |
关闭防火墙和SElinux
虚拟机基础配置脚本如下进行步骤前,先跑一遍
#!/bin/bash
# **********************************************************
# * File Name : rocky_linux
# * Author : Elk
# * Email : zzdict@gmail.com / elk_deer@foxmail.com
# * Create time : 2024-06-15 20:12
# * Description :
# **********************************************************
# 检查是否以 root 用户运行脚本
if [ "$(id -u)" -ne 0 ]; then
tput bold
tput setaf 1
tput setaf 3
echo "请以 root 用户运行此脚本。"
tput sgr0
exit 1
fi
# 启用网络接口
enable_network_interface() {
local interface=$1
if ip link set "$interface" up; then
tput bold
tput setaf 2
echo "网络接口 $interface 已启用。"
tput sgr0
else
tput bold
tput setaf 1
echo "无法启用网络接口 $interface,请检查接口名称。"
tput sgr0
exit 1
fi
}
# 配置 YUM 源
configure_yum_repos() {
sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.aliyun.com/rockylinux|g' \
-i.bak \
/etc/yum.repos.d/Rocky-*.repo
tput bold
tput setaf 2
echo "YUM 源配置已更新。"
tput sgr0
dnf makecache
yum -y install epel-release
}
# 停止和禁用防火墙,禁用 SELinux
configure_security() {
systemctl stop firewalld && systemctl disable firewalld
firewall-cmd --reload
tput bold
tput setaf 2
echo "防火墙已停止并禁用。"
tput sgr0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
tput bold
tput setaf 2
echo "SELinux 已禁用。"
tput sgr0
}
# 检查并安装chrony,进行时间同步
function install_and_sync_time_with_chrony() {
# 检查是否已经安装chrony
if ! command -v chronyd &>/dev/null; then
sudo dnf install -y chrony &> /dev/null
# 检查安装是否成功
if ! command -v chronyd &>/dev/null; then
tput bold
tput setaf 1
echo "安装 chrony 失败。请检查您的包管理器并重试。"
tput sgr0
exit 1
else
tput bold
tput setaf 2
echo "chrony 安装成功。"
tput sgr0
fi
else
tput bold
tput setaf 2
echo "chrony 已安装。"
tput sgr0
fi
# 确保安装其他必要的软件包
sudo dnf install -y vim wget unzip tar lrzsz &> /dev/null
if [ $? -eq 0 ]; then
tput bold
tput setaf 2
echo "其他软件包安装成功。"
tput sgr0
else
tput bold
tput setaf 1
echo "安装其他软件包失败。"
tput sgr0
exit 1
fi
}
# 启动 chronyd 服务并启用开机启动
if sudo systemctl start chronyd && sudo systemctl enable chronyd; then
tput bold
tput setaf 2
echo "chronyd 服务已成功启动并设置为开机启动。"
tput sgr0
else
tput bold
tput setaf 1
echo "启动或启用 chronyd 服务失败。请检查 systemctl 状态。"
tput sgr0
exit 1
fi
# 强制同步时间
sudo chronyc -a makestep
tput bold
tput setaf 2
echo "时间同步已成功完成。"
tput sgr0
}
# 自定义 IP 地址
configure_ip_address() {
tput bold
tput blink
tput setaf 1
read -p "******输入你要设置的IP >>> : " ip_a
tput sgr0
tput bold
tput blink
tput setaf 6
read -p "******输入你要设置的网关>>> : " gat
tput sgr0
tput bold
tput blink
tput setaf 3
read -p "******输入你要设置的DNS>>> : " dnns
tput sgr0
# 判断当前连接的名字
connection_name=$(nmcli -t -f NAME,DEVICE con show --active | grep -E "ens33|Wired connection 1" | cut -d: -f1)
if [[ "$connection_name" == "ens33" ]]; then
# 针对 ens33 连接进行配置
nmcli con mod "ens33" ipv4.method manual ipv4.addresses "${ip_a}/24" ipv4.gateway "${gat}" ipv4.dns "${dnns}" autoconnect yes
elif [[ "$connection_name" == "Wired connection 1" ]]; then
# 针对 Wired connection 1 连接进行配置
nmcli con mod "Wired connection 1" ipv4.method manual ipv4.addresses "${ip_a}/24" ipv4.gateway "${gat}" ipv4.dns "${dnns}" autoconnect yes
else
tput bold
tput setaf 1
echo "无法识别的网络连接名称:$connection_name"
tput sgr0
return 1
fi
tput setab 5
tput setaf 15
tput bold
echo "IP 地址配置成功,即将重启系统。"
tput sgr0
nmcli con up "$connection_name"
reboot
}
# 主函数
main() {
local interface="ens33"
enable_network_interface "$interface"
configure_yum_repos
configure_security
install_and_sync_time_with_chrony
configure_ip_address "$interface"
}
# 调用主函数
main
来到官网下载两个不同的版本
下载两个不同版本进行,从低版本升级高版本实验。
将下载好的两个安装包上传到虚拟机中,如下所示两个安装包
[root@localhost ~]# ll
total 2268
-rw-------. 1 root root 815 Jun 6 14:00 anaconda-ks.cfg
-rw-r--r-- 1 root root 1062124 Jun 22 00:38 nginx-1.20.2.tar.gz
-rw-r--r-- 1 root root 1244738 Jun 21 21:19 nginx-1.26.1.tar.gz
-rw-r--r-- 1 root root 4251 Jun 17 23:50 rocky_linux.sh
四. 安装一个模拟被升级旧Nginx版本
1.安装依赖工具
[root@localhost ~]# yum install -y gcc gcc-c++ pcre-devel openssl-devel zlib-devel
2.解压1.20.2版本安装包
[root@localhost ~]# ls
anaconda-ks.cfg nginx-1.20.2.tar.gz nginx-1.26.1.tar.gz rocky_linux.sh
[root@localhost ~]# tar -zxf nginx-1.20.2.tar.gz
3.进入解压后的目录
[root@localhost ~]# ls
anaconda-ks.cfg nginx-1.20.2 nginx-1.20.2.tar.gz nginx-1.26.1.tar.gz rocky_linux.sh
[root@localhost ~]# cd nginx-1.20.2
[root@localhost nginx-1.20.2]# ls
CHANGES CHANGES.ru LICENSE README auto conf configure contrib html man src
4.预编译
在这里面,是少两个模块,没写进代码里,用来模拟这样少模块的场景,便于后面升级来扩展。
[root@localhost nginx-1.20.2]# ./configure --prefix=/usr/local/nginx --group=nginx --user=nginx --sbin-path=/usr/local/nginx/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/tmp/nginx/client_body --http-proxy-temp-path=/tmp/nginx/proxy --http-fastcgi-temp-path=/tmp/nginx/fastcgi --pid-path=/var/run/nginx.pid --lock-path=/var/lock/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_gzip_static_module --with-pcre --with-http_realip_module
5.编译安装
[root@localhost nginx-1.20.2]# make && make install
6.创建一个nginx的系统用户
[root@localhost nginx-1.20.2]# useradd --system --no-create-home --shell /sbin/nologin nginx
7.创建目录用于临时存储客户端请求数据和缓存文件
[root@localhost nginx-1.20.2]# mkdir -p /tmp/nginx/client_body
8.启动nginx
[root@localhost nginx-1.20.2]# /usr/local/nginx/sbin/nginx
9.验证nginx正常工作
[root@localhost nginx-1.20.2]# curl -Ik 192.168.226.20
HTTP/1.1 200 OK
Server: nginx/1.20.2
Date: Sat, 22 Jun 2024 05:16:03 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Sat, 22 Jun 2024 01:00:47 GMT
Connection: keep-alive
ETag: "6676223f-264"
Accept-Ranges: bytes
或者浏览器打开访问改主机IP地址
五. 模拟真实业务场景
使用go代码对该主机地址进行不间断请求,模拟用户访问,模拟真实业务场景。
通过观察升级过程中是否会有失败,即业务中断的访问。当然,极少数个例的失败是可接受的。
package main
import (
"fmt"
"io"
"net/http"
"time"
)
// ANSI color codes
const (
RedColor = "\033[31m"
GreenColor = "\033[32m"
YellowColor = "\033[33m"
ResetColor = "\033[0m"
)
// 记录成功、失败、无法响应以及服务暂时不可用(503)的请求数量
var successCount int
var failureCount int
var unresponsiveCount int
var tempUnavailableCount int // 新增变量来跟踪503 Service Unavailable状态的请求数量
// makeRequest 向给定的 URL 发送一个 GET 请求,并根据响应的成功与否以不同颜色打印出响应状态和所花费的时间。
func makeRequest(url string, attempt int) {
startTime := time.Now()
resp, err := http.Get(url)
elapsedTime := time.Since(startTime)
if err != nil {
// 如果因为网络问题或服务器问题导致请求失败,我们将其计为“无法响应”
fmt.Printf("[%d] %sRequest unresponsive: %s %.4f seconds%s\n", attempt, RedColor, err, elapsedTime.Seconds(), ResetColor)
unresponsiveCount++
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
fmt.Printf("Error closing response body: %s\n", err)
}
}(resp.Body)
color := GreenColor
if resp.StatusCode == 503 {
color = YellowColor
tempUnavailableCount++ // 对返回503状态码的请求进行统计
} else if resp.StatusCode != 200 {
color = RedColor
failureCount++
} else {
successCount++
}
// 根据状态码,以对应的颜色打印响应状态码和耗时
fmt.Printf("[%d] %s%d %.4f seconds%s\n", attempt, color, resp.StatusCode, elapsedTime.Seconds(), ResetColor)
}
func main() {
url := "http://192.168.226.20" //选择你要测试的地址
totalAttempts := 20000 //自定义请求次数
startTime := time.Now()
for attempt := 1; attempt <= totalAttempts; attempt++ {
makeRequest(url, attempt)
time.Sleep(1 * time.Second) // 在每次请求之间等待1秒
}
totalElapsedTime := time.Since(startTime)
successRate := (float64(successCount) / float64(totalAttempts)) * 100
failureRate := (float64(failureCount) / float64(totalAttempts)) * 100
unresponsiveRate := (float64(unresponsiveCount) / float64(totalAttempts)) * 100
tempUnavailableRate := (float64(tempUnavailableCount) / float64(totalAttempts)) * 100 // 计算503状态码比率
fmt.Printf("%sTotal elapsed time: %.4f seconds%s\n", ResetColor, totalElapsedTime.Seconds(), ResetColor)
fmt.Printf("成功率: %.2f%%, 失败率: %.2f%%, 无法响应的请求比: %.2f%%, 服务暂时不可用请求比率(503): %.2f%%\n", successRate, failureRate, unresponsiveRate, tempUnavailableRate)
}
如图:
六. 下载获取一个扩展模块
1.下载扩展模块
[root@localhost ~]# cd
[root@localhost ~]# wget https://github.com/zls0424/ngx_req_status/archive/master.zip -O ngx_req_status.zip
2. 解压并查看
[root@localhost ~]# unzip ngx_req_status.zip
Archive: ngx_req_status.zip
428ffbb511fcb456218b4d2e6fb2f6f3e5abcf08
creating: ngx_req_status-master/
inflating: ngx_req_status-master/README
inflating: ngx_req_status-master/README.md
inflating: ngx_req_status-master/config
inflating: ngx_req_status-master/module_patch.sh
inflating: ngx_req_status-master/ngx_http_req_status_module.c
inflating: ngx_req_status-master/write_filter-1.7.11.patch
inflating: ngx_req_status-master/write_filter.patch
[root@localhost ~]# ll
total 2280
-rw-------. 1 root root 815 Jun 6 14:00 anaconda-ks.cfg
drwxr-xr-x 9 1001 1001 186 Jun 22 00:58 nginx-1.20.2
-rw-r--r-- 1 root root 1062124 Jun 22 00:38 nginx-1.20.2.tar.gz
drwxr-xr-x 8 502 games 158 May 29 22:30 nginx-1.26.1
-rw-r--r-- 1 root root 1244738 Jun 21 21:19 nginx-1.26.1.tar.gz
drwxr-xr-x 2 root root 169 Oct 21 2015 ngx_req_status-master
-rw-r--r-- 1 root root 11237 Jun 22 13:50 ngx_req_status.zip
-rw-r--r-- 1 root root 4251 Jun 17 23:50 rocky_linux.sh
3.解压升级的nginx版本压缩包
[root@localhost ~]# cd
[root@localhost ~]# tar -zxf nginx-1.26.1.tar.gz
4.将该模块拷贝到升级的nginx版本解压后的目录里
[root@localhost ~]# cp -r ngx_req_status-master/ nginx-1.26.1/
5.安装一些依赖包,并确保都已安装
[root@localhost ~]# yum -y install pcre pcre-devel openssl openssl-devel gcc gcc-c++ zlib zlib-devel
[root@localhost ~]# yum -y install patch.x86_64
七. 模拟升级Nginx
1.进入升级的nginx版本目录里,可以看到刚拷贝进来的扩展模块也在
[root@localhost ~]# cd nginx-1.26.1/
[root@localhost nginx-1.26.1]# ll
total 828
-rw-r--r-- 1 502 games 327587 May 29 22:30 CHANGES
-rw-r--r-- 1 502 games 501144 May 29 22:30 CHANGES.ru
-rw-r--r-- 1 502 games 1397 May 28 21:28 LICENSE
-rw-r--r-- 1 502 games 49 May 28 21:28 README
drwxr-xr-x 6 502 games 4096 Jun 22 13:29 auto
drwxr-xr-x 2 502 games 168 Jun 22 13:29 conf
-rwxr-xr-x 1 502 games 2611 May 28 21:28 configure
drwxr-xr-x 4 502 games 72 Jun 22 13:29 contrib
drwxr-xr-x 2 502 games 40 Jun 22 13:29 html
drwxr-xr-x 2 502 games 21 Jun 22 13:29 man
drwxr-xr-x 2 root root 169 Jun 22 13:58 ngx_req_status-master
drwxr-xr-x 9 502 games 91 May 29 22:30 src
2.应用相应的补丁
[root@localhost nginx-1.26.1]# patch -p1 < ./ngx_req_status-master/write_filter-1.7.11.patch
3.预编译
和上面那个预编译的步骤相比,多加了两个模块
[root@localhost nginx-1.26.1]# ./configure --prefix=/usr/local/nginx --group=nginx --user=nginx --sbin-path=/usr/local/nginx/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/tmp/nginx/client_body --http-proxy-temp-path=/tmp/nginx/proxy --http-fastcgi-temp-path=/tmp/nginx/fastcgi --pid-path=/var/run/nginx.pid --lock-path=/var/lock/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_gzip_static_module --with-pcre --with-http_realip_module --with-stream --with-http_image_filter_module --add-module=./ngx_req_status-master
4.编译
[root@localhost nginx-1.26.1]# make
升级不要再执行make install
5.备份旧版本nginx
[root@localhost nginx]# cd /usr/local/nginx/sbin
[root@localhost sbin]# mv nginx nginx.bak
[root@localhost sbin]# ll
total 4268
-rwxr-xr-x 1 root root 4368512 Jun 22 09:00 nginx.bak
6.拷贝新版本的nginx到当前目录
[root@localhost sbin]# cp /root/nginx-1.26.1/objs/nginx ./
[root@localhost sbin]# ll
total 9336
-rwxr-xr-x 1 root root 5186960 Jun 22 14:20 nginx
-rwxr-xr-x 1 root root 4368512 Jun 22 09:00 nginx.bak
7.测试新版本的nginx是否正常
[root@localhost sbin]# ./nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
8.给nginx发送平滑迁移信号(若不清楚pid路径,请查看nginx配置文件)
[root@localhost sbin]# kill -USR2 `cat /var/run/nginx.pid`
9.查看nginx pid,会出现一个nginx.pid.oldbin
[root@localhost sbin]# ll /var/run/nginx.pid*
-rw-r--r-- 1 root root 5 Jun 22 14:25 /var/run/nginx.pid
-rw-r--r-- 1 root root 5 Jun 22 13:14 /var/run/nginx.pid.oldbin
10.查看当前nginx进程
会有两组nginx进程,一组是前面旧版本的13:14分运行的,一组是刚刚14:24运行的,即新起来的进程
[root@localhost sbin]# ps aux |grep nginx
root 1435 0.0 0.1 9556 2528 ? Ss 13:14 0:00 nginx: master process /usr/local/nginx/sbin/nginx
nginx 1436 0.0 0.2 13948 5088 ? S 13:14 0:03 nginx: worker process
root 6202 0.0 0.3 9684 6528 ? S 14:24 0:00 nginx: master process /usr/local/nginx/sbin/nginx
nginx 6203 0.0 0.2 13980 5100 ? S 14:24 0:00 nginx: worker process
root 6207 0.0 0.1 3876 1920 pts/0 S+ 14:26 0:00 grep --color=auto nginx
11.从容关闭旧的Nginx进程
此时旧版本的worker进程会关闭
[root@localhost sbin]# kill -WINCH `cat /var/run/nginx.pid.oldbin`
注意声明:
不重载配置启动旧的工作进程,该步骤是在如果新进程察觉到有问题,回滚启用旧nginx进程。如果没有问题,此步骤不操作。
kill -HUP `cat /var/run/nginx.pid.oldbin`
12.结束工作进程,完成此次升级
[root@localhost sbin]# kill -QUIT `cat /var/run/nginx.pid.oldbin`
13.验证Nginx是否升级成功
[root@localhost sbin]# /usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.26.1
built by gcc 11.4.1 20231218 (Red Hat 11.4.1-3) (GCC)
built with OpenSSL 3.0.7 1 Nov 2022
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx --group=nginx --user=nginx --sbin-path=/usr/local/nginx/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/tmp/nginx/client_body --http-proxy-temp-path=/tmp/nginx/proxy --http-fastcgi-temp-path=/tmp/nginx/fastcgi --pid-path=/var/run/nginx.pid --lock-path=/var/lock/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_gzip_static_module --with-pcre --with-http_realip_module --with-stream --add-module=./ngx_req_status-master
[root@localhost sbin]# curl -Ik 192.168.226.20
HTTP/1.1 200 OK
Server: nginx/1.26.1
Date: Sat, 22 Jun 2024 06:38:12 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Sat, 22 Jun 2024 01:00:47 GMT
Connection: keep-alive
ETag: "6676223f-264"
Accept-Ranges: bytes
旧版本的nginx需要观察一段时间,确定不会影响业务需求再进行删除。