Appearance
生产环境迁移最佳实践
目录
生产环境 vs 开发环境
关键差异
| 特性 | 开发环境 | 生产环境 |
|---|---|---|
| 数据重要性 | 测试数据,可丢失 | 真实业务数据,不可丢失 |
| 停机容忍度 | 高 | 低(要求 99.9%+ 可用性) |
| 迁移方式 | database update | SQL 脚本审查后执行 |
| 回滚能力 | 简单重置 | 复杂,需完整计划 |
| 性能要求 | 低 | 高(影响用户体验) |
| 安全性 | 宽松 | 严格(权限、审计) |
| 团队协作 | 单人 | 多人协作,需协调 |
生产环境风险
⚠️ 主要风险:
1. 数据丢失
- 删除列/表导致数据永久丢失
- 数据类型变更失败
2. 服务中断
- 迁移执行时间长
- 锁表导致查询阻塞
3. 兼容性问题
- 应用程序版本与数据库不匹配
- 多个实例同时启动
4. 性能下降
- 新索引影响写入性能
- 统计信息过期
5. 回滚困难
- 没有备份无法恢复
- 回滚脚本未测试迁移策略选择
策略 1: SQL 脚本部署(推荐)
bash
# 步骤 1: 生成幂等脚本
dotnet ef migrations script --idempotent -o production.sql
# 步骤 2: 代码审查
git add production.sql
git commit -m "Add migration script for v2.1"
git push
# 步骤 3: DBA 审查
# - 检查是否有破坏性变更
# - 评估执行时间
# - 确认回滚方案
# 步骤 4: 在维护窗口执行
sqlcmd -S production-server -d MyDatabase -U admin -P password -i production.sql
# 步骤 5: 验证
sqlcmd -S production-server -d MyDatabase -Q "SELECT * FROM __EFMigrationsHistory ORDER BY AppliedOn DESC"优点:
- ✅ 完全可控
- ✅ DBA 可以审查和优化
- ✅ 可以在低峰期执行
- ✅ 有审计日志
- ✅ 可重复执行(幂等)
缺点:
- ❌ 需要手动执行
- ❌ 流程较长
策略 2: 应用启动时自动迁移
csharp
// Program.cs (.NET 8 Minimal API)
var app = builder.Build();
// ⚠️ 仅用于开发/测试环境
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
try
{
app.Logger.LogInformation("Applying migrations...");
await db.Database.MigrateAsync();
app.Logger.LogInformation("Migrations applied successfully");
}
catch (Exception ex)
{
app.Logger.LogError(ex, "Failed to apply migrations");
throw;
}
}
app.Run();生产环境改进版(带锁和重试):
csharp
public static class MigrationExtensions
{
public static async Task ApplyMigrationsWithLockAsync(this WebApplication app)
{
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var logger = app.Logger;
const string lockKey = "DbMigrationLock";
const int lockTimeout = 300; // 5分钟
try
{
// 获取分布式锁(防止多实例冲突)
var lockAcquired = await AcquireMigrationLockAsync(db, lockKey, lockTimeout);
if (!lockAcquired)
{
logger.LogWarning("Could not acquire migration lock. Another instance may be migrating.");
return;
}
logger.LogInformation("Migration lock acquired");
// 检查待应用迁移
var pendingMigrations = await db.Database.GetPendingMigrationsAsync();
var pendingList = pendingMigrations.ToList();
if (!pendingList.Any())
{
logger.LogInformation("Database is up to date. No migrations to apply.");
return;
}
logger.LogInformation("Found {Count} pending migrations: {Migrations}",
pendingList.Count, string.Join(", ", pendingList));
// 应用迁移
var stopwatch = Stopwatch.StartNew();
await db.Database.MigrateAsync();
stopwatch.Stop();
logger.LogInformation("Migrations applied successfully in {ElapsedMs}ms",
stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while applying migrations");
throw;
}
finally
{
// 释放锁
await ReleaseMigrationLockAsync(db, lockKey);
logger.LogInformation("Migration lock released");
}
}
private static async Task<bool> AcquireMigrationLockAsync(AppDbContext db, string lockKey, int timeout)
{
// SQL Server: 使用 sp_getapplock
var result = await db.Database.ExecuteSqlRawAsync(
"EXEC sp_getapplock @Resource = {0}, @LockMode = 'Exclusive', @LockTimeout = {1}",
lockKey, timeout * 1000);
return result >= 0;
}
private static async Task ReleaseMigrationLockAsync(AppDbContext db, string lockKey)
{
await db.Database.ExecuteSqlRawAsync(
"EXEC sp_releaseapplock @Resource = {0}",
lockKey);
}
}
// 使用
var app = builder.Build();
await app.ApplyMigrationsWithLockAsync();
app.Run();策略 3: CI/CD 管道集成
yaml
# Azure DevOps Pipeline
stages:
- stage: Build
jobs:
- job: BuildAndTest
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'build'
- task: DotNetCoreCLI@2
inputs:
command: 'test'
- stage: Generate_Migration_Script
dependsOn: Build
jobs:
- job: GenerateScript
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'custom'
custom: 'ef'
arguments: 'migrations script --idempotent -o $(Build.ArtifactStagingDirectory)/migration.sql'
- publish: $(Build.ArtifactStagingDirectory)/migration.sql
artifact: MigrationScript
- stage: Deploy_Database
dependsOn: Generate_Migration_Script
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: ExecuteMigration
environment: Production
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: MigrationScript
- task: SqlAzureDacpacDeployment@1
inputs:
azureSubscription: 'Production-SQL'
ServerName: '$(SqlServer)'
DatabaseName: '$(SqlDatabase)'
deployType: 'SqlTask'
SqlFile: '$(Pipeline.Workspace)/MigrationScript/migration.sql'
IpDetectionMethod: 'AutoDetect'
- stage: Deploy_Application
dependsOn: Deploy_Database
jobs:
- deployment: DeployWebApp
environment: Production
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'Production-WebApp'
appName: '$(WebAppName)'
package: '$(Pipeline.Workspace)/**/*.zip'零停机部署
蓝绿部署策略
阶段 1: 数据库向后兼容变更
┌─────────────────────────────────────┐
│ 1. 添加新列(允许 NULL) │
│ 2. 创建新索引 │
│ 3. 创建新表 │
│ 4. 不删除旧列/表 │
└─────────────────────────────────────┘
↓
阶段 2: 部署新版本应用
┌─────────────────────────────────────┐
│ 1. 部署 v2.0 应用(蓝环境) │
│ 2. 新旧版本同时运行 │
│ 3. 逐步切换流量到 v2.0 │
│ 4. 监控错误率和性能 │
└─────────────────────────────────────┘
↓
阶段 3: 清理(下一个版本)
┌─────────────────────────────────────┐
│ 1. 确认 v2.0 稳定运行 │
│ 2. 停止 v1.0 应用 │
│ 3. 在 v2.1 中删除旧列/表 │
└─────────────────────────────────────┘示例: 重命名列的零停机方案
csharp
// ===== 版本 1.0: 添加新列 =====
public partial class AddNewEmailColumn : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 添加新列(允许 NULL)
migrationBuilder.AddColumn<string>(
name: "EmailAddress",
table: "Customers",
type: "nvarchar(100)",
nullable: true);
}
}
// 应用程序 v1.0: 同时写入两列
public class CustomerService
{
public async Task UpdateEmailAsync(int customerId, string email)
{
var customer = await _context.Customers.FindAsync(customerId);
// 向后兼容: 同时写入旧列和新列
customer.Email = email; // 旧列
customer.EmailAddress = email; // 新列
await _context.SaveChangesAsync();
}
}
// ===== 版本 1.1: 数据迁移 =====
public partial class MigrateEmailData : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 将旧列数据复制到新列
migrationBuilder.Sql(@"
UPDATE Customers
SET EmailAddress = Email
WHERE EmailAddress IS NULL AND Email IS NOT NULL
");
}
}
// 应用程序 v1.1: 优先读取新列,降级到旧列
public class CustomerDto
{
public string Email
{
get => EmailAddress ?? LegacyEmail;
set
{
EmailAddress = value;
LegacyEmail = value;
}
}
private string EmailAddress { get; set; }
private string LegacyEmail { get; set; }
}
// ===== 版本 2.0: 删除旧列 =====
public partial class DropOldEmailColumn : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Email",
table: "Customers");
}
}
// 应用程序 v2.0: 只使用新列
public class Customer
{
public string EmailAddress { get; set; } // 只有新列
}并行部署脚本
bash
#!/bin/bash
# deploy.sh - 零停机部署脚本
set -e
echo "=== Starting zero-downtime deployment ==="
# 步骤 1: 应用数据库迁移
echo "Step 1: Applying database migrations..."
sqlcmd -S $DB_SERVER -d $DB_NAME -U $DB_USER -P $DB_PASS -i migration.sql
if [ $? -ne 0 ]; then
echo "❌ Database migration failed"
exit 1
fi
echo "✅ Database migration successful"
# 步骤 2: 部署新版本(蓝环境)
echo "Step 2: Deploying v2.0 to blue environment..."
az webapp deployment source config-zip \
--resource-group $RESOURCE_GROUP \
--name $WEB_APP_BLUE \
--src app-v2.0.zip
echo "✅ Blue environment deployed"
# 步骤 3: 健康检查
echo "Step 3: Running health checks..."
for i in {1..10}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://$WEB_APP_BLUE.azurewebsites.net/health)
if [ $STATUS -eq 200 ]; then
echo "✅ Health check passed"
break
fi
echo "Health check attempt $i failed (status: $STATUS)"
sleep 10
done
# 步骤 4: 切换流量
echo "Step 4: Switching traffic to blue environment..."
az webapp config hostname add \
--webapp-name $WEB_APP_BLUE \
--resource-group $RESOURCE_GROUP \
--hostname $PRODUCTION_HOSTNAME
echo "✅ Traffic switched to v2.0"
# 步骤 5: 监控
echo "Step 5: Monitoring for 10 minutes..."
sleep 600
ERROR_RATE=$(curl -s https://monitoring/api/error-rate?app=$WEB_APP_BLUE)
if (( $(echo "$ERROR_RATE > 5" | bc -l) )); then
echo "❌ Error rate too high ($ERROR_RATE%). Rolling back..."
# 执行回滚
rollback
else
echo "✅ Deployment successful! Error rate: $ERROR_RATE%"
fi回滚与灾难恢复
回滚策略
策略 1: 数据库回滚
bash
# 生成回滚脚本(从当前版本回滚到上一个版本)
dotnet ef migrations script CurrentVersion PreviousVersion -o rollback.sql
# 执行回滚
sqlcmd -S production-server -d MyDatabase -U admin -P password -i rollback.sql
# 验证
sqlcmd -S production-server -d MyDatabase -Q "SELECT TOP 10 * FROM __EFMigrationsHistory ORDER BY AppliedOn DESC"策略 2: 数据库还原
bash
# 步骤 1: 从备份还原
RESTORE DATABASE MyDatabase
FROM DISK = 'C:\Backups\MyDatabase_PreMigration.bak'
WITH REPLACE, RECOVERY;
# 步骤 2: 验证数据
SELECT COUNT(*) FROM Customers;
SELECT TOP 5 * FROM Orders ORDER BY OrderDate DESC;策略 3: 应用程序回滚
bash
# Azure App Service: 切换到旧版本
az webapp deployment source config-zip \
--resource-group $RESOURCE_GROUP \
--name $WEB_APP \
--src app-v1.9.zip # 回滚到 v1.9
# Kubernetes: 回滚部署
kubectl rollout undo deployment/myapp灾难恢复计划
markdown
# 灾难恢复 Runbook
## 场景 1: 迁移失败
**症状**: 迁移执行出错,数据库处于不一致状态
**恢复步骤**:
1. 立即停止应用程序
2. 从迁移前备份还原数据库
3. 分析错误日志
4. 修复迁移脚本
5. 重新执行迁移
**预计恢复时间**: 15-30 分钟
---
## 场景 2: 迁移后性能严重下降
**症状**: 查询响应时间增加 10x+,CPU/IO 飙升
**恢复步骤**:
1. 识别问题查询(使用 Query Store)
2. 更新统计信息: `UPDATE STATISTICS TableName WITH FULLSCAN`
3. 重建索引: `ALTER INDEX ALL ON TableName REBUILD`
4. 如果仍无改善,回滚迁移
**预计恢复时间**: 30-60 分钟
---
## 场景 3: 数据损坏
**症状**: 数据不一致,外键约束失败
**恢复步骤**:
1. 立即停止所有写入操作
2. 评估损坏范围
3. 从备份还原
4. 手动修复受损数据
5. 重新执行迁移
**预计恢复时间**: 1-4 小时监控与告警
关键指标监控
csharp
// 迁移健康检查端点
app.MapGet("/health/migrations", async (AppDbContext db) =>
{
var pendingMigrations = await db.Database.GetPendingMigrationsAsync();
var appliedMigrations = await db.Database.GetAppliedMigrationsAsync();
var lastMigration = appliedMigrations.LastOrDefault();
return Results.Ok(new
{
Status = pendingMigrations.Any() ? "Warning" : "Healthy",
PendingMigrations = pendingMigrations.Count(),
LastAppliedMigration = lastMigration,
TotalMigrations = appliedMigrations.Count()
});
});
// Prometheus 指标
public class MigrationMetrics
{
private static readonly Counter MigrationCounter = Metrics
.CreateCounter("ef_migrations_applied_total", "Total migrations applied");
private static readonly Histogram MigrationDuration = Metrics
.CreateHistogram("ef_migration_duration_seconds", "Migration execution duration");
public static async Task TrackMigrationAsync(Func<Task> migrationAction)
{
using var timer = MigrationDuration.NewTimer();
await migrationAction();
MigrationCounter.Inc();
}
}告警规则
yaml
# Prometheus AlertManager 配置
groups:
- name: ef_core_alerts
rules:
- alert: MigrationFailure
expr: increase(ef_migrations_failed_total[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "EF Core migration failed"
description: "Migration failed on {{ $labels.instance }}"
- alert: SlowMigration
expr: histogram_quantile(0.95, ef_migration_duration_seconds_bucket) > 300
for: 5m
labels:
severity: warning
annotations:
summary: "Migration is taking too long"
description: "95th percentile migration time is {{ $value }}s"
- alert: PendingMigrations
expr: ef_pending_migrations_count > 0
for: 10m
labels:
severity: info
annotations:
summary: "Pending migrations detected"
description: "{{ $value }} migrations waiting to be applied".NET 8/9/10 新特性
.NET 8: 改进的迁移诊断
csharp
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString)
.EnableDetailedErrors()
.LogTo((eventId, logLevel) =>
{
if (eventId.Id == RelationalEventId.MigrationApplying.Id)
return LogLevel.Information;
return LogLevel.None;
}, Console.WriteLine);
});
// 输出:
// info: Applying migration '20240115083000_AddProductDescription'.
// info: Migration '20240115083000_AddProductDescription' applied successfully in 125ms..NET 9: 迁移性能优化
csharp
// .NET 9 优化了大型迁移的应用速度
// 对于包含 100+ 实体的项目,迁移应用速度提升 40-60%
await db.Database.MigrateAsync(); // 更快!.NET 10: 智能迁移(路线图)
预计特性:
- 自动检测破坏性变更
- 迁移影响分析
- 自动化回滚脚本生成
- 迁移依赖关系可视化
完整部署流程
标准操作流程(SOP)
markdown
# EF Core 迁移生产部署 SOP
## 准备阶段(T-7天)
- [ ] 开发团队完成迁移开发和测试
- [ ] 生成幂等 SQL 脚本
- [ ] 提交脚本到 Git
- [ ] 通知 DBA 团队审查
## 审查阶段(T-3天)
- [ ] DBA 审查 SQL 脚本
- [ ] 评估执行时间和影响
- [ ] 确认回滚方案
- [ ] 安排维护窗口
## 预演阶段(T-1天)
- [ ] 在 Staging 环境执行迁移
- [ ] 验证功能和性能
- [ ] 执行回滚演练
- [ ] 确认监控告警正常
## 执行阶段(T日)
- [ ] T-1小时: 通知相关团队
- [ ] T-30分钟: 备份数据库
- [ ] T-15分钟: 进入维护模式
- [ ] T-0: 执行迁移脚本
- [ ] T+5分钟: 验证迁移成功
- [ ] T+10分钟: 部署应用程序
- [ ] T+15分钟: 退出维护模式
- [ ] T+30分钟: 监控系统稳定性
## 观察阶段(T+1天)
- [ ] 监控错误率和性能
- [ ] 收集用户反馈
- [ ] 如有问题,执行回滚
## 收尾阶段(T+7天)
- [ ] 确认系统稳定
- [ ] 归档迁移脚本
- [ ] 更新文档
- [ ] 复盘会议检查清单
markdown
## 迁移前检查清单
- [ ] 迁移已在开发/测试环境验证
- [ ] SQL 脚本已生成并审查
- [ ] 数据库已备份
- [ ] 回滚脚本已准备并测试
- [ ] 维护窗口已安排
- [ ] 相关团队已通知
- [ ] 监控告警已配置
- [ ] On-call 人员已就位
## 迁移后检查清单
- [ ] 迁移历史表已更新
- [ ] 关键表行数验证
- [ ] 外键约束验证
- [ ] 索引存在且有效
- [ ] 存储过程/视图正常
- [ ] 应用程序连接正常
- [ ] 关键业务流程测试通过
- [ ] 性能指标正常
- [ ] 错误率正常
- [ ] 用户反馈正常总结
核心要点
- 生产环境: 始终使用 SQL 脚本,不要直接
database update - 零停机: 采用蓝绿部署,向后兼容变更
- 回滚计划: 始终准备回滚脚本并测试
- 监控告警: 实时监控迁移状态和性能
- 流程规范: 遵循标准操作流程(SOP)
最佳实践对比
| 实践 | 推荐 | 不推荐 |
|---|---|---|
| 部署方式 | SQL 脚本审查 | 自动 Migrate() |
| 执行时机 | 维护窗口 | 高峰期 |
| 回滚策略 | 预先测试的回滚脚本 | 临时决定 |
| 监控 | 实时指标+告警 | 事后检查 |
| 备份 | 迁移前完整备份 | 无备份 |
决策流程
需要部署迁移?
├─ 开发环境 → database update
├─ 测试环境 → SQL 脚本(快速审查)
└─ 生产环境 → 完整流程
├─ 生成幂等脚本
├─ DBA 审查
├─ Staging 预演
├─ 备份数据库
├─ 维护窗口执行
├─ 监控验证
└─ 准备回滚