Repository: cilium/cilium
Pull request #39087: cilium: streamline distributed lru with prealloc
Author: login: borkmann
Labels: name: release-note/misc
description: This PR makes changes that have no direct user impact.
name: backport-done/1.17
description: The backport for Cilium 1.17.x for this PR is done.
Created: 2025-04-22T15:22:27Z | Merged: 2025-04-23T07:54:05Z

(see commit desc)

```release-note
Fix LRU maps to streamline distributed LRU flag implementation with map prealloc handling
```


Diff:
diff --git a/bpf/lib/common.h b/bpf/lib/common.h
index d4246c85cb300..e0e488199fece 100644
--- a/bpf/lib/common.h
+++ b/bpf/lib/common.h
@@ -42,6 +42,12 @@
 #define CONDITIONAL_PREALLOC BPF_F_NO_PREALLOC
 #endif
 
+#ifdef NO_COMMON_MEM_MAPS
+#define LRU_MEM_FLAVOR BPF_F_NO_COMMON_LRU
+#else
+#define LRU_MEM_FLAVOR 0
+#endif
+
 #if defined(ENABLE_EGRESS_GATEWAY)
 #define ENABLE_EGRESS_GATEWAY_COMMON
 #endif
diff --git a/bpf/lib/conntrack_map.h b/bpf/lib/conntrack_map.h
index 9b82df5812b7b..5262f8fd5f00e 100644
--- a/bpf/lib/conntrack_map.h
+++ b/bpf/lib/conntrack_map.h
@@ -49,6 +49,7 @@ struct {
 		__type(key, struct ipv6_ct_tuple);
 		__type(value, struct ct_entry);
 		__uint(max_entries, CT_MAP_SIZE_TCP);
+		__uint(map_flags, LRU_MEM_FLAVOR);
 	});
 } cilium_per_cluster_ct_tcp6 __section_maps_btf;
 
@@ -63,6 +64,7 @@ struct {
 		__type(key, struct ipv6_ct_tuple);
 		__type(value, struct ct_entry);
 		__uint(max_entries, CT_MAP_SIZE_ANY);
+		__uint(map_flags, LRU_MEM_FLAVOR);
 	});
 } cilium_per_cluster_ct_any6 __section_maps_btf;
 #endif
@@ -127,6 +129,7 @@ struct per_cluster_ct_map4_inner_map {
 		__type(key, struct ipv4_ct_tuple);
 		__type(value, struct ct_entry);
 		__uint(max_entries, CT_MAP_SIZE_TCP);
+		__uint(map_flags, LRU_MEM_FLAVOR);
 #ifndef BPF_TEST
 };
 #else
diff --git a/bpf/lib/ipv4.h b/bpf/lib/ipv4.h
index 5f78077aa71f9..382b2ef9b58bf 100644
--- a/bpf/lib/ipv4.h
+++ b/bpf/lib/ipv4.h
@@ -33,6 +33,7 @@ struct {
 	__type(value, struct ipv4_frag_l4ports);
 	__uint(pinning, LIBBPF_PIN_BY_NAME);
 	__uint(max_entries, CILIUM_IPV4_FRAG_MAP_MAX_ENTRIES);
+	__uint(map_flags, LRU_MEM_FLAVOR);
 } cilium_ipv4_frag_datagrams __section_maps_btf;
 #endif
 
diff --git a/bpf/lib/ipv6.h b/bpf/lib/ipv6.h
index d0a1773ef0c6c..4c9626b41d594 100644
--- a/bpf/lib/ipv6.h
+++ b/bpf/lib/ipv6.h
@@ -56,6 +56,7 @@ struct {
 	__type(value, struct ipv6_frag_l4ports);
 	__uint(pinning, LIBBPF_PIN_BY_NAME);
 	__uint(max_entries, CILIUM_IPV6_FRAG_MAP_MAX_ENTRIES);
+	__uint(map_flags, LRU_MEM_FLAVOR);
 } cilium_ipv6_frag_datagrams __section_maps_btf;
 #endif
 
diff --git a/bpf/lib/lb.h b/bpf/lib/lb.h
index 416eabcf05189..724accfbad3a5 100644
--- a/bpf/lib/lb.h
+++ b/bpf/lib/lb.h
@@ -61,6 +61,7 @@ struct {
 	__type(value, struct lb_affinity_val);
 	__uint(pinning, LIBBPF_PIN_BY_NAME);
 	__uint(max_entries, CILIUM_LB_AFFINITY_MAP_MAX_ENTRIES);
+	__uint(map_flags, LRU_MEM_FLAVOR);
 } cilium_lb6_affinity __section_maps_btf;
 #endif
 
@@ -82,6 +83,7 @@ struct {
 	__type(value, struct lb6_health);
 	__uint(pinning, LIBBPF_PIN_BY_NAME);
 	__uint(max_entries, CILIUM_LB_BACKENDS_MAP_MAX_ENTRIES);
+	__uint(map_flags, LRU_MEM_FLAVOR);
 } cilium_lb6_health __section_maps_btf;
 #endif
 
@@ -148,6 +150,7 @@ struct {
 	__type(value, struct lb_affinity_val);
 	__uint(pinning, LIBBPF_PIN_BY_NAME);
 	__uint(max_entries, CILIUM_LB_AFFINITY_MAP_MAX_ENTRIES);
+	__uint(map_flags, LRU_MEM_FLAVOR);
 } cilium_lb4_affinity __section_maps_btf;
 #endif
 
@@ -169,6 +172,7 @@ struct {
 	__type(value, struct lb4_health);
 	__uint(pinning, LIBBPF_PIN_BY_NAME);
 	__uint(max_entries, CILIUM_LB_BACKENDS_MAP_MAX_ENTRIES);
+	__uint(map_flags, LRU_MEM_FLAVOR);
 } cilium_lb4_health __section_maps_btf;
 #endif
 
diff --git a/bpf/lib/nat.h b/bpf/lib/nat.h
index 573b7c33cb68e..236c26e535ec2 100644
--- a/bpf/lib/nat.h
+++ b/bpf/lib/nat.h
@@ -139,6 +139,7 @@ struct per_cluster_snat_mapping_ipv4_inner_map {
 	__type(key, struct ipv4_ct_tuple);
 	__type(value, struct ipv4_nat_entry);
 	__uint(max_entries, SNAT_MAPPING_IPV4_SIZE);
+	__uint(map_flags, LRU_MEM_FLAVOR);
 #ifndef BPF_TEST
 };
 #else
@@ -1203,6 +1204,7 @@ struct {
 		__type(key, struct ipv6_ct_tuple);
 		__type(value, struct ipv6_nat_entry);
 		__uint(max_entries, SNAT_MAPPING_IPV6_SIZE);
+		__uint(map_flags, LRU_MEM_FLAVOR);
 	});
 } cilium_per_cluster_snat_v6_external __section_maps_btf;
 #endif
diff --git a/bpf/lib/ratelimit.h b/bpf/lib/ratelimit.h
index 0f0dd2e1170bf..a27f0c937e3ca 100644
--- a/bpf/lib/ratelimit.h
+++ b/bpf/lib/ratelimit.h
@@ -29,6 +29,7 @@ struct {
 	__type(value, struct ratelimit_value);
 	__uint(pinning, LIBBPF_PIN_BY_NAME);
 	__uint(max_entries, 1024);
+	__uint(map_flags, LRU_MEM_FLAVOR);
 } cilium_ratelimit __section_maps_btf;
 
 struct ratelimit_metrics_key {
diff --git a/daemon/cmd/daemon_main.go b/daemon/cmd/daemon_main.go
index 1252d9f6e331d..37b37ade61cb4 100644
--- a/daemon/cmd/daemon_main.go
+++ b/daemon/cmd/daemon_main.go
@@ -1141,6 +1141,9 @@ func initEnv(vp *viper.Viper) {
 	if option.Config.PreAllocateMaps {
 		bpf.EnableMapPreAllocation()
 	}
+	if option.Config.BPFDistributedLRU {
+		bpf.EnableMapDistributedLRU()
+	}
 
 	scopedLog := log.WithFields(logrus.Fields{
 		logfields.Path + ".RunDir": option.Config.RunDir,
diff --git a/pkg/bpf/bpf.go b/pkg/bpf/bpf.go
index eec94868aa712..9481dff351654 100644
--- a/pkg/bpf/bpf.go
+++ b/pkg/bpf/bpf.go
@@ -16,11 +16,13 @@ var (
 	log = logging.DefaultLogger.WithField(logfields.LogSubsys, "bpf")
 
 	preAllocateMapSetting uint32 = BPF_F_NO_PREALLOC
+	noCommonLRUMapSetting uint32 = 0
 )
 
 const (
 	// Flags for BPF_MAP_CREATE. Must match values from linux/bpf.h
-	BPF_F_NO_PREALLOC = 1 << 0
+	BPF_F_NO_PREALLOC   = 1 << 0
+	BPF_F_NO_COMMON_LRU = 1 << 1
 )
 
 // EnableMapPreAllocation enables BPF map pre-allocation on map types that
@@ -38,9 +40,22 @@ func DisableMapPreAllocation() {
 	atomic.StoreUint32(&preAllocateMapSetting, BPF_F_NO_PREALLOC)
 }
 
-// GetPreAllocateMapFlags returns the map flags for map which use conditional
-// pre-allocation.
-func GetPreAllocateMapFlags(t ebpf.MapType) uint32 {
+// EnableMapDistributedLRU enables the LRU map no-common-LRU feature which
+// splits backend memory pools among CPUs to avoid sharing a common backend
+// pool where frequent allocation/frees might content on internal spinlocks.
+func EnableMapDistributedLRU() {
+	atomic.StoreUint32(&noCommonLRUMapSetting, BPF_F_NO_COMMON_LRU)
+}
+
+// DisableMapDistributedLRU disables the LRU map no-common-LRU feature which
+// is the default case.
+func DisableMapDistributedLRU() {
+	atomic.StoreUint32(&noCommonLRUMapSetting, 0)
+}
+
+// GetMapMemoryFlags returns relevant map memory allocation flags which
+// the user requested.
+func GetMapMemoryFlags(t ebpf.MapType) uint32 {
 	switch t {
 	// LPM Tries don't support preallocation.
 	case ebpf.LPMTrie:
@@ -48,6 +63,9 @@ func GetPreAllocateMapFlags(t ebpf.MapType) uint32 {
 	// Support disabling preallocation for these map types.
 	case ebpf.Hash, ebpf.PerCPUHash, ebpf.HashOfMaps:
 		return atomic.LoadUint32(&preAllocateMapSetting)
+	// Support no-common LRU backend memory
+	case ebpf.LRUHash, ebpf.LRUCPUHash:
+		return atomic.LoadUint32(&noCommonLRUMapSetting)
 	}
 
 	return 0
diff --git a/pkg/bpf/bpf_test.go b/pkg/bpf/bpf_test.go
index a00f1a72c2e94..8252521804280 100644
--- a/pkg/bpf/bpf_test.go
+++ b/pkg/bpf/bpf_test.go
@@ -11,16 +11,22 @@ import (
 )
 
 func TestDefaultMapFlags(t *testing.T) {
-	require.Equal(t, uint32(BPF_F_NO_PREALLOC), GetPreAllocateMapFlags(ebpf.LPMTrie))
-	require.Equal(t, uint32(0), GetPreAllocateMapFlags(ebpf.Array))
-	require.Equal(t, uint32(0), GetPreAllocateMapFlags(ebpf.LRUHash))
+	require.Equal(t, uint32(BPF_F_NO_PREALLOC), GetMapMemoryFlags(ebpf.Hash))
+	require.Equal(t, uint32(0), GetMapMemoryFlags(ebpf.LRUHash))
+	require.Equal(t, uint32(BPF_F_NO_PREALLOC), GetMapMemoryFlags(ebpf.LPMTrie))
+	require.Equal(t, uint32(0), GetMapMemoryFlags(ebpf.Array))
 
-	require.Equal(t, uint32(BPF_F_NO_PREALLOC), GetPreAllocateMapFlags(ebpf.Hash))
 	EnableMapPreAllocation()
-	require.Equal(t, uint32(0), GetPreAllocateMapFlags(ebpf.Hash))
-
-	require.Equal(t, uint32(BPF_F_NO_PREALLOC), GetPreAllocateMapFlags(ebpf.LPMTrie))
-	require.Equal(t, uint32(0), GetPreAllocateMapFlags(ebpf.Array))
-	require.Equal(t, uint32(0), GetPreAllocateMapFlags(ebpf.LRUHash))
+	require.Equal(t, uint32(0), GetMapMemoryFlags(ebpf.Hash))
+	require.Equal(t, uint32(0), GetMapMemoryFlags(ebpf.LRUHash))
+	require.Equal(t, uint32(BPF_F_NO_PREALLOC), GetMapMemoryFlags(ebpf.LPMTrie))
+	require.Equal(t, uint32(0), GetMapMemoryFlags(ebpf.Array))
 	DisableMapPreAllocation()
+
+	EnableMapDistributedLRU()
+	require.Equal(t, uint32(BPF_F_NO_PREALLOC), GetMapMemoryFlags(ebpf.Hash))
+	require.Equal(t, uint32(BPF_F_NO_COMMON_LRU), GetMapMemoryFlags(ebpf.LRUHash))
+	require.Equal(t, uint32(BPF_F_NO_PREALLOC), GetMapMemoryFlags(ebpf.LPMTrie))
+	require.Equal(t, uint32(0), GetMapMemoryFlags(ebpf.Array))
+	DisableMapDistributedLRU()
 }
diff --git a/pkg/bpf/map_linux.go b/pkg/bpf/map_linux.go
index bb96d3ff86ada..7024358238b78 100644
--- a/pkg/bpf/map_linux.go
+++ b/pkg/bpf/map_linux.go
@@ -518,10 +518,10 @@ func (m *Map) openOrCreate(pin bool) error {
 		return err
 	}
 
-	m.spec.Flags |= GetPreAllocateMapFlags(m.spec.Type)
+	m.spec.Flags |= GetMapMemoryFlags(m.spec.Type)
 
 	if m.spec.InnerMap != nil {
-		m.spec.InnerMap.Flags |= GetPreAllocateMapFlags(m.spec.InnerMap.Type)
+		m.spec.InnerMap.Flags |= GetMapMemoryFlags(m.spec.InnerMap.Type)
 	}
 
 	if pin {
@@ -1538,7 +1538,7 @@ func (m *Map) resolveErrors(ctx context.Context) error {
 //
 // Returns true if the map was upgraded.
 func (m *Map) CheckAndUpgrade(desired *Map) bool {
-	flags := desired.Flags() | GetPreAllocateMapFlags(desired.Type())
+	flags := desired.Flags() | GetMapMemoryFlags(desired.Type())
 
 	return objCheck(
 		m.m,
diff --git a/pkg/datapath/linux/config/config.go b/pkg/datapath/linux/config/config.go
index 6dc6c2e6328cd..372aaaa616786 100644
--- a/pkg/datapath/linux/config/config.go
+++ b/pkg/datapath/linux/config/config.go
@@ -139,12 +139,6 @@ func (h *HeaderfileWriter) WriteNodeConfig(w io.Writer, cfg *datapath.LocalNodeC
 
 	cDefinesMap["KERNEL_HZ"] = fmt.Sprintf("%d", option.Config.KernelHz)
 
-	if option.Config.BPFDistributedLRU {
-		cDefinesMap["LRU_MEM_FLAVOR"] = "BPF_F_NO_COMMON_LRU"
-	} else {
-		cDefinesMap["LRU_MEM_FLAVOR"] = "0"
-	}
-
 	if option.Config.EnableIPv6 && option.Config.EnableIPv6FragmentsTracking {
 		cDefinesMap["ENABLE_IPV6_FRAGMENTS"] = "1"
 		cDefinesMap["CILIUM_IPV6_FRAG_MAP_MAX_ENTRIES"] = fmt.Sprintf("%d", option.Config.FragmentsMapEntries)
@@ -215,6 +209,9 @@ func (h *HeaderfileWriter) WriteNodeConfig(w io.Writer, cfg *datapath.LocalNodeC
 	if option.Config.PreAllocateMaps {
 		cDefinesMap["PREALLOCATE_MAPS"] = "1"
 	}
+	if option.Config.BPFDistributedLRU {
+		cDefinesMap["NO_COMMON_MEM_MAPS"] = "1"
+	}
 
 	cDefinesMap["EVENTS_MAP_RATE_LIMIT"] = fmt.Sprintf("%d", option.Config.BPFEventsDefaultRateLimit)
 	cDefinesMap["EVENTS_MAP_BURST_LIMIT"] = fmt.Sprintf("%d", option.Config.BPFEventsDefaultBurstLimit)
diff --git a/pkg/ebpf/map.go b/pkg/ebpf/map.go
index 4d5ec07964566..148e813ac9927 100644
--- a/pkg/ebpf/map.go
+++ b/pkg/ebpf/map.go
@@ -117,7 +117,7 @@ func (m *Map) OpenOrCreate() error {
 		PinPath: bpf.TCGlobalsPath(),
 	}
 
-	m.spec.Flags |= bpf.GetPreAllocateMapFlags(m.spec.Type)
+	m.spec.Flags |= bpf.GetMapMemoryFlags(m.spec.Type)
 
 	path := bpf.MapPath(m.spec.Name)
 
diff --git a/pkg/maps/multicast/subscribermap.go b/pkg/maps/multicast/subscribermap.go
index 02c12f962e6e7..1d686a0be232d 100644
--- a/pkg/maps/multicast/subscribermap.go
+++ b/pkg/maps/multicast/subscribermap.go
@@ -387,7 +387,7 @@ func (v *SubscriberV4Val) ToSubsciberV4() (*SubscriberV4, error) {
 }
 
 func newSubscriberV4InnerMapSpec() *ebpf.MapSpec {
-	flags := bpf.GetPreAllocateMapFlags(ebpf.Hash)
+	flags := bpf.GetMapMemoryFlags(ebpf.Hash)
 	return &ebpf.MapSpec{
 		Name:       "cilium_mcast_subscriber_v4_inner",
 		Type:       ebpf.Hash,
diff --git a/pkg/maps/policymap/policymap.go b/pkg/maps/policymap/policymap.go
index f8084b2422abe..49d10f1e73af1 100644
--- a/pkg/maps/policymap/policymap.go
+++ b/pkg/maps/policymap/policymap.go
@@ -481,7 +481,7 @@ func parseEndpointID(mapPath string) (uint16, error) {
 func newPolicyMap(id uint16, maxEntries int, stats *StatsMap) (*PolicyMap, error) {
 	path := bpf.LocalMapPath(MapName, id)
 	mapType := ebpf.LPMTrie
-	flags := bpf.GetPreAllocateMapFlags(mapType)
+	flags := bpf.GetMapMemoryFlags(mapType)
 
 	return &PolicyMap{
 		Map: bpf.NewMap(


Reviews:
user: login: rgo3
body: So changes to `WriteNodeConfig` are deprecated but I think it's OK to make an exception here to make it easier to backport this, as the new config mechanism isn't available in 1.17. Maybe refactoring `NO_COMMON_MEM_MAPS`/`LRU_MEM_FLAVOR` can be an easy follow-up. cc @ti-mo 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:57:44Z
user: login: tommyp1ckles
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T23:55:09Z
user: login: jschwinger233
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T03:53:54Z
user: login: tommyp1ckles
body: just random misc points, looks good otherwise.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T04:00:52Z
user: login: doniacld
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T07:06:11Z
user: login: borkmann
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T07:47:12Z
user: login: borkmann
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T07:48:00Z
user: login: borkmann
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T07:53:02Z

Review comments:
diff_hunk: @@ -49,6 +49,7 @@ struct {
 		__type(key, struct ipv6_ct_tuple);
 		__type(value, struct ct_entry);
 		__uint(max_entries, CT_MAP_SIZE_TCP);
+		__uint(map_flags, LRU_MEM_FLAVOR);
path: bpf/lib/conntrack_map.h
user: login: tommyp1ckles
body: We may want a release note that if you had distributed LRU enabled previously, you may see disruptions during an upgrade as may force some maps to be recreated? 
created_at: 2025-04-22T23:47:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,6 +49,7 @@ struct {
 		__type(key, struct ipv6_ct_tuple);
path: bpf/lib/conntrack_map.h
user: login: tommyp1ckles
body: Double checking, `cilium_policystats` is already hard coded to use per cpu lru, we don't want to have that controlled by this, right?  
created_at: 2025-04-22T23:50:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,11 +16,13 @@ var (
 	log = logging.DefaultLogger.WithField(logfields.LogSubsys, "bpf")
 
 	preAllocateMapSetting uint32 = BPF_F_NO_PREALLOC
+	noCommonLRUMapSetting uint32 = 0
path: pkg/bpf/bpf.go
user: login: tommyp1ckles
body: not part of the review but I've never been a fan of the way we set `NO_PREALLOC` as a global. Wonder if there's a better way to do these. 
created_at: 2025-04-23T03:48:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,6 +49,7 @@ struct {
 		__type(key, struct ipv6_ct_tuple);
 		__type(value, struct ct_entry);
 		__uint(max_entries, CT_MAP_SIZE_TCP);
+		__uint(map_flags, LRU_MEM_FLAVOR);
path: bpf/lib/conntrack_map.h
user: login: borkmann
body: Will tweak the release note a bit, agree. Actually, distributed LRU enabled previously had the issue to always cause interruptions on each restart.
created_at: 2025-04-23T07:47:12Z
author_association: MEMBER
diff_hunk: @@ -49,6 +49,7 @@ struct {
 		__type(key, struct ipv6_ct_tuple);
path: bpf/lib/conntrack_map.h
user: login: borkmann
body: Right, I thought about it, but if its already hard-coded, I would leave it at that since there is less reason to go the other way around to enable it without the flag.
created_at: 2025-04-23T07:48:00Z
author_association: MEMBER
diff_hunk: @@ -16,11 +16,13 @@ var (
 	log = logging.DefaultLogger.WithField(logfields.LogSubsys, "bpf")
 
 	preAllocateMapSetting uint32 = BPF_F_NO_PREALLOC
+	noCommonLRUMapSetting uint32 = 0
path: pkg/bpf/bpf.go
user: login: borkmann
body: I'm not a fan of it either. In particular, this should be a per-map setting, not agent-global, and then the per-map specifics should be fed into the loader eventually to configure the BPF side. Perhaps we can put this up onto the roadmap to rework similar to what @rgo3 suggested. I can create an issue.
created_at: 2025-04-23T07:53:01Z
author_association: MEMBER

PR comments:
user: login: borkmann
created_at: 2025-04-22T15:22:42Z
body: /test
author_association: MEMBER
user: login: borkmann
created_at: 2025-04-22T16:44:35Z
body: (e2e hits https://github.com/cilium/cilium/issues/38989)
author_association: MEMBER