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

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

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

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

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

ОБНОВЛЕНИЕ 2. На самом деле, я был не прав в одном аспекте, это не всегда 4/n-4 распределение. ядро по крайней мере казалось относительно согласованным) — вот фрагмент результатов, когда потоки возвращают свою задержку (мс) вместо своего 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 медленный запрос, дал две очень медленные строки. Это были два вызова одного и того же фьютекса, один из которых занял больше времени, чем второй, но оба заняли больше времени, чем все вызовы других фьютексов (большинство из них были 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

person druckermanly    schedule 01.02.2018    source источник
comment
Сколько ядер у вашего процессора? Сколько виртуальных ядер? А что будет, если увеличить до шести потоков? Или восемь? Все ли они работают так же плохо? И плохо работает только пятый поток или все потоки?   -  person Some programmer dude    schedule 01.02.2018
comment
Я должен был указать это в своем вопросе - что интересно, это виртуальная машина - lscpu перечисляет 1 ЦП, 1 поток на ядро ​​​​и 1 ядро ​​​​на сокет с 1 сокетом.   -  person druckermanly    schedule 01.02.2018
comment
Обновление процессора в моей виртуальной машине для использования 4-ядерных ядер и повторный запуск программы привели к тому же поведению. Изменение кода для возврата миллисекунд, затраченных на выполнение программы, вместо кода ответа, показало, что это всегда были 4 потоки, занимающие ~ 100 мс, и n-4 потоки, занимающие ~ 5500 мс.   -  person druckermanly    schedule 01.02.2018
comment
Обновление: приведенное выше утверждение неверно, оно не так последовательно при использовании нескольких ядер   -  person druckermanly    schedule 01.02.2018
comment
На какой.операционной системе.вы.запускаете.curl?   -  person lalala    schedule 03.02.2018
comment
Ради конкретики, 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 ([email protected]) (gcc version 6.3.0 20170516 (Debian 6.3.0-18) ) #1 SMP Debian 4.9.65-3+deb9u1 (2017-12-23).   -  person druckermanly    schedule 03.02.2018
comment
Примечания: я также уверен, что OpenSSL 1.1.0g не нуждается в функции блокировки обратный вызов... Кроме того, обратный вызов вашего идентификатора предполагает, что внутренние данные потока не изменяются (OpenSSL разыменует указатель, чтобы просмотреть первые 8 байтов thread_t). Безопаснее возвращать указатель на то, что на самом деле не меняется... подумайте: static unsigned long *thread_id(void) { static __thread unsigned long ret = (unsigned long)pthread_self(); return &ret; }.   -  person Myst    schedule 03.02.2018
comment
@Myst Я забыл указать это в своем обновлении (делаю это сейчас), но я прекратил блокировку после обновления до 1.1.0g. (По общему признанию, логика блокировки была без особых раздумий украдена из здесь.)   -  person druckermanly    schedule 03.02.2018
comment
@druckermanly - не могли бы вы удалить init_locks() из исходного кода? Это не требуется для примера, и я подозреваю, что это может вызвать кривую работу OpenSSL (особенно если curl_global_init вызывает какие-либо функции библиотеки OpenSSL, что может вызвать проблему с порядком).   -  person Myst    schedule 03.02.2018
comment
Да, после того, как я обновился до версий curl и openssl, собранных из исходников, я удалил логику блокировки. Проблема сохраняется. Я обновил свой исходный код, чтобы отразить эти изменения.   -  person druckermanly    schedule 04.02.2018
comment
Это не ответ. Просто несколько заметок для отладки. Попробуйте использовать wirehark (tcpdump) и понаблюдайте за TCP-разговором. Попробуйте программу из кафе. Может у домашнего роутера проблемы?   -  person Kumaresh AK    schedule 04.02.2018
comment
На всякий случай, не могли бы вы повторить попытку с -pthread вместо -lpthread?   -  person YSC    schedule 05.02.2018
comment
@YSC Ничего не менял.   -  person druckermanly    schedule 05.02.2018


Ответы (2)


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

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

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

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

curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);

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

person druckermanly    schedule 05.02.2018
comment
Просто добавлю, что если у вас слишком длинные DNS-запросы, рассмотрите возможность использования curl_multi. Узнайте о совместном использовании кеша DNS в curl.haxx.se/libcurl/c/curl_multi_add_handle.html. - person Hanoch Giner; 06.02.2018

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

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

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

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

person Mark Lakata    schedule 17.06.2021