Cómo dejar de hacer DDoS en la API de otra persona y empezar a vivir

Hablemos de formas de limitar la cantidad de solicitudes salientes en una aplicación distribuida. Esto es necesario si la API externa no le permite acceder a ella cuando lo desee.





Introductorio

.   . , - , API , .   , .   .  ,  —   . , :       .    .   , , ID .     .   ,   — .





, ,   ,   .  :   1000  .





 — .  ,    N .   —  . - - .





  (1000/20)    50   .





.NET
private const int RequestsLimit = 50;

private static readonly SemaphoreSlim Throttler = 
  new SemaphoreSlim(RequestsLimit);

async Task<HttpResponseMessage> InvokeServiceAsync(HttpClient client)
{
	try
	{
		await Throttler.WaitAsync().ConfigureAwait(false);
		return await client.GetAsync("todos/1").ConfigureAwait(false);
	}
	finally
	{
		Throttler.Release();
	}
}
      
      



.NET Core HttpClient,   ,      ,    .    ,   .





, .





  , ,   . ,   ,       .  —   ,  .  —  , -     .  ,   ,       .





:





:













:









,   . .   —      -throttler-.   , ,  —   ,    .   ? ,    Redis.





  Redis (  ). ,     .





 Redis ,     .





 Lua. Lua  Redis, , .    ,   ,   .





, . , , ,   . - -      . ,   . , , , API-   .





Redis
--[[  
	KEYS[1] -   
	ARGV[1] -    
	ARGV[2] -  ,      
	ARGV[3] -    
]]--   

--      ,  
-- Redis-    
redis.replicate_commands()

local unix_time = redis.call('TIME')[1]   

--     TTL 
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', unix_time - ARGV[1])   

--      
local count = redis.call('zcard', KEYS[1])   

if count < tonumber(ARGV[3]) then
	--    ,   
	--      (   ) 	
	redis.call('ZADD', KEYS[1], unix_time, ARGV[2])       
	
	--     (,  )    
	return count 
end   
return nil
      
      



  . . ,  ..   .NET .





Redis .





, ,   1000  .    Redis   .





  , ,   .





public sealed class RedisSemaphore
{
	private static readonly string AcquireScript = "...";
	private static readonly int TimeToLiveInSeconds = 300;
	
	private readonly Func<ConnectionMultiplexer> _redisFactory;
	
	public RedisSemaphore(Func<ConnectionMultiplexer> redisFactory)
	{
		_redisFactory = redisFactory;
	}
	
	public async Task<LockHandler> AcquireAsync(string name, int limit)
	{
		var handler = new LockHandler(this, name);
		
		do
		{
			var redisDb = _redisFactory().GetDatabase();
			
			var rawResult = await redisDb
				.ScriptEvaluateAsync(AcquireScript, new RedisKey[] { name },
					new RedisValue[] { TimeToLiveInSeconds, handler.Id, limit })
				.ConfigureAwait(false);

			var acquired = !rawResult.IsNull;
			if (acquired)
				break;

			await Task.Delay(10).ConfigureAwait(false);
		} while (true);

		return handler;
	}

	public async Task ReleaseAsync(LockHandler handler, string name)
	{
		var redis = _redisFactory().GetDatabase();
		
		await redis.SortedSetRemoveAsync(name, handler.Id)
			.ConfigureAwait(false);
	}
}

public sealed class LockHandler : IAsyncDisposable
{
	private readonly RedisSemaphore _semaphore;
	private readonly string _name;
	
	public LockHandler(RedisSemaphore semaphore, string name)
	{
		_semaphore = semaphore;
		_name = name;
		
		Id = Guid.NewGuid().ToString();		
	}
	
	public string Id { get; }

	public async ValueTask DisposeAsync()
	{
		await _semaphore.ReleaseAsync(this, _name).ConfigureAwait(false);
	}
}
      
      



, .





:

















:













  1. Redis-









  Redis  ,         .       . - , ,     . . , Redis , , SaaS.





– . , . , .





Creo que es posible implementar la limitación de las solicitudes salientes a nivel de infraestructura, pero me parece conveniente tener control sobre el estado de bloqueo en el código. Además, configurarlo en nubes extranjeras probablemente será complicado. ¿Alguna vez ha tenido que limitar el número de solicitudes salientes? ¿Cómo lo haces?








All Articles