MEDIUM: connection: move idle connection trees to ceb64

Idle connection trees currently require a 56-byte conn_hash_node per
connection, which can be reduced to 32 bytes by moving to ceb64. While
ceb64 is theoretically slower, in practice here we're essentially
dealing with trees that almost always contain a single key and many
duplicates. In this case, ceb64 insert and lookup functions become
faster than eb64 ones because all duplicates are a list accessed in
O(1) while it's a subtree for eb64. In tests it is impossible to tell
the difference between the two, so it's worth reducing the memory
usage.

This commit brings the following memory savings to conn_hash_node
(one per backend connection), and to srv_per_thread (one per thread
and per server):

     struct       before  after  delta
  conn_hash_nodea   56     32     -24
  srv_per_thread    96     72     -24

The delicate part is conn_delete_from_tree(), because we need to
know the tree root the connection is attached to. But thanks to
recent cleanups, it's now clear enough (i.e. idle/safe/avail vs
session are easy to distinguish).
This commit is contained in:
Willy Tarreau 2025-09-12 15:28:33 +02:00
parent 95b8adff67
commit ceaf8c1220
11 changed files with 73 additions and 60 deletions

View File

@ -28,7 +28,7 @@
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <import/ebtree-t.h>
#include <import/cebtree.h>
#include <import/ist.h>
#include <haproxy/api-t.h>
@ -566,7 +566,7 @@ enum conn_hash_params_t {
#define CONN_HASH_PARAMS_TYPE_COUNT 7
#define CONN_HASH_PAYLOAD_LEN \
(((sizeof(((struct conn_hash_node *)0)->node.key)) * 8) - CONN_HASH_PARAMS_TYPE_COUNT)
(((sizeof(((struct conn_hash_node *)0)->key)) * 8) - CONN_HASH_PARAMS_TYPE_COUNT)
#define CONN_HASH_GET_PAYLOAD(hash) \
(((hash) << CONN_HASH_PARAMS_TYPE_COUNT) >> CONN_HASH_PARAMS_TYPE_COUNT)
@ -660,7 +660,8 @@ struct connection {
* A connection is identified by a hash generated from its specific parameters
*/
struct conn_hash_node {
struct eb64_node node; /* contains the hashing key */
struct ceb_node node; /* indexes the hashing key for safe/idle/avail */
uint64_t key; /* the hashing key, also used by session-owned */
struct connection *conn; /* connection owner of the node */
};

View File

@ -265,15 +265,17 @@ struct tree_occ {
/* Each server will have one occurrence of this structure per thread */
struct srv_per_thread {
struct mt_list streams; /* streams using this server (used by "shutdown server sessions") */
struct eb_root idle_conns; /* Shareable idle connections */
struct eb_root safe_conns; /* Safe idle connections */
struct eb_root avail_conns; /* Connections in use, but with still new streams available */
struct mt_list sess_conns; /* Connections attached to a session which cannot be shared across clients */
/* Secondary idle conn storage used in parallel to idle/safe trees.
* Used to sort them by last usage and purge them in reverse order.
*/
struct list idle_conn_list;
/* connection trees to look them up by name */
struct ceb_root *idle_conns; /* Shareable idle connections */
struct ceb_root *safe_conns; /* Safe idle connections */
struct ceb_root *avail_conns; /* Connections in use, but with still new streams available */
};
/* Each server will have one occurrence of this structure per thread group */

View File

@ -96,8 +96,8 @@ int snr_resolution_error_cb(struct resolv_requester *requester, int error_code);
struct server *snr_check_ip_callback(struct server *srv, void *ip, unsigned char *ip_family);
struct task *srv_cleanup_idle_conns(struct task *task, void *ctx, unsigned int state);
void srv_release_conn(struct server *srv, struct connection *conn);
struct connection *srv_lookup_conn(struct eb_root *tree, uint64_t hash);
struct connection *srv_lookup_conn_next(struct connection *conn);
struct connection *srv_lookup_conn(struct ceb_root **tree, uint64_t hash);
struct connection *srv_lookup_conn_next(struct ceb_root **tree, struct connection *conn);
void _srv_add_idle(struct server *srv, struct connection *conn, int is_safe);
int srv_add_to_idle_list(struct server *srv, struct connection *conn, int is_safe);

View File

@ -18,7 +18,7 @@
#include <ctype.h>
#include <sys/types.h>
#include <import/ebmbtree.h>
#include <import/ceb64_tree.h>
#include <haproxy/api.h>
#include <haproxy/acl.h>
@ -1365,7 +1365,7 @@ check_tgid:
* tree; unsafe requests are looked up in the safe conns tree.
*/
int search_tree = is_safe ? 1 : 0; // 0 = idle, 1 = safe
struct eb_root *tree;
struct ceb_root **tree;
if (!srv->curr_idle_thr[i] || i == tid)
continue;
@ -1387,7 +1387,7 @@ check_tgid:
found = 1;
break;
}
conn = srv_lookup_conn_next(conn);
conn = srv_lookup_conn_next(tree, conn);
}
} while (!found && ++search_tree <= 1);
@ -1477,20 +1477,19 @@ static int do_connect_server(struct stream *s, struct connection *conn)
* if any.
*/
static struct connection *
takeover_random_idle_conn(struct eb_root *root, int curtid)
takeover_random_idle_conn(struct ceb_root **root, int curtid)
{
struct conn_hash_node *hash_node;
struct connection *conn = NULL;
struct eb64_node *node = eb64_first(root);
while (node) {
hash_node = eb64_entry(node, struct conn_hash_node, node);
hash_node = ceb64_item_first(root, node, key, struct conn_hash_node);
while (hash_node) {
conn = hash_node->conn;
if (conn && conn->mux->takeover && conn->mux->takeover(conn, curtid, 1) == 0) {
if (conn->mux->takeover && conn->mux->takeover(conn, curtid, 1) == 0) {
conn_delete_from_tree(conn, curtid);
return conn;
}
node = eb64_next(node);
hash_node = ceb64_item_next(root, node, key, hash_node);
}
return NULL;
@ -1696,7 +1695,7 @@ int be_reuse_connection(int64_t hash, struct session *sess,
* Idle conns are necessarily looked up on the same thread so
* that there is no concurrency issues.
*/
if (!eb_is_empty(&srv->per_thr[tid].avail_conns)) {
if (!ceb_isempty(&srv->per_thr[tid].avail_conns)) {
srv_conn = srv_lookup_conn(&srv->per_thr[tid].avail_conns, hash);
if (srv_conn) {
/* connection cannot be in idle list if used as an avail idle conn. */
@ -1975,7 +1974,7 @@ int connect_server(struct stream *s)
srv_conn->flags |= CO_FL_OPT_TOS;
}
srv_conn->hash_node->node.key = hash;
srv_conn->hash_node->key = hash;
} else if (srv && (srv->flags & SRV_F_STRICT_MAXCONN))
_HA_ATOMIC_DEC(&srv->curr_total_conns);
}

View File

@ -12,7 +12,7 @@
#include <errno.h>
#include <import/ebmbtree.h>
#include <import/ceb64_tree.h>
#include <haproxy/api.h>
#include <haproxy/arg.h>
@ -81,8 +81,26 @@ struct conn_tlv_list *conn_get_tlv(struct connection *conn, int type)
*/
void conn_delete_from_tree(struct connection *conn, int thr)
{
LIST_DEL_INIT(&conn->idle_list);
eb64_delete(&conn->hash_node->node);
struct ceb_root **conn_tree;
struct server *srv = __objt_server(conn->target);
/* We need to determine where the connection is attached, if at all.
* - if it's in a tree and not in the idle_list, it's the avail_tree
* - if it's in a tree and has CO_FL_SAFE_LIST, it's the safe_tree
* - if it's in a tree and has CO_FL_IDLE_LIST, it's the idle_tree
* - if it's not in a tree and has CO_FL_SESS_IDLE, it's in the
* session's list (but we don't care here).
*/
if (LIST_INLIST(&conn->idle_list)) {
LIST_DEL_INIT(&conn->idle_list);
conn_tree = (conn->flags & CO_FL_SAFE_LIST) ?
&srv->per_thr[thr].safe_conns :
&srv->per_thr[thr].idle_conns;
} else {
conn_tree = &srv->per_thr[thr].avail_conns;
}
ceb64_item_delete(conn_tree, node, key, conn->hash_node);
}
int conn_create_mux(struct connection *conn, int *closed_connection)
@ -556,7 +574,7 @@ static void conn_backend_deinit(struct connection *conn)
/* Make sure the connection is not left in the idle connection tree */
if (conn->hash_node != NULL)
BUG_ON(conn->hash_node->node.node.leaf_p != NULL);
BUG_ON(ceb_intree(&conn->hash_node->node));
pool_free(pool_head_conn_hash_node, conn->hash_node);
conn->hash_node = NULL;
@ -2992,7 +3010,7 @@ int conn_reverse(struct connection *conn)
}
hash = conn_calculate_hash(&hash_params);
conn->hash_node->node.key = hash;
conn->hash_node->key = hash;
conn->target = &srv->obj_type;
srv_use_conn(srv, conn);

View File

@ -3812,7 +3812,7 @@ static void fcgi_detach(struct sedesc *sd)
TRACE_DEVEL("reusable idle connection", FCGI_EV_STRM_END, fconn->conn);
return;
}
else if (!fconn->conn->hash_node->node.node.leaf_p &&
else if (!ceb_intree(&fconn->conn->hash_node->node) &&
fcgi_avail_streams(fconn->conn) > 0 && objt_server(fconn->conn->target) &&
!LIST_INLIST(&fconn->conn->sess_el)) {
srv_add_to_avail_list(__objt_server(fconn->conn->target), fconn->conn);

View File

@ -5630,7 +5630,7 @@ static void h2_detach(struct sedesc *sd)
return;
}
else if (!h2c->conn->hash_node->node.node.leaf_p &&
else if (!ceb_intree(&h2c->conn->hash_node->node) &&
h2_avail_streams(h2c->conn) > 0 && objt_server(h2c->conn->target) &&
!LIST_INLIST(&h2c->conn->sess_el)) {
srv_add_to_avail_list(__objt_server(h2c->conn->target), h2c->conn);

View File

@ -3936,7 +3936,7 @@ static void qmux_strm_detach(struct sedesc *sd)
conn = NULL;
goto end;
}
else if (!conn->hash_node->node.node.leaf_p &&
else if (!ceb_intree(&conn->hash_node->node) &&
qmux_avail_streams(conn) &&
objt_server(conn->target)) {
TRACE_DEVEL("mark connection as available for reuse", QMUX_EV_STRM_END, conn);

View File

@ -3069,7 +3069,7 @@ static void spop_detach(struct sedesc *sd)
TRACE_DEVEL("reusable idle connection", SPOP_EV_STRM_END);
return;
}
else if (!spop_conn->conn->hash_node->node.node.leaf_p &&
else if (!ceb_intree(&spop_conn->conn->hash_node->node) &&
spop_avail_streams(spop_conn->conn) > 0 && objt_server(spop_conn->conn->target) &&
!LIST_INLIST(&spop_conn->conn->sess_el)) {
srv_add_to_avail_list(__objt_server(spop_conn->conn->target), spop_conn->conn);

View File

@ -16,6 +16,7 @@
#include <ctype.h>
#include <errno.h>
#include <import/ceb64_tree.h>
#include <import/cebis_tree.h>
#include <import/eb64tree.h>
@ -5976,9 +5977,9 @@ static int srv_init_per_thr(struct server *srv)
return -1;
for (i = 0; i < global.nbthread; i++) {
srv->per_thr[i].idle_conns = EB_ROOT;
srv->per_thr[i].safe_conns = EB_ROOT;
srv->per_thr[i].avail_conns = EB_ROOT;
srv->per_thr[i].idle_conns = NULL;
srv->per_thr[i].safe_conns = NULL;
srv->per_thr[i].avail_conns = NULL;
MT_LIST_INIT(&srv->per_thr[i].sess_conns);
MT_LIST_INIT(&srv->per_thr[i].streams);
@ -7272,17 +7273,14 @@ void srv_release_conn(struct server *srv, struct connection *conn)
/* retrieve a connection from its <hash> in <tree>
* returns NULL if no connection found
*/
struct connection *srv_lookup_conn(struct eb_root *tree, uint64_t hash)
struct connection *srv_lookup_conn(struct ceb_root **tree, uint64_t hash)
{
struct eb64_node *node = NULL;
struct conn_hash_node *hash_node;
struct connection *conn = NULL;
struct conn_hash_node *hash_node = NULL;
node = eb64_lookup(tree, hash);
if (node) {
hash_node = eb64_entry(node, struct conn_hash_node, node);
hash_node = ceb64_item_lookup(tree, node, key, hash, struct conn_hash_node);
if (hash_node)
conn = hash_node->conn;
}
return conn;
}
@ -7290,19 +7288,15 @@ struct connection *srv_lookup_conn(struct eb_root *tree, uint64_t hash)
/* retrieve the next connection sharing the same hash as <conn>
* returns NULL if no connection found
*/
struct connection *srv_lookup_conn_next(struct connection *conn)
struct connection *srv_lookup_conn_next(struct ceb_root **tree, struct connection *conn)
{
struct eb64_node *node = NULL;
struct connection *next_conn = NULL;
struct conn_hash_node *hash_node = NULL;
struct conn_hash_node *hash_node;
node = eb64_next_dup(&conn->hash_node->node);
if (node) {
hash_node = eb64_entry(node, struct conn_hash_node, node);
next_conn = hash_node->conn;
}
hash_node = ceb64_item_next_dup(tree, node, key, conn->hash_node);
if (hash_node)
conn = hash_node->conn;
return next_conn;
return conn;
}
/* Add <conn> in <srv> idle trees. Set <is_safe> if connection is deemed safe
@ -7317,11 +7311,11 @@ struct connection *srv_lookup_conn_next(struct connection *conn)
*/
void _srv_add_idle(struct server *srv, struct connection *conn, int is_safe)
{
struct eb_root *tree = is_safe ? &srv->per_thr[tid].safe_conns :
struct ceb_root **tree = is_safe ? &srv->per_thr[tid].safe_conns :
&srv->per_thr[tid].idle_conns;
/* first insert in idle or safe tree. */
eb64_insert(tree, &conn->hash_node->node);
ceb64_item_insert(tree, node, key, conn->hash_node);
/* insert in list sorted by connection usage. */
LIST_APPEND(&srv->per_thr[tid].idle_conn_list, &conn->idle_list);
@ -7344,8 +7338,8 @@ int srv_add_to_idle_list(struct server *srv, struct connection *conn, int is_saf
((srv->proxy->options & PR_O_REUSE_MASK) != PR_O_REUSE_NEVR) &&
ha_used_fds < global.tune.pool_high_count &&
(srv->max_idle_conns == -1 || srv->max_idle_conns > srv->curr_idle_conns) &&
((eb_is_empty(&srv->per_thr[tid].safe_conns) &&
(is_safe || eb_is_empty(&srv->per_thr[tid].idle_conns))) ||
((ceb_isempty(&srv->per_thr[tid].safe_conns) &&
(is_safe || ceb_isempty(&srv->per_thr[tid].idle_conns))) ||
(ha_used_fds < global.tune.pool_low_count &&
(srv->curr_used_conns + srv->curr_idle_conns <=
MAX(srv->curr_used_conns, srv->est_need_conns) + srv->low_idle_conns ||
@ -7403,7 +7397,7 @@ void srv_add_to_avail_list(struct server *srv, struct connection *conn)
{
/* connection cannot be in idle list if used as an avail idle conn. */
BUG_ON(LIST_INLIST(&conn->idle_list));
eb64_insert(&srv->per_thr[tid].avail_conns, &conn->hash_node->node);
ceb64_item_insert(&srv->per_thr[tid].avail_conns, node, key, conn->hash_node);
}
struct task *srv_cleanup_idle_conns(struct task *task, void *context, unsigned int state)
@ -7504,11 +7498,11 @@ remove:
*/
static void srv_close_idle_conns(struct server *srv)
{
struct eb_root **cleaned_tree;
struct ceb_root ***cleaned_tree;
int i;
for (i = 0; i < global.nbthread; ++i) {
struct eb_root *conn_trees[] = {
struct ceb_root **conn_trees[] = {
&srv->per_thr[i].idle_conns,
&srv->per_thr[i].safe_conns,
&srv->per_thr[i].avail_conns,
@ -7516,11 +7510,10 @@ static void srv_close_idle_conns(struct server *srv)
};
for (cleaned_tree = conn_trees; *cleaned_tree; ++cleaned_tree) {
while (!eb_is_empty(*cleaned_tree)) {
struct eb64_node *node = eb64_first(*cleaned_tree);
struct conn_hash_node *conn_hash_node = eb64_entry(node, struct conn_hash_node, node);
struct connection *conn = conn_hash_node->conn;
while (!ceb_isempty(*cleaned_tree)) {
struct connection *conn;
conn = ceb64_item_first(*cleaned_tree, node, key, struct conn_hash_node)->conn;
if (conn->ctrl->ctrl_close)
conn->ctrl->ctrl_close(conn);
conn_delete_from_tree(conn, i);

View File

@ -785,7 +785,7 @@ struct connection *session_get_conn(struct session *sess, void *target, int64_t
/* Search into pconns for a connection with matching params and available streams. */
list_for_each_entry(srv_conn, &pconns->conn_list, sess_el) {
if ((srv_conn->hash_node && srv_conn->hash_node->node.key == hash) &&
if ((srv_conn->hash_node && srv_conn->hash_node->key == hash) &&
srv_conn->mux &&
(srv_conn->mux->avail_streams(srv_conn) > 0) &&
!(srv_conn->flags & CO_FL_WAIT_XPRT)) {