Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
fast-gateway.service:
image: aidotnet/fast-gateway
image: registry.cn-shenzhen.aliyuncs.com/token-ai/fast-gateway
container_name: fast-gateway
restart: always
build:
Expand Down
15 changes: 15 additions & 0 deletions src/Core/Entities/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,19 @@ public sealed class Server
/// 请求超时时间(单位:秒)。默认900秒(15分钟)
/// </summary>
public int Timeout { get; set; } = 900;

/// <summary>
/// 是否启用请求级故障转移
/// </summary>
public bool EnableRequestFailover { get; set; }

/// <summary>
/// 请求级故障转移的连接超时(毫秒)
/// </summary>
public int FailoverConnectTimeoutMs { get; set; } = 150;

/// <summary>
/// 单个请求故障转移总预算(毫秒)
/// </summary>
public int FailoverBudgetMs { get; set; } = 500;
}
15 changes: 15 additions & 0 deletions src/FastGateway/Dto/ServerDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,19 @@ public class ServerDto
/// 请求超时时间(单位:秒)。默认900秒(15分钟)
/// </summary>
public int Timeout { get; set; } = 900;

/// <summary>
/// 是否启用请求级故障转移
/// </summary>
public bool EnableRequestFailover { get; set; }

/// <summary>
/// 请求级故障转移的连接超时(毫秒)
/// </summary>
public int FailoverConnectTimeoutMs { get; set; } = 150;

/// <summary>
/// 单个请求故障转移总预算(毫秒)
/// </summary>
public int FailoverBudgetMs { get; set; } = 500;
}
55 changes: 39 additions & 16 deletions src/FastGateway/Gateway/FastGatewayForwarderHttpClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,48 @@
using System.Diagnostics;
using FastGateway.Tunnels;
using System.Diagnostics;
using System.Net;
using Yarp.ReverseProxy.Forwarder;

namespace FastGateway.Gateway
namespace FastGateway.Gateway;

internal sealed class FastGatewayForwarderHttpClientFactory(
TunnelClientFactory tunnelClientFactory,
StandardForwarderHttpClientFactory standardForwarderHttpClientFactory)
: IForwarderHttpClientFactory
{
public class FastGatewayForwarderHttpClientFactory : IForwarderHttpClientFactory
private const string ClientModeMetadataKey = "FastGateway.ClientMode";
private const string TunnelClientMode = "Tunnel";

public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context)
{
public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context)
if (context.NewMetadata is not null &&
context.NewMetadata.TryGetValue(ClientModeMetadataKey, out var clientMode) &&
string.Equals(clientMode, TunnelClientMode, StringComparison.OrdinalIgnoreCase))
{
var handler = new SocketsHttpHandler
{
UseProxy = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None | DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
UseCookies = false,
EnableMultipleHttp2Connections = true,
ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),
ConnectTimeout = TimeSpan.FromSeconds(600),
};

return new HttpMessageInvoker(handler, disposeHandler: true);
return tunnelClientFactory.CreateClient(context);
}

return standardForwarderHttpClientFactory.CreateClient(context);
}
}

public sealed class StandardForwarderHttpClientFactory : ForwarderHttpClientFactory
{
protected override void ConfigureHandler(ForwarderHttpClientContext context, SocketsHttpHandler handler)
{
handler.UseProxy = false;
handler.AllowAutoRedirect = false;
handler.AutomaticDecompression = DecompressionMethods.None | DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli;
handler.UseCookies = false;
handler.ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current);
handler.ConnectTimeout = TimeSpan.FromSeconds(1);
handler.PooledConnectionLifetime = TimeSpan.FromMinutes(10);
handler.PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2);
handler.ResponseDrainTimeout = TimeSpan.FromSeconds(10);
handler.EnableMultipleHttp2Connections = true;
handler.EnableMultipleHttp3Connections = false;
handler.MaxConnectionsPerServer = context.NewConfig?.MaxConnectionsPerServer ?? 1024;

base.ConfigureHandler(context, handler);
}
}
78 changes: 76 additions & 2 deletions src/FastGateway/Gateway/Gateway.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using FastGateway.Tunnels;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Timeouts;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.WebSockets;
Expand All @@ -20,6 +21,7 @@
using Yarp.ReverseProxy;
using Yarp.ReverseProxy.Configuration;
using Yarp.ReverseProxy.Health;
using Yarp.ReverseProxy.LoadBalancing;
using Yarp.ReverseProxy.Model;
using Yarp.ReverseProxy.Forwarder;
using Yarp.ReverseProxy.Transforms;
Expand All @@ -33,6 +35,9 @@ public static class Gateway
{
private const string Root = "Root";
private const string GatewayVersionHeader = "X-FastGateway-Version";
private const string ClientModeMetadataKey = "FastGateway.ClientMode";
private const string TunnelClientMode = "Tunnel";
private const string StandardClientMode = "Standard";
private static readonly ConcurrentDictionary<string, WebApplication> GatewayWebApplications = new();

private static readonly DestinationConfig StaticProxyDestination = new() { Address = "http://127.0.0.1" };
Expand Down Expand Up @@ -381,6 +386,10 @@ public static async Task BuilderGateway(Server server, DomainName[] domainNames,
builder.Services.AddRateLimitService(rateLimits);

builder.Services.AddTunnel();
builder.Services.AddSingleton<StandardForwarderHttpClientFactory>();
builder.Services.AddSingleton<FastGatewayForwarderHttpClientFactory>();
builder.Services.AddSingleton<IForwarderHttpClientFactory>(s => s.GetRequiredService<FastGatewayForwarderHttpClientFactory>());
builder.Services.AddSingleton<ConfigurationService>();

if (server.StaticCompress)
builder.Services.AddResponseCompression();
Expand Down Expand Up @@ -481,12 +490,26 @@ public static async Task BuilderGateway(Server server, DomainName[] domainNames,
app.UseInitGatewayMiddleware();

app.UseRequestTimeouts();
app.Use(async (context, next) =>
{
if (IsSseRequest(context.Request))
{
context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout();
context.Response.Headers.CacheControl = "no-cache";
context.Response.Headers["X-Accel-Buffering"] = "no";
}

await next(context);
});

app.UseRateLimitMiddleware(rateLimits);

// 黑名单默认启用(安全防护),白名单按服务开关控制
app.UseBlacklistMiddleware(blacklistAndWhitelists, enableBlacklist: true, enableWhitelist: server.EnableWhitelist);

app.UseClusterRequestFailover(server.Id, gatewayVersion);
app.UseProxyErrorResponse(gatewayVersion);

app.UseAbnormalIpMonitoring(server.Id);

GatewayWebApplications.TryAdd(server.Id, app);
Expand Down Expand Up @@ -576,6 +599,50 @@ private static WebApplication UseInitGatewayMiddleware(this WebApplication app)
return app;
}

private static HttpClientConfig CreateHttpClientConfig()
{
return new HttpClientConfig
{
MaxConnectionsPerServer = 1024,
EnableMultipleHttp2Connections = true
};
}

private static ForwarderRequestConfig CreateHttpRequestConfig(Server server)
{
var timeoutSeconds = server.Timeout > 0 ? server.Timeout : 900;
if (timeoutSeconds < 600) timeoutSeconds = 600;

return new ForwarderRequestConfig
{
ActivityTimeout = TimeSpan.FromSeconds(timeoutSeconds),
AllowResponseBuffering = false
};
}

private static Dictionary<string, string> CreateClusterMetadata(string? service)
{
return new Dictionary<string, string>(1)
{
{
ClientModeMetadataKey,
IsTunnelService(service) ? TunnelClientMode : StandardClientMode
}
};
}

private static bool IsTunnelService(string? service)
{
if (string.IsNullOrWhiteSpace(service)) return false;
return service.StartsWith("http://node_", StringComparison.OrdinalIgnoreCase)
|| service.StartsWith("https://node_", StringComparison.OrdinalIgnoreCase);
}

private static bool IsSseRequest(HttpRequest request)
{
return request.Headers.Accept.ToString().Contains("text/event-stream", StringComparison.OrdinalIgnoreCase);
}

private static (IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters) BuildConfig(
DomainName[] domainNames, Server server)
{
Expand Down Expand Up @@ -644,7 +711,10 @@ private static (IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig>
config
}
},
HealthCheck = CreateHealthCheckConfig(domainName)
HealthCheck = CreateHealthCheckConfig(domainName),
HttpClient = CreateHttpClientConfig(),
HttpRequest = CreateHttpRequestConfig(server),
Metadata = CreateClusterMetadata(domainName.Service)
};

clusters.Add(cluster);
Expand All @@ -661,7 +731,11 @@ private static (IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig>
{
ClusterId = domainName.Id,
Destinations = destinations,
HealthCheck = CreateHealthCheckConfig(domainName)
LoadBalancingPolicy = LoadBalancingPolicies.LeastRequests,
HealthCheck = CreateHealthCheckConfig(domainName),
HttpClient = CreateHttpClientConfig(),
HttpRequest = CreateHttpRequestConfig(server),
Metadata = CreateClusterMetadata(null)
};

clusters.Add(cluster);
Expand Down
Loading
Loading