Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 59569e3

Browse files
authoredFeb 29, 2024··
Stop limiting num-connections based on num-known-IPs (Improve S3-Express performance) (#407)
**Issue:** Disappointing S3-Express performance. **Description of changes:** Stop limiting num-connections based on num-known-IPs. **Diagnosing the issue:** We found that num-connections was never getting very high, because [num-connections scales based on the num-known-IPs](https://github.com/awslabs/aws-c-s3/blob/593c2ab24608d3e78708d51657be22f6ab99cb50/source/s3_client.c#L179). S3-Express endpoints have very few IPs, so their num-connections weren't scaling very high. The algorithm was adding 10 connections per known-IP. On a 100Gb/s machine, this maxed out at 250 connections once 25 IPs were known. But S3-Express endpoints only have 4 unique IPs, so they never got higher than 40 connections. This algorithm was written back when S3 returned 1 IP per DNS query. The intention was to throttle connections until more IPs were known, in order to spread load among S3's server fleet. However, as of Aug 2023 [S3 provides multiple IPs per DNS query](https://aws.amazon.com/about-aws/whats-new/2023/08/amazon-s3-multivalue-answer-response-dns-queries/). So now, we can scale up to max connections after the first DNS query and still be spreading load. We also believed that spreading load was a key to good performance. But I found that spreading the load didn't have much impact on performance (at least now, in 2024, on the 100Gb/s machine I was using). Tests where I hard-coded a single IP and hit it with max-connections didn't differ much from tests where the load was spread among 8 IPs or 100 IPs. I want to get this change out quickly and help S3-Express, so I picked magic numbers where the num-connections math ends up with the same result as the old algorithm. Normal S3 performance is mildly improved (max-connections is reached immediately, instead of scaling up over 30sec as it finds more IPs). S3 Express performance is MUCH improved. **Future Work:** Improve this algorithm further: - expect higher throughput on connections to S3 Express - expect lower throughput on connections transferring small objects - dynamic scaling without a bunch of magic numbers ??? (sounds cool, but I don't have any ideas how this would work yet)
1 parent 593c2ab commit 59569e3

File tree

4 files changed

+47
-104
lines changed

4 files changed

+47
-104
lines changed
 

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ The AWS-C-S3 library is an asynchronous AWS S3 client focused on maximizing thro
55
### Key features:
66
- **Automatic Request Splitting**: Improves throughput by automatically splitting the request into part-sized chunks and performing parallel uploads/downloads of these chunks over multiple connections. There's a cap on the throughput of single S3 connection, the only way to go faster is multiple parallel connections.
77
- **Automatic Retries**: Increases resilience by retrying individual failed chunks of a file transfer, eliminating the need to restart transfers from scratch after an intermittent error.
8-
- **DNS Load Balancing**: DNS resolver continuously harvests Amazon S3 IP addresses. When load is spread across the S3 fleet, overall throughput is better than if all connections were hammering the same IP simultaneously.
8+
- **DNS Load Balancing**: DNS resolver continuously harvests Amazon S3 IP addresses. When load is spread across the S3 fleet, overall throughput more reliable than if all connections are going to a single IP.
99
- **Advanced Network Management**: The client incorporates automatic request parallelization, effective timeouts and retries, and efficient connection reuse. This approach helps to maximize throughput and network utilization, and to avoid network overloads.
1010
- **Thread Pools and Async I/O**: Avoids bottlenecks associated with single-thread processing.
1111
- **Parallel Reads**: When uploading a large file from disk, reads from multiple parts of the file in parallel. This is faster than reading the file sequentially from beginning to end.

‎include/aws/s3/private/s3_client_impl.h

+3-6
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,8 @@ struct aws_s3_client {
242242
/* Throughput target in Gbps that we are trying to reach. */
243243
const double throughput_target_gbps;
244244

245-
/* The calculated ideal number of VIP's based on throughput target and throughput per vip. */
246-
const uint32_t ideal_vip_count;
245+
/* The calculated ideal number of HTTP connections, based on throughput target and throughput per connection. */
246+
const uint32_t ideal_connection_count;
247247

248248
/**
249249
* For multi-part upload, content-md5 will be calculated if the AWS_MR_CONTENT_MD5_ENABLED is specified
@@ -484,10 +484,7 @@ struct aws_s3_endpoint *aws_s3_endpoint_acquire(struct aws_s3_endpoint *endpoint
484484
void aws_s3_endpoint_release(struct aws_s3_endpoint *endpoint);
485485

486486
AWS_S3_API
487-
extern const uint32_t g_max_num_connections_per_vip;
488-
489-
AWS_S3_API
490-
extern const uint32_t g_num_conns_per_vip_meta_request_look_up[];
487+
extern const uint32_t g_min_num_connections;
491488

492489
AWS_S3_API
493490
extern const size_t g_expect_timeout_offset_ms;

‎source/s3_client.c

+24-42
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,22 @@ struct aws_s3_meta_request_work {
5151

5252
static const enum aws_log_level s_log_level_client_stats = AWS_LL_INFO;
5353

54+
/* max-requests-in-flight = ideal-num-connections * s_max_requests_multiplier */
5455
static const uint32_t s_max_requests_multiplier = 4;
5556

56-
/* TODO Provide analysis on origins of this value. */
57-
static const double s_throughput_per_vip_gbps = 4.0;
58-
59-
/* Preferred amount of active connections per meta request type. */
60-
const uint32_t g_num_conns_per_vip_meta_request_look_up[AWS_S3_META_REQUEST_TYPE_MAX] = {
61-
10, /* AWS_S3_META_REQUEST_TYPE_DEFAULT */
62-
10, /* AWS_S3_META_REQUEST_TYPE_GET_OBJECT */
63-
10, /* AWS_S3_META_REQUEST_TYPE_PUT_OBJECT */
64-
10 /* AWS_S3_META_REQUEST_TYPE_COPY_OBJECT */
65-
};
57+
/* This is used to determine the ideal number of HTTP connections. Algorithm is roughly:
58+
* num-connections-max = throughput-target-gbps / s_throughput_per_connection_gbps
59+
*
60+
* Magic value based on: match results of the previous algorithm,
61+
* where throughput-target-gpbs of 100 resulted in 250 connections.
62+
*
63+
* TODO: Improve this algorithm (expect higher throughput for S3 Express,
64+
* expect lower throughput for small objects, etc)
65+
*/
66+
static const double s_throughput_per_connection_gbps = 100.0 / 250;
6667

67-
/* Should be max of s_num_conns_per_vip_meta_request_look_up */
68-
const uint32_t g_max_num_connections_per_vip = 10;
68+
/* After throughput math, clamp the min/max number of connections */
69+
const uint32_t g_min_num_connections = 10; /* Magic value based on: 10 was old behavior */
6970

7071
/**
7172
* Default part size is 8 MiB to reach the best performance from the experiments we had.
@@ -151,32 +152,9 @@ uint32_t aws_s3_client_get_max_active_connections(
151152
struct aws_s3_client *client,
152153
struct aws_s3_meta_request *meta_request) {
153154
AWS_PRECONDITION(client);
155+
(void)meta_request;
154156

155-
uint32_t num_connections_per_vip = g_max_num_connections_per_vip;
156-
uint32_t num_vips = client->ideal_vip_count;
157-
158-
if (meta_request != NULL) {
159-
num_connections_per_vip = g_num_conns_per_vip_meta_request_look_up[meta_request->type];
160-
161-
struct aws_s3_endpoint *endpoint = meta_request->endpoint;
162-
AWS_ASSERT(endpoint != NULL);
163-
164-
AWS_ASSERT(client->vtable->get_host_address_count);
165-
size_t num_known_vips = client->vtable->get_host_address_count(
166-
client->client_bootstrap->host_resolver, endpoint->host_name, AWS_GET_HOST_ADDRESS_COUNT_RECORD_TYPE_A);
167-
168-
/* If the number of known vips is less than our ideal VIP count, clamp it. */
169-
if (num_known_vips < (size_t)num_vips) {
170-
num_vips = (uint32_t)num_known_vips;
171-
}
172-
}
173-
174-
/* We always want to allow for at least one VIP worth of connections. */
175-
if (num_vips == 0) {
176-
num_vips = 1;
177-
}
178-
179-
uint32_t max_active_connections = num_vips * num_connections_per_vip;
157+
uint32_t max_active_connections = client->ideal_connection_count;
180158

181159
if (client->max_active_connections_override > 0 &&
182160
client->max_active_connections_override < max_active_connections) {
@@ -530,7 +508,7 @@ struct aws_s3_client *aws_s3_client_new(
530508
}
531509
/* Setup cannot fail after this point. */
532510

533-
if (client_config->throughput_target_gbps != 0.0) {
511+
if (client_config->throughput_target_gbps > 0.0) {
534512
*((double *)&client->throughput_target_gbps) = client_config->throughput_target_gbps;
535513
} else {
536514
*((double *)&client->throughput_target_gbps) = s_default_throughput_target_gbps;
@@ -539,10 +517,14 @@ struct aws_s3_client *aws_s3_client_new(
539517
*((enum aws_s3_meta_request_compute_content_md5 *)&client->compute_content_md5) =
540518
client_config->compute_content_md5;
541519

542-
/* Determine how many vips are ideal by dividing target-throughput by throughput-per-vip. */
520+
/* Determine how many connections are ideal by dividing target-throughput by throughput-per-connection. */
543521
{
544-
double ideal_vip_count_double = client->throughput_target_gbps / s_throughput_per_vip_gbps;
545-
*((uint32_t *)&client->ideal_vip_count) = (uint32_t)ceil(ideal_vip_count_double);
522+
double ideal_connection_count_double = client->throughput_target_gbps / s_throughput_per_connection_gbps;
523+
/* round up and clamp */
524+
ideal_connection_count_double = ceil(ideal_connection_count_double);
525+
ideal_connection_count_double = aws_max_double(g_min_num_connections, ideal_connection_count_double);
526+
ideal_connection_count_double = aws_min_double(UINT32_MAX, ideal_connection_count_double);
527+
*(uint32_t *)&client->ideal_connection_count = (uint32_t)ideal_connection_count_double;
546528
}
547529

548530
client->cached_signing_config = aws_cached_signing_config_new(client, client_config->signing_config);
@@ -1687,7 +1669,7 @@ static bool s_s3_client_should_update_meta_request(
16871669
size_t num_known_vips = client->vtable->get_host_address_count(
16881670
client->client_bootstrap->host_resolver, endpoint->host_name, AWS_GET_HOST_ADDRESS_COUNT_RECORD_TYPE_A);
16891671
if (num_known_vips == 0 && (client->threaded_data.num_requests_being_prepared +
1690-
client->threaded_data.request_queue_size) >= g_max_num_connections_per_vip) {
1672+
client->threaded_data.request_queue_size) >= g_min_num_connections) {
16911673
return false;
16921674
}
16931675

‎tests/s3_data_plane_tests.c

+19-55
Original file line numberDiff line numberDiff line change
@@ -259,70 +259,46 @@ static int s_test_s3_client_get_max_active_connections(struct aws_allocator *all
259259

260260
struct aws_s3_client *mock_client = aws_s3_tester_mock_client_new(&tester);
261261
*((uint32_t *)&mock_client->max_active_connections_override) = 0;
262-
*((uint32_t *)&mock_client->ideal_vip_count) = 10;
262+
*((uint32_t *)&mock_client->ideal_connection_count) = 100;
263263
mock_client->client_bootstrap = &mock_client_bootstrap;
264264
mock_client->vtable->get_host_address_count = s_test_get_max_active_connections_host_address_count;
265265

266266
struct aws_s3_meta_request *mock_meta_requests[AWS_S3_META_REQUEST_TYPE_MAX];
267267

268268
for (size_t i = 0; i < AWS_S3_META_REQUEST_TYPE_MAX; ++i) {
269-
/* Verify that g_max_num_connections_per_vip and g_num_conns_per_vip_meta_request_look_up are set up
270-
* correctly.*/
271-
ASSERT_TRUE(g_max_num_connections_per_vip >= g_num_conns_per_vip_meta_request_look_up[i]);
272-
273269
/* Setup test data. */
274270
mock_meta_requests[i] = aws_s3_tester_mock_meta_request_new(&tester);
275271
mock_meta_requests[i]->type = i;
276272
mock_meta_requests[i]->endpoint = aws_s3_tester_mock_endpoint_new(&tester);
277273
}
278274

279-
/* With host count at 0, we should allow for one VIP worth of max-active-connections. */
280-
{
281-
s_test_max_active_connections_host_count = 0;
282-
283-
ASSERT_TRUE(
284-
aws_s3_client_get_max_active_connections(mock_client, NULL) ==
285-
mock_client->ideal_vip_count * g_max_num_connections_per_vip);
286-
287-
for (size_t i = 0; i < AWS_S3_META_REQUEST_TYPE_MAX; ++i) {
288-
ASSERT_TRUE(
289-
aws_s3_client_get_max_active_connections(mock_client, mock_meta_requests[i]) ==
290-
g_num_conns_per_vip_meta_request_look_up[i]);
291-
}
292-
}
293-
294275
s_test_max_active_connections_host_count = 2;
295276

296277
/* Behavior should not be affected by max_active_connections_override since it is 0, and should just be in relation
297-
* to ideal-vip-count and host-count. */
278+
* to ideal-connection-count. */
298279
{
299-
ASSERT_TRUE(
300-
aws_s3_client_get_max_active_connections(mock_client, NULL) ==
301-
mock_client->ideal_vip_count * g_max_num_connections_per_vip);
280+
ASSERT_TRUE(aws_s3_client_get_max_active_connections(mock_client, NULL) == mock_client->ideal_connection_count);
302281

303282
for (size_t i = 0; i < AWS_S3_META_REQUEST_TYPE_MAX; ++i) {
304283
ASSERT_TRUE(
305284
aws_s3_client_get_max_active_connections(mock_client, mock_meta_requests[i]) ==
306-
s_test_max_active_connections_host_count * g_num_conns_per_vip_meta_request_look_up[i]);
285+
mock_client->ideal_connection_count);
307286
}
308287
}
309288

310289
/* Max active connections override should now cap the calculated amount of active connections. */
311290
{
312291
*((uint32_t *)&mock_client->max_active_connections_override) = 3;
313292

314-
ASSERT_TRUE(
315-
mock_client->max_active_connections_override <
316-
mock_client->ideal_vip_count * g_max_num_connections_per_vip);
293+
/* Assert that override is low enough to have effect */
294+
ASSERT_TRUE(mock_client->max_active_connections_override < mock_client->ideal_connection_count);
317295

318296
ASSERT_TRUE(
319297
aws_s3_client_get_max_active_connections(mock_client, NULL) ==
320298
mock_client->max_active_connections_override);
321299

322300
for (size_t i = 0; i < AWS_S3_META_REQUEST_TYPE_MAX; ++i) {
323-
ASSERT_TRUE(
324-
mock_client->max_active_connections_override <
325-
s_test_max_active_connections_host_count * g_num_conns_per_vip_meta_request_look_up[i]);
301+
ASSERT_TRUE(mock_client->max_active_connections_override < mock_client->ideal_connection_count);
326302

327303
ASSERT_TRUE(
328304
aws_s3_client_get_max_active_connections(mock_client, mock_meta_requests[i]) ==
@@ -334,22 +310,17 @@ static int s_test_s3_client_get_max_active_connections(struct aws_allocator *all
334310
{
335311
*((uint32_t *)&mock_client->max_active_connections_override) = 100000;
336312

337-
ASSERT_TRUE(
338-
mock_client->max_active_connections_override >
339-
mock_client->ideal_vip_count * g_max_num_connections_per_vip);
313+
/* Assert that override is NOT low enough to have effect */
314+
ASSERT_TRUE(mock_client->max_active_connections_override > mock_client->ideal_connection_count);
340315

341-
ASSERT_TRUE(
342-
aws_s3_client_get_max_active_connections(mock_client, NULL) ==
343-
mock_client->ideal_vip_count * g_max_num_connections_per_vip);
316+
ASSERT_TRUE(aws_s3_client_get_max_active_connections(mock_client, NULL) == mock_client->ideal_connection_count);
344317

345318
for (size_t i = 0; i < AWS_S3_META_REQUEST_TYPE_MAX; ++i) {
346-
ASSERT_TRUE(
347-
mock_client->max_active_connections_override >
348-
s_test_max_active_connections_host_count * g_num_conns_per_vip_meta_request_look_up[i]);
319+
ASSERT_TRUE(mock_client->max_active_connections_override > mock_client->ideal_connection_count);
349320

350321
ASSERT_TRUE(
351322
aws_s3_client_get_max_active_connections(mock_client, mock_meta_requests[i]) ==
352-
s_test_max_active_connections_host_count * g_num_conns_per_vip_meta_request_look_up[i]);
323+
mock_client->ideal_connection_count);
353324
}
354325
}
355326

@@ -822,12 +793,12 @@ static int s_test_s3_update_meta_requests_trigger_prepare(struct aws_allocator *
822793
struct aws_client_bootstrap mock_bootstrap;
823794
AWS_ZERO_STRUCT(mock_bootstrap);
824795

825-
const uint32_t ideal_vip_count = 10;
796+
const uint32_t ideal_connection_count = 100;
826797

827798
struct aws_s3_client *mock_client = aws_s3_tester_mock_client_new(&tester);
828799
mock_client->client_bootstrap = &mock_bootstrap;
829800
mock_client->vtable->get_host_address_count = s_test_s3_update_meta_request_trigger_prepare_get_host_address_count;
830-
*((uint32_t *)&mock_client->ideal_vip_count) = ideal_vip_count;
801+
*((uint32_t *)&mock_client->ideal_connection_count) = ideal_connection_count;
831802
aws_linked_list_init(&mock_client->threaded_data.request_queue);
832803
aws_linked_list_init(&mock_client->threaded_data.meta_requests);
833804

@@ -872,27 +843,20 @@ static int s_test_s3_update_meta_requests_trigger_prepare(struct aws_allocator *
872843
&mock_meta_request_with_work->client_process_work_threaded_data.node);
873844
aws_s3_meta_request_acquire(mock_meta_request_with_work);
874845

875-
/* With no known addresses, the amount of requests that can be prepared should only be enough for one VIP. */
846+
/* With no known addresses, the amount of requests that can be prepared should be lower. */
876847
{
877848
s_test_s3_update_meta_request_trigger_prepare_host_address_count = 0;
878849
aws_s3_client_update_meta_requests_threaded(mock_client);
879850

880851
ASSERT_SUCCESS(s_validate_prepared_requests(
881-
mock_client, g_max_num_connections_per_vip, mock_meta_request_with_work, mock_meta_request_without_work));
852+
mock_client, g_min_num_connections, mock_meta_request_with_work, mock_meta_request_without_work));
882853
}
883854

884-
/* When the number of known addresses is greater than or equal to the ideal vip count, the max number of requests
885-
* should be reached. */
855+
/* When the number of known addresses is 1+, the max number of requests should be reached. */
886856
{
887857
const uint32_t max_requests_prepare = aws_s3_client_get_max_requests_prepare(mock_client);
888858

889-
s_test_s3_update_meta_request_trigger_prepare_host_address_count = (size_t)(ideal_vip_count);
890-
aws_s3_client_update_meta_requests_threaded(mock_client);
891-
892-
ASSERT_SUCCESS(s_validate_prepared_requests(
893-
mock_client, max_requests_prepare, mock_meta_request_with_work, mock_meta_request_without_work));
894-
895-
s_test_s3_update_meta_request_trigger_prepare_host_address_count = (size_t)(ideal_vip_count + 1);
859+
s_test_s3_update_meta_request_trigger_prepare_host_address_count = 1;
896860
aws_s3_client_update_meta_requests_threaded(mock_client);
897861

898862
ASSERT_SUCCESS(s_validate_prepared_requests(
@@ -980,7 +944,7 @@ static int s_test_s3_client_update_connections_finish_result(struct aws_allocato
980944
mock_client->vtable->create_connection_for_request =
981945
s_s3_test_meta_request_has_finish_result_client_create_connection_for_request;
982946

983-
*((uint32_t *)&mock_client->ideal_vip_count) = 1;
947+
*((uint32_t *)&mock_client->ideal_connection_count) = 1;
984948

985949
aws_linked_list_init(&mock_client->threaded_data.request_queue);
986950

0 commit comments

Comments
 (0)
Please sign in to comment.