Skip to main content

应用发布管理

目标#

  • 实现应用程序不中断自动发布

实现原理#

  • 运行中的应用服务进程需要记录处理中的请求数及执行中的存储过程数
  • 需要重新启动应用服务时守护进程向应用服务进程发送重新启动信号
  • 应用服务进程接收到重新启动信号时不再接收新的请求,也不再启动新的存储过程

    此时,如果接收到新的请求,应用服务需返回 503 Service Unavailable 错误,根据前面章节的配置,负载均衡服务器在取得该错误后会将请求继续转发给其他可用的应用服务器。

  • 当处理完所有请求和存储过程后,重新启动应用服务进程

代码示例#

下面以 PM2(作为守护进程)和 Node.js(作为应用服务)为例。

假设有如下服务器实例:

内网 IP 地址用途分布式
文件系统
挂载点
备注
10.0.0.1代码发布服务器/mnt/dfs应用工程包上传路径:/mnt/dfs/src
应用工程包解压路径:/mnt/dfs/dist/example-{timestamp}
最新代码路径:/mnt/dfs/dist/example → /mnt/dfs/dist/example-{timestamp}
10.0.0.2应用服务器#1/mnt/dfs应用工程路径:/var/www/example → /mnt/dfs/dist/example
....../mnt/dfs应用工程路径:/var/www/example → /mnt/dfs/dist/example
10.0.0.n应用服务器#n/mnt/dfs应用工程路径:/var/www/example → /mnt/dfs/dist/example

配置应用服务工程(example)的 package.json 文件中的重新启动脚本:

package.json

{
...,
"scripts": {
...,
"start": "pm2 start ./app.json --env production",
"restart": "pm2 restart example",
"stop": "pm2 stop example"
}
}

app.json

{
"name": "example",
"script": "index.js",
"exec_mode": "cluster",
"instances": 4,
"watch": false,
"wait_ready": true,
"listen_timeout": 5000,
"max_restarts": 5,
"kill_timeout": 5000,
"env": {
"NODE_ENV": "development"
},
"env_production": {
"NODE_ENV": "production"
},
"merge_logs": true,
"log_date_format": "YY-MM-DD HH:mm:ss",
"error_file": "/var/www/log/example-error.log",
"out_file": "/var/www/log/example-output.log",
"pid_file": "/var/www/log/example.pid"
}

index.js(参考:PM2: Graceful Stop

'use strict';
let state = null;
let processing = 0;
process.on('SIGINT', () => {
// TODO: 执行进程中止前的处理,例如在全局标记当前状态为重新启动中
state = 'restarting';
});
...
// 若应用服务接收到进程终止的信号则不再接收新的请求
app.use((req, res, next) => {
if (state === 'restarting') {
res.statusCode = 503;
res.end();
return;
}
processing++;
// 请求处理完成时更新处理中的请求计数,
// 若已接收到终止信号且已完成所有请求的处理则结束当前进程
res.on('finish', () => {
processing--;
if (state === 'restarting'
&& processing === 0) {
process.exit(0);
}
});
next();
});
...

在代码发布服务器上设置定时任务,每隔一段时间(如5分钟)执行以下代码,以实现自动更新服务器代码:

#!/bin/bash
timestamp=`date +"%Y%m%d%H%M%S"`
baseDir=/mnt/dfs
sourceFile=${baseDir}/src/example.tar.gz
distDir=${baseDir}/dist
distFile=${distDir}/example.tar.gz
latestDir=${distDir}/example-${timestamp}
latestLink=${distDir}/example
# 若无工程压缩包文件则结束
if ! [ -f ${sourceFile} ]
then
exit 0
fi
fileSize=`wc -c < ${sourceFile}`
sleep 1
currentFileSize=`wc -c < ${sourceFile}`
# 若文件尚未写入完毕(上传中时)则结束
if ! [ ${fileSize} = ${currentFileSize} ]
then
exit 0
fi
# 解压缩压缩包并安装依赖模块
mv ${sourceFile} ${distDir}
tar -xf ${distFile} --directory=${distDir}
mv ${distDir}/example ${latestDir}
cd ${latestDir}
npm install
# 更新软链接
cd ..
ln -sfn ${latestDir} ${latestLink}
# 记录最后发布时间
echo ${timestamp} > ${latestDir}/RELEASE
rm -f ${distFile}
exit 0

在应用服务器上设置定时任务,每隔一段时间(如5分钟)执行以下代码,以实现自动启动应用服务进程:

避免所有应用服务器同时执行以下脚本,否则可能将导致短时间内服务器无响应。

#!/bin/bash
restartAtFile=/var/www/EXAMPLE_RESTART
restartAt=0 # 应用服务最后重新启动时间
releaseAt=0 # 应用服务代码最后发布时间
if [ -f ${restartAtFile} ]
then
read restartAt < ${restartAtFile}
fi
read releaseAt < /var/www/example/RELEASE
# 若代码发布时间早于服务启动时间则结束
if [ ${releaseAt} -lt ${restartAt} ]
then
exit 0
fi
# 重新启动应用服务进程
cd /var/www/example
npm run restart
# 记录应用服务进程重新启动时间
cd ..
echo `date +"%Y%m%d%H%M%S"` > ${restartAtFile}

至此,将应用服务的代码打包并上传到代码发布服务器的 /mnt/dfs/src 路径下后,应用服务进程即可在设定的时间内完成自动发布和重新启动。