在多线程环境中使用 libcurl 会导致与 DNS 查找相关的性能非常低
Using libcurl in a multithreaded environment causes VERY slow performance related to DNS lookup
您必须原谅相当大的代码块,但我相信这是对我的问题的近乎最小的再现。该问题并非孤立于 example.com
,而是存在于许多其他站点。
如果我有 4 个线程主动发出网络请求,curl 可以 100% 正常工作。
如果我再添加一个线程,该线程的执行时间将增加约 10 倍。我觉得我一定遗漏了一些明显的东西,但我现在想不起来了。
UPDATE 更多信息:这些测试在虚拟机中进行。独立于机器可用的内核数量,其中四个请求耗时约 100 毫秒,其余请求耗时约 5500 毫秒。
更新 2:实际上,我在一方面是错误的,它并不总是 4
/ n-4
分布——当我更改为 4
个核心,有时我会得到不同的结果分布(运行 在 1 个核心上至少 似乎 相对一致) - 这是结果的片段当 运行 在 4 核 VM 上运行时,线程 return 它们的延迟(毫秒)而不是它们的 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
更新 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
成为 return 所需时间的上限].
更新 5,最重要的:
运行 strace -T ./a.out 2>&1 | vim -
下的程序有 5 个线程,当程序只有 1 个慢请求时,产生了两条非常慢的行。这是对同一个 futex 的两次调用,一次比第二次花费的时间长,但都比 all 其他 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
如我的 UPDATE 5.
所示,该错误出在 DNS 解析中的某处
这与在 getaddrinfo
的某处查找 IPV6 有关。
四处搜索表明这通常是 ISP 问题,或者过度激进的数据包过滤问题,再加上其他问题(我不知道是什么),这使得这成为一个非常奇怪的边缘案例。
按照 this page 上的说明进行操作可得出以下解决方法/解决方案:
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
这消除了我认为的问题。 IPV6很难。 :(
如果http服务是基于mongoose或者CivetWeb的,看这个回答
libcurl delays for 1 second before uploading data, command-line curl does not
问题是curl在header中发出了Expect:100-continue
,但是mongoose/civetweb没有响应。 Curl 在 1000 毫秒后超时并继续。
上面的答案显示了如何修复 curl 或 CivetWeb。
您必须原谅相当大的代码块,但我相信这是对我的问题的近乎最小的再现。该问题并非孤立于 example.com
,而是存在于许多其他站点。
如果我有 4 个线程主动发出网络请求,curl 可以 100% 正常工作。
如果我再添加一个线程,该线程的执行时间将增加约 10 倍。我觉得我一定遗漏了一些明显的东西,但我现在想不起来了。
UPDATE 更多信息:这些测试在虚拟机中进行。独立于机器可用的内核数量,其中四个请求耗时约 100 毫秒,其余请求耗时约 5500 毫秒。
更新 2:实际上,我在一方面是错误的,它并不总是 4
/ n-4
分布——当我更改为 4
个核心,有时我会得到不同的结果分布(运行 在 1 个核心上至少 似乎 相对一致) - 这是结果的片段当 运行 在 4 核 VM 上运行时,线程 return 它们的延迟(毫秒)而不是它们的 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
更新 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
成为 return 所需时间的上限].
更新 5,最重要的:
运行 strace -T ./a.out 2>&1 | vim -
下的程序有 5 个线程,当程序只有 1 个慢请求时,产生了两条非常慢的行。这是对同一个 futex 的两次调用,一次比第二次花费的时间长,但都比 all 其他 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
如我的 UPDATE 5.
所示,该错误出在 DNS 解析中的某处这与在 getaddrinfo
的某处查找 IPV6 有关。
四处搜索表明这通常是 ISP 问题,或者过度激进的数据包过滤问题,再加上其他问题(我不知道是什么),这使得这成为一个非常奇怪的边缘案例。
按照 this page 上的说明进行操作可得出以下解决方法/解决方案:
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
这消除了我认为的问题。 IPV6很难。 :(
如果http服务是基于mongoose或者CivetWeb的,看这个回答
libcurl delays for 1 second before uploading data, command-line curl does not
问题是curl在header中发出了Expect:100-continue
,但是mongoose/civetweb没有响应。 Curl 在 1000 毫秒后超时并继续。
上面的答案显示了如何修复 curl 或 CivetWeb。