Skip to content

生产环境迁移最佳实践 ​

目录 ​


生产环境 vs 开发环境 ​

关键差异 ​

特性开发环境生产环境
数据重要性测试数据,可丢失真实业务数据,不可丢失
停机容忍度高低(要求 99.9%+ 可用性)
迁移方式database updateSQL 脚本审查后执行
回滚能力简单重置复杂,需完整计划
性能要求低高(影响用户体验)
安全性宽松严格(权限、审计)
团队协作单人多人协作,需协调

生产环境风险 ​

⚠️ 主要风险:

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 人员已就位

## 迁移后检查清单

- [ ] 迁移历史表已更新
- [ ] 关键表行数验证
- [ ] 外键约束验证
- [ ] 索引存在且有效
- [ ] 存储过程/视图正常
- [ ] 应用程序连接正常
- [ ] 关键业务流程测试通过
- [ ] 性能指标正常
- [ ] 错误率正常
- [ ] 用户反馈正常

总结 ​

核心要点 ​

  1. 生产环境: 始终使用 SQL 脚本,不要直接 database update
  2. 零停机: 采用蓝绿部署,向后兼容变更
  3. 回滚计划: 始终准备回滚脚本并测试
  4. 监控告警: 实时监控迁移状态和性能
  5. 流程规范: 遵循标准操作流程(SOP)

最佳实践对比 ​

实践推荐不推荐
部署方式SQL 脚本审查自动 Migrate()
执行时机维护窗口高峰期
回滚策略预先测试的回滚脚本临时决定
监控实时指标+告警事后检查
备份迁移前完整备份无备份

决策流程 ​

需要部署迁移?
├─ 开发环境 → database update
├─ 测试环境 → SQL 脚本(快速审查)
└─ 生产环境 → 完整流程
    ├─ 生成幂等脚本
    ├─ DBA 审查
    ├─ Staging 预演
    ├─ 备份数据库
    ├─ 维护窗口执行
    ├─ 监控验证
    └─ 准备回滚

下一步 ​

基于 MIT 许可发布