多线程 HTTP GET 请求在大约 900 次下载后严重减慢

sha*_*ker 5 multithreading python-multithreading python-3.x python-requests concurrent.futures

我正在尝试使用从 Amazon S3 下载大约 3,000 个文件(每个文件的大小可能为 3 MB)requests_futures,但下载速度在大约 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)
Run Code Online (Sandbox Code Playgroud)

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

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

就其价值而言,我以前能够使用类似的方法在大约 4 秒内从 S3 下载约 1500 个文件,尽管文件要小一个数量级

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

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

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

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

duf*_*ymo 0

这并不奇怪。

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

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

会发生什么?一次只能运行一个线程,因此操作系统上下文会切换每个线程,让每个线程都有机会运行。一个线程工作,其他线程休眠,直到它们依次被唤醒以完成自己的工作。在这种情况下,你不能比单线程做得更好。

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

阅读阿姆达尔定律