最近發現有客戶在大量的請求我們的介面,出於效能考慮遂新增了請求頻率限制。
由於我們介面請求的是.Net Core寫的API閘道器,所以可以直接新增一箇中介軟體,中介軟體中使用請求的地址當key,通過配置中心讀取對應的請求頻率引數設定,然後通過設定redis的過期時間就能實現了。
新增一箇中介軟體ApiThrottleMiddleware,使用httpContext.Request.Path獲取請求的介面,然後以次為key去讀取配置中心設定的請求頻率設定。(Ps:使用_configuration.GetSection(apiUrl).Get<ApiThrottleConfig>()不知為何返回值為null,這個還在查)
1 public class ApiThrottleMiddleware 2 { 3 private readonly RequestDelegate _next; 4 private IConfiguration _configuration; 5 private readonly IRedisRunConfigDatabaseProvider _redisRunConfigDatabaseProvider; 6 private readonly IDatabase _database; 7 8 public ApiThrottleMiddleware(RequestDelegate next, 9 IConfiguration configuration, 10 IRedisRunConfigDatabaseProvider redisRunConfigDatabaseProvider) 11 { 12 _next = next; 13 _configuration = configuration; 14 _redisRunConfigDatabaseProvider = redisRunConfigDatabaseProvider; 15 _database = _redisRunConfigDatabaseProvider.GetDatabase(); 16 } 17 18 public async Task Invoke(HttpContext httpContext) 19 { 20 var middlewareContext = httpContext.GetOrCreateMiddlewareContext(); 21 var apiUrl = httpContext.Request.Path.ToString(); 22 23 var jsonValue= _configuration.GetSection(apiUrl).Value; 24 var apiThrottleConfig=JsonConvert.DeserializeObject<ApiThrottleConfig>(jsonValue); 25 //var apiThrottleConfig = _configuration.GetSection(apiUrl).Get<ApiThrottleConfig>(); 26 27 await _next.Invoke(httpContext); 28 } 29 }
我們使用的配置中心是Apollo,設定的格式如下,其中Duration為請求間隔/秒,Limit為呼叫次數。(下圖設定為每分鐘允許請求10次)
(Ps: 由於在API限流中介軟體前我們已經通過了一個介面簽名驗證的中介軟體了,所以我們可以拿到呼叫客戶的具體資訊)
如果請求地址沒有配置請求頻率控制,則直接跳過。否則先通過SortedSetLengthAsync獲取對應key的記錄數,其中key我們使用了 $"{客戶Id}:{外掛編碼}:{請求地址}",以此來限制每個客戶,每個外掛對應的某個介面來控制請求頻率。獲取key對應集合,當前時間-配置的時間段到當前時間的記錄。
1 /// <summary> 2 /// 獲取key 3 /// </summary> 4 /// <param name="signInfo"></param> 5 /// <param name="apiUrl">介面地址</param> 6 /// <returns></returns> 7 private string GetApiRecordKey(InterfaceSignInfo signInfo,string apiUrl) 8 { 9 var key = $"{signInfo.LicNo}:{signInfo.PluginCode}:{apiUrl}"; 10 return key; 11 } 12 13 /// <summary> 14 /// 獲取介面呼叫次數 15 /// </summary> 16 /// <param name="signInfo"></param> 17 /// <param name="apiUrl">介面地址</param> 18 /// <param name="duration">超時時間</param> 19 /// <returns></returns> 20 public async Task<long> GetApiRecordCountAsync(InterfaceSignInfo signInfo, string apiUrl, int duration) 21 { 22 var key = GetApiRecordKey(signInfo, apiUrl); 23 var nowTicks = DateTime.Now.Ticks; 24 return await _database.SortedSetLengthAsync(key, nowTicks - TimeSpan.FromSeconds(duration).Ticks, nowTicks); 25 }
如果請求次數大於等於我們設定的頻率就直接返回介面呼叫頻率超過限制錯誤,否則則在key對應的集合中新增一條記錄,同時將對應key的過期時間設定為我們配置的限制時間。
/// <summary> /// 獲取介面呼叫次數 /// </summary> /// <param name="signInfo"></param> /// <param name="apiUrl">介面地址</param> /// <param name="duration">超時時間</param> /// <returns></returns> public async Task<long> GetApiRecordCountAsync(InterfaceSignInfo signInfo, string apiUrl, int duration) { var key = GetApiRecordKey(signInfo, apiUrl); var nowTicks = DateTime.Now.Ticks; return await _database.SortedSetLengthAsync(key, nowTicks - TimeSpan.FromSeconds(duration).Ticks, nowTicks); }
然後只需要在Startup中,在API簽名驗證中介軟體後呼叫我們這個API限流中介軟體就行了。
以下為完整的程式碼
1 using ApiGateway.Core.Configuration; 2 using ApiGateway.Core.Domain.Authentication; 3 using ApiGateway.Core.Domain.Configuration; 4 using ApiGateway.Core.Domain.Errors; 5 using Microsoft.AspNetCore.Http; 6 using Microsoft.Extensions.Configuration; 7 using Newtonsoft.Json; 8 using StackExchange.Redis; 9 using System; 10 using System.Threading.Tasks; 11 12 namespace ApiGateway.Core.Middleware.Api 13 { 14 /// <summary> 15 /// API限流中介軟體 16 /// </summary> 17 public class ApiThrottleMiddleware 18 { 19 private readonly RequestDelegate _next; 20 private IConfiguration _configuration; 21 private readonly IRedisRunConfigDatabaseProvider _redisRunConfigDatabaseProvider; 22 private readonly IDatabase _database; 23 24 public ApiThrottleMiddleware(RequestDelegate next, 25 IConfiguration configuration, 26 IRedisRunConfigDatabaseProvider redisRunConfigDatabaseProvider) 27 { 28 _next = next; 29 _configuration = configuration; 30 _redisRunConfigDatabaseProvider = redisRunConfigDatabaseProvider; 31 _database = _redisRunConfigDatabaseProvider.GetDatabase(); 32 } 33 34 public async Task Invoke(HttpContext httpContext) 35 { 36 var middlewareContext = httpContext.GetOrCreateMiddlewareContext(); 37 var apiUrl = httpContext.Request.Path.ToString(); 38 39 var jsonValue= _configuration.GetSection(apiUrl).Value; 40 var apiThrottleConfig=JsonConvert.DeserializeObject<ApiThrottleConfig>(jsonValue); 41 //var apiThrottleConfig = _configuration.GetSection(apiUrl).Get<ApiThrottleConfig>(); 42 if (apiThrottleConfig!=null) 43 { 44 var count = await GetApiRecordCountAsync(middlewareContext.InterfaceSignInfo, apiUrl, apiThrottleConfig.Duration); 45 if (count >= apiThrottleConfig.Limit) 46 { 47 middlewareContext.Errors.Add(new Error("介面呼叫頻率超過限制", GatewayErrorCode.OverThrottleError)); 48 } 49 else 50 { 51 await AddApiRecordCountAsync(middlewareContext.InterfaceSignInfo, apiUrl, apiThrottleConfig.Duration); 52 } 53 } 54 55 await _next.Invoke(httpContext); 56 } 57 58 /// <summary> 59 /// 獲取介面呼叫次數 60 /// </summary> 61 /// <param name="signInfo"></param> 62 /// <param name="apiUrl">介面地址</param> 63 /// <param name="duration">超時時間</param> 64 /// <returns></returns> 65 public async Task<long> GetApiRecordCountAsync(InterfaceSignInfo signInfo, string apiUrl, int duration) 66 { 67 var key = GetApiRecordKey(signInfo, apiUrl); 68 var nowTicks = DateTime.Now.Ticks; 69 return await _database.SortedSetLengthAsync(key, nowTicks - TimeSpan.FromSeconds(duration).Ticks, nowTicks); 70 } 71 72 /// <summary> 73 /// 新增呼叫次數 74 /// </summary> 75 /// <param name="signInfo"></param> 76 /// <param name="apiUrl">介面地址</param> 77 /// <param name="duration">超時時間</param> 78 /// <returns></returns> 79 public async Task AddApiRecordCountAsync(InterfaceSignInfo signInfo, string apiUrl, int duration) 80 { 81 var key = GetApiRecordKey(signInfo, apiUrl); 82 var nowTicks = DateTime.Now.Ticks; 83 await _database.SortedSetAddAsync(key, nowTicks.ToString(), nowTicks); 84 await _database.KeyExpireAsync(key, TimeSpan.FromSeconds(duration)); 85 } 86 87 /// <summary> 88 /// 獲取key 89 /// </summary> 90 /// <param name="signInfo"></param> 91 /// <param name="apiUrl">介面地址</param> 92 /// <returns></returns> 93 private string GetApiRecordKey(InterfaceSignInfo signInfo,string apiUrl) 94 { 95 var key = $"{signInfo.LicNo}:{signInfo.PluginCode}:{apiUrl}"; 96 return key; 97 } 98 } 99 }