绞杀者模式
大约 11 分钟约 3240 字
绞杀者模式
简介
绞杀者(Strangler Fig)是一种渐进式系统迁移模式。通过在旧系统外层逐步构建新系统,将功能一点一点从旧系统迁移到新系统,最终完全替代旧系统。
绞杀者模式的名字来源于热带雨林中的绞杀榕(Strangler Fig)—— 这种植物在宿主树周围生长,逐渐用根须缠绕宿主,最终完全替代宿主。在软件架构中,这个比喻非常形象地描述了渐进式系统替换的过程。
绞杀者模式由 Martin Fowler 提出,最初用于描述将单体应用逐步迁移为微服务架构的过程。它的核心理念是:不做"大爆炸"式的重写,而是像绞杀榕一样,在旧系统外围逐步生长新系统,最终"绞杀"旧系统。
特点
结构分析
迁移流程图
阶段 1: 全部走旧系统
+--------+ +--------+
| Client | -> | Legacy | (100%)
+--------+ +--------+
阶段 2: 部分走新系统
+--------+ +----------+
| Client | -> | Router | -> 新系统 (已迁移功能)
+--------+ +----------+ -> 旧系统 (未迁移功能)
阶段 3: 全部走新系统
+--------+ +--------+
| Client | -> | New | (100%)
+--------+ +--------+
旧系统下线绞杀者模式组件
+----------+ +----------------+ +-----------+
| API | --> | Strangler | --> | Legacy |
| Gateway | | Router/Facade | | System |
+----------+ +----------------+ +-----------+
| ^
v |
+-----------+ +-----------+
| New | | Data Sync |
| System | ------>| (双写) |
+-----------+ +-----------+实现
API 路由迁移
// 迁移路由表 — 控制请求流向新旧系统
public class MigrationRouter
{
private readonly HashSet<string> _migratedEndpoints = new();
private readonly Dictionary<string, string> _versionMap = new();
public MigrationRouter MapNew(string endpoint, string version = "v2")
{
_migratedEndpoints.Add(endpoint);
_versionMap[endpoint] = version;
return this;
}
public bool IsMigrated(string endpoint) => _migratedEndpoints.Contains(endpoint);
public string GetVersion(string endpoint) => _versionMap.GetValueOrDefault(endpoint, "v1");
}
// 迁移中间件
public class StranglerMiddleware
{
private readonly RequestDelegate _next;
private readonly MigrationRouter _router;
private readonly IServiceProvider _services;
public StranglerMiddleware(RequestDelegate next, MigrationRouter router, IServiceProvider services)
{
_next = next; _router = router; _services = services;
}
public async Task InvokeAsync(HttpContext context)
{
var endpoint = $"{context.Request.Method} {context.Request.Path}";
if (_router.IsMigrated(endpoint))
{
// 路由到新系统
context.Items["SystemVersion"] = _router.GetVersion(endpoint);
Console.WriteLine($"[新系统] {endpoint}");
}
else
{
// 路由到旧系统
context.Items["SystemVersion"] = "legacy";
Console.WriteLine($"[旧系统] {endpoint}");
}
await _next(context);
}
}
// 注册迁移
var router = new MigrationRouter();
router.MapNew("GET /api/users");
router.MapNew("POST /api/orders");
router.MapNew("GET /api/products");
app.UseMiddleware<StranglerMiddleware>(router);功能开关迁移
// 功能开关服务
public interface IFeatureFlagService
{
bool IsEnabled(string feature, string? userId = null);
}
public class FeatureFlagService : IFeatureFlagService
{
private readonly Dictionary<string, FeatureFlag> _flags = new();
public FeatureFlagService()
{
_flags["new_user_api"] = new() { Enabled = true, Percentage = 100 };
_flags["new_order_api"] = new() { Enabled = true, Percentage = 50 }; // 灰度 50%
_flags["new_search"] = new() { Enabled = false }; // 未上线
}
public bool IsEnabled(string feature, string? userId = null)
{
if (!_flags.TryGetValue(feature, out var flag)) return false;
if (!flag.Enabled) return false;
if (flag.Percentage >= 100) return true;
if (userId == null) return false;
return (userId.GetHashCode() % 100) < flag.Percentage; // 按用户灰度
}
}
public record FeatureFlag { public bool Enabled { get; init; } public int Percentage { get; init; } = 100; }
// API 控制器 — 双写双读
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly IFeatureFlagService _flags;
private readonly LegacyUserService _legacy;
private readonly NewUserService _new;
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(Guid id)
{
if (_flags.IsEnabled("new_user_api", User.Identity?.Name))
{
// 新系统
var user = await _new.GetUserAsync(id);
return Ok(user);
}
// 旧系统
var legacyUser = await _legacy.GetUserAsync(id);
return Ok(legacyUser);
}
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
if (_flags.IsEnabled("new_user_api"))
{
// 双写 — 同时写入新旧系统
var newResult = await _new.CreateUserAsync(request);
await _legacy.CreateUserAsync(request); // 后台同步到旧系统
return Created("", newResult);
}
return Ok(await _legacy.CreateUserAsync(request));
}
}数据迁移管道
public class DataMigrationPipeline
{
private readonly List<(string Name, Func<Task<int>> Migrate, Func<Task> Rollback)> _steps = new();
public DataMigrationPipeline AddStep(string name, Func<Task<int>> migrate, Func<Task> rollback)
{
_steps.Add((name, migrate, rollback));
return this;
}
public async Task ExecuteAsync()
{
var completed = new Stack<(string Name, Func<Task> Rollback)>();
foreach (var step in _steps)
{
try
{
Console.WriteLine($"迁移: {step.Name}");
var count = await step.Migrate();
completed.Push((step.Name, step.Rollback));
Console.WriteLine($"完成: {step.Name} ({count} 条记录)");
}
catch (Exception ex)
{
Console.WriteLine($"失败: {step.Name} - {ex.Message}");
// 回滚已完成步骤
while (completed.Count > 0)
{
var done = completed.Pop();
Console.WriteLine($"回滚: {done.Name}");
await done.Rollback();
}
throw;
}
}
}
}
// 使用 — 分步迁移用户数据
var pipeline = new DataMigrationPipeline()
.AddStep("迁移角色表", async () => { /* SQL 迁移 */ return await Task.FromResult(50); }, async () => { /* 回滚 */ })
.AddStep("迁移用户基本信息", async () => { return await Task.FromResult(10000); }, async () => { })
.AddStep("迁移用户地址", async () => { return await Task.FromResult(8000); }, async () => { })
.AddStep("迁移用户偏好", async () => { return await Task.FromResult(5000); }, async () => { });
await pipeline.ExecuteAsync();实战:迁移进度仪表盘
public class MigrationDashboard
{
private readonly Dictionary<string, MigrationStatus> _modules = new();
public void RegisterModule(string name)
{
_modules[name] = new MigrationStatus(name);
}
public void MarkMigrated(string name)
{
if (_modules.TryGetValue(name, out var status))
{
status.State = MigrationState.Completed;
status.CompletedAt = DateTime.UtcNow;
}
}
public void PrintReport()
{
var total = _modules.Count;
var completed = _modules.Values.Count(m => m.State == MigrationState.Completed);
var percentage = total > 0 ? (double)completed / total * 100 : 0;
Console.WriteLine($"迁移进度: {completed}/{total} ({percentage:F1}%)");
Console.WriteLine(new string('-', 40));
foreach (var module in _modules.Values)
{
var icon = module.State switch
{
MigrationState.NotStarted => " ",
MigrationState.InProgress => ">",
MigrationState.Completed => "*",
_ => "?"
};
Console.WriteLine($" [{icon}] {module.Name,-20} {module.State}");
}
}
}
public class MigrationStatus
{
public string Name { get; }
public MigrationState State { get; set; } = MigrationState.NotStarted;
public DateTime? CompletedAt { get; set; }
public MigrationStatus(string name) => Name = name;
}
public enum MigrationState { NotStarted, InProgress, Completed, RolledBack }最佳实践
- 按业务边界拆分:优先迁移独立的业务模块,而非技术层面。
- 双写保证数据一致性:写入新系统时同步写入旧系统,直到完全切换。
- 灰度发布:先对少量用户开放新系统,验证无误后逐步扩大范围。
- 数据同步验证:迁移后对比新旧系统数据,确保一致性。
- 保留旧系统回滚能力:在完全确认新系统稳定之前,保留旧系统的运行能力。
数据同步策略
双写一致性方案
// 双写管理器 — 保证新旧系统数据一致性
public class DualWriteManager
{
private readonly NewOrderService _newService;
private readonly LegacyOrderService _legacyService;
private readonly IReliabilityQueue _queue;
private readonly ILogger _logger;
public DualWriteManager(
NewOrderService newService,
LegacyOrderService legacyService,
IReliabilityQueue queue,
ILogger logger)
{
_newService = newService;
_legacyService = legacyService;
_queue = queue;
_logger = logger;
}
// 方案1:同步双写 — 强一致但性能差
public async Task<OrderResult> CreateOrderSync(OrderRequest request)
{
// 先写新系统
var newResult = await _newService.CreateAsync(request);
try
{
// 再写旧系统
await _legacyService.CreateAsync(request);
}
catch (Exception ex)
{
_logger.LogError(ex, "旧系统写入失败,加入补偿队列");
await _queue.EnqueueAsync(new CompensationTask(request, "legacy"));
}
return newResult;
}
// 方案2:异步双写 — 性能好但最终一致
public async Task<OrderResult> CreateOrderAsync(OrderRequest request)
{
var newResult = await _newService.CreateAsync(request);
// 异步写入旧系统
_ = Task.Run(async () =>
{
try { await _legacyService.CreateAsync(request); }
catch (Exception ex)
{
_logger.LogError(ex, "异步双写旧系统失败");
await _queue.EnqueueAsync(new CompensationTask(request, "legacy"));
}
});
return newResult;
}
}数据校验工具
// 迁移后数据一致性校验
public class DataConsistencyChecker
{
private readonly LegacyDb _legacyDb;
private readonly NewDb _newDb;
public async Task<ConsistencyReport> CompareUsers(int batchSize = 1000)
{
var report = new ConsistencyReport();
var legacyUsers = await _legacyDb.GetUsersAsync();
var newUsers = await _newDb.GetUsersAsync();
var legacyDict = legacyUsers.ToDictionary(u => u.Id);
var newDict = newUsers.ToDictionary(u => u.Id);
// 检查遗漏
foreach (var id in legacyDict.Keys.Except(newDict.Keys))
report.MissingInNew.Add(id);
foreach (var id in newDict.Keys.Except(legacyDict.Keys))
report.MissingInLegacy.Add(id);
// 检查数据不一致
foreach (var id in legacyDict.Keys.Intersect(newDict.Keys))
{
var legacy = legacyDict[id];
var current = newDict[id];
if (legacy.Name != current.Name)
report.Differences.Add($"用户{id}: Name 不一致 '{legacy.Name}' vs '{current.Name}'");
if (legacy.Email != current.Email)
report.Differences.Add($"用户{id}: Email 不一致 '{legacy.Email}' vs '{current.Email}'");
}
report.IsConsistent = report.MissingInNew.Count == 0
&& report.MissingInLegacy.Count == 0
&& report.Differences.Count == 0;
return report;
}
}
public class ConsistencyReport
{
public bool IsConsistent { get; set; }
public List<int> MissingInNew { get; } = new();
public List<int> MissingInLegacy { get; } = new();
public List<string> Differences { get; } = new();
public void Print()
{
Console.WriteLine($"数据一致性: {(IsConsistent ? "通过" : "不通过")}");
Console.WriteLine($"新系统缺失: {MissingInNew.Count} 条");
Console.WriteLine($"旧系统缺失: {MissingInLegacy.Count} 条");
Console.WriteLine($"数据差异: {Differences.Count} 条");
foreach (var diff in Differences.Take(10))
Console.WriteLine($" {diff}");
}
}灰度发布实现
按用户百分比的灰度
public class GradualRolloutStrategy
{
private readonly Dictionary<string, int> _rolloutRules = new();
// 配置灰度规则
public void Configure(string feature, int percentage)
{
_rolloutRules[feature] = Math.Clamp(percentage, 0, 100);
}
// 判断是否命中灰度
public bool ShouldRouteToNew(string feature, string userId)
{
if (!_rolloutRules.TryGetValue(feature, out var percentage))
return false;
if (percentage >= 100) return true;
if (percentage <= 0) return false;
// 基于用户ID的哈希值确保同一用户始终路由到同一系统
var hash = Math.Abs(userId.GetHashCode());
return (hash % 100) < percentage;
}
}
// 灰度发布控制器
[ApiController]
[Route("api/[controller]")]
public class MigrationController : ControllerBase
{
private readonly GradualRolloutStrategy _rollout;
private readonly NewOrderService _newService;
private readonly LegacyOrderService _legacyService;
[HttpGet("{orderId}")]
public async Task<IActionResult> GetOrder(string orderId)
{
var userId = User.Identity?.Name ?? "anonymous";
if (_rollout.ShouldRouteToNew("order_query", userId))
{
// 走新系统
var result = await _newService.GetOrderAsync(orderId);
return Ok(result);
}
// 走旧系统
var legacyResult = await _legacyService.GetOrderAsync(orderId);
return Ok(legacyResult);
}
}自动化回滚机制
public class MigrationGuard
{
private readonly ILogger _logger;
private readonly TimeSpan _evaluationWindow;
private Dictionary<string, List<double>> _errorRates = new();
public MigrationGuard(ILogger logger, TimeSpan? evaluationWindow = null)
{
_logger = logger;
_evaluationWindow = evaluationWindow ?? TimeSpan.FromMinutes(5);
}
// 记录请求结果
public void RecordRequest(string endpoint, bool success, double responseTimeMs)
{
if (!_errorRates.ContainsKey(endpoint))
_errorRates[endpoint] = new List<double>();
_errorRates[endpoint].Add(success ? 0 : 1);
// 只保留时间窗口内的数据
var cutoff = DateTime.UtcNow - _evaluationWindow;
// 简化:只保留最近1000条
if (_errorRates[endpoint].Count > 1000)
_errorRates[endpoint] = _errorRates[endpoint].TakeLast(500).ToList();
}
// 检查是否需要回滚
public bool ShouldRollback(string endpoint, double threshold = 0.05)
{
if (!_errorRates.TryGetValue(endpoint, out var rates) || rates.Count < 100)
return false;
var errorRate = rates.Average();
if (errorRate > threshold)
{
_logger.LogWarning(
"端点 {Endpoint} 错误率 {ErrorRate:P} 超过阈值 {Threshold:P},建议回滚",
endpoint, errorRate, threshold);
return true;
}
return false;
}
// 检查是否可以扩大灰度范围
public bool ShouldExpand(string endpoint, double threshold = 0.01)
{
if (!_errorRates.TryGetValue(endpoint, out var rates) || rates.Count < 1000)
return false;
var errorRate = rates.TakeLast(500).Average();
return errorRate < threshold;
}
}迁移实战检查清单
迁移前准备:
[ ] 完整的业务流程梳理(API 列表、数据流向、依赖关系)
[ ] 新旧系统并行运行环境搭建
[ ] 数据同步方案设计和验证
[ ] 灰度发布策略制定(百分比、用户分组、地域等)
[ ] 监控和告警配置(错误率、响应时间、数据一致性)
[ ] 回滚方案和演练
迁移中执行:
[ ] 按模块逐步迁移(优先独立模块)
[ ] 每次迁移后运行数据一致性校验
[ ] 灰度发布从 1% -> 5% -> 10% -> 50% -> 100%
[ ] 每个阶段观察足够时间(至少 24 小时)
[ ] 记录所有异常和问题
迁移后收尾:
[ ] 全量流量切换到新系统
[ ] 观察期(至少一周)确认稳定
[ ] 下线旧系统(保留数据备份)
[ ] 清理路由层和双写逻辑
[ ] 更新文档和架构图优点
缺点
总结
绞杀者模式通过渐进式迁移替代大爆炸重写。核心是引入路由层(中间件/API Gateway)控制请求流向新旧系统,配合功能开关实现灰度发布。数据迁移使用分步管道,每步可独立回滚。建议在大型单体应用向微服务迁移、技术栈升级等场景使用绞杀者模式。
绞杀者模式的本质价值在于:当你需要替换一个大型遗留系统时,不要试图一次性重写,而是逐步"生长"新系统,像绞杀榕一样慢慢替代旧系统。这种渐进式的方法虽然整体耗时更长,但风险更低、更可控。
关键知识点
- 模式不是目标,降低耦合和控制变化才是目标。
- 先找变化点、稳定点和协作边界,再决定是否引入模式。
- 同一个模式在不同规模下的收益和代价差异很大。
项目落地视角
- 优先画出参与对象、依赖方向和调用链,再落到代码。
- 把模式放到一个真实场景里,比如支付、规则引擎、工作流或插件扩展。
- 配合单元测试或契约测试,保证重构后的行为没有漂移。
常见误区
- 为了看起来"高级"而套模式。
- 把简单问题拆成过多抽象层,导致阅读和排障都变难。
- 只会背 UML,不会解释为什么这里需要这个模式。
进阶路线
- 继续关注模式之间的组合用法,而不是孤立记忆。
- 从业务建模、演进策略和团队协作角度看模式的适用性。
- 把模式结论沉淀为项目模板、基类或约束文档。
适用场景
- 当你准备把《绞杀者模式》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在业务规则频繁变化、分支增多或对象协作复杂时引入。
- 当你希望提高扩展性,但又不想把系统拆得过度抽象时,这类主题很有参考价值。
落地建议
- 先识别变化点,再决定是否引入模式,而不是反过来套模板。
- 优先为模式的边界、依赖和调用路径画出简单结构图。
- 把模式落到一个明确场景,例如支付、规则计算、插件扩展或工作流。
排错清单
- 检查抽象层是否过多,导致调用路径和责任不清晰。
- 确认引入模式后是否真的减少了条件分支和重复代码。
- 警惕"为了模式而模式",尤其是在简单业务里。
复盘问题
- 如果把《绞杀者模式》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《绞杀者模式》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《绞杀者模式》最大的收益和代价分别是什么?
