部署流程
部署流程是将代码从开发环境安全、可靠地发布到生产环境的系统化过程。一套成熟的部署流程能够最小化停机时间、降低发布风险,并确保出问题时能够快速回滚。本节将介绍 Git 部署、蓝绿部署、金丝雀发布、零停机部署和回滚策略。
基础概念
部署流程的核心目标
- 可靠性:确保每次部署都是可预测的
- 可回滚:出问题时能快速恢复
- 零停机:用户无感知更新
- 可审计:完整的部署日志和记录
- 自动化:减少人为错误
部署策略对比
| 策略 | 停机时间 | 复杂度 | 风险 | 适用场景 |
|---|---|---|---|---|
| 停机部署 | 有 | 低 | 高 | 内部系统、非关键服务 |
| 滚动部署 | 无 | 中 | 中 | 有状态应用 |
| 蓝绿部署 | 无 | 中 | 低 | 无状态应用 |
| 金丝雀发布 | 无 | 高 | 最低 | 大规模关键服务 |
| A/B 部署 | 无 | 高 | 低 | 需要灰度验证的功能 |
Git 部署
基于 Git Hook 的自动部署
bash
#!/bin/bash
# deploy/post-receive hook
# 位于服务器裸仓库的 hooks/ 目录
set -euo pipefail
DEPLOY_DIR="/var/www/app/current"
SHARED_DIR="/var/www/app/shared"
RELEASES_DIR="/var/www/app/releases"
REVISION=$(git rev-parse HEAD)
TIMESTAMP=$(date +%Y%m%d%H%M%S)
RELEASE_DIR="${RELEASES_DIR}/${TIMESTAMP}"
echo "=== Deploying revision ${REVISION} ==="
echo "=== Release directory: ${RELEASE_DIR} ==="
# 1. 创建新 release 目录
mkdir -p "$RELEASE_DIR"
# 2. 导出代码到 release 目录
git --work-tree="$RELEASE_DIR" --git-dir=/var/www/app/repo checkout -f HEAD
# 3. 创建共享链接
ln -sf "${SHARED_DIR}/.env" "${RELEASE_DIR}/.env"
ln -sf "${SHARED_DIR}/storage" "${RELEASE_DIR}/storage"
ln -sf "${SHARED_DIR}/public/uploads" "${RELEASE_DIR}/public/uploads"
# 4. 安装依赖
cd "$RELEASE_DIR"
composer install --no-dev --optimize-autoloader --no-interaction --no-progress
# 5. 运行迁移
php artisan migrate --force
# 6. 清理缓存
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# 7. 切换 symlink
ln -sfn "$RELEASE_DIR" "$DEPLOY_DIR"
# 8. 重启 PHP-FPM
sudo systemctl reload php-fpm
# 9. 清理旧 release(保留最近 5 个)
ls -1dt "${RELEASES_DIR}"/* | tail -n +6 | xargs rm -rf
echo "=== Deployment complete! ==="
echo "=== Revision: ${REVISION} ==="Capistrano 风格部署
bash
#!/bin/bash
# deploy/deploy.sh - Capistrano 风格的部署脚本
set -euo pipefail
APP_NAME="my-app"
DEPLOY_USER="deployer"
DEPLOY_SERVER="deploy@example.com"
DEPLOY_PATH="/var/www/${APP_NAME}"
KEEP_RELEASES=5
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
deploy() {
local BRANCH=${1:-main}
local TIMESTAMP=$(date +%Y%m%d%H%M%S)
local RELEASE_PATH="${DEPLOY_PATH}/releases/${TIMESTAMP}"
local SHARED_PATH="${DEPLOY_PATH}/shared"
local CURRENT_PATH="${DEPLOY_PATH}/current"
log_info "Starting deployment of branch '${BRANCH}'..."
# 创建 release 目录
ssh "${DEPLOY_SERVER}" "mkdir -p ${RELEASE_PATH}"
# 同步代码
log_info "Syncing code..."
rsync -avz --delete \
--exclude='.git' \
--exclude='node_modules' \
--exclude='vendor' \
--exclude='.env' \
--exclude='storage' \
./ "${DEPLOY_SERVER}:${RELEASE_PATH}/"
# 远程执行部署
ssh "${DEPLOY_SERVER}" bash << REMOTE_SCRIPT
set -euo pipefail
cd ${RELEASE_PATH}
# 创建共享链接
ln -sf ${SHARED_PATH}/.env .env
ln -sf ${SHARED_PATH}/storage storage
ln -sf ${SHARED_PATH}/public/uploads public/uploads
# 安装依赖
composer install --no-dev --optimize-autoloader --no-interaction --no-progress
# 运行迁移
php artisan migrate --force
# 缓存优化
php artisan config:cache
php artisan route:cache
php artisan view:cache
# 切换 symlink
ln -sfn ${RELEASE_PATH} ${CURRENT_PATH}
# 重启服务
sudo systemctl reload php-fpm
# 清理旧 release
cd ${DEPLOY_PATH}/releases
ls -1dt */ | tail -n +$((KEEP_RELEASES + 1)) | xargs rm -rf
REMOTE_SCRIPT
log_info "Deployment complete!"
}
rollback() {
local PREVIOUS_RELEASE=$(ssh "${DEPLOY_SERVER}" "ls -1dt ${DEPLOY_PATH}/releases/*/ | head -2 | tail -1")
if [ -z "$PREVIOUS_RELEASE" ]; then
log_error "No previous release to rollback to"
exit 1
fi
log_warn "Rolling back to ${PREVIOUS_RELEASE}..."
ssh "${DEPLOY_SERVER}" "ln -sfn ${PREVIOUS_RELEASE} ${DEPLOY_PATH}/current && sudo systemctl reload php-fpm"
log_info "Rollback complete!"
}
case "${1:-deploy}" in
deploy) deploy "${2:-main}" ;;
rollback) rollback ;;
*) echo "Usage: $0 {deploy|rollback} [branch]" ;;
esac蓝绿部署
蓝绿部署原理
text
┌─────────────┐
│ Load │
│ Balancer │
└──┬───────┬──┘
│ │
┌──────────┘ └──────────┐
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Blue (v1) │ │ Green (v2) │
│ ● Active │ │ ○ Standby │
│ Traffic: 100%│ │ Traffic: 0% │
└─────────────┘ └─────────────┘
↓ Deploy v2
┌─────────────┐ ┌─────────────┐
│ Blue (v1) │ │ Green (v2) │
│ ○ Standby │ │ ● Active │
│ Traffic: 0% │ │ Traffic: 100%│
└─────────────┘ └─────────────┘Nginx 蓝绿部署配置
nginx
# /etc/nginx/sites-available/app-blue.conf (Blue 环境)
upstream app_blue {
server unix:/var/run/php-fpm/app-blue.sock;
}
server {
listen 8080;
server_name _;
root /var/www/app/releases/blue/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app_blue;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
# /etc/nginx/sites-available/app-green.conf (Green 环境)
upstream app_green {
server unix:/var/run/php-fpm/app-green.sock;
}
server {
listen 8081;
server_name _;
root /var/www/app/releases/green/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app_green;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
# 主配置 - 通过切换 upstream 实现蓝绿切换
upstream app_main {
server 127.0.0.1:8080; # 指向 Blue 或 Green
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_main;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}蓝绿部署脚本
bash
#!/bin/bash
# deploy/blue-green.sh
set -euo pipefail
CURRENT_COLOR=$(cat /var/www/app/current_color 2>/dev/null || echo "blue")
NEW_COLOR=$([ "$CURRENT_COLOR" = "blue" ] && echo "green" || echo "blue")
echo "Current: ${CURRENT_COLOR} → New: ${NEW_COLOR}"
# 部署新版本到新环境
rsync -avz --delete ./ "${NEW_COLOR}_deploy:/var/www/app/releases/${NEW_COLOR}/"
# 在新环境中安装依赖和迁移
ssh "${NEW_COLOR}_deploy" << 'SCRIPT'
cd /var/www/app/releases/COLOR/
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
sudo systemctl reload php-fpm
SCRIPT
# 切换流量
sudo sed -i "s/127.0.0.1:8080/127.0.0.1:80$( [ "$NEW_COLOR" = "green" ] && echo 81 || echo 80)/" /etc/nginx/conf.d/app-switch.conf
sudo nginx -s reload
# 保存当前颜色
echo "$NEW_COLOR" > /var/www/app/current_color
echo "Switched to ${NEW_COLOR}"金丝雀发布
金丝雀发布原理
text
Phase 1: 1% 流量到新版本
Main (v1): 99% → Canary (v2): 1%
Phase 2: 10% 流量到新版本
Main (v1): 90% → Canary (v2): 10%
Phase 3: 50% 流量到新版本
Main (v1): 50% → Canary (v2): 50%
Phase 4: 100% 流量到新版本
Main (v1): 0% → Canary (v2): 100%Nginx 金丝雀配置
nginx
# 使用 split_clients 实现金丝雀
split_clients "${remote_addr}" $canary {
1% canary; # 1% 的用户路由到金丝雀
* main; # 其余路由到主线
}
upstream app_main {
server unix:/var/run/php-fpm/app-main.sock;
}
upstream app_canary {
server unix:/var/run/php-fpm/app-canary.sock;
}
server {
listen 80;
server_name example.com;
location / {
# 根据规则选择后端
if ($canary = "canary") {
proxy_pass http://app_canary;
break;
}
proxy_pass http://app_main;
}
location ~ \.php$ {
if ($canary = "canary") {
fastcgi_pass app_canary;
break;
}
fastcgi_pass app_main;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}零停机部署
零停机部署要点
bash
#!/bin/bash
# deploy/zero-downtime.sh
set -euo pipefail
# 1. 健康检查函数
health_check() {
local max_attempts=30
local attempt=1
while [ $attempt -le $max_attempts ]; do
if curl -sf http://localhost/health > /dev/null 2>&1; then
echo "Health check passed (attempt ${attempt})"
return 0
fi
echo "Health check failed (attempt ${attempt}/${max_attempts})"
sleep 2
attempt=$((attempt + 1))
done
echo "Health check failed after ${max_attempts} attempts"
return 1
}
# 2. 预部署检查
if ! health_check; then
echo "ERROR: Application is not healthy before deployment"
exit 1
fi
# 3. 部署到新目录
NEW_RELEASE="/var/www/app/releases/$(date +%Y%m%d%H%M%S)"
mkdir -p "$NEW_RELEASE"
git archive HEAD | tar -x -C "$NEW_RELEASE"
cd "$NEW_RELEASE"
# 4. 后台安装依赖
composer install --no-dev --optimize-autoloader --no-interaction --no-progress
# 5. 运行迁移(支持向后兼容的迁移)
php artisan migrate --force
# 6. 预热缓存
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# 7. 原子切换 symlink
ln -sfn "$NEW_RELEASE" /var/www/app/current
# 8. 优雅重启 PHP-FPM(不中断正在处理的请求)
kill -USR2 $(cat /var/run/php-fpm/php-fpm.pid)
# 9. 后置健康检查
sleep 3
if ! health_check; then
echo "ERROR: Application is not healthy after deployment, rolling back!"
ln -sfn "$PREVIOUS_RELEASE" /var/www/app/current
kill -USR2 $(cat /var/run/php-fpm/php-fpm.pid)
exit 1
fi
echo "Zero-downtime deployment complete!"回滚策略
快速回滚
bash
#!/bin/bash
# deploy/rollback.sh
set -euo pipefail
DEPLOY_PATH="/var/www/app"
CURRENT_RELEASE=$(readlink "${DEPLOY_PATH}/current")
PREVIOUS_RELEASE=$(ls -1dt ${DEPLOY_PATH}/releases/*/ | head -2 | tail -1)
echo "Current release: ${CURRENT_RELEASE}"
echo "Rolling back to: ${PREVIOUS_RELEASE}"
# 切换 symlink
ln -sfn "${PREVIOUS_RELEASE}" "${DEPLOY_PATH}/current"
# 如果需要回滚数据库
# php artisan migrate:rollback --step=1 --force
# 优雅重启
kill -USR2 $(cat /var/run/php-fpm/php-fpm.pid)
echo "Rollback complete!"自动回滚脚本
bash
#!/bin/bash
# deploy/auto-rollback.sh
# 部署后自动健康检查,失败则自动回滚
set -euo pipefail
DEPLOY_PATH="/var/www/app"
HEALTH_URL="http://localhost/health"
MAX_WAIT=60
WAIT_INTERVAL=3
check_health() {
local elapsed=0
while [ $elapsed -lt $MAX_WAIT ]; do
if curl -sf "${HEALTH_URL}" > /dev/null 2>&1; then
return 0
fi
sleep $WAIT_INTERVAL
elapsed=$((elapsed + WAIT_INTERVAL))
done
return 1
}
# 部署完成后执行健康检查
if ! check_health; then
echo "ERROR: Health check failed after ${MAX_WAIT}s, initiating rollback..."
# 获取前一个 release
PREVIOUS=$(ls -1dt ${DEPLOY_PATH}/releases/*/ | head -2 | tail -1)
if [ -n "$PREVIOUS" ]; then
ln -sfn "$PREVIOUS" "${DEPLOY_PATH}/current"
kill -USR2 $(cat /var/run/php-fpm/php-fpm.pid)
echo "Rollback to ${PREVIOUS} complete"
else
echo "No previous release available for rollback!"
fi
exit 1
fi
echo "Deployment verified successfully!"实战示例
完整的 CI/CD 部署脚本
bash
#!/bin/bash
# deploy/ci-deploy.sh
# 由 CI/CD pipeline 调用的部署脚本
set -euo pipefail
# 环境变量(由 CI 提供)
# DEPLOY_ENV, DEPLOY_SERVER, BRANCH, REVISION
DEPLOY_PATH="/var/www/${APP_NAME}"
RELEASE_DIR="${DEPLOY_PATH}/releases/${REVISION}"
SHARED_DIR="${DEPLOY_PATH}/shared"
# 日志
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; }
# 步骤 1: 准备环境
log "Creating release directory..."
mkdir -p "$RELEASE_DIR"
# 步骤 2: 同步代码
log "Syncing code..."
rsync -az --delete \
--exclude='.git' \
--exclude='node_modules' \
--exclude='vendor' \
./ "$RELEASE_DIR/"
# 步骤 3: 安装依赖
log "Installing dependencies..."
cd "$RELEASE_DIR"
composer install --no-dev --optimize-autoloader --classmap-authoritative --no-interaction
# 步骤 4: 运行迁移
log "Running migrations..."
php artisan migrate --force
# 步骤 5: 缓存优化
log "Optimizing caches..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# 步骤 6: 链接共享文件
log "Linking shared files..."
ln -sf "${SHARED_DIR}/.env" "${RELEASE_DIR}/.env"
ln -sf "${SHARED_DIR}/storage" "${RELEASE_DIR}/storage"
# 步骤 7: 切换版本
log "Switching to new release..."
ln -sfn "${RELEASE_DIR}" "${DEPLOY_PATH}/current"
# 步骤 8: 重启服务
log "Reloading PHP-FPM..."
sudo systemctl reload php-fpm
# 步骤 9: 清理旧版本
log "Cleaning old releases..."
ls -1dt "${DEPLOY_PATH}/releases/"*/ | tail -n +6 | xargs rm -rf
# 步骤 10: 健康检查
log "Running health check..."
if ! curl -sf "http://localhost/health" > /dev/null; then
log "ERROR: Health check failed! Rolling back..."
ln -sfn "${PREVIOUS_RELEASE}" "${DEPLOY_PATH}/current"
sudo systemctl reload php-fpm
exit 1
fi
log "Deployment successful! Revision: ${REVISION}"注意事项
部署安全
部署脚本应使用专用部署用户,不应使用 root。通过 SSH key 而非密码认证。限制部署服务器的访问权限。
最佳实践
- 自动化一切:减少手动操作,使用 CI/CD pipeline
- 原子切换:使用 symlink 实现版本切换
- 共享文件分离:.env、storage、uploads 等不随版本变化
- 保留多个版本:至少保留 5 个历史版本用于快速回滚
- 健康检查:部署后自动验证应用可用性
- 分阶段发布:先灰度验证,再全量发布
- 数据库向后兼容:迁移应该是增量的,支持新旧版本共存
下一节
继续学习:Docker 部署