perf: add zero-copy forwarding using splice syscall on linux
Some checks failed
Build and Test / coverage (push) Successful in 42s
Build and Test / build (push) Failing after 11m59s

refactor: simplify config hot reload by removing stale config list
fix: improve connection event handling and buffering logic
fix: prevent multiple upstream connections by closing existing pairs
This commit is contained in:
retoor 2026-01-27 20:53:29 +01:00
parent 3abeb0e226
commit 5f60edd4ce
5 changed files with 95 additions and 20 deletions

View File

@ -14,6 +14,14 @@
## Version 0.14.0 - 2026-01-27
add zero-copy forwarding using splice syscall on linux
**Changes:** 4 files, 105 lines
**Languages:** C (105 lines)
## Version 0.13.0 - 2026-01-27
extract base64 decoding into separate module

View File

@ -14,7 +14,6 @@
static time_t config_file_mtime = 0;
app_config_t *config = NULL;
static app_config_t *stale_configs_head = NULL;
static app_config_t *config_create_from_json(cJSON *root) {
app_config_t *new_config = calloc(1, sizeof(app_config_t));
@ -105,6 +104,11 @@ int config_load(const char *filename) {
return 0;
}
struct stat st;
if (stat(filename, &st) == 0) {
config_file_mtime = st.st_mtime;
}
if (config) {
config_ref_dec(config);
}
@ -137,14 +141,6 @@ void config_free(void) {
config_ref_dec(config);
config = NULL;
}
app_config_t *current = stale_configs_head;
while (current) {
app_config_t *next = current->next;
config_ref_dec(current);
current = next;
}
stale_configs_head = NULL;
}
void config_create_default(const char *filename) {
@ -240,14 +236,16 @@ int config_hot_reload(const char *filename) {
return 0;
}
app_config_t *old_config = config;
config = new_config;
if (old_config) {
old_config->next = stale_configs_head;
stale_configs_head = old_config;
struct stat st;
if (stat(filename, &st) == 0) {
config_file_mtime = st.st_mtime;
}
if (config) {
config_ref_dec(config);
}
config = new_config;
log_info("Hot-reload complete: %d routes loaded", new_config->route_count);
return 1;
}

View File

@ -153,7 +153,8 @@ void connection_close(int fd) {
size_t buffered = pair->read_buf.tail - pair->read_buf.head;
if (buffered > 0) {
epoll_utils_modify(pair->fd, EPOLLIN | EPOLLOUT);
log_debug("Processing %zu buffered bytes from client fd %d after upstream fd %d closed", buffered, pair->fd, fd);
client_handle_read(pair);
}
} else if (conn->type == CONN_TYPE_CLIENT && pair->type == CONN_TYPE_UPSTREAM) {
pair->pair = NULL;
@ -355,7 +356,7 @@ static void handle_client_event(connection_t *conn, uint32_t events) {
int n = connection_do_read(conn);
if (n > 0) {
forwarding_handle(conn, conn->pair, 0);
} else if (n < 0) {
} else if (n <= 0) {
conn->state = CLIENT_STATE_CLOSING;
}
}
@ -401,10 +402,10 @@ static void handle_client_event(connection_t *conn, uint32_t events) {
conn->state = CLIENT_STATE_CLOSING;
} else {
conn->state = CLIENT_STATE_READING_HEADERS;
conn->read_buf.head = 0;
conn->read_buf.tail = 0;
memset(&conn->request, 0, sizeof(conn->request));
epoll_utils_modify(conn->fd, EPOLLIN);
if (buffer_available_read(&conn->read_buf) > 0) {
client_handle_read(conn);
}
}
} else {
epoll_utils_modify(conn->fd, EPOLLIN);

View File

@ -52,15 +52,77 @@ static ssize_t forwarding_write_all(connection_t *dst, const char *data, size_t
return (ssize_t)total_written;
}
#ifdef __linux__
int forwarding_try_splice(connection_t *src, connection_t *dst) {
if (src->splice_pipe[0] < 0 || src->splice_pipe[1] < 0) {
return -1;
}
ssize_t bytes_to_pipe = splice(src->fd, NULL, src->splice_pipe[1], NULL,
CHUNK_SIZE, SPLICE_F_NONBLOCK | SPLICE_F_MOVE);
if (bytes_to_pipe <= 0) {
if (bytes_to_pipe == 0) return 0;
if (errno == EAGAIN || errno == EWOULDBLOCK) return -1;
return -2;
}
ssize_t bytes_from_pipe = splice(src->splice_pipe[0], NULL, dst->fd, NULL,
(size_t)bytes_to_pipe, SPLICE_F_NONBLOCK | SPLICE_F_MOVE);
if (bytes_from_pipe < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
char discard[CHUNK_SIZE];
if (read(src->splice_pipe[0], discard, (size_t)bytes_to_pipe) < 0) {
// Ignore
}
return -1;
}
return -2;
}
src->last_activity = cached_time;
dst->last_activity = cached_time;
if (src->vhost_stats) {
monitor_record_bytes(src->vhost_stats, bytes_from_pipe, 0);
monitor_record_splice_transfer(src->vhost_stats, (size_t)bytes_from_pipe);
}
return (int)bytes_from_pipe;
}
#else
int forwarding_try_splice(connection_t *src, connection_t *dst) {
(void)src;
(void)dst;
return -1;
}
#endif
void forwarding_handle(connection_t *conn, connection_t *pair, int direction) {
if (!conn || !pair) return;
#ifdef __linux__
int is_response = (direction == 1);
if (conn->can_splice &&
buffer_available_read(&conn->read_buf) == 0 &&
buffer_available_read(&pair->write_buf) == 0 &&
(!is_response || conn->response_headers_parsed)) {
int splice_result = forwarding_try_splice(conn, pair);
if (splice_result > 0) return;
if (splice_result == 0) {
conn->state = CLIENT_STATE_CLOSING;
return;
}
if (splice_result == -2) {
conn->state = CLIENT_STATE_CLOSING;
pair->state = CLIENT_STATE_CLOSING;
return;
}
}
#endif
size_t available = conn->read_buf.tail - conn->read_buf.head;
if (available == 0) return;

View File

@ -11,6 +11,7 @@
#include "socket_utils.h"
#include "epoll_utils.h"
#include "patch.h"
#include "connection.h"
#include <stdio.h>
#include <stdlib.h>
@ -176,6 +177,11 @@ void upstream_connect(connection_t *client, const char *data, size_t data_len) {
}
}
if (client->pair) {
log_debug("Closing existing upstream fd %d before new connection for client fd %d", client->pair->fd, client->fd);
connection_close(client->pair->fd);
}
epoll_utils_add(up_fd, EPOLLIN | EPOLLOUT);
connection_t *up = &connections[up_fd];