aiohttp:限制并行请求的速率

时间:2018-02-08 09:39:49

标签: python parallel-processing python-asyncio aiohttp

API通常具有用户必须遵循的速率限制。举个例子,我们需要50个请求/秒。顺序请求需要0.5-1秒,因此太慢而不能接近该限制。但是,使用aiohttp的并行请求超出了速率限制。

要以允许的速度快速轮询API,需要对并行呼叫进行速率限制。

到目前为止我找到的示例装饰session.get,大致如下:

session.get = rate_limited(max_calls_per_second)(session.get)

这适用于顺序调用。尝试在并行调用中实现此功能并不能按预期工作。

以下是一些代码示例:

async with aiohttp.ClientSession() as session:
    session.get = rate_limited(max_calls_per_second)(session.get)
    tasks = (asyncio.ensure_future(download_coroutine(  
          timeout, session, url)) for url in urls)
    process_responses_function(await asyncio.gather(*tasks))

这个问题是它会对任务的排队进行速率限制。 gather的执行仍然会或多或少地同时发生。最糟糕的是两个世界; - )。

是的,我在这里找到了一个类似的问题aiohttp: set maximum number of requests per second,但是没有回复回答限制请求率的实际问题。此外,the blog post from Quentin Pradet仅适用于对排队进行速率限制。

将其打包:如何限制并行aiohttp请求的每秒请求数

3 个答案:

答案 0 :(得分:5)

我通过用基于漏桶算法的内置速率限制器创建aiohttp.ClientSession()的子类来解决此问题。我使用asyncio.Queue()进行速率限制,而不是信号量。我只覆盖了_request()方法。我发现这种方法更清洁,因为您仅将session = aiohttp.ClientSession()替换为session = ThrottledClientSession(rate_limit=15)

代码:

class ThrottledClientSession(aiohttp.ClientSession):
    """Rate-throttled client session class inherited from aiohttp.ClientSession)""" 
MIN_SLEEP = 0.1

def __init__(self, rate_limit: float =None, *args,**kwargs) -> None: 
    super().__init__(*args,**kwargs)
    self.rate_limit = rate_limit
    self._fillerTask = None
    self._queue = None
    self._start_time = time.time()
    if rate_limit != None:
        if rate_limit <= 0:
            raise ValueError('rate_limit must be positive')
        self._queue = asyncio.Queue(min(2, int(rate_limit)+1))
        self._fillerTask = asyncio.create_task(self._filler(rate_limit))

 
def _get_sleep(self) -> list:
    if self.rate_limit != None:
        return max(1/self.rate_limit, self.MIN_SLEEP)
    return None
    
async def close(self) -> None:
    """Close rate-limiter's "bucket filler" task"""
    if self._fillerTask != None:
        self._fillerTask.cancel()
    try:
        await asyncio.wait_for(self._fillerTask, timeout= 0.5)
    except asyncio.TimeoutError as err:
        print(str(err))
    await super().close()


async def _filler(self, rate_limit: float = 1):
    """Filler task to fill the leaky bucket algo"""
    try:
        if self._queue == None:
            return 
        self.rate_limit = rate_limit
        sleep = self._get_sleep()
        updated_at = time.monotonic()
        fraction = 0
        extra_increment = 0
        for i in range(0,self._queue.maxsize):
            self._queue.put_nowait(i)
        while True:
            if not self._queue.full():
                now = time.monotonic()
                increment = rate_limit * (now - updated_at)
                fraction += increment % 1
                extra_increment = fraction // 1
                items_2_add = int(min(self._queue.maxsize - self._queue.qsize(), int(increment) + extra_increment))
                fraction = fraction % 1
                for i in range(0,items_2_add):
                    self._queue.put_nowait(i)
                updated_at = now
            await asyncio.sleep(sleep)
    except asyncio.CancelledError:
        print('Cancelled')
    except Exception as err:
        print(str(err))


async def _allow(self) -> None:
    if self._queue != None:
        # debug 
        #if self._start_time == None:
        #    self._start_time = time.time()
        await self._queue.get()
        self._queue.task_done()
    return None


async def _request(self, *args,**kwargs):
    """Throttled _request()"""
    await self._allow()
    return await super()._request(*args,**kwargs)

答案 1 :(得分:2)

如果我理解你,你想限制同时请求的数量吗?

asyncio内有一个名为Semaphore的对象,它的作用类似于异步RLock

semaphore = asyncio.Semaphore(50)
#...
async def limit_wrap(url):
    async with semaphore:
        # do what you want
#...
results = asyncio.gather([limit_wrap(url) for url in urls])

更新

假设我发出50个并发请求,它们都在2秒内完成。因此,它没有触及限制(每秒只有25个请求)。

这意味着我应该发出100个并发请求,它们也会在2秒内完成(每秒50个请求)。但在你真正提出这些要求之前,你怎么能确定它们会完成多长时间?

或者,如果您不介意每秒完成的请求,但每秒发出的请求数。你可以:

async def loop_wrap(urls):
    for url in urls:
        asyncio.ensure_future(download(url))
        await asyncio.sleep(1/50)

asyncio.ensure_future(loop_wrap(urls))
loop.run_forever()

上面的代码将每隔Future秒创建一个1/50个实例。

答案 2 :(得分:0)

我喜欢@sraw通过asyncio来解决这个问题,但是他们的回答对我来说并没有什么意义。由于我不知道我的下载呼叫速度是快于还是慢于速率限制,因此我希望可以选择在请求较慢时并行运行多个请求,而在请求非常快时一次运行一个,因此我总是正确的限制速率。

我这样做是通过与生产者一起使用队列来进行的,该生产者会以速率限制生成新任务,然后许多消费者要么很快就等待下一个任务,要么将所有工作备份在队列中如果它们很慢,并且将在处理器/网络允许的速度下运行:

import asyncio
from datetime import datetime 

async def download(url):
  # download or whatever
  task_time = 1/10
  await asyncio.sleep(task_time)
  result = datetime.now()
  return result, url

async def producer_fn(queue, urls, max_per_second):
  for url in urls:
    await queue.put(url)
    await asyncio.sleep(1/max_per_second)
 
async def consumer(work_queue, result_queue):
  while True:
    url = await work_queue.get()
    result = await download(url)
    work_queue.task_done()
    await result_queue.put(result)

urls = range(20)
async def main():
  work_queue = asyncio.Queue()
  result_queue = asyncio.Queue()

  num_consumer_tasks = 10
  max_per_second = 5
  consumers = [asyncio.create_task(consumer(work_queue, result_queue))
               for _ in range(num_consumer_tasks)]    
  producer = asyncio.create_task(producer_fn(work_queue, urls, max_per_second))
  await producer

  # wait for the remaining tasks to be processed
  await work_queue.join()
  # cancel the consumers, which are now idle
  for c in consumers:
    c.cancel()

  while not result_queue.empty():
    result, url = await result_queue.get()
    print(f'{url} finished at {result}')
 
asyncio.run(main())