在多线程环境中使用libcurl会导致与DNS查找相关的非常缓慢的性能问题。

15

不好意思,这里有一大块代码,但我认为这几乎是我的问题的最小复现。这个问题不仅出现在 example.com 上,而且还在许多其他网站上持续存在。

如果我有4个线程正在进行网络请求,则 curl 的工作完全正常。

如果我添加一个以上的线程,则该线程执行所需的时间会增加约10倍。我觉得肯定有些明显的东西我没注意到,但眼下我还没想出来。

更新 更多信息:这些测试是在虚拟机中进行的。无论机器可用的核心数是多少,其中四个请求都需要 ~100ms,其余请求则需要 ~5500ms。

更新 2: 实际上,在一个方面我错了,它并不总是 4 / n-4 的分布 - 当我改为 4 个核心时,有时会得到不同的结果分布(至少在 1 个核心上运行似乎相对一致) - 这是当线程返回它们的延迟(毫秒)而不是 http 状态码时,在 4 核 VM 上运行的结果片段:

   191  191
   198  198  167
   209  208  202  208
   215  207  214  209  209
  5650  213 5649  222  193  207
   206  201  164  205  201  201  205
  5679 5678 5666 5678  216  173  205  175
  5691  212  179  206 5685 5688  211 5691 5680
  5681  199  210 5678 5663  213 5679  212 5666  428

更新 3: 我从头构建了curl和openssl,移除了锁定(因为openssl 1.1.0g不需要它),但问题仍然存在。(以下是验证结果):

std::cout << "CURL:\n  " << curl_version_info(CURLVERSION_NOW)->ssl_version
          << "\n";
std::cout << "SSLEAY:\n  " << SSLeay_version(SSLEAY_VERSION) << "\n";

输出:
CURL:                       
  OpenSSL/1.1.0g            
SSLEAY:                     
  OpenSSL 1.1.0g  2 Nov 2017

举个例子,延迟时间如下:

   191  191
   197  197  196
   210  210  201  210
   212  212  199  200  165
  5656 5654  181  214  181  212
  5653 5651 5647  211  206  205  162
  5681 5674 5669  165  201  204  201 5681
  5880 5878 5657 5662  197  209 5664  173  174
  5906 5653 5664 5905 5663  173 5666  173  165  204

更新4:将CURLOPT_CONNECTTIMEOUT_MS设置为x,使x成为返回所需时间的上限。

更新5,最重要的:

在5个线程下使用strace -T ./a.out 2>&1 | vim -运行程序时,当程序只有1个缓慢请求时,会产生两个非常缓慢的行。它们是对同一futex的两次调用,第一次比第二次花费的时间更长,但两次调用都比所有其他futex调用(大多数为0.000011毫秒)花费更长的时间(这两次调用分别需要5.4秒和0.2秒才能解锁)。

此外,我验证了缓慢性完全出现在curl_easy_perform中。

futex(0x7efcb66439d0, FUTEX_WAIT, 3932, NULL) = 0 <5.390086>
futex(0x7efcb76459d0, FUTEX_WAIT, 3930, NULL) = 0 <0.204908>

最终,在查看源代码后,我发现问题出在DNS查找中。无论问题在哪里或是什么原因,用IP地址替换主机名只是一种权宜之计。

-----------


下面是我精简复制的问题,使用g++ -lpthread -lcurl -lcrypto main.cc编译,链接到从源代码构建的openssl和libcurl版本。

#include <chrono>
#include <iomanip>
#include <iostream>
#include <thread>
#include <vector>
#include <curl/curl.h>
#include <openssl/crypto.h>

size_t NoopWriteFunction(void *buffer, size_t size, size_t nmemb, void *userp) {
  return size * nmemb;
};

int GetUrl() {
  CURL *hnd = curl_easy_init();

  curl_easy_setopt(hnd, CURLOPT_URL, "https://www.example.com/");
  curl_easy_setopt(hnd, CURLOPT_HEADERFUNCTION, NoopWriteFunction);
  curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, NoopWriteFunction);
  curl_easy_setopt(hnd, CURLOPT_SSH_KNOWNHOSTS, "/home/web/.ssh/known_hosts");

  CURLcode ret = curl_easy_perform(hnd);
  long http_code = 0;
  curl_easy_getinfo(hnd, CURLINFO_RESPONSE_CODE, &http_code);

  curl_easy_cleanup(hnd);
  hnd = NULL;
  if (ret != CURLE_OK) {
    return -ret;
  }
  return http_code;
}

int main() {
  curl_global_init(CURL_GLOBAL_ALL);

  for (int i = 1; i < 10; i++) {
    std::vector<std::thread> threads;
    int response_code[10]{};
    auto clock = std::chrono::high_resolution_clock();
    auto start = clock.now();
    threads.resize(i);
    for (int j = 0; j < i; j++) {
      threads.emplace_back(std::thread(
          [&response_code](int x) { response_code[x] = GetUrl(); }, j));
    }
    for (auto &t : threads) {
      if (t.joinable()) {
        t.join();
      }
    }
    auto end = clock.now();
    int time_to_execute =
        std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
            .count();
    std::cout << std::setw(10) << time_to_execute;
    for (int j = 0; j < i; j++) {
      std::cout << std::setw(5) << response_code[j];
    }
    std::cout << "\n";
  }
}

当我在我的机器上运行程序时,我得到了以下结果(我可以更改域名,但结果基本相同):

   123  200
    99  200  200
   113  200  200  200
   119  200  200  200  200
  5577  200  200  200  200  200
  5600  200  200  200  200  200  200
  5598  200  200  200  200  200  200  200
  5603  200  200  200  200  200  200  200  200
  5606  200  200  200  200  200  200  200  200  200

以下是我的curl版本和openssl版本:

$curl --version
curl 7.52.1 (x86_64-pc-linux-gnu) libcurl/7.52.1 OpenSSL/1.0.2l zlib/1.2.8 libidn2/0.16 libpsl/0.17.0 (+libidn2/0.16) libssh2/1.7.0 nghttp2/1.18.1 librtmp/2.3
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL
$ openssl version
OpenSSL 1.1.0f  25 May 2017

你的处理器有多少个核心?有多少个“虚拟”核心?如果将线程增加到六个或八个,会发生什么?它们都表现得很差吗?只是第五个线程表现不佳,还是所有线程都表现不佳? - Some programmer dude
为了更加具体,uname -a 的输出结果为:Linux web 4.9.0-4-amd64 #1 SMP Debian 4.9.65-3+deb9u1 (2017-12-23) x86_64 GNU/Linux,而 cat /proc/version 的输出结果为 Linux version 4.9.0-4-amd64 (debian-kernel@lists.debian.org) (gcc version 6.3.0 20170516 (Debian 6.3.0-18) ) #1 SMP Debian 4.9.65-3+deb9u1 (2017-12-23) - druckermanly
@druckermanly - 你能从源代码中移除init_locks()吗?这个函数在示例中并不需要,而且我怀疑它可能会对OpenSSL造成影响(特别是如果curl_global_init调用了任何OpenSSL库函数,这可能会导致顺序问题)。 - Myst
是的,在我更新了从源代码构建的curl和openssl版本后,我删除了锁定逻辑。问题仍然存在。我已经更新了我的源代码以反映这些更改。 - druckermanly
这不是答案,只是一些调试笔记。尝试使用Wireshark(tcpdump)并观察TCP对话。尝试从咖啡店运行程序。也许家用路由器有问题? - Kumaresh AK
显示剩余8条评论
2个回答

7
UPDATE 5表明问题出现在DNS解析中,与IPV6的查找有关,在getaddrinfo中。
搜索表明这通常是ISP问题或过于激进的数据包过滤问题,再加上其他因素(我不知道是什么),使其成为一个非常奇怪的边缘案例。
按照此页面上的说明执行以下解决方案/解决方法:
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);

这解决了我所认为的问题。IPV6 确实很难 :(


补充一下,如果你遇到 DNS 查询时间过长的情况,考虑使用 curl_multi。在 https://curl.haxx.se/libcurl/c/curl_multi_add_handle.html 中了解它的 DNS 缓存共享。 - Hanoch Giner

-1

如果 HTTP 服务是基于 mongoose 或 CivetWeb,可以查看此答案

libcurl delays for 1 second before uploading data, command-line curl does not

问题在于 curl 在头部发送了 Expect:100-continue ,但 mongoose/civetweb 并未对其做出响应。Curl 在 1000 毫秒后超时并继续执行。

上面的答案展示了如何修复 curl 或 CivetWeb 中的任意一个。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接