Yarp是微软开源的一个用.net实现的反向代理工具包 , 仓库地址 https://github.com/microsoft/reverse-proxy
官方默认使用配置文件配置路由,有老哥把它集成到数据库中做成了可配置热更新的方式 : https://www.cnblogs.com/fanshaoO/p/14603159.html
由于部门使用spring cloud gateway作为网关,故想实现一个类似的功能 , 废话不多说,直接上代码 :
SteeltoeLookupMiddleware :
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Steeltoe.Discovery; using Yarp.ReverseProxy.Middleware; namespace Yarp.ReverseProxy.Steeltoe { internal class SteeltoeLookupMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly SteeltoeProvider _steeltoeProvider; public SteeltoeLookupMiddleware( RequestDelegate next, ILogger<SteeltoeLookupMiddleware> logger, IDiscoveryClient discoveryClient = null) { _next = next ?? throw new ArgumentNullException(nameof(next)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _steeltoeProvider = new SteeltoeProvider(discoveryClient); } public Task Invoke(HttpContext context) { var proxyFeature = context.GetRequiredProxyFeature(); var cluster = proxyFeature.ClusterSnapshot.Options; if (cluster.Metadata == null || !cluster.Metadata.TryGetValue("ServiceId", out var serviceId)) { return _next(context); } var result = _steeltoeProvider.FindAffinitizedDestinations(context, serviceId); if (result.Status == Yarp.ReverseProxy.Service.SessionAffinity.AffinityStatus.OK) { proxyFeature.AvailableDestinations = result.Destinations; } return _next(context); } } }
SteeltoeProvider :
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Steeltoe.Common.Discovery; using Steeltoe.Discovery; using Yarp.ReverseProxy.Abstractions; using Yarp.ReverseProxy.RuntimeModel; using Yarp.ReverseProxy.Service.SessionAffinity; namespace Yarp.ReverseProxy.Steeltoe { public class SteeltoeProvider { public SteeltoeProvider(IDiscoveryClient discoveryClient = null) { _discoveryClient = discoveryClient; } private readonly IDiscoveryClient _discoveryClient; public AffinityResult FindAffinitizedDestinations(HttpContext context, string serviceId) { var instances = _discoveryClient?.GetInstances(serviceId); if (instances == null) { return new AffinityResult(null, AffinityStatus.AffinityKeyExtractionFailed); } if (instances.Count == 0) { return new AffinityResult(null, AffinityStatus.AffinityKeyNotSet); } var destinationInfos = instances.Select(CreateDestinationInfo).ToList(); return new AffinityResult(destinationInfos, AffinityStatus.OK); } private DestinationInfo CreateDestinationInfo(IServiceInstance serviceInstance) { var destinationId = GetInstanceId(serviceInstance) ?? serviceInstance.ServiceId; var destinationInfo = new DestinationInfo(destinationId); var destination = new Destination { Address = serviceInstance.Uri.ToString() }; var config = new DestinationConfig(destination); SetDestinationConfig(destinationInfo, config); return destinationInfo; } private void SetDestinationConfig(DestinationInfo destinationInfo, DestinationConfig config) { _setDestinationConfigInvoker.Invoke(destinationInfo, config); } private readonly Action<DestinationInfo, DestinationConfig> _setDestinationConfigInvoker = CreateDestinationConfigInvoker(); private static Action<DestinationInfo, DestinationConfig> CreateDestinationConfigInvoker() { var destinationInfoInstance = Expression.Parameter(typeof(DestinationInfo), "destinationInfo"); var destinationConfigInstance = Expression.Parameter(typeof(DestinationConfig), "destinationConfig"); var property = typeof(DestinationInfo).GetProperty("Config"); var body = Expression.Assign(Expression.Property(destinationInfoInstance, property), destinationConfigInstance); return Expression.Lambda<Action<DestinationInfo, DestinationConfig>>(body, destinationInfoInstance, destinationConfigInstance).Compile(); } private readonly System.Collections.Concurrent.ConcurrentDictionary<Type, Func<IServiceInstance, string>> _getInstanceIdMap = new System.Collections.Concurrent.ConcurrentDictionary<Type, Func<IServiceInstance, string>>(); private string GetInstanceId(IServiceInstance serviceInstance) { if (_getInstanceIdMap.TryGetValue(serviceInstance.GetType(), out var invoker)) { return invoker?.Invoke(serviceInstance); } invoker = _getInstanceIdMap.GetOrAdd(serviceInstance.GetType(), type => { var property = type.GetProperty("InstanceId"); if (property == null || !property.CanRead) { return null; } var instance = Expression.Parameter(typeof(IServiceInstance), "instance"); var convertInstance = Expression.Convert(instance, type); var body = Expression.Property(convertInstance, property); return Expression.Lambda<Func<IServiceInstance, string>>(body, instance).Compile(); }); return invoker?.Invoke(serviceInstance); } } }
使用方式也很简单 :
app.UseEndpoints(endpoints => { endpoints.MapReverseProxy(proxyPipeline => { proxyPipeline.UseAffinitizedDestinationLookup(); proxyPipeline.UseMiddleware<SteeltoeLookupMiddleware>(); proxyPipeline.UseProxyLoadBalancing(); proxyPipeline.UsePassiveHealthChecks(); }) ; });
需要注意几点:
1.为了避免所有集群都走注册中心,需要显式的配置一下 ServiceId在MetaData中
2.如果不配置 Destinations节点 , 返回的集群为null
{ "ReverseProxy": { "Routes": [ { "serviceApiInvoke": { "ClusterId": "serviceApiInvoke", "Match": { "Path": "/service-api-invoke/{*any}" }, "Transforms": { { "PathRemovePrefix": "/service-api-invoke" } } } } ], "Clusters": { "serviceApiInvoke": { "Destinations": { "cluster1/destination1": { "Address": "https://example.com/" }, "MetaData": { "ServiceId": "service-api-invoke" } } } } } }
这样能初步替换spring cloud gateway了``