Buy commercial curl support. We
help you work out your issues, debug your libcurl applications, use the API,
port to new platforms, add new features and more. With a team lead by the
curl founder Daniel himself.
High latency with 100 active handles
- Contemporary messages sorted: [ by date ] [ by thread ] [ by subject ] [ by author ] [ by messages with attachments ]
From: Avni, Maor via curl-library <curl-library_at_lists.haxx.se>
Date: Mon, 25 Nov 2024 10:14:10 +0000
Hi,
We're having high latency issues when using CURL in our product. We've reduced the problem to the piece of code below.
Github project: https://github.com/maor-avni-hpe/curlmultipoc
Our product performs thousands of HTTP requests to Azure/AWS from a machine running on Azure/AWS. The transfer sizes vary from 128K to several MBs.
We're using the multi API.
The attached program accepts 5 arguments:
* URL
* Chunk size to download in K
* Total GB to download
* Batch Size
* Maximum Open Handles
* The program requests <batch size> downloads, each transfer <chunk size>, until reaching total GB.
* The program makes sure that no more than <max open handles> are requested from CURL.
* When a download completes its handle is removed and a new one is added until batch is completed.
The program is executed on a VM running on Azure, downloading a file from an Azure storage account, to make sure that the network is as predictable as possible. The file downloaded is a 10GB file, although we always request the same first few bytes. We ran it both on Linux and Windows.
Linux: Debian 12.8, curl 7.88
Windows: Server 2022, curl 7.88, curl 8.10
The results are for downloading a 5GB file using 128K chunks.
The results:
* single unlimited batch, 50 handles: Linux: 7s, Windows: 11s.
* single unlimited batch, 100 handles: Linux: 6s, Windows 11s.
* 50 per batch, 50 handles: Linux 12s, Windows 15s.
* 100 per batch, 50 handles: Linux 9s, Windows 14s.
* 100 per batch, 100 handles: Linux 36s, Windows 290s.
Logging the remaining FDs after each poll shows that when running 100 handles there's 1 handle that takes longer to finish.
Our product works in the 100/100 configuration. Under load we get very high latency and low data rates. This test explains why we get those slowdowns.
Is there a reasonable explanation for this slowdown?
Is it our usage of the curl API?
What can be done to improve the performance?
#include <iostream>
#include <list>
#include <set>
#include <string>
#include <thread>
#include <vector>
#include <curl/curl.h>
struct Chunk
{
std::string data;
size_t size;
};
size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp)
{
size_t totalSize = size * nmemb;
Chunk* chunk = (Chunk*)userp;
// chunk->data.append((char *)contents, totalSize);
chunk->size += totalSize;
// sleep for 0.1ms
return totalSize;
}
CURL* createCurlHandle(long start, long end, Chunk* chunk, std::string url)
{
CURL* curl = curl_easy_init();
if (curl)
{
std::string range = std::to_string(start) + "-" + std::to_string(end);
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
curl_easy_setopt(curl, CURLOPT_RANGE, range.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, chunk);
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 128L * 1024L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // Enable SSL verification
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); // Verify the host
}
return curl;
}
int main(int argc, char* argv[])
{
if (argc != 6)
{
std::cerr << "Usage: " << argv[0] << " <URL> <Chunk Size> <Total GB> <Batch Size> <Number of Concurrent Connections>" << std::endl;
return 1;
}
std::string url = argv[1];
long chunkSize = std::stol(argv[2]) * 1024;
long totalGB = std::stol(argv[3]);
int batchSize = std::stoi(argv[4]);
int numConnections = std::stoi(argv[5]);
// Initialize CURL
curl_global_init(CURL_GLOBAL_DEFAULT);
// Get the total size of the file
CURL* curl = curl_easy_init();
double fileSize = 0.0;
if (curl)
{
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
curl_easy_setopt(curl, CURLOPT_HEADER, 1L);
curl_easy_setopt(curl, CURLOPT_FILETIME, 1L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 128L * 1024L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // Enable SSL verification
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); // Verify the host
CURLcode res = curl_easy_perform(curl);
if (res == CURLE_OK)
{
curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &fileSize);
}
else
{
// get the http status code
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
std::cerr << "Failed to get file size: " << curl_easy_strerror(res) << " HTTP code: " << http_code << std::endl;
return 1;
}
curl_easy_cleanup(curl);
}
long long numOfChunks = (long long)totalGB * 1024 * 1024 * 1024 / chunkSize;
long long totalBatches;
if (batchSize == 0)
{
batchSize = numOfChunks;
totalBatches = 1;
}
else
totalBatches = numOfChunks / batchSize;
std::vector<Chunk> chunks;
std::set<CURL*> handles;
std::cout << "chunks: " << numOfChunks << std::endl;
CURLM* multi_handle = curl_multi_init();
// curl_multi_setopt(multi_handle, CURLMOPT_MAXCONNECTS, 0);
// curl_multi_setopt(multi_handle, CURLMOPT_MAX_TOTAL_CONNECTIONS, 1);
// curl_multi_setopt(multi_handle, CURLMOPT_PIPELINING, 0L);
// measure time taken to download
clock_t outerStart, outerEnd;
outerStart = clock();
std::cout << outerStart << std::endl;
int still_running = 0;
int maxHandles = 0;
int maxPolledFd = 0;
long long currentBatch = 0;
do
{
do
{
// Add new connections
while (handles.size() < numConnections && chunks.size() < batchSize)
{
long start = 0;
long end = chunkSize - 1;
if (end >= fileSize) end = fileSize - 1;
Chunk chunk;
auto handle = createCurlHandle(start, end, &chunk, url);
handles.insert(handle);
curl_multi_add_handle(multi_handle, handle);
chunks.push_back(chunk);
}
if (handles.size() > maxHandles)
{
maxHandles = handles.size();
}
// Perform connections
CURLMcode mc = curl_multi_perform(multi_handle, &still_running);
// Wait for activity, timeout or "nothing"
int currentPolledFd = 0;
if (still_running)
{
// measure call time in milliseconds
clock_t start, end;
start = clock();
mc = curl_multi_poll(multi_handle, NULL, 0, 1000, nullptr);
end = clock();
double time_taken = double(end - start) / double(CLOCKS_PER_SEC);
//if (time_taken > 0.1)
//std::cout << "Time taken to poll: " << time_taken << " seconds. Still running: " << still_running << std::endl;
if (mc != CURLM_OK)
{
std::cerr << "curl_multi_poll failed: " << curl_multi_strerror(mc) << std::endl;
}
if (currentPolledFd > maxPolledFd)
{
maxPolledFd = currentPolledFd;
}
}
int handlesBeforeClean = handles.size();
while (CURLMsg* msg = curl_multi_info_read(multi_handle, &still_running))
{
if (msg->msg == CURLMSG_DONE)
{
// std::cout << "http request done" << std::endl;
CURL* handle = msg->easy_handle;
CURLcode result = msg->data.result;
if (result != CURLE_OK)
{
std::cerr << "Download failed: " << curl_easy_strerror(result) << std::endl;
}
handles.erase(handle);
curl_multi_remove_handle(multi_handle, handle);
curl_easy_cleanup(handle);
}
}
int handlesAfterClean = handles.size();
// print handles before clean, after clean, and max on one line, but only if there isn't any change from previous iteration.
// std::cout << "Handles before clean: " << handlesBeforeClean << " Handles after clean: " << handlesAfterClean << " Max handles: " << maxHandles << std::endl;
} while (!(!still_running && handles.empty() && chunks.size() >= batchSize));
++currentBatch;
chunks.clear();
} while (currentBatch < totalBatches);
outerEnd = clock();
std::cout << outerEnd << std::endl;
double time_taken = double(outerEnd - outerStart) / double(CLOCKS_PER_SEC);
std::cout << "Time taken to download: " << time_taken << " seconds" << std::endl;
// calculate download speed
double downloadSpeed = (fileSize / 1024) / time_taken;
std::cout << "Download speed: " << downloadSpeed << " KB/s" << std::endl;
std::cout << "max polled fd: " << maxPolledFd << std::endl;
curl_multi_cleanup(multi_handle);
curl_global_cleanup();
// Combine chunks
std::string result;
for (const auto& chunk : chunks)
{
result.append(chunk.data);
}
// Output the result
std::cout << "Downloaded data: " << result << std::endl;
return 0;
}
Maor Avni
Date: Mon, 25 Nov 2024 10:14:10 +0000
Hi,
We're having high latency issues when using CURL in our product. We've reduced the problem to the piece of code below.
Github project: https://github.com/maor-avni-hpe/curlmultipoc
Our product performs thousands of HTTP requests to Azure/AWS from a machine running on Azure/AWS. The transfer sizes vary from 128K to several MBs.
We're using the multi API.
The attached program accepts 5 arguments:
* URL
* Chunk size to download in K
* Total GB to download
* Batch Size
* Maximum Open Handles
* The program requests <batch size> downloads, each transfer <chunk size>, until reaching total GB.
* The program makes sure that no more than <max open handles> are requested from CURL.
* When a download completes its handle is removed and a new one is added until batch is completed.
The program is executed on a VM running on Azure, downloading a file from an Azure storage account, to make sure that the network is as predictable as possible. The file downloaded is a 10GB file, although we always request the same first few bytes. We ran it both on Linux and Windows.
Linux: Debian 12.8, curl 7.88
Windows: Server 2022, curl 7.88, curl 8.10
The results are for downloading a 5GB file using 128K chunks.
The results:
* single unlimited batch, 50 handles: Linux: 7s, Windows: 11s.
* single unlimited batch, 100 handles: Linux: 6s, Windows 11s.
* 50 per batch, 50 handles: Linux 12s, Windows 15s.
* 100 per batch, 50 handles: Linux 9s, Windows 14s.
* 100 per batch, 100 handles: Linux 36s, Windows 290s.
Logging the remaining FDs after each poll shows that when running 100 handles there's 1 handle that takes longer to finish.
Our product works in the 100/100 configuration. Under load we get very high latency and low data rates. This test explains why we get those slowdowns.
Is there a reasonable explanation for this slowdown?
Is it our usage of the curl API?
What can be done to improve the performance?
#include <iostream>
#include <list>
#include <set>
#include <string>
#include <thread>
#include <vector>
#include <curl/curl.h>
struct Chunk
{
std::string data;
size_t size;
};
size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp)
{
size_t totalSize = size * nmemb;
Chunk* chunk = (Chunk*)userp;
// chunk->data.append((char *)contents, totalSize);
chunk->size += totalSize;
// sleep for 0.1ms
return totalSize;
}
CURL* createCurlHandle(long start, long end, Chunk* chunk, std::string url)
{
CURL* curl = curl_easy_init();
if (curl)
{
std::string range = std::to_string(start) + "-" + std::to_string(end);
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
curl_easy_setopt(curl, CURLOPT_RANGE, range.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, chunk);
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 128L * 1024L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // Enable SSL verification
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); // Verify the host
}
return curl;
}
int main(int argc, char* argv[])
{
if (argc != 6)
{
std::cerr << "Usage: " << argv[0] << " <URL> <Chunk Size> <Total GB> <Batch Size> <Number of Concurrent Connections>" << std::endl;
return 1;
}
std::string url = argv[1];
long chunkSize = std::stol(argv[2]) * 1024;
long totalGB = std::stol(argv[3]);
int batchSize = std::stoi(argv[4]);
int numConnections = std::stoi(argv[5]);
// Initialize CURL
curl_global_init(CURL_GLOBAL_DEFAULT);
// Get the total size of the file
CURL* curl = curl_easy_init();
double fileSize = 0.0;
if (curl)
{
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
curl_easy_setopt(curl, CURLOPT_HEADER, 1L);
curl_easy_setopt(curl, CURLOPT_FILETIME, 1L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 128L * 1024L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // Enable SSL verification
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); // Verify the host
CURLcode res = curl_easy_perform(curl);
if (res == CURLE_OK)
{
curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &fileSize);
}
else
{
// get the http status code
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
std::cerr << "Failed to get file size: " << curl_easy_strerror(res) << " HTTP code: " << http_code << std::endl;
return 1;
}
curl_easy_cleanup(curl);
}
long long numOfChunks = (long long)totalGB * 1024 * 1024 * 1024 / chunkSize;
long long totalBatches;
if (batchSize == 0)
{
batchSize = numOfChunks;
totalBatches = 1;
}
else
totalBatches = numOfChunks / batchSize;
std::vector<Chunk> chunks;
std::set<CURL*> handles;
std::cout << "chunks: " << numOfChunks << std::endl;
CURLM* multi_handle = curl_multi_init();
// curl_multi_setopt(multi_handle, CURLMOPT_MAXCONNECTS, 0);
// curl_multi_setopt(multi_handle, CURLMOPT_MAX_TOTAL_CONNECTIONS, 1);
// curl_multi_setopt(multi_handle, CURLMOPT_PIPELINING, 0L);
// measure time taken to download
clock_t outerStart, outerEnd;
outerStart = clock();
std::cout << outerStart << std::endl;
int still_running = 0;
int maxHandles = 0;
int maxPolledFd = 0;
long long currentBatch = 0;
do
{
do
{
// Add new connections
while (handles.size() < numConnections && chunks.size() < batchSize)
{
long start = 0;
long end = chunkSize - 1;
if (end >= fileSize) end = fileSize - 1;
Chunk chunk;
auto handle = createCurlHandle(start, end, &chunk, url);
handles.insert(handle);
curl_multi_add_handle(multi_handle, handle);
chunks.push_back(chunk);
}
if (handles.size() > maxHandles)
{
maxHandles = handles.size();
}
// Perform connections
CURLMcode mc = curl_multi_perform(multi_handle, &still_running);
// Wait for activity, timeout or "nothing"
int currentPolledFd = 0;
if (still_running)
{
// measure call time in milliseconds
clock_t start, end;
start = clock();
mc = curl_multi_poll(multi_handle, NULL, 0, 1000, nullptr);
end = clock();
double time_taken = double(end - start) / double(CLOCKS_PER_SEC);
//if (time_taken > 0.1)
//std::cout << "Time taken to poll: " << time_taken << " seconds. Still running: " << still_running << std::endl;
if (mc != CURLM_OK)
{
std::cerr << "curl_multi_poll failed: " << curl_multi_strerror(mc) << std::endl;
}
if (currentPolledFd > maxPolledFd)
{
maxPolledFd = currentPolledFd;
}
}
int handlesBeforeClean = handles.size();
while (CURLMsg* msg = curl_multi_info_read(multi_handle, &still_running))
{
if (msg->msg == CURLMSG_DONE)
{
// std::cout << "http request done" << std::endl;
CURL* handle = msg->easy_handle;
CURLcode result = msg->data.result;
if (result != CURLE_OK)
{
std::cerr << "Download failed: " << curl_easy_strerror(result) << std::endl;
}
handles.erase(handle);
curl_multi_remove_handle(multi_handle, handle);
curl_easy_cleanup(handle);
}
}
int handlesAfterClean = handles.size();
// print handles before clean, after clean, and max on one line, but only if there isn't any change from previous iteration.
// std::cout << "Handles before clean: " << handlesBeforeClean << " Handles after clean: " << handlesAfterClean << " Max handles: " << maxHandles << std::endl;
} while (!(!still_running && handles.empty() && chunks.size() >= batchSize));
++currentBatch;
chunks.clear();
} while (currentBatch < totalBatches);
outerEnd = clock();
std::cout << outerEnd << std::endl;
double time_taken = double(outerEnd - outerStart) / double(CLOCKS_PER_SEC);
std::cout << "Time taken to download: " << time_taken << " seconds" << std::endl;
// calculate download speed
double downloadSpeed = (fileSize / 1024) / time_taken;
std::cout << "Download speed: " << downloadSpeed << " KB/s" << std::endl;
std::cout << "max polled fd: " << maxPolledFd << std::endl;
curl_multi_cleanup(multi_handle);
curl_global_cleanup();
// Combine chunks
std::string result;
for (const auto& chunk : chunks)
{
result.append(chunk.data);
}
// Output the result
std::cout << "Downloaded data: " << result << std::endl;
return 0;
}
Maor Avni
-- Unsubscribe: https://lists.haxx.se/mailman/listinfo/curl-library Etiquette: https://curl.se/mail/etiquette.htmlReceived on 2024-11-25