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

dru*_*nly 15 c++ multithreading virtual-machine libcurl futex

你将不得不原谅相当大的代码块,但我相信这是对我的问题的近乎最小的再现.问题不是孤立的,example.com而是持续存在于许多其他网站上.

如果我有4个线程主动发出网络请求,curl工作100%罚款.

如果我再添加一个线程,该线程需要大约10倍的时间来执行.我觉得我必须错过一些明显的东西,但它现在逃脱了我.

更新更多信息:这些测试位于虚拟机中.与机器可用的内核数量无关,其中四个请求需要大约100毫秒,其余请求大约需要5500毫秒.

更新2:实际上,我在一个方面是错的,它并不总是4/ n-4分布 - 当我改为4核心时,有时我得到一个不同的结果分布(在1核上运行至少看起来相对一致) - 这里是一个当线程在4核VM上运行时返回其延迟(ms)而不是其http代码时的结果片段:

   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
Run Code Online (Sandbox Code Playgroud)

更新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";
Run Code Online (Sandbox Code Playgroud)

输出:

CURL:                       
  OpenSSL/1.1.0g            
SSLEAY:                     
  OpenSSL 1.1.0g  2 Nov 2017
Run Code Online (Sandbox Code Playgroud)

使用示例延迟:

   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
Run Code Online (Sandbox Code Playgroud)

更新4:设置CURLOPT_CONNECTTIMEOUT_MS等于x使得x它需要返回时间的上限.

更新5,最重要的是:

strace -T ./a.out 2>&1 | vim -使用5个线程运行程序,当程序只有1个慢速请求时,产生两个非常慢的行.这是对同一个futex的两个调用,一个比第二个更长,但两个都比所有其他futex调用花费的时间更长(大多数是0.000011 ms,这两个调用需要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>
Run Code Online (Sandbox Code Playgroud)

最后,在一些环顾源代码中,我发现错误是在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";
  }
}
Run Code Online (Sandbox Code Playgroud)

当我在我的机器上运行程序时,我得到以下结果(我可以将域更改为任何内容,结果是相同的):

   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
Run Code Online (Sandbox Code Playgroud)

这是我的卷曲版本和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
Run Code Online (Sandbox Code Playgroud)

dru*_*nly 7

该错误位于DNS解析中的某个位置,如我的更新5所示.

这与某些地方的IPV6查找有关getaddrinfo.

搜索周围表明这通常是一个ISP问题,或过度积极的数据包过滤问题,结合其他东西(我不知道),这使得这是一个非常奇怪的边缘情况.

按照此页面上的说明进行以下解决方法/解决方案:

curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
Run Code Online (Sandbox Code Playgroud)

这消除了我所认识到的问题.IPV6很难.:(