How to stop DDoS-ing someone else's API and start living

Let's talk about ways to limit the number of outbound requests in a distributed application. This is necessary if the external API does not allow you to access it whenever you want.





Introductory

.   . , - , 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.





– . , . , .





I think it is possible to implement throttling of outgoing requests at the infrastructure level, but I find it convenient to have control over the lock state in the code. Also, setting it up in foreign clouds will probably be tricky. Have you ever had to limit the number of outgoing requests? How do you do it?








All Articles