在处理多个微服务或后端API时,一个统一的入口点——API网关——是必要的。它负责处理认证、日志、限流、请求转换等横切关注点。然而,构建这样一个网关通常面临两个极端选择:要么采用功能全面但配置复杂且成本高昂的商业产品(如Azure API Management),要么从零开始构建一个单体的、难以维护的代理服务。当业务需求快速变化,需要频繁增删或修改网关逻辑时,这两种方案都显得僵化。
我们需要一种更灵活的架构:一个轻量级的、可插拔的网关。它的核心应该是一个稳定的管道,而具体的业务逻辑(如认证方式、路由规则)则作为“插件”动态地组合起来。这种架构不仅易于扩展,还能让不同团队独立开发和部署自己的网关插件,而无需触动核心框架。
方案权衡:走向插件化的必然性
在项目初期,我们曾评估过几种实现方式。
方案A:单一的巨无霸Azure Function
这是最直接的思路。创建一个HttpTrigger Function,内部用大量的if-else
或switch
语句来处理不同的路由和逻辑。
- 优势: 开发启动快,初期逻辑简单时非常直观。
- 劣势: 随着逻辑增多,这个Function会迅速膨胀成一个难以维护的怪兽。违反了单一职责原则,任何微小的改动(比如调整一个API的日志级别)都需要重新部署整个Function,测试回归的范围巨大,风险极高。这种设计也使得并行开发变得异常困难。
方案B:功能完备的Azure API Management (APIM)
APIM是Azure提供的托管API网关服务,功能强大,通过策略(Policy)XML来定义处理逻辑。
- 优势: 功能全面,从缓存、认证到限流一应俱全,稳定性有保障。
- 劣势: 对于某些场景来说,它过于“重”。策略是用XML定义的,复杂的逻辑表达起来非常繁琐,而且缺乏编程语言的灵活性。成本是另一个重要考量,对于中小型项目或内部工具,APIM的成本可能过高。最关键的是,它的定制化能力有限,当我们需要实现非常规的认证协议或复杂的动态路由逻辑时,会感到束缚。
最终选择:基于设计模式的插件化Azure Function网关
我们决定走一条中间路线:利用Azure Functions的Serverless特性,结合经典的设计模式,构建一个我们自己的、轻量级且高度可扩展的网关。这个网关本身是一个HttpTrigger Function,但它内部不包含任何硬编码的业务逻辑。它的唯一职责是加载并执行一个由多个“插件”组成的管道。
这种架构的核心是责任链模式 (Chain of Responsibility Pattern) 和 **策略模式 (Strategy Pattern)**。
- 责任链模式 用于构建处理管道。每个插件都是链条上的一个节点,请求会依次通过这些节点,每个节点可以选择处理请求、修改请求、或者直接中断链条并返回响应。
- 策略模式 用于处理动态决策,尤其是在路由阶段。网关可以根据请求头、路径或其他参数,动态选择一个路由策略来决定将请求转发到哪个后端服务。
这种设计使得每个功能(认证、日志、限流、路由)都是一个独立的、可单独测试和部署的类库。网关核心保持稳定,业务逻辑通过插件的组合来动态实现。
核心实现:构建插件化处理管道
我们的目标是创建一个请求处理管道,每个插件都是管道的一部分。
1. 定义插件契约
首先,我们需要一个所有插件都必须遵守的接口。这个接口定义了插件的核心行为。
// IPluginContext.cs
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
namespace PluggableGateway.Interfaces
{
/// <summary>
/// 插件执行上下文,在整个请求管道中传递。
/// 它携带了原始请求、响应构建器以及一个属性包用于插件间通信。
/// </summary>
public interface IPluginContext
{
HttpRequest HttpRequest { get; }
HttpResponse HttpResponse { get; }
/// <summary>
/// 一个键值对集合,用于在插件之间传递自定义数据。
/// 例如,认证插件可以将用户信息放入其中,供后续插件使用。
/// </summary>
IDictionary<string, object> Properties { get; }
/// <summary>
/// 目标后端服务的URI,由路由插件设置。
/// </summary>
Uri TargetUri { get; set; }
/// <summary>
/// 标记请求是否应被提前终止。
/// 如果一个插件将此设置为true,管道将停止执行并立即返回。
/// </summary>
bool IsTerminated { get; set; }
}
}
// IPlugin.cs
using System.Threading.Tasks;
namespace PluggableGateway.Interfaces
{
/// <summary>
/// 所有网关插件必须实现的接口。
/// </summary>
public interface IPlugin
{
/// <summary>
/// 插件的执行顺序。值越小,越先执行。
/// </summary>
int Order { get; }
/// <summary>
/// 插件的唯一名称。
/// </summary>
string Name { get; }
/// <summary>
/// 执行插件的核心逻辑。
/// </summary>
/// <param name="context">请求上下文。</param>
/// <param name="next">指向管道中下一个插件的委托。</param>
Task ExecuteAsync(IPluginContext context, Func<IPluginContext, Task> next);
}
}
IPluginContext
是贯穿整个处理管道的状态载体。IPlugin
接口定义了插件的基本结构,其中 ExecuteAsync
方法的设计是责任链模式的关键。它接收下一个插件的委托 next
,插件可以在执行完自己的逻辑后,通过调用 await next(context)
将控制权交给下一个插件。
2. 实现管道构建器
我们需要一个机制来发现所有已注册的插件,并根据它们的Order
属性将它们组装成一个可执行的管道。
// PipelineBuilder.cs
using PluggableGateway.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace PluggableGateway.Core
{
public class PipelineBuilder
{
private readonly IEnumerable<IPlugin> _plugins;
// 通过依赖注入获取所有实现了IPlugin接口的服务
public PipelineBuilder(IEnumerable<IPlugin> plugins)
{
// 按Order属性升序排序,确保执行顺序
_plugins = plugins.OrderBy(p => p.Order).ToList();
}
public Func<IPluginContext, Task> Build()
{
// 管道的末端是一个什么都不做的委托
Func<IPluginContext, Task> pipeline = context => Task.CompletedTask;
// 从后往前构建委托链。这是责任链模式的一种经典实现方式。
// 最后一个插件的 next 指向 Task.CompletedTask。
// 倒数第二个插件的 next 指向最后一个插件的执行委托。
// 以此类推,直到第一个插件。
foreach (var plugin in _plugins.Reverse())
{
var nextPluginInChain = pipeline;
pipeline = context => plugin.ExecuteAsync(context, nextPluginInChain);
}
return pipeline;
}
}
}
PipelineBuilder
的 Build
方法是整个设计的核心。它通过一个巧妙的foreach
循环,将所有插件的ExecuteAsync
方法链接成一个巨大的委托链。调用最终返回的Func<IPluginContext, Task>
时,请求就会按照预设的顺序流经所有插件。
3. 编写具体插件
现在我们可以开始编写具体的插件了。
认证插件 (Authentication Plugin)
这个插件检查请求头中是否包含有效的API Key。
// AuthenticationPlugin.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
namespace PluggableGateway.Plugins
{
public class AuthenticationPlugin : IPlugin
{
private readonly ILogger<AuthenticationPlugin> _logger;
private readonly string _expectedApiKey;
public int Order => 100;
public string Name => "AuthenticationPlugin";
public AuthenticationPlugin(ILogger<AuthenticationPlugin> logger, IConfiguration configuration)
{
_logger = logger;
// 从配置中读取API Key,而不是硬编码
_expectedApiKey = configuration["ExpectedApiKey"];
if (string.IsNullOrEmpty(_expectedApiKey))
{
throw new InvalidOperationException("ExpectedApiKey is not configured.");
}
}
public async Task ExecuteAsync(IPluginContext context, Func<IPluginContext, Task> next)
{
_logger.LogInformation("Executing {PluginName}...", Name);
if (!context.HttpRequest.Headers.TryGetValue("X-Api-Key", out var apiKey) || apiKey != _expectedApiKey)
{
_logger.LogWarning("Authentication failed. Invalid or missing API Key.");
context.HttpResponse.StatusCode = StatusCodes.Status401Unauthorized;
// 将终止标记设为true,管道将在此处停止
context.IsTerminated = true;
await context.HttpResponse.WriteAsync("Unauthorized");
return;
}
_logger.LogInformation("Authentication successful.");
// 认证通过,将控制权交给下一个插件
await next(context);
}
}
}
这个插件演示了如何中断管道。如果认证失败,它会设置IsTerminated
为true
并直接返回,next(context)
不会被调用。
动态路由插件 (Dynamic Routing Plugin)
这是策略模式的应用场景。我们根据请求的某个特性(例如路径)来选择不同的路由策略。
首先,定义策略接口和具体策略。
// IRoutingStrategy.cs
namespace PluggableGateway.Interfaces
{
public interface IRoutingStrategy
{
string Name { get; }
Uri GetTargetUri(IPluginContext context);
}
}
// ServiceARoutingStrategy.cs
using Microsoft.Extensions.Configuration;
namespace PluggableGateway.Routing
{
public class ServiceARoutingStrategy : IRoutingStrategy
{
private readonly Uri _backendUri;
public string Name => "ServiceA";
public ServiceARoutingStrategy(IConfiguration configuration)
{
// 路由目标也从配置中读取
_backendUri = new Uri(configuration["BackendServices:ServiceA"]);
}
public Uri GetTargetUri(IPluginContext context)
{
// 简单的直接路由
return _backendUri;
}
}
}
// ServiceBRoutingStrategy.cs
namespace PluggableGateway.Routing
{
public class ServiceBRoutingStrategy : IRoutingStrategy
{
private readonly Uri _backendUri;
public string Name => "ServiceB";
public ServiceBRoutingStrategy(IConfiguration configuration)
{
_backendUri = new Uri(configuration["BackendServices:ServiceB"]);
}
public Uri GetTargetUri(IPluginContext context)
{
// 可以在这里实现更复杂的逻辑,例如从路径中提取参数
var path = context.HttpRequest.Path.Value;
return new Uri(_backendUri, path);
}
}
}
然后,路由插件本身负责选择并执行策略。
// DynamicRoutingPlugin.cs
using System.Collections.Generic;
using System.Linq;
namespace PluggableGateway.Plugins
{
public class DynamicRoutingPlugin : IPlugin
{
private readonly ILogger<DynamicRoutingPlugin> _logger;
private readonly Dictionary<string, IRoutingStrategy> _strategies;
public int Order => 200; // 在认证之后执行
public string Name => "DynamicRoutingPlugin";
public DynamicRoutingPlugin(ILogger<DynamicRoutingPlugin> logger, IEnumerable<IRoutingStrategy> strategies)
{
_logger = logger;
// 通过依赖注入获取所有路由策略,并存入字典以便快速查找
_strategies = strategies.ToDictionary(s => s.Name, s => s, StringComparer.OrdinalIgnoreCase);
}
public async Task ExecuteAsync(IPluginContext context, Func<IPluginContext, Task> next)
{
_logger.LogInformation("Executing {PluginName}...", Name);
// 示例:根据请求头 'X-Route-To' 选择策略
if (!context.HttpRequest.Headers.TryGetValue("X-Route-To", out var routeTo) ||
!_strategies.TryGetValue(routeTo, out var strategy))
{
_logger.LogWarning("Routing failed. No matching strategy for header value: {RouteTo}", routeTo);
context.HttpResponse.StatusCode = StatusCodes.Status400BadRequest;
context.IsTerminated = true;
await context.HttpResponse.WriteAsync("Invalid route specified.");
return;
}
var targetUri = strategy.GetTargetUri(context);
if (targetUri == null)
{
_logger.LogError("Strategy {StrategyName} returned a null URI.", strategy.Name);
context.HttpResponse.StatusCode = StatusCodes.Status500InternalServerError;
context.IsTerminated = true;
await context.HttpResponse.WriteAsync("Routing logic failed.");
return;
}
// 将计算出的目标URI存入上下文,供后续插件(如代理插件)使用
context.TargetUri = targetUri;
_logger.LogInformation("Request routed to {TargetUri} by strategy {StrategyName}", targetUri, strategy.Name);
await next(context);
}
}
}
代理插件 (Proxy Plugin)
这是管道的最后一环,它负责将请求真正地转发到后端服务。
// ProxyPlugin.cs
using System.Net.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace PluggableGateway.Plugins
{
public class ProxyPlugin : IPlugin
{
private readonly ILogger<ProxyPlugin> _logger;
private readonly IHttpClientFactory _httpClientFactory;
public int Order => 1000; // 通常是最后一个业务插件
public string Name => "ProxyPlugin";
public ProxyPlugin(ILogger<ProxyPlugin> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
}
public async Task ExecuteAsync(IPluginContext context, Func<IPluginContext, Task> next)
{
if (context.TargetUri == null)
{
_logger.LogError("ProxyPlugin cannot execute because TargetUri is not set in the context.");
context.HttpResponse.StatusCode = StatusCodes.Status500InternalServerError;
context.IsTerminated = true; // 终止管道
await context.HttpResponse.WriteAsync("Gateway configuration error: Target URI not found.");
return;
}
_logger.LogInformation("Proxying request to {TargetUri}", context.TargetUri);
var client = _httpClientFactory.CreateClient("GatewayProxyClient");
// 1. 创建转发请求
var requestMessage = new HttpRequestMessage();
var requestMethod = context.HttpRequest.Method;
// 复制请求方法和内容
requestMessage.Method = new HttpMethod(requestMethod);
if (HttpMethods.IsPost(requestMethod) || HttpMethods.IsPut(requestMethod) || HttpMethods.IsPatch(requestMethod))
{
requestMessage.Content = new StreamContent(context.HttpRequest.Body);
}
// 2. 复制请求头
foreach (var header in context.HttpRequest.Headers)
{
// HttpRequestMessage.Content.Headers 和 HttpRequestMessage.Headers 不能重复添加
if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
{
requestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
}
// 3. 设置请求URI
requestMessage.RequestUri = new Uri(UriHelper.BuildAbsolute(context.TargetUri.Scheme, new HostString(context.TargetUri.Host, context.TargetUri.Port), context.TargetUri.AbsolutePath, context.HttpRequest.Path, context.HttpRequest.QueryString));
// 4. 发送请求
using var responseMessage = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.HttpRequest.HttpContext.RequestAborted);
// 5. 将后端响应复制回客户端
context.HttpResponse.StatusCode = (int)responseMessage.StatusCode;
foreach (var header in responseMessage.Headers)
{
context.HttpResponse.Headers[header.Key] = header.Value.ToArray();
}
foreach (var header in responseMessage.Content.Headers)
{
context.HttpResponse.Headers[header.Key] = header.Value.ToArray();
}
await responseMessage.Content.CopyToAsync(context.HttpResponse.Body);
// 在代理之后,通常不应再有其他插件,但为了保持管道的完整性,仍然调用 next
await next(context);
}
}
}
4. 组装网关Function和依赖注入
最后,我们需要一个Azure Function作为入口点,并配置好依赖注入来注册所有插件和策略。
graph TD subgraph Azure Function App A[HttpTrigger: GatewayFunction] --> B{PipelineBuilder}; B --> C{Plugin & Strategy DI Container}; C --> D1[AuthPlugin]; C --> D2[RoutingPlugin]; C --> D3[ProxyPlugin]; C --> S1[ServiceARoutingStrategy]; C --> S2[ServiceBRoutingStrategy]; A -- Builds & Executes --> P[Request Pipeline]; P -- Request Flow --> D1; D1 --> D2; D2 -- Uses --> S1; D2 -- or --> S2; D2 --> D3; D3 -- Forwards to --> E[Backend Service]; end
Startup.cs / Program.cs (for .NET Isolated worker)
// Program.cs (using .NET 7 Isolated Worker model)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PluggableGateway.Core;
using PluggableGateway.Interfaces;
using PluggableGateway.Plugins;
using PluggableGateway.Routing;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
// 注册核心服务
services.AddSingleton<PipelineBuilder>();
services.AddHttpClient("GatewayProxyClient");
// 注册所有插件
// DI容器会自动找到所有实现了IPlugin的类
services.AddSingleton<IPlugin, AuthenticationPlugin>();
services.AddSingleton<IPlugin, DynamicRoutingPlugin>();
services.AddSingleton<IPlugin, ProxyPlugin>();
// 注册所有路由策略
services.AddSingleton<IRoutingStrategy, ServiceARoutingStrategy>();
services.AddSingleton<IRoutingStrategy, ServiceBRoutingStrategy>();
})
.Build();
host.Run();
GatewayFunction.cs
// GatewayFunction.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using System.Threading.Tasks;
using PluggableGateway.Core;
namespace PluggableGateway
{
public class GatewayFunction
{
private readonly PipelineBuilder _pipelineBuilder;
private readonly ILogger<GatewayFunction> _logger;
public GatewayFunction(PipelineBuilder pipelineBuilder, ILogger<GatewayFunction> logger)
{
_pipelineBuilder = pipelineBuilder;
_logger = logger;
}
[Function("Gateway")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "put", "delete", "patch", Route = "{*path}")] HttpRequestData req)
{
_logger.LogInformation("Gateway received a request.");
var pipeline = _pipelineBuilder.Build();
// 注意:在.NET Isolated模型中,HttpContext的直接访问方式有变化
// 为了简化示例,我们假设一个上下文实现可以被构建
// 在真实项目中,你需要一个适配器来桥接HttpRequestData和IPluginContext
var context = new PluginContext(req); // 这是一个假设的实现
try
{
await pipeline(context);
// 如果管道没有生成响应,则返回一个默认的成功响应
if (context.HttpResponse.StatusCode == 0) // 假设StatusCode为0表示未设置
{
var defaultResponse = req.CreateResponse(HttpStatusCode.OK);
await defaultResponse.WriteStringAsync("Request processed successfully by gateway.");
return defaultResponse;
}
// 将IPluginContext中的响应转换回HttpResponseData
return await context.ConvertToHttpResponseDataAsync(req);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred in the gateway pipeline.");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteStringAsync("An unexpected error occurred.");
return errorResponse;
}
}
}
}
注意: 上述GatewayFunction.cs
代码中的PluginContext
和ConvertToHttpResponseDataAsync
是伪代码,用于说明概念。在真实的.NET Isolated Worker项目中,需要编写具体的适配器逻辑来处理HttpRequestData
和HttpResponseData
。
local.settings.json
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"ExpectedApiKey": "my-secret-api-key",
"BackendServices:ServiceA": "https://api.service-a.com",
"BackendServices:ServiceB": "http://localhost:7072/api/"
}
}
架构的扩展性与局限性
这种基于插件的架构提供了极大的灵活性。需要添加一个新的功能,比如请求限流?只需编写一个RateLimitingPlugin
,实现IPlugin
接口,并在Startup.cs
中注册它即可,无需修改任何现有代码。这完全符合开闭原则。不同团队可以并行开发自己的插件,并通过配置来决定在不同环境中启用哪些插件。
然而,这个方案也并非没有缺点。
- 冷启动延迟: 作为一个Azure Function,它会面临冷启动问题。对于一个需要低延迟的网关来说,这可能是一个关键的性能瓶颈。在生产环境中,必须使用Premium Plan或App Service Plan并启用”Always On”来缓解这个问题,但这会增加成本。
- 性能开销: 责任链中的每一次
await next(context)
都涉及一次异步状态机的切换。虽然开销很小,但在一个包含大量插件的高性能链条中,累积的开销可能会变得显著。它永远不会比在一个单一进程内直接调用方法快。 - 复杂性管理: 随着插件数量的增加,插件之间的依赖关系和执行顺序可能会变得复杂。必须有严格的文档和规范来管理
Order
属性,避免出现顺序错乱导致的逻辑错误。例如,路由插件必须在代理插件之前执行。 - 无状态限制: Azure Functions本质上是无状态的。如果插件需要共享状态(例如,用于限流的计数器),则必须依赖外部存储,如Redis或Cosmos DB,这会增加架构的复杂性和延迟。
总的来说,这种方法最适合那些需要高度定制化和快速迭代,同时又能接受Serverless所带来的延迟特性的场景。它在灵活性、可维护性和开发效率之间找到了一个很好的平衡点,是标准APIM和单体Function之间一个有价值的折中方案。