Использование libcurl в многопоточной среде приводит к ОЧЕНЬ низкой производительности, связанной с поиском DNS

Вам придется простить довольно большой блок кода, но я считаю, что это почти минимальное воспроизведение моей проблемы. Проблема не изолирована от example.com но сохраняется на многих других сайтах.

Если у меня 4 потока активно делают сетевые запросы, curl работает на 100% нормально.

Если я добавлю еще один поток, выполнение этого потока займет ~10 раз. Я чувствую, что, должно быть, упускаю что-то очевидное, но это ускользает от меня прямо сейчас.

ОБНОВЛЕНИЕ с дополнительной информацией: эти тесты на виртуальной машине. Независимо от количества ядер, доступных для машины, четыре запроса занимают ~100 мс, а остальные - ~5500 мс.

ОБНОВЛЕНИЕ 2: На самом деле, я был неправ в одном аспекте, это не всегда 4 / n-4 распределение - когда я изменился на 4 Ядра, иногда я получаю другое распределение результатов (работа на 1 ядре, по крайней мере, казалась относительно последовательной) - вот фрагмент результатов, когда потоки возвращают свою задержку (мс) вместо кода http при работе на 4-х ядерной ВМ:

   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, САМОЕ ВАЖНОЕ:

Запуск программы под strace -T ./a.out 2>&1 | vim - с 5 потоками, когда у программы был только 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

2 ответа

Решение

Ошибка где-то в разрешении DNS, как указано моим ОБНОВЛЕНИЕМ 5.

Это связано с поиском IPV6, где-то в getaddrinfo,

Поиск вокруг указывает, что это обычно проблема ISP или чрезмерно агрессивная проблема фильтрации пакетов, в сочетании с чем-то другим (что, я не знаю), что делает это действительно очень странным крайним случаем.

Следование инструкциям на этой странице приводит к следующему обходному пути / решению:

curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);

Что устраняет проблему, как я ее понял. IPV6 это сложно.:(

Если служба http основана на mongoose или CivetWeb, посмотрите этот ответ

libcurl задерживает на 1 секунду перед загрузкой данных, curl командной строки не

Проблема в том, что curl отправляет Expect:100-continueв шапке, но mongoose / civetweb на него не отвечает. Время ожидания завитка истекает через 1000 мс и продолжается.

Ответ выше показывает, как исправить curl или CivetWeb.

Другие вопросы по тегам