/*compile-command: (set -x; gcc -g -O2 -Wall -Wshadow -Wswitch -Wmissing-declarations -lm -lrt \ -o "${0/.c/}" "$0") exit $? */ /* * loadtest.c: Perform a simple load test on an HTTP server. * Written by Andrew Church * This program is placed in the public domain. * * Usage: loadtest url|[random-seed]@url-list [test-params] * test-params: start-rps [stop-rps [step-size [step-interval [hold-period * [request-timeout]]]]] * defaults: 1 10 1 10 60 60 * * Load testing starts at start-rps requests per second, increasing by * step-size requests/second every step-interval seconds until reaching * (but not exceeding) stop-rps. Each request is given request-timeout * seconds to receive a complete response, after which time the request is * considered to have timed out and is aborted. Requests are then sent at * the final rate for hold-period seconds, after which no more requests are * sent and the process waits for all pending requests to complete or time * out. * * If given a single URL, the same URL is used for all requests; if given * a URL list, URLs will be selected pseudorandomly from the file. The * pseudorandom sequence is initialized with the integer random-seed, * which is taken to be zero if omitted. Note that the IP address lookup * will only be performed once per URL. * * Results are printed to stdout as a set of space-separated fields (all * integers, with times in nanoseconds): * 1: Elapsed time since the beginning of the test. * 2: Number of requests sent. * 3: Number of 2xx responses. * 4: Number of 3xx responses. * 5: Number of 4xx responses. * 6: Number of 5xx responses. * 7: Number of empty responses (server disconnected without sending data). * 8: Number of unrecognized or invalid responses. * 9: Number of connection failures. * 10: Number of timed-out responses. * 11: Total latency of 2xx responses. * 12: Total latency of 3xx responses. * 13: Total latency of 4xx responses. * 14: Total latency of 5xx responses. * 15: Total latency of empty responses. * 16: Total latency of unrecognized or invalid responses. * 17: Total latency of connection failures. * Latency values represent the sum over all associated responses of the * time between the initial connect() call and: * - for valid HTTP responses, receipt of the last byte of the body; * - for empty HTTP responses, detection of the closed connection; * - for invalid responses, receipt of enough data to verify that the * response is invalid (typically the first TCP packet); * - for connection failures, the error return from connect(). * * General test progress information is written to stderr. * * Note that all processing is done in a single thread; thus local system * latency (particularly in writing to stdout or stderr) may affect the * results. */ /*************************************************************************/ /*************************************************************************/ #define index builtin_index #define time builtin_time #include #include #include #include #include #include #include #include #include #include #include #include #include #undef index #undef time #if defined(__GNUC__) || defined(__clang__) # define PURE_FUNCTION __attribute__((pure)) #else # define PURE_FUNCTION /*nothing*/ #endif /*-----------------------------------------------------------------------*/ /* Data for each URL to test. */ typedef struct URLInfo URLInfo; struct URLInfo { char *url; // The entire URL (only kept for reference purposes). char *host; // The hostname to pass in the Host: header. char *path; // The path to pass in the GET request. struct sockaddr_in address; // The IP address and port to connect to. }; static URLInfo *urls; static unsigned int num_urls; /* Data for each step of the load profile. */ typedef struct LoadProfile LoadProfile; struct LoadProfile { uint64_t ns_per_request; // Time between requests, in nanoseconds. uint32_t num_requests; // Number of requests to perform. }; static LoadProfile *load_profiles; int num_load_profiles; /* Data for each open connection. */ typedef struct Connection Connection; struct Connection { struct timespec connect_time; // Time of the initial connect() call. int socket; // File descriptor for the socket. enum {CONNECT, SEND_REQUEST, READ_RESPONSE, READ_BODY} state; unsigned int response_group; // Response code group (2=2xx, 3=3xx, etc.). struct sockaddr *address; // Address to which the socket is connecting. unsigned int address_size; // Size of address data. uint32_t data_len; // Bytes of data stored in the I/O buffer. char *buffer; // I/O buffer (allocated separately). }; static Connection *connections; /* Number of actively-used connections (the array is allocated large enough * for all connections to fit, so we don't bother keeping track of the size). * The array is kept packed, so all entries after this are also free. */ static unsigned int num_connections; /* Buffer for poll(), allocated with the same number of elements as * connections[]. */ struct pollfd *pollfds; /* Buffer size for connection I/O. A buffer of this size will be allocated * for each active connection. */ #define CONNECTION_BUFFER 65536 /* Load test parameters. */ static double start_rps = 1; static double stop_rps = 10; static double step_size = 1; static double step_interval = 10; static double hold_period = 60; static double request_timeout = 60; static uint64_t request_timeout_ns; // Calculated at runtime. /* Clock ID for use with clock_gettime(). */ static clockid_t clock_id; /* Start time of the load test. */ static struct timespec start_time; /* Running totals of load test data. */ static uint64_t requests_sent; static uint64_t responses_2xx; static uint64_t responses_3xx; static uint64_t responses_4xx; static uint64_t responses_5xx; static uint64_t responses_empty; static uint64_t responses_invalid; static uint64_t responses_failed; static uint64_t responses_timeout; static uint64_t latency_2xx; static uint64_t latency_3xx; static uint64_t latency_4xx; static uint64_t latency_5xx; static uint64_t latency_empty; static uint64_t latency_invalid; static uint64_t latency_failed; /*-----------------------------------------------------------------------*/ /* Local function declarations. */ /** * add_url: Parse and add the given URL to the global URL list. * * [Parameters] * url: URL to add. * [Return value] * True (nonzero) on success, false (zero) on error. */ static int add_url(const char *url); /** * add_load_profile: Add the given load profile to the global profile list. * * [Parameters] * rps: Requests per second for this profile. * time: Length of time to execute this profile. * [Return value] * True (nonzero) on success, false (zero) on error. */ static int add_load_profile(double rps, double time); /** * start_request: Start a request for the given URL. * * [Parameters] * url: URLInfo structure for URL to request. * [Return value] * True (nonzero) on success, false (zero) on error (other than * connection failure, which is treated as a successful attempt and * counted as a failed request). */ static int start_request(const URLInfo *url); /** * check_all_connections: Check the state of each connection and perform * any pending I/O possible, or return if no I/O is possible before the * specified timeout. * * [Parameters] * timeout: Timeout in nanoseconds, or a negative value for no timeout. * [Return value] * None. */ static void check_all_connections(int64_t timeout); /** * end_connection: Update request counter and latency variables for a * completed request, and clear the associated connection from the table. * * [Parameters] * index: Index of connection in the connections[] array. * counter_ptr: Pointer to counter variable to update. * latency_ptr: Pointer to latency variable to update, or NULL if none. * [Return value] * None. */ static void end_connection(unsigned int index, uint64_t *counter_ptr, uint64_t *latency_ptr); /** * output_stats: Write load test statistics to standard output. * * [Parameters] * now: Timestamp to output as the first field (in nanoseconds). * [Return value] * None. */ static void output_stats(uint64_t now); /** * timespec_sub: Return the difference between the times indicated by * two timespec structures, in nanoseconds. * * [Parameters] * a, b: Pointers to timespec structures. * [Return value] * a - b */ static inline PURE_FUNCTION int64_t timespec_sub( const struct timespec * const a, const struct timespec * const b); /*************************************************************************/ /*************************************************************************/ int main(int argc, char **argv) { /* Make sure load test outputs get written once per line even if * writing to a file/pipe. */ setvbuf(stdout, NULL, _IOLBF, 0); if (argc < 2 || argv[1][0] == '-') { usage: fprintf(stderr, "\ Usage: %s url|[random-seed]@url-list [test-params]\n\ test-params: start-rps [stop-rps [step-size [step-interval [hold-period\n\ [request-timeout]]]]]\n\ defaults: 1 10 1 10 60 60\n", argv[0]); return 1; } /* Set up test parameters. */ #define PARSE_ARG(index, var) do { \ const int __index = (index); \ if (argc > __index) { \ const char *__arg = argv[__index]; \ char *s; \ var = strtod(__arg, &s); \ if (*s != 0 || var <= 0) { \ char namebuf[100]; \ snprintf(namebuf, sizeof(namebuf), "%s", #var); \ s = strchr(namebuf, '_'); \ if (s) { \ *s = 0; \ } \ fprintf(stderr, "Invalid value for %s: %s", namebuf, __arg); \ goto usage; \ } \ } \ } while (0) PARSE_ARG(2, start_rps); PARSE_ARG(3, stop_rps); PARSE_ARG(4, step_size); PARSE_ARG(5, step_interval); PARSE_ARG(6, hold_period); PARSE_ARG(7, request_timeout); #undef PARSE_ARG request_timeout_ns = (uint64_t)round(request_timeout * 1.0e9); /* Parse the test URL(s). */ char *s; const unsigned long random_seed = strtoul(argv[1], &s, 0); srand((unsigned int)random_seed); if (*s != '@') { if (!add_url(argv[1])) { return 1; } } else { fprintf(stderr, "Reading URLs from %s...\n", s+1); FILE *f = fopen(s+1, "r"); if (!f) { perror(s+1); return 1; } int line = 0; char buf[10000]; while (fgets(buf, sizeof(buf), f)) { line++; unsigned int len = strlen(buf); if (len > 0 && buf[len-1] == '\n') { buf[--len] = 0; } else { /* Could just be a missing newline at the end of the file; * see if we can read any more data before erroring out. */ if (fgetc(f) != EOF) { fprintf(stderr, "%s: line %d: URL too long (%u bytes" " maximum), aborting.\n", s+1, line, (unsigned int)sizeof(buf)); fclose(f); return 1; } } if (len > 0 && buf[len-1] == '\r') { buf[--len] = 0; } if (!add_url(buf)) { fprintf(stderr, "%s: line %d: Failed to add URL, aborting.\n", s+1, line); fclose(f); return 1; } } fclose(f); fprintf(stderr, "...Done.\n"); if (!num_urls) { fprintf(stderr, "No URLs found in file!\n"); return 1; } } /* Calculate the load profile. */ double rps; for (rps = start_rps; rps + step_size <= stop_rps; rps += step_size) { if (!add_load_profile(rps, step_interval)) { return 1; } } if (!add_load_profile(rps, hold_period)) { return 1; } /* Allocate connection records for all connections that will be made. */ uint32_t total_requests = 0; unsigned int i; for (i = 0; i < num_load_profiles; i++) { total_requests += load_profiles[i].num_requests; } if (total_requests == 0) { fprintf(stderr, "No requests to be made!\n"); return 1; } connections = malloc(sizeof(*connections) * total_requests); if (!connections) { fprintf(stderr, "No memory for connection array!\n"); return 1; } num_connections = 0; pollfds = malloc(sizeof(*pollfds) * total_requests); if (!pollfds) { fprintf(stderr, "No memory for poll() array!\n"); return 1; } /* Perform the load test. */ requests_sent = 0; responses_2xx = 0; responses_3xx = 0; responses_4xx = 0; responses_5xx = 0; responses_invalid = 0; responses_failed = 0; responses_timeout = 0; latency_2xx = 0; latency_3xx = 0; latency_4xx = 0; latency_5xx = 0; latency_invalid = 0; latency_failed = 0; fprintf(stderr, "Starting load test.\n"); if (clock_gettime(CLOCK_MONOTONIC, &start_time) == 0) { clock_id = CLOCK_MONOTONIC; } else { if (clock_gettime(CLOCK_REALTIME, &start_time) != 0) { perror("clock_gettime(CLOCK_REALTIME)"); return 1; } clock_id = CLOCK_REALTIME; } uint64_t next_request_time = 0; uint64_t next_output_time = 1000000000; for (i = 0; i < num_load_profiles; i++) { uint32_t requests_left = load_profiles[i].num_requests; const uint64_t request_delay = load_profiles[i].ns_per_request; fprintf(stderr, "Load profile: %g rps for %g seconds\n", 1.0e9 / request_delay, request_delay * requests_left / 1.0e9); while (requests_left > 0) { struct timespec now_ts; clock_gettime(clock_id, &now_ts); uint64_t now = timespec_sub(&now_ts, &start_time); while (now >= next_request_time) { if (requests_left == 0) { break; } const unsigned int url_index = (unsigned int)rand() % num_urls; if (!start_request(&urls[url_index])) { fprintf(stderr, "Failed to start request for %s," " aborting.\n", urls[url_index].url); return 1; } requests_left--; next_request_time += request_delay; clock_gettime(clock_id, &now_ts); now = timespec_sub(&now_ts, &start_time); } check_all_connections(next_request_time - now); clock_gettime(clock_id, &now_ts); now = timespec_sub(&now_ts, &start_time); if (now >= next_output_time) { output_stats(now); while (now >= next_output_time) { next_output_time += 1000000000; } } } } fprintf(stderr, "Waiting for all requests to finish...\n"); while (num_connections > 0) { check_all_connections(-1); struct timespec now_ts; clock_gettime(clock_id, &now_ts); uint64_t now = timespec_sub(&now_ts, &start_time); if (now >= next_output_time) { output_stats(now); while (now >= next_output_time) { next_output_time += 1000000000; } } } struct timespec now_ts; clock_gettime(clock_id, &now_ts); output_stats(timespec_sub(&now_ts, &start_time)); fprintf(stderr, "Load test complete.\n"); return 0; } /*-----------------------------------------------------------------------*/ static int add_url(const char *url) { /* First parse the URL, making sure it's valid. */ unsigned int default_port; const char *protocol_end = url + strspn(url, "abcdefghijklmnopqrstuvwxyz"); if (strncmp(protocol_end, "://", 3) != 0) { fprintf(stderr, "Malformed URL: %s\n", url); return 0; } if (protocol_end - url == 4 && strncmp(url, "http", 4) == 0) { default_port = 80; } else { fprintf(stderr, "Protocol %.*s not supported for URL: %s\n", (int)(protocol_end - url), url, url); return 0; } const char *host = protocol_end + 3; const char *path = host + strcspn(host, "/"); const char *host_end = path; if (strcspn(host, "@") < host_end - host) { host += strcspn(host, "@") + 1; } if (strcspn(host, ":") < host_end - host) { host_end = host + strcspn(host, ":"); } if (host_end == host) { fprintf(stderr, "Hostname missing from URL: %s\n", url); return 0; } unsigned int port = default_port; if (*host_end == ':') { if (host_end+1 == path) { fprintf(stderr, "Port number missing from URL: %s\n", url); return 0; } char *s; port = strtoul(host_end+1, &s, 10); if (s != path) { fprintf(stderr, "Invalid port number %.*s in URL: %s\n", (int)(path - (host_end+1)), host_end+1, url); return 0; } if (port == 0 || port > 65535) { fprintf(stderr, "Port number out of range in URL: %s\n", url); return 0; } } /* Look up the IP address for the hostname. */ struct sockaddr_in address; char hostbuf[10000]; if (snprintf(hostbuf, sizeof(hostbuf), "%.*s", (int)(host_end - host), host) >= sizeof(hostbuf)) { fprintf(stderr, "Buffer overflow on hostname: %.*s\n", (int)(host_end - host), host); return 0; } struct addrinfo *lookup; const struct addrinfo lookup_hints = {.ai_family = AF_INET}; int error = getaddrinfo(hostbuf, NULL, &lookup_hints, &lookup); if (error != 0) { fprintf(stderr, "Failed to resolve hostname %s: %s\n", hostbuf, gai_strerror(error)); return 0; } memcpy(&address, lookup->ai_addr, sizeof(address)); freeaddrinfo(lookup); address.sin_port = htons(port); /* Now add the (verified) data to the URL array. */ urls = realloc(urls, sizeof(*urls) * (num_urls + 1)); if (!urls) { goto nomem_return; } URLInfo * const url_info = &urls[num_urls]; url_info->url = strdup(url); if (!url_info->url) { goto nomem_return; } url_info->host = malloc((host_end - host) + 1); if (!url_info->host) { goto nomem_free_url; } memcpy(url_info->host, host, host_end - host); url_info->host[host_end - host] = 0; url_info->path = strdup(*path ? path : "/"); if (!url_info->path) { goto nomem_free_host; } url_info->address = address; num_urls++; return 1; nomem_free_host: free(url_info->host); nomem_free_url: free(url_info->url); nomem_return: fprintf(stderr, "Out of memory!\n"); return 0; } /*-----------------------------------------------------------------------*/ static int add_load_profile(double rps, double time) { load_profiles = realloc(load_profiles, sizeof(*load_profiles) * (num_load_profiles + 1)); if (!load_profiles) { fprintf(stderr, "Out of memory!\n"); return 0; } LoadProfile * const load_profile = &load_profiles[num_load_profiles]; load_profile->ns_per_request = (uint64_t)round(1.0e9 / rps); load_profile->num_requests = (uint32_t)round(rps * time); num_load_profiles++; return 1; } /*-----------------------------------------------------------------------*/ static int start_request(const URLInfo *url) { char *buffer = malloc(CONNECTION_BUFFER); if (!buffer) { fprintf(stderr, "No memory for connection buffer to %s\n", url->url); return 0; } const uint32_t data_len = snprintf(buffer, CONNECTION_BUFFER, "GET %s HTTP/1.1\r\nHost: %s\r\n" "Connection: close\r\n\r\n", url->path, url->host); if (data_len >= CONNECTION_BUFFER) { fprintf(stderr, "Send buffer overflow for URL: %s\n", url->url); free(buffer); return 0; } const int sock = socket(url->address.sin_family, SOCK_STREAM, IPPROTO_TCP); if (sock < 0) { perror("socket()"); free(buffer); return 0; } if (fcntl(sock, F_SETFL, O_NONBLOCK) != 0) { perror("fcntl(sock, F_SETFL, O_NONBLOCK)"); close(sock); free(buffer); return 0; } Connection * const conn = &connections[num_connections++]; conn->socket = sock; conn->address = (struct sockaddr *)&url->address; conn->address_size = sizeof(url->address); conn->buffer = buffer; conn->data_len = data_len; clock_gettime(clock_id, &conn->connect_time); const int result = connect(sock, conn->address, conn->address_size); if (result == 0) { conn->state = SEND_REQUEST; } else if (errno == EINPROGRESS) { conn->state = CONNECT; } else { end_connection(num_connections-1, &responses_failed, &latency_failed); } requests_sent++; return 1; } /*-----------------------------------------------------------------------*/ static void check_all_connections(int64_t timeout) { unsigned int i; struct timespec now_ts; clock_gettime(clock_id, &now_ts); for (i = 0; i < num_connections; i++) { if (timespec_sub(&now_ts, &connections[i].connect_time) > request_timeout_ns ) { end_connection(i, &responses_timeout, NULL); i--; } else { pollfds[i].fd = connections[i].socket; if (connections[i].state == CONNECT || connections[i].state == SEND_REQUEST ) { pollfds[i].events = POLLOUT; } else { pollfds[i].events = POLLIN; } } } if (num_connections == 0) { return; // Nothing to do! } int timeout_ms; if (timeout >= 0) { timeout_ms = timeout / 1000000; } else { timeout_ms = -1; } const int result = poll(pollfds, num_connections, timeout_ms); if (result < 0) { perror("poll()"); return; } else if (result == 0) { return; // No activity before the timeout. } for (i = 0; i < num_connections; i++) { Connection * const conn = &connections[i]; if (!pollfds[i].revents) { continue; } switch (conn->state) { case CONNECT: { const int connect_result = connect(conn->socket, conn->address, conn->address_size); if (connect_result != 0) { end_connection(i, &responses_failed, &latency_failed); i--; break; } conn->state = SEND_REQUEST; /* Fall through to SEND_REQUEST processing. */ } case SEND_REQUEST: { const ssize_t bytes_sent = send(conn->socket, conn->buffer, conn->data_len, MSG_NOSIGNAL); if (bytes_sent < 0) { end_connection(i, &responses_failed, &latency_failed); i--; break; } else if ((size_t)bytes_sent < conn->data_len) { conn->data_len -= bytes_sent; memmove(conn->buffer, conn->buffer + bytes_sent, conn->data_len); } else { conn->data_len = 0; conn->state = READ_RESPONSE; } break; } case READ_RESPONSE: { const ssize_t bytes_read = recv( conn->socket, conn->buffer + conn->data_len, CONNECTION_BUFFER - conn->data_len, 0 ); if (bytes_read == 0 && conn->data_len == 0) { end_connection(i, &responses_empty, &latency_empty); i--; break; } else if (bytes_read <= 0) { end_connection(i, &responses_invalid, &latency_invalid); i--; break; } conn->data_len += bytes_read; if (conn->data_len >= 9 && memcmp(conn->buffer, "HTTP/1.1 ", 9) != 0 && memcmp(conn->buffer, "HTTP/1.0 ", 9) != 0 ) { end_connection(i, &responses_invalid, &latency_invalid); i--; break; } if (conn->data_len >= 13 && ((conn->buffer[ 9] < '0' || conn->buffer[ 9] > '9') || (conn->buffer[10] < '0' || conn->buffer[10] > '9') || (conn->buffer[11] < '0' || conn->buffer[11] > '9') || (conn->buffer[12] != ' ')) ) { end_connection(i, &responses_invalid, &latency_invalid); i--; break; } conn->response_group = conn->buffer[9] - '0'; conn->state = READ_BODY; /* Fall through to READ_BODY processing. */ } case READ_BODY: { const ssize_t bytes_read = recv(conn->socket, conn->buffer, CONNECTION_BUFFER, 0); if (bytes_read < 0 && errno == EAGAIN) { /* Could happen if we fall through from READ_RESPONSE. */ } else if (bytes_read < 0) { end_connection(i, &responses_invalid, &latency_invalid); i--; } else if (bytes_read == 0) { uint64_t *counter_ptr, *latency_ptr; if (conn->response_group == 2) { counter_ptr = &responses_2xx; latency_ptr = &latency_2xx; } else if (conn->response_group == 3) { counter_ptr = &responses_3xx; latency_ptr = &latency_3xx; } else if (conn->response_group == 4) { counter_ptr = &responses_4xx; latency_ptr = &latency_4xx; } else if (conn->response_group == 5) { counter_ptr = &responses_5xx; latency_ptr = &latency_5xx; } else { counter_ptr = &responses_invalid; latency_ptr = &latency_invalid; } end_connection(i, counter_ptr, latency_ptr); i--; } break; } } // switch (conn->state) } // for (i = 0; i < num_connections; i++) } /*-----------------------------------------------------------------------*/ static void end_connection(unsigned int index, uint64_t *counter_ptr, uint64_t *latency_ptr) { Connection * const conn = &connections[index]; struct timespec now_ts; clock_gettime(clock_id, &now_ts); (*counter_ptr)++; if (latency_ptr) { (*latency_ptr) += timespec_sub(&now_ts, &conn->connect_time); } shutdown(conn->socket, SHUT_RDWR); close(conn->socket); free(conn->buffer); num_connections--; if (index < num_connections) { *conn = connections[num_connections]; pollfds[index].revents = pollfds[num_connections].revents; } } /*-----------------------------------------------------------------------*/ static void output_stats(uint64_t now) { printf("%llu %llu %llu %llu %llu %llu %llu %llu %llu" " %llu %llu %llu %llu %llu %llu %llu %llu\n", (unsigned long long)now, (unsigned long long)requests_sent, (unsigned long long)responses_2xx, (unsigned long long)responses_3xx, (unsigned long long)responses_4xx, (unsigned long long)responses_5xx, (unsigned long long)responses_empty, (unsigned long long)responses_invalid, (unsigned long long)responses_failed, (unsigned long long)responses_timeout, (unsigned long long)latency_2xx, (unsigned long long)latency_3xx, (unsigned long long)latency_4xx, (unsigned long long)latency_5xx, (unsigned long long)latency_empty, (unsigned long long)latency_invalid, (unsigned long long)latency_failed); } /*-----------------------------------------------------------------------*/ static inline PURE_FUNCTION int64_t timespec_sub( const struct timespec * const a, const struct timespec * const b) { const int64_t a_ns = (a->tv_sec * (int64_t)1000000000) + a->tv_nsec; const int64_t b_ns = (b->tv_sec * (int64_t)1000000000) + b->tv_nsec; return a_ns - b_ns; } /*************************************************************************/ /*************************************************************************/ /* * Local variables: * c-file-style: "stroustrup" * c-file-offsets: ((case-label . *) (statement-case-intro . *)) * indent-tabs-mode: nil * End: * * vim: expandtab shiftwidth=4: */