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
请求的每秒请求数?
答案 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())