fengling-gateway/.planning/phases/006-gateway-plugin-research/006-RESEARCH.md
movingsam 52eba07097 feat: add MigrationTool for gateway config migration (IMPL-7)
- Create MigrationTool console app for exporting DB config to K8s YAML
- Support dry-run mode and validation
- Add Npgsql and YamlDotNet dependencies
2026-03-08 00:35:04 +08:00

6.6 KiB
Raw Blame History

阶段 6 研究:网关插件技术调研与实现

研究日期: 2026-03-04 状态: 已完成


1. 现有基础设施分析

1.1 已有插件抽象层

项目已创建 Fengling.Gateway.Plugin.Abstractions 程序集,定义了核心插件接口:

// 已定义的接口
public interface IGatewayPlugin
{
    string Name { get; }
    string Version { get; }
    string? Description { get; }
    Task OnLoadAsync();
    Task OnUnloadAsync();
}

public interface IRequestPlugin : IGatewayPlugin { ... }
public interface IResponsePlugin : IGatewayPlugin { ... }
public interface IRouteTransformPlugin : IGatewayPlugin { ... }
public interface ILoadBalancePlugin : IGatewayPlugin { ... }

1.2 缺失的组件

  • 插件加载器 - 动态加载程序集的机制
  • 插件生命周期管理 - 加载/卸载/热重载
  • 插件隔离 - AssemblyLoadContext 隔离
  • YARP 集成 - 将插件集成到 YARP 管道

2. YARP 扩展点

2.1 中间件管道

YARP 使用 ASP.NET Core 中间件管道,允许自定义注入点:

app.MapReverseProxy(proxyPipeline =>
{
    proxyPipeline.Use(async (context, next) =>
    {
        // 插件前置执行
        await pluginFeature.ExecutePreProxyAsync(context);
        await next();
        // 插件后置执行
        await pluginFeature.ExecutePostProxyAsync(context);
    });
});

2.2 Transform 管道(推荐)

Transform 是修改请求/响应的推荐方式:

builder.Services.AddReverseProxy()
    .AddTransforms(context =>
    {
        var plugins = PluginManager.GetTransformPlugins(context.Route);
        foreach (var plugin in plugins)
        {
            context.AddRequestTransform(plugin.ApplyAsync);
        }
    });

2.3 路由扩展

YARP 2.0+ 支持自定义路由元数据,插件可消费:

{
  "Routes": {
    "api-route": {
      "Extensions": {
        "PluginConfig": {
          "PluginId": "rate-limiter",
          "MaxRequests": 100
        }
      }
    }
  }
}

3. .NET 插件加载最佳实践

3.1 AssemblyLoadContext 隔离

使用 可卸载的 AssemblyLoadContext 配合 AssemblyDependencyResolver

public sealed class PluginLoadContext : AssemblyLoadContext
{
    private readonly AssemblyDependencyResolver _resolver;
    private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions";

    public PluginLoadContext(string pluginPath) : base(isCollectible: true)
    {
        _resolver = new AssemblyDependencyResolver(pluginPath);
    }

    protected override Assembly? Load(AssemblyName assemblyName)
    {
        // 共享契约程序集(防止类型身份问题)
        if (assemblyName.Name == _sharedAssemblyName)
            return null; // 回退到默认 ALC

        var path = _resolver.ResolveAssemblyToPath(assemblyName);
        return path != null ? LoadFromAssemblyPath(path) : null;
    }
}

3.2 影子复制(热重载必需)

Windows 锁定加载的 DLL需要影子复制

public static string CreateShadowCopy(string pluginDirectory)
{
    var shadowDir = Path.Combine(
        Path.GetTempPath(), 
        "PluginShadows", 
        $"{Path.GetFileName(pluginDirectory)}_{Guid.NewGuid():N}"
    );
    CopyDirectory(pluginDirectory, shadowDir);
    return shadowDir;
}

3.3 插件句柄模式

public sealed class PluginHandle : IAsyncDisposable
{
    private readonly PluginLoadContext _alc;
    private readonly IGatewayPlugin _plugin;

    public async ValueTask DisposeAsync()
    {
        if (_plugin is IAsyncDisposable ad) await ad.DisposeAsync();
        _alc.Unload(); // 计划回收
    }
}

3.4 卸载验证

public static bool VerifyUnload(WeakReference weakRef, int maxAttempts = 10)
{
    for (var i = 0; i < maxAttempts && weakRef.IsAlive; i++)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
    return !weakRef.IsAlive;
}

4. 插件隔离模式

4.1 契约边界(最重要的设计决策)

规则:契约必须驻留在 默认 ALC,绝不在插件 ALC 中。

插件项目配置:

<ProjectReference Include="..\Plugin.Abstractions\Plugin.Abstractions.csproj">
  <Private>false</Private>
  <ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>

4.2 静态缓存问题

问题:主机单例缓存插件类型会阻止卸载。

解决方案:只使用 DTO 跨越边界,不传递插件类型。


5. 实现架构

5.1 组件结构

src/
├── Fengling.Gateway.Plugin.Abstractions/  # 已存在
│   └── IGatewayPlugin.cs
├── yarpgateway/
│   ├── Plugins/
│   │   ├── PluginLoadContext.cs      # ALC 隔离
│   │   ├── PluginLoader.cs           # 加载逻辑
│   │   ├── PluginHost.cs             # 生命周期管理
│   │   └── PluginMiddleware.cs       # YARP 集成
│   └── Program.cs
└── plugins/                          # 插件目录
    └── sample-plugin/
        └── SamplePlugin.csproj

5.2 插件目录结构

plugins/
├── rate-limiter/
│   ├── RateLimiterPlugin.dll
│   ├── RateLimiterPlugin.deps.json
│   └── plugin.json              # 元数据
└── jwt-transform/
    └── ...

5.3 插件元数据 (plugin.json)

{
  "id": "rate-limiter",
  "name": "Rate Limiter Plugin",
  "version": "1.0.0",
  "entryPoint": "RateLimiterPlugin.RateLimiterPlugin",
  "interfaces": ["IRequestPlugin"],
  "dependencies": []
}

6. 验证架构

6.1 测试策略

  1. 单元测试PluginLoadContext 隔离验证
  2. 集成测试:插件加载/卸载/热重载
  3. 性能测试:插件执行开销

6.2 成功标准验证

标准 验证方法
动态加载插件 单元测试:从目录加载并执行
插件相互隔离 单元测试:异常不传播到其他插件
热加载/卸载 集成测试WeakReference 验证卸载

7. 推荐库

用途
网关核心 Yarp.ReverseProxy 2.3.0+
插件加载 自定义 AssemblyLoadContext
元数据读取 System.Reflection.Metadata
依赖注入 Microsoft.Extensions.DependencyInjection

8. 关键注意事项

  1. 不要在主机单例中缓存插件类型
  2. 始终用 WeakReference 测试卸载
  3. Windows 上必须影子复制以支持热重载
  4. 使用仅元数据发现避免仅扫描而加载程序集
  5. 谨慎处理原生依赖(它们不能干净卸载)

研究完成2026-03-04