From 3c639c38d43fe3376c0de536fcb442fcb767fe6b Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Sun, 10 May 2026 09:03:01 -0500 Subject: [PATCH] affinity: bind worker memory to local NUMA node Suricata already pins worker threads to NUMA-local CPUs via the existing cpu-affinity logic, but every heap allocation those workers make still goes through plain malloc and lands on whatever NUMA node the kernel picked for the first touch. When workers and their flow records end up on different nodes the cross-NUMA traffic shows up as packet loss on multi-socket boxes. Add a new threading.set-memory-affinity option, opt-in, that calls numa_set_localalloc() on each worker right after CPU pinning so all subsequent allocations on that thread prefer the local node. Requires libnuma at build time and is a no-op on single-node systems. Bug: #8141. --- configure.ac | 11 ++++++- doc/userguide/configuration/suricata-yaml.rst | 11 +++++++ src/runmodes.c | 20 +++++++++++ src/runmodes.h | 1 + src/tm-threads.c | 33 +++++++++++++++++++ suricata.yaml.in | 6 ++++ 6 files changed, 81 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 7f430ef471..a8f07ae731 100644 --- a/configure.ac +++ b/configure.ac @@ -1415,13 +1415,22 @@ AC_CHECK_FUNCS([bpf_xdp_query_id]) ]) + # Optional libnuma probe (used for memory affinity binding, also + # mandatory for DPDK and Napatech below). Sets HAVE_LIBNUMA and links + # -lnuma when available. + AC_CHECK_LIB(numa, numa_available, [ + have_libnuma=yes + AC_DEFINE([HAVE_LIBNUMA], [1], [Define to 1 if you have libnuma.]) + LIBS="${LIBS} -lnuma" + ], [have_libnuma=no]) + # DPDK support enable_dpdk_bond_pmd="no" AC_ARG_ENABLE(dpdk, AS_HELP_STRING([--enable-dpdk], [Enable DPDK support [default=no]]), [enable_dpdk=$enableval],[enable_dpdk=no]) AS_IF([test "x$enable_dpdk" = "xyes"], [ - AC_CHECK_LIB(numa, numa_available,, [numa_found="no"]) + if test "$have_libnuma" = "no"; then numa_found="no"; fi if test "$numa_found" = "no"; then echo echo " ERROR! libnuma not found by pkg-config, go get it" diff --git a/doc/userguide/configuration/suricata-yaml.rst b/doc/userguide/configuration/suricata-yaml.rst index 88d266a0dc..d66c9173a7 100644 --- a/doc/userguide/configuration/suricata-yaml.rst +++ b/doc/userguide/configuration/suricata-yaml.rst @@ -919,6 +919,17 @@ packets to process. The detect threads running on other cores will process more packets. This is only the case after setting the option to 'yes'. +On NUMA systems where ``set-cpu-affinity`` is enabled you can also opt +into binding each thread's memory to its local NUMA node so per-flow, +per-thread and packet pool allocations stay on the same node as the +worker CPU:: + + set-memory-affinity: yes + +This requires Suricata to be built with libnuma (already pulled in by +``--enable-dpdk`` and the Napatech build). On a single NUMA node system +the option has no effect. + *Example 7 Balancing workload* .. image:: suricata-yaml/balancing_workload.png diff --git a/src/runmodes.c b/src/runmodes.c index da7d88a243..06c7b65b85 100644 --- a/src/runmodes.c +++ b/src/runmodes.c @@ -60,6 +60,7 @@ int debuglog_enabled = 0; bool threading_set_cpu_affinity = false; +bool threading_set_memory_affinity = false; uint64_t threading_set_stack_size = 0; /* Runmode Global Thread Names */ @@ -963,6 +964,25 @@ void RunModeInitializeThreadSettings(void) if (threading_set_cpu_affinity) { AffinitySetupLoadFromConfig(); } + + int memory_affinity = 0; + if ((SCConfGetBool("threading.set-memory-affinity", &memory_affinity)) == 0) { + threading_set_memory_affinity = false; + } else { + threading_set_memory_affinity = memory_affinity == 1; + } + if (threading_set_memory_affinity && !threading_set_cpu_affinity) { + SCLogWarning("threading.set-memory-affinity has no effect without " + "threading.set-cpu-affinity, ignoring"); + threading_set_memory_affinity = false; + } +#ifndef HAVE_LIBNUMA + if (threading_set_memory_affinity) { + SCLogWarning("threading.set-memory-affinity requested but Suricata " + "was built without libnuma, ignoring"); + threading_set_memory_affinity = false; + } +#endif if ((SCConfGetFloat("threading.detect-thread-ratio", &threading_detect_ratio)) != 1) { if (SCConfGetNode("threading.detect-thread-ratio") != NULL) WarnInvalidConfEntry("threading.detect-thread-ratio", "%s", "1"); diff --git a/src/runmodes.h b/src/runmodes.h index 4d11413550..ed05fea082 100644 --- a/src/runmodes.h +++ b/src/runmodes.h @@ -104,6 +104,7 @@ void RunModeEnablesBypassManager(void); int RunModeNeedsBypassManager(void); extern bool threading_set_cpu_affinity; +extern bool threading_set_memory_affinity; extern float threading_detect_ratio; extern uint64_t threading_set_stack_size; diff --git a/src/tm-threads.c b/src/tm-threads.c index 675735f6dc..f730c4c0fd 100644 --- a/src/tm-threads.c +++ b/src/tm-threads.c @@ -49,6 +49,10 @@ #include "util-validate.h" #include "source-pcap-file-helper.h" +#ifdef HAVE_LIBNUMA +#include +#endif + #ifdef PROFILE_LOCKING thread_local uint64_t mutex_lock_contention; thread_local uint64_t mutex_lock_wait_ticks; @@ -856,6 +860,33 @@ int TmThreadGetNbThreads(uint8_t type) return thread_affinity[type].nb_threads; } +/** + * \brief Bind subsequent allocations on this thread to the local NUMA node. + * + * Must be called after the thread's CPU affinity has been pinned. Uses + * MPOL_LOCAL (via numa_set_localalloc) so allocations prefer the node of + * the running CPU but can fall back to other nodes when local memory is + * exhausted. No-op when libnuma is not compiled in or the system has a + * single NUMA node. + */ +static void SetMemoryAffinityLocal(const char *name) +{ +#ifdef HAVE_LIBNUMA + if (!threading_set_memory_affinity) + return; + if (numa_available() == -1) + return; + if (numa_max_node() < 1) + return; + numa_set_localalloc(); + SCLogPerf("Setting memory affinity for thread \"%s\" to local NUMA node, " + "thread id %lu", + name, SCGetThreadIdLong()); +#else + (void)name; +#endif +} + /** * \brief Set the thread options (cpu affinitythread). * Priority should be already set by pthread_create. @@ -869,6 +900,7 @@ TmEcode TmThreadSetupOptions(ThreadVars *tv) "%"PRIu16", thread id %lu", tv->name, tv->cpu_affinity, SCGetThreadIdLong()); SetCPUAffinity(tv->cpu_affinity); + SetMemoryAffinityLocal(tv->name); } #if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun @@ -915,6 +947,7 @@ TmEcode TmThreadSetupOptions(ThreadVars *tv) "thread id %lu", tv->thread_priority, tv->name, SCGetThreadIdLong()); } + SetMemoryAffinityLocal(tv->name); TmThreadSetPrio(tv); } #endif diff --git a/suricata.yaml.in b/suricata.yaml.in index 859c443903..648c68424f 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -1907,6 +1907,12 @@ spm-algo: auto # Suricata is multi-threaded. Here the threading can be influenced. threading: set-cpu-affinity: no + # When set-cpu-affinity is enabled, also bind thread heap allocations + # to the local NUMA node so per-flow, per-thread and packet pool memory + # stays on the same NUMA as the worker CPU. Requires libnuma at build + # time (already present when DPDK or Napatech is enabled). Has no effect + # on single-NUMA systems. + set-memory-affinity: no autopin: no # Tune cpu affinity of threads. Each family of threads can be bound # to specific CPUs.