【问题标题】:Multithreaded HTTP GET requests slow down badly after ~900 downloads大约 900 次下载后,多线程 HTTP GET 请求速度变慢
【发布时间】:2016-10-27 00:07:57
【问题描述】:

我正在尝试使用 requests_futures 从 Amazon S3 下载大约 3,000 个文件(每个文件大小可能为 3 MB),但下载速度在大约 900 之后严重减慢,并且实际上开始运行比基本 for-循环。

我似乎没有耗尽内存或 CPU 带宽。然而,我的机器上的 Wifi 连接似乎几乎没有减慢:我从每秒几千个数据包下降到只有 3-4 个。最奇怪的是,在 Python 进程退出之前我无法加载任何网站并且我重新启动了我的 wifi 适配器。

究竟是什么原因造成的,我该如何调试它?

如果有帮助,这是我的 Python 代码:

import requests
from requests_futures.sessions import FuturesSession
from concurrent.futures import ThreadPoolExecutor, as_completed

# get a nice progress bar
from tqdm import tqdm

def download_threaded(urls, thread_pool, session):
    futures_session = FuturesSession(executor=thread_pool, session=session)
    futures_mapping = {}
    for i, url in enumerate(urls):
        future = futures_session.get(url)
        futures_mapping[future] = i
    
    results = [None] * len(futures_mapping)

    with tqdm(total=len(futures_mapping), desc="Downloading") as progress:
        for future in as_completed(futures_mapping):
            try:
                response = future.result()
                result = response.text
            except Exception as e:
                result = e
            i = futures_mapping[future]
            results[i] = result
            progress.update()

    return results

s3_paths = []  # some big list of file paths on Amazon S3
def make_s3_url(path):
    return "https://{}.s3.amazonaws.com/{}".format(BUCKET_NAME, path)

urls = map(make_s3_url, s3_paths)
with ThreadPoolExecutor() as thread_pool:
    with requests.session() as session:
        results = download_threaded(urls, thread_pool, session)

使用我尝试过的各种方法进行编辑:

  • time.sleep(0.25) 在每个 future.result() 之后(性能在 900 左右急剧下降)
  • 4 个线程,而不是默认的 20 个(性能会逐渐下降,但仍会下降到基本为零)
  • 1 个线程(性能在 900 左右急剧下降,但间歇性恢复)
  • ProcessPoolExecutor 而不是 ThreadPoolExecutor(性能在 900 左右急剧下降)
  • 当状态大于 200 时调用 raise_for_status() 抛出异常,然后通过将其打印为警告来捕获此异常(不显示警告)
  • 在完全不同的网络上使用以太网而不是 wifi(没有变化)
  • 在普通请求会话中创建期货,而不是使用 FutureSession(这是我最初所做的,在尝试解决问题时发现 requests_futures)
  • 只运行下载只运行故障点附近的一小部分文件(例如文件 850 到文件 950)——这里的性能很好,print(response.status_code) 一直显示 200,并且没有发现异常。

不管怎样,我之前使用类似的方法能够在大约 4 秒内从 S3 下载约 1500 个文件,尽管文件要小一个数量级

今天有时间我会尝试的事情:

  • 使用 for 循环
  • 在 shell 中使用 Curl
  • 在 shell 中使用 Curl + Parallel
  • 使用 urllib2

编辑:看起来线程的数量是稳定的,但是当性能开始变差时,“空闲唤醒”的数量似乎从几百飙升到几千。这个数字是什么意思,我可以用它来解决这个问题吗?

未来的编辑 2:我从来没有最终解决这个问题。我没有在一个应用程序中完成所有操作,而是将文件列表分块并在单独的终端窗口中使用单独的 Python 调用运行每个块。丑陋但有效!问题的原因将永远是个谜,但我认为这是我当时工作机器的网络堆栈深处的某种问题。

【问题讨论】:

  • 当您向 Wi-Fi 驱动程序发送会话打开请求时,这可能是您的 Wi-Fi 驱动程序中的一个错误,即使不是,创建 1000 个线程似乎也不是一个好策略。为什么不尝试with ThreadPoolExecutor(max_workers=n) as thread_pool: 并搜索不会导致问题的n?注意“3.5版更改:如果max_workersNone或未给出,则默认为机器上的处理器数量,乘以5”,according to the docs
  • 我更新了我的评论;但是,您的 FuturesSession() 调用可能会使正在使用的线程数加倍 - 试试 n_cores * 2.5?
  • @KenY-N 我确实在使用 3.5,所以这将有 20 个工人。我会尝试更少。
  • @KenY-N 我首先从那个答案中得到了使用 FutureSession 的想法
  • 从哪里下载文件?它来自一个服务器,还是一小组服务器?是否会因为服务器看到来自您机器的大量请求而限制下载速率?

标签: multithreading python-3.x python-requests python-multithreading concurrent.futures


【解决方案1】:

这并不奇怪。

当线程数多于内核数时,您不会获得任何并行性。

您可以通过将问题简化为具有多个线程的单核来证明这一点。

会发生什么?您一次只能运行一个线程,因此操作系统上下文会切换每个线程以让每个人都轮流。一个线程工作,其他线程睡觉,直到他们轮流醒来做自己的事。在那种情况下,你不能比单线程做得更好。

您可能会做得更糟,因为上下文切换和为每个线程分配的内存(每个 1MB)也是有代价的。

阅读Amdahl's Law

【讨论】:

  • 如果缓慢是由于上下文切换开销造成的,那么它开始会不会很慢并保持这种状态?起初,这运行得非常快。还不允许 CPU 切换任务是多线程 I/O 绑定进程的全部意义吗?还是 CPU 需要主动处理 HTTP 请求?
  • 线程越多情况会变得更糟,因为会有更多的上下文切换。
  • 当然,但是线程数不应该突然增加 1/3
  • 我不相信我正在创建额外的线程。据我了解我使用的功能,预先创建了 20 个线程(4 核 x 5,默认值),然后在下载完成时由 ThreadPoolExecutor 重新使用。因此我的问题;也许我在代码中做错了我没有意识到是错误的。
  • 我尝试了 4 个线程而不是 20 个;性能仍然在 900 大关附近严重下降,但逐渐下降
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-02-09
  • 2020-12-28
  • 2017-08-10
  • 1970-01-01
  • 2020-11-03
  • 1970-01-01
相关资源
最近更新 更多