From a2587781735a609a82656f76a70dfc716b29e0f3 Mon Sep 17 00:00:00 2001
From: Tobias Klauser <tklauser@distanz.ch>
Date: Thu, 9 Apr 2020 16:08:55 +0200
Subject: [PATCH 10/10] daemon: add option to dynamically size BPF maps based
 on system memory

Add the new `bpf-map-dynamic-size-ratio` option which will size the
large BPF maps (currently CT TCP and non-TCP, NAT and policy) based on
the total system memory.

If set to a value > 0.0, the given ratio of the total system memory will
be used to allocate the BPF maps. The default scaling was chosen
heuristically such that on a node with 4GiB of total system memory, the
maps will have roughly the default size when using a ratio of 0.03 (3%).
The amount of memory will be distributed among the TCP CT, non-TCP CT,
NAT and policy tables proportional to their default sizes.

If the user specifies any of the map sizes explicitly using an option,
that value will take precedence over the dynamically determined size for
that particular map, i.e. when using

```
cilium-agent --bpf-map-dynamic-size-ratio 0.03 --bpf-ct-global-tcp-max 100000
```

the TCP conntrack BPF map will be allocated for a maximum of 100000
entries while the non-TCP conntrack, NAT and policy maps will have their
size determined dynamically based on total system memory.

As with the manually specified sizes, the dynamically determined map
sizes are always clamped to respective minimum and maximum number of
entires for each map.

Updates #10056

Signed-off-by: Tobias Klauser <tklauser@distanz.ch>
---
 Documentation/architecture.rst                |  15 +-
 Documentation/cmdref/cilium-agent.md          |   1 +
 Documentation/install/upgrade.rst             |  18 ++
 Documentation/spelling_wordlist.txt           |   1 +
 daemon/daemon_main.go                         |  13 ++
 .../charts/config/templates/configmap.yaml    |   4 +
 install/kubernetes/cilium/values.yaml         |   7 +
 install/kubernetes/quick-install.yaml         |   4 +
 pkg/maps/ctmap/ctmap.go                       |   2 +-
 pkg/maps/ctmap/types.go                       |   4 +
 pkg/maps/nat/ipv4.go                          |   3 +
 pkg/maps/nat/ipv6.go                          |   3 +
 pkg/maps/nat/nat.go                           |   8 +-
 pkg/maps/nat/types.go                         |   6 +
 pkg/maps/policymap/policymap.go               |  10 +-
 pkg/option/config.go                          | 142 +++++++++++++-
 pkg/option/config_test.go                     | 177 ++++++++++++++++++
 17 files changed, 405 insertions(+), 13 deletions(-)

diff --git a/Documentation/architecture.rst b/Documentation/architecture.rst
index d7a38ef1b..f836c3285 100644
--- a/Documentation/architecture.rst
+++ b/Documentation/architecture.rst
@@ -45,7 +45,7 @@ hook see :ref:`bpf_guide`.
   tc ingress hook can be coupled with above XDP hook. When this is done it
   is reasonable to assume that the majority of the traffic at this
   point is legitimate and destined for the host.
-  
+
   Containers typically use a virtual device called a veth pair which acts
   as a virtual wire connecting the container to the host. By attaching to
   the TC ingress hook of the host side of this veth pair Cilium can monitor
@@ -54,7 +54,7 @@ hook see :ref:`bpf_guide`.
   network traffic to the host side virtual devices with another BPF program
   attached to the tc ingress hook as well Cilium can monitor and enforce
   policy on all traffic entering or exiting the node.
-  
+
   Depending on the use case, containers may also be connected through ipvlan
   devices instead of a veth pair. In this mode, the physical device in the
   host is the ipvlan master where virtual ipvlan devices in slave mode are
@@ -257,6 +257,17 @@ Proxy Map                node             512k            Max 512k concurrent re
 Tunnel                   node             64k             Max 32k nodes (IPv4+IPv6) or 64k nodes (IPv4 or IPv6) across all clusters
 ======================== ================ =============== =====================================================
 
+For some BPF maps, the upper capacity limit can be overridden using command
+line options for ``cilium-agent``. A given capacity can be set using
+``--bpf-ct-global-tcp-max``, ``--bpf-ct-global-any-max``,
+``--bpf-nat-global-max``, ``--bpf-policy-map-max``, and
+``--bpf-fragments-map-max``.
+
+Using ``--bpf-map-dynamic-size-ratio`` the upper capacity limits of the
+connection tracking, NAT, and policy maps are determined at agent startup based
+on the given ratio of the total system memory. For example a given ratio of 0.03
+leads to 3% of the total system memory to be used for these maps.
+
 Kubernetes Integration
 ======================
 
diff --git a/Documentation/cmdref/cilium-agent.md b/Documentation/cmdref/cilium-agent.md
index e7e7a755a..65a06dc04 100644
--- a/Documentation/cmdref/cilium-agent.md
+++ b/Documentation/cmdref/cilium-agent.md
@@ -32,6 +32,7 @@ cilium-agent [flags]
       --bpf-ct-timeout-regular-tcp-syn duration       Establishment timeout for entries in TCP CT table (default 1m0s)
       --bpf-ct-timeout-service-any duration           Timeout for service entries in non-TCP CT table (default 1m0s)
       --bpf-ct-timeout-service-tcp duration           Timeout for established service entries in TCP CT table (default 6h0m0s)
+      --bpf-fragments-map-max int                     Maximum number of entries in fragments tracking map (default 8192)
       --bpf-nat-global-max int                        Maximum number of entries for the global BPF NAT table (default 524288)
       --bpf-policy-map-max int                        Maximum number of entries in endpoint policy map (per endpoint) (default 16384)
       --bpf-root string                               Path to BPF filesystem
diff --git a/Documentation/install/upgrade.rst b/Documentation/install/upgrade.rst
index 001b2aa86..d09eb7109 100644
--- a/Documentation/install/upgrade.rst
+++ b/Documentation/install/upgrade.rst
@@ -547,6 +547,24 @@ Upgrading from >=1.7.0 to 1.8.y
       helm upgrade cilium --namespace=kube-system \\
       --set global.bpf.natMax=841429
 
+New ConfigMap Options
+~~~~~~~~~~~~~~~~~~~~~
+
+  * ``bpf-map-dynamic-size-ratio`` has been added to allow sizing of the TCP CT,
+    non-TCP CT, NAT and policy BPF maps based on the total system memory. This
+    option allows to specify a ratio (0.0-1.0) of total system memory to use for
+    these maps. On new installations, this ratio is set to 0.03 by default,
+    leading to 3% of the total system memory to be allocated for these maps. On
+    a node with 4 GiB of total system memory this ratio corresponds
+    approximately to the default BPF map sizes. A value of 0.0 will disable
+    sizing of the BPF maps based on system memory. Any BPF map sizes configured
+    manually using the ``ctTcpMax``, ``ctAnyMax``, ``natMax`` options will take
+    precedence over the dynamically determined value.
+
+    On upgrades of existing installations, this option is disable by default,
+    i.e. it is set to 0.0. Users wanting to use this feature need to enable it
+    explicitly in their `ConfigMap`, see section :ref:`upgrade_configmap`.
+
 Deprecated options
 ~~~~~~~~~~~~~~~~~~
 
diff --git a/Documentation/spelling_wordlist.txt b/Documentation/spelling_wordlist.txt
index 4c2618fbe..586be122c 100644
--- a/Documentation/spelling_wordlist.txt
+++ b/Documentation/spelling_wordlist.txt
@@ -38,6 +38,7 @@ Fastabend
 GCE
 Gartrell
 Geneve
+GiB
 Github
 Golang
 Gospodarek
diff --git a/daemon/daemon_main.go b/daemon/daemon_main.go
index 4b3a933a4..127a2f897 100644
--- a/daemon/daemon_main.go
+++ b/daemon/daemon_main.go
@@ -51,7 +51,10 @@ import (
 	"github.com/cilium/cilium/pkg/loadinfo"
 	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/logging/logfields"
+	"github.com/cilium/cilium/pkg/maps/ctmap"
 	"github.com/cilium/cilium/pkg/maps/ctmap/gc"
+	"github.com/cilium/cilium/pkg/maps/nat"
+	"github.com/cilium/cilium/pkg/maps/policymap"
 	"github.com/cilium/cilium/pkg/metrics"
 	monitorAPI "github.com/cilium/cilium/pkg/monitor/api"
 	"github.com/cilium/cilium/pkg/node"
@@ -680,6 +683,9 @@ func init() {
 	flags.Int(option.PolicyMapEntriesName, defaults.PolicyMapEntries, "Maximum number of entries in endpoint policy map (per endpoint)")
 	option.BindEnv(option.PolicyMapEntriesName)
 
+	flags.Float64(option.MapEntriesGlobalDynamicSizeRatioName, 0.0, "Ratio (0.0-1.0) of total system memory to use for dynamic sizing of CT, NAT and policy BPF maps. Set to 0.0 to disable dynamic BPF map sizing (default: 0.0)")
+	option.BindEnv(option.MapEntriesGlobalDynamicSizeRatioName)
+
 	flags.String(option.CMDRef, "", "Path to cmdref output directory")
 	flags.MarkHidden(option.CMDRef)
 	option.BindEnv(option.CMDRef)
@@ -819,6 +825,13 @@ func initConfig() {
 }
 
 func initEnv(cmd *cobra.Command) {
+	option.Config.SetMapElementSizes(
+		// for the conntrack and NAT element size we assume the largest possible
+		// key size, i.e. IPv6 keys
+		ctmap.SizeofCtKey6Global+ctmap.SizeofCtEntry,
+		nat.SizeofNatKey6+nat.SizeofNatEntry6,
+		policymap.SizeofPolicyKey+policymap.SizeofPolicyEntry)
+
 	// Prepopulate option.Config with options from CLI.
 	option.Config.Populate()
 
diff --git a/install/kubernetes/cilium/charts/config/templates/configmap.yaml b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
index 77fa80523..605c55d38 100644
--- a/install/kubernetes/cilium/charts/config/templates/configmap.yaml
+++ b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
@@ -140,6 +140,10 @@ data:
   # table.
   bpf-nat-global-max: "{{ .Values.global.bpf.natMax }}"
 
+  # Specifies the ratio (0.0-1.0) of total system memory to use for dynamic
+  # sizing of the TCP CT, non-TCP CT, NAT and policy BPF maps.
+  bpf-map-dynamic-size-ratio: "{{ .Values.global.bpf.mapDynamicSizeRatio }}"
+
   # Pre-allocation of map entries allows per-packet latency to be reduced, at
   # the expense of up-front memory allocation for the entries in the maps. The
   # default value below will minimize memory usage in the default installation;
diff --git a/install/kubernetes/cilium/values.yaml b/install/kubernetes/cilium/values.yaml
index 302e7b4c3..09459dd9f 100644
--- a/install/kubernetes/cilium/values.yaml
+++ b/install/kubernetes/cilium/values.yaml
@@ -215,6 +215,13 @@ global:
     # policyMapMax is the maximum number of entries in endpoint policy map (per endpoint)
     policyMapMax: 16384
 
+    # mapDynamicSizeRatio is the ratio (0.0-1.0) of total system memory to use
+    # for dynamic sizing of CT, NAT and policy BPF maps. If set to 0.0, dynamic
+    # sizing of BPF maps is disabled. The default value of 0.03 (3%) leads to
+    # approximately the default BPF map sizes on a node with 4 GiB of total
+    # system memory.
+    mapDynamicSizeRatio: 0.03
+
     # monitorAggregation is the level of aggregation for datapath trace events
     monitorAggregation: medium
 
diff --git a/install/kubernetes/quick-install.yaml b/install/kubernetes/quick-install.yaml
index ab2f4a261..47dd39b48 100644
--- a/install/kubernetes/quick-install.yaml
+++ b/install/kubernetes/quick-install.yaml
@@ -83,6 +83,10 @@ data:
   # table.
   bpf-nat-global-max: "841429"
 
+  # Specifies the ratio (0.0-1.0) of total system memory to use for dynamic
+  # sizing of the TCP CT, non-TCP CT, NAT and policy BPF maps.
+  bpf-map-dynamic-size-ratio: "0.03"
+
   # Pre-allocation of map entries allows per-packet latency to be reduced, at
   # the expense of up-front memory allocation for the entries in the maps. The
   # default value below will minimize memory usage in the default installation;
diff --git a/pkg/maps/ctmap/ctmap.go b/pkg/maps/ctmap/ctmap.go
index 5f08f7f09..4defb8083 100644
--- a/pkg/maps/ctmap/ctmap.go
+++ b/pkg/maps/ctmap/ctmap.go
@@ -119,7 +119,7 @@ func setupMapInfo(mapType MapType, define string, mapKey bpf.MapKey, keySize int
 		keySize:   keySize,
 		// the value type is CtEntry for all CT maps
 		mapValue:   &CtEntry{},
-		valueSize:  int(unsafe.Sizeof(CtEntry{})),
+		valueSize:  SizeofCtEntry,
 		maxEntries: maxEntries,
 		parser:     bpf.ConvertKeyValue,
 		natMap:     nat,
diff --git a/pkg/maps/ctmap/types.go b/pkg/maps/ctmap/types.go
index 9b8941e15..d90455fb4 100644
--- a/pkg/maps/ctmap/types.go
+++ b/pkg/maps/ctmap/types.go
@@ -381,6 +381,8 @@ type CtKey6Global struct {
 	tuple.TupleKey6Global
 }
 
+const SizeofCtKey6Global = int(unsafe.Sizeof(CtKey6Global{}))
+
 // NewValue creates a new bpf.MapValue.
 func (k *CtKey6Global) NewValue() bpf.MapValue { return &CtEntry{} }
 
@@ -483,6 +485,8 @@ type CtEntry struct {
 	LastRxReport     uint32 `align:"last_rx_report"`
 }
 
+const SizeofCtEntry = int(unsafe.Sizeof(CtEntry{}))
+
 // GetValuePtr returns the unsafe.Pointer for s.
 func (c *CtEntry) GetValuePtr() unsafe.Pointer { return unsafe.Pointer(c) }
 
diff --git a/pkg/maps/nat/ipv4.go b/pkg/maps/nat/ipv4.go
index 2f7c19198..a9a627d43 100644
--- a/pkg/maps/nat/ipv4.go
+++ b/pkg/maps/nat/ipv4.go
@@ -35,6 +35,9 @@ type NatEntry4 struct {
 	Port      uint16     `align:"to_sport"`
 }
 
+// SizeofNatEntry4 is the size of the NatEntry4 type in bytes.
+const SizeofNatEntry4 = int(unsafe.Sizeof(NatEntry4{}))
+
 // GetValuePtr returns the unsafe.Pointer for n.
 func (n *NatEntry4) GetValuePtr() unsafe.Pointer { return unsafe.Pointer(n) }
 
diff --git a/pkg/maps/nat/ipv6.go b/pkg/maps/nat/ipv6.go
index 8cc29c79a..34fe92bab 100644
--- a/pkg/maps/nat/ipv6.go
+++ b/pkg/maps/nat/ipv6.go
@@ -35,6 +35,9 @@ type NatEntry6 struct {
 	Port      uint16     `align:"to_sport"`
 }
 
+// SizeofNatEntry6 is the size of the NatEntry6 type in bytes.
+const SizeofNatEntry6 = int(unsafe.Sizeof(NatEntry6{}))
+
 // GetValuePtr returns the unsafe.Pointer for n.
 func (n *NatEntry6) GetValuePtr() unsafe.Pointer { return unsafe.Pointer(n) }
 
diff --git a/pkg/maps/nat/nat.go b/pkg/maps/nat/nat.go
index acd4945e6..f1603ee0d 100644
--- a/pkg/maps/nat/nat.go
+++ b/pkg/maps/nat/nat.go
@@ -77,14 +77,14 @@ func NewMap(name string, v4 bool, entries int) *Map {
 
 	if v4 {
 		mapKey = &NatKey4{}
-		sizeKey = int(unsafe.Sizeof(NatKey4{}))
+		sizeKey = SizeofNatKey4
 		mapValue = &NatEntry4{}
-		sizeVal = int(unsafe.Sizeof(NatEntry4{}))
+		sizeVal = SizeofNatEntry4
 	} else {
 		mapKey = &NatKey6{}
-		sizeKey = int(unsafe.Sizeof(NatKey6{}))
+		sizeKey = SizeofNatKey6
 		mapValue = &NatEntry6{}
-		sizeVal = int(unsafe.Sizeof(NatEntry6{}))
+		sizeVal = SizeofNatEntry6
 	}
 	return &Map{
 		Map: *bpf.NewMap(
diff --git a/pkg/maps/nat/types.go b/pkg/maps/nat/types.go
index 3a802185d..05bdbec1f 100644
--- a/pkg/maps/nat/types.go
+++ b/pkg/maps/nat/types.go
@@ -45,6 +45,9 @@ type NatKey4 struct {
 	tuple.TupleKey4Global
 }
 
+// SizeofNatKey4 is the size of the NatKey4 type in bytes.
+const SizeofNatKey4 = int(unsafe.Sizeof(NatKey4{}))
+
 // NewValue creates a new bpf.MapValue.
 func (k *NatKey4) NewValue() bpf.MapValue { return &NatEntry4{} }
 
@@ -80,6 +83,9 @@ type NatKey6 struct {
 	tuple.TupleKey6Global
 }
 
+// SizeofNatKey6 is the size of the NatKey6 type in bytes.
+const SizeofNatKey6 = int(unsafe.Sizeof(NatKey6{}))
+
 // NewValue creates a new bpf.MapValue.
 func (k *NatKey6) NewValue() bpf.MapValue { return &NatEntry6{} }
 
diff --git a/pkg/maps/policymap/policymap.go b/pkg/maps/policymap/policymap.go
index 413ad2ead..8733c5e39 100644
--- a/pkg/maps/policymap/policymap.go
+++ b/pkg/maps/policymap/policymap.go
@@ -76,6 +76,9 @@ type PolicyKey struct {
 	TrafficDirection uint8  `align:"egress"`
 }
 
+// SizeofPolicyKey is the size of type PolicyKey.
+const SizeofPolicyKey = int(unsafe.Sizeof(PolicyKey{}))
+
 // PolicyEntry represents an entry in the BPF policy map for an endpoint. It must
 // match the layout of policy_entry in bpf/lib/common.h.
 // +k8s:deepcopy-gen=true
@@ -89,6 +92,9 @@ type PolicyEntry struct {
 	Bytes     uint64 `align:"bytes"`
 }
 
+// SizeofPolicyEntry is the size of type PolicyEntry.
+const SizeofPolicyEntry = int(unsafe.Sizeof(PolicyEntry{}))
+
 func (pe *PolicyEntry) GetValuePtr() unsafe.Pointer { return unsafe.Pointer(pe) }
 func (pe *PolicyEntry) NewValue() bpf.MapValue      { return &PolicyEntry{} }
 
@@ -264,9 +270,9 @@ func newMap(path string) *PolicyMap {
 			path,
 			mapType,
 			&PolicyKey{},
-			int(unsafe.Sizeof(PolicyKey{})),
+			SizeofPolicyKey,
 			&PolicyEntry{},
-			int(unsafe.Sizeof(PolicyEntry{})),
+			SizeofPolicyEntry,
 			MaxEntries,
 			flags, 0,
 			bpf.ConvertKeyValue,
diff --git a/pkg/option/config.go b/pkg/option/config.go
index 187a38286..ea1a5b973 100644
--- a/pkg/option/config.go
+++ b/pkg/option/config.go
@@ -38,6 +38,7 @@ import (
 	"github.com/cilium/cilium/pkg/metrics"
 
 	"github.com/prometheus/client_golang/prometheus"
+	"github.com/shirou/gopsutil/mem"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/viper"
 )
@@ -457,12 +458,23 @@ const (
 	// and is 2/3 of the full CT size as a heuristic
 	NATMapEntriesGlobalDefault = int((CTMapEntriesGlobalTCPDefault + CTMapEntriesGlobalAnyDefault) * 2 / 3)
 
+	// MapEntriesGlobalDynamicSizeRatioName is the name of the option to
+	// set the ratio of total system memory to use for dynamic sizing of the
+	// CT, NAT and policy BPF maps.
+	MapEntriesGlobalDynamicSizeRatioName = "bpf-map-dynamic-size-ratio"
+
 	// LimitTableMin defines the minimum CT or NAT table limit
 	LimitTableMin = 1 << 10 // 1Ki entries
 
 	// LimitTableMax defines the maximum CT or NAT table limit
 	LimitTableMax = 1 << 24 // 16Mi entries (~1GiB of entries per map)
 
+	// PolicyMapMin defines the minimum policy map limit.
+	PolicyMapMin = 1 << 8
+
+	// PolicyMapMax defines the minimum policy map limit.
+	PolicyMapMax = 1 << 16
+
 	// NATMapEntriesGlobalName configures max entries for BPF NAT table
 	NATMapEntriesGlobalName = "bpf-nat-global-max"
 
@@ -1415,6 +1427,16 @@ type DaemonConfig struct {
 	EndpointStatus map[string]struct{}
 
 	k8sEnableAPIDiscovery bool
+
+	// sizeofCTElement is the size of an element (key + value) in the CT map.
+	sizeofCTElement int
+
+	// sizeofNATElement is the size of an element (key + value) in the NAT map.
+	sizeofNATElement int
+
+	// sizeofPolicyElement is the size of an element (key + value) in the
+	// policy map.
+	sizeofPolicyElement int
 }
 
 var (
@@ -1764,9 +1786,6 @@ func (c *DaemonConfig) Populate() {
 	c.AnnotateK8sNode = viper.GetBool(AnnotateK8sNode)
 	c.AutoCreateCiliumNodeResource = viper.GetBool(AutoCreateCiliumNodeResource)
 	c.BPFCompilationDebug = viper.GetBool(BPFCompileDebugName)
-	c.CTMapEntriesGlobalTCP = viper.GetInt(CTMapEntriesGlobalTCPName)
-	c.CTMapEntriesGlobalAny = viper.GetInt(CTMapEntriesGlobalAnyName)
-	c.NATMapEntriesGlobal = viper.GetInt(NATMapEntriesGlobalName)
 	c.BPFRoot = viper.GetString(BPFRoot)
 	c.CertDirectory = viper.GetString(CertsDirectory)
 	c.CGroupRoot = viper.GetString(CGroupRoot)
@@ -1864,7 +1883,6 @@ func (c *DaemonConfig) Populate() {
 	c.NAT46Range = viper.GetString(NAT46Range)
 	c.FlannelMasterDevice = viper.GetString(FlannelMasterDevice)
 	c.FlannelUninstallOnExit = viper.GetBool(FlannelUninstallOnExit)
-	c.PolicyMapEntries = viper.GetInt(PolicyMapEntriesName)
 	c.PProf = viper.GetBool(PProf)
 	c.PreAllocateMaps = viper.GetBool(PreAllocateMapsName)
 	c.PrependIptablesChains = viper.GetBool(PrependIptablesChainsName)
@@ -1894,6 +1912,10 @@ func (c *DaemonConfig) Populate() {
 		c.ipv4NativeRoutingCIDR = cidr.MustParseCIDR(nativeCIDR)
 	}
 
+	if err := c.calculateBPFMapSizes(); err != nil {
+		log.Fatal(err)
+	}
+
 	// toFQDNs options
 	// When the poller is enabled, the default MinTTL is lowered. This is to
 	// avoid caching large sets of identities generated by a poller (it runs
@@ -2101,6 +2123,118 @@ func (c *DaemonConfig) populateHostServicesProtos() error {
 	return nil
 }
 
+func (c *DaemonConfig) calculateBPFMapSizes() error {
+	// BPF map size options
+	// Any map size explicitly set via option will override the dynamic
+	// sizing.
+	c.CTMapEntriesGlobalTCP = viper.GetInt(CTMapEntriesGlobalTCPName)
+	c.CTMapEntriesGlobalAny = viper.GetInt(CTMapEntriesGlobalAnyName)
+	c.NATMapEntriesGlobal = viper.GetInt(NATMapEntriesGlobalName)
+	c.PolicyMapEntries = viper.GetInt(PolicyMapEntriesName)
+
+	// Don't attempt dynamic sizing if any of the sizeof members was not
+	// populated by the daemon (or any other caller).
+	if c.sizeofCTElement == 0 || c.sizeofNATElement == 0 || c.sizeofPolicyElement == 0 {
+		return nil
+	}
+
+	// Allow the range (0.0, 1.0] because the dynamic size will anyway be
+	// clamped to the table limits. Thus, a ratio of e.g. 0.98 will not lead
+	// to 98% of the total memory being allocated for BPF maps.
+	dynamicSizeRatio := viper.GetFloat64(MapEntriesGlobalDynamicSizeRatioName)
+	if 0.0 < dynamicSizeRatio && dynamicSizeRatio <= 1.0 {
+		vms, err := mem.VirtualMemory()
+		if err != nil || vms == nil {
+			log.WithError(err).Fatal("Failed to get system memory")
+		}
+		c.calculateDynamicBPFMapSizes(vms.Total, dynamicSizeRatio)
+	} else if dynamicSizeRatio < 0.0 {
+		return fmt.Errorf("specified dynamic map size ratio %f must be ≥ 0.0", dynamicSizeRatio)
+	} else if dynamicSizeRatio > 1.0 {
+		return fmt.Errorf("specified dynamic map size ratio %f must be ≤ 1.0", dynamicSizeRatio)
+	}
+	return nil
+}
+
+// SetMapElementSizes sets the BPF map element sizes (key + value) used for
+// dynamic BPF map size calculations in calculateDynamicBPFMapSizes.
+func (c *DaemonConfig) SetMapElementSizes(sizeofCTElement, sizeofNATElement, sizeofPolicyElement int) {
+	c.sizeofCTElement = sizeofCTElement
+	c.sizeofNATElement = sizeofNATElement
+	c.sizeofPolicyElement = sizeofPolicyElement
+}
+
+func (c *DaemonConfig) calculateDynamicBPFMapSizes(totalMemory uint64, dynamicSizeRatio float64) {
+	// Heuristic:
+	// Distribute relative to map default entries among the different maps.
+	// Cap each map size by the maximum. Map size provided by the user will
+	// override the calculated value and also the max. There will be a check
+	// for maximum size later on in DaemonConfig.Validate()
+	//
+	// Calculation examples:
+	//
+	// Memory   CT TCP  CT Any      NAT  Policy
+	//
+	//  512MB    33140   16570    33140    1035
+	//    1GB    66280   33140    66280    2071
+	//    4GB   265121  132560   265121    8285
+	//   16GB  1060485  530242  1060485   33140
+	memoryAvailableForMaps := int(float64(totalMemory) * dynamicSizeRatio)
+	log.Debugf("Memory available for map entries (%.3f of %d): %d", dynamicSizeRatio, totalMemory, memoryAvailableForMaps)
+	totalMapMemoryDefault := CTMapEntriesGlobalTCPDefault*c.sizeofCTElement +
+		CTMapEntriesGlobalAnyDefault*c.sizeofCTElement +
+		NATMapEntriesGlobalDefault*c.sizeofNATElement +
+		defaults.PolicyMapEntries*c.sizeofPolicyElement
+	log.Debugf("Total memory for default map entries: %d", totalMapMemoryDefault)
+
+	getEntries := func(entriesDefault, min, max int) int {
+		entries := (entriesDefault * memoryAvailableForMaps) / totalMapMemoryDefault
+		if entries < min {
+			entries = min
+		} else if entries > max {
+			log.Debugf("clamped from %d to %d", entries, max)
+			entries = max
+		}
+		return entries
+	}
+
+	// If value for a particular map was explicitly set by an
+	// option, disable dynamic sizing for this map and use the
+	// provided size.
+	if !viper.IsSet(CTMapEntriesGlobalTCPName) {
+		c.CTMapEntriesGlobalTCP =
+			getEntries(CTMapEntriesGlobalTCPDefault, LimitTableMin, LimitTableMax)
+		log.Debugf("option %s set by dynamic sizing to %v (default %v)",
+			CTMapEntriesGlobalTCPName, c.CTMapEntriesGlobalTCP, CTMapEntriesGlobalTCPDefault)
+	} else {
+		log.Debugf("option %s set by user to %v", CTMapEntriesGlobalTCPName, c.CTMapEntriesGlobalTCP)
+	}
+	if !viper.IsSet(CTMapEntriesGlobalAnyName) {
+		c.CTMapEntriesGlobalAny =
+			getEntries(CTMapEntriesGlobalAnyDefault, LimitTableMin, LimitTableMax)
+		log.Debugf("option %s set by dynamic sizing to %v (default %v)",
+			CTMapEntriesGlobalAnyName, c.CTMapEntriesGlobalAny, CTMapEntriesGlobalAnyDefault)
+	} else {
+		log.Debugf("option %s set by user to %v", CTMapEntriesGlobalAnyName, c.CTMapEntriesGlobalAny)
+	}
+	if !viper.IsSet(NATMapEntriesGlobalName) {
+		c.NATMapEntriesGlobal =
+			getEntries(NATMapEntriesGlobalDefault, LimitTableMin, LimitTableMax)
+		log.Debugf("option %s set by dynamic sizing to %v (default %v)",
+			NATMapEntriesGlobalName, c.NATMapEntriesGlobal, NATMapEntriesGlobalDefault)
+	} else {
+		log.Debugf("option %s set by user to %v", NATMapEntriesGlobalName, c.NATMapEntriesGlobal)
+	}
+	if !viper.IsSet(PolicyMapEntriesName) {
+		c.PolicyMapEntries =
+			getEntries(defaults.PolicyMapEntries, PolicyMapMin, PolicyMapMax)
+		log.Debugf("option %s set by dynamic sizing to %v (default %v)",
+			PolicyMapEntriesName, c.PolicyMapEntries, defaults.PolicyMapEntries)
+	} else {
+		log.Debugf("option %s set by user to %v", PolicyMapEntriesName, c.PolicyMapEntries)
+	}
+}
+
 func sanitizeIntParam(paramName string, paramDefault int) int {
 	intParam := viper.GetInt(paramName)
 	if intParam <= 0 {
diff --git a/pkg/option/config_test.go b/pkg/option/config_test.go
index 46757710b..c0303ec1b 100644
--- a/pkg/option/config_test.go
+++ b/pkg/option/config_test.go
@@ -24,6 +24,8 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/cilium/cilium/pkg/defaults"
+	"github.com/google/go-cmp/cmp"
 	flag "github.com/spf13/pflag"
 	"github.com/spf13/viper"
 	. "gopkg.in/check.v1"
@@ -245,3 +247,178 @@ func (s *OptionSuite) TestEndpointStatusValues(c *C) {
 		c.Assert(ok, Equals, true)
 	}
 }
+
+const (
+	_   = iota
+	KiB = 1 << (10 * iota)
+	MiB
+	GiB
+)
+
+func TestBPFMapSizeCalculation(t *testing.T) {
+	type sizes struct {
+		CTMapSizeTCP  int
+		CTMapSizeAny  int
+		NATMapSize    int
+		PolicyMapSize int
+	}
+	tests := []struct {
+		name        string
+		totalMemory uint64
+		ratio       float64
+		want        sizes
+		preTestRun  func()
+	}{
+		{
+			name: "static default sizes",
+			// zero memory and ratio: skip calculateDynamicBPFMapSizes
+			want: sizes{
+				CTMapSizeTCP:  CTMapEntriesGlobalTCPDefault,
+				CTMapSizeAny:  CTMapEntriesGlobalAnyDefault,
+				NATMapSize:    NATMapEntriesGlobalDefault,
+				PolicyMapSize: defaults.PolicyMapEntries,
+			},
+			preTestRun: func() {
+				viper.Set(CTMapEntriesGlobalTCPName, CTMapEntriesGlobalTCPDefault)
+				viper.Set(CTMapEntriesGlobalAnyName, CTMapEntriesGlobalAnyDefault)
+				viper.Set(NATMapEntriesGlobalName, NATMapEntriesGlobalDefault)
+				viper.Set(PolicyMapEntriesName, defaults.PolicyMapEntries)
+			},
+		},
+		{
+			name: "static, non-default sizes inside range",
+			// zero memory and ratio: skip calculateDynamicBPFMapSizes
+			want: sizes{
+				CTMapSizeTCP:  CTMapEntriesGlobalTCPDefault + 128,
+				CTMapSizeAny:  CTMapEntriesGlobalAnyDefault - 64,
+				NATMapSize:    NATMapEntriesGlobalDefault + 256,
+				PolicyMapSize: defaults.PolicyMapEntries - 32,
+			},
+			preTestRun: func() {
+				viper.Set(CTMapEntriesGlobalTCPName, CTMapEntriesGlobalTCPDefault+128)
+				viper.Set(CTMapEntriesGlobalAnyName, CTMapEntriesGlobalAnyDefault-64)
+				viper.Set(NATMapEntriesGlobalName, NATMapEntriesGlobalDefault+256)
+				viper.Set(PolicyMapEntriesName, defaults.PolicyMapEntries-32)
+			},
+		},
+		{
+			name:        "dynamic size without any static sizes (512MB, 3%)",
+			totalMemory: 512 * MiB,
+			ratio:       0.03,
+			want: sizes{
+				CTMapSizeTCP:  68246,
+				CTMapSizeAny:  34123,
+				NATMapSize:    68246,
+				PolicyMapSize: 2132,
+			},
+		},
+		{
+			name:        "dynamic size without any static sizes (1GiB, 3%)",
+			totalMemory: 1 * GiB,
+			ratio:       0.03,
+			want: sizes{
+				CTMapSizeTCP:  136492,
+				CTMapSizeAny:  68246,
+				NATMapSize:    136492,
+				PolicyMapSize: 4265,
+			},
+		},
+		{
+			name:        "dynamic size without any static sizes (2GiB, 3%)",
+			totalMemory: 2 * GiB,
+			ratio:       0.03,
+			want: sizes{
+				CTMapSizeTCP:  272985,
+				CTMapSizeAny:  136492,
+				NATMapSize:    272985,
+				PolicyMapSize: 8530,
+			},
+		},
+		{
+			name:        "dynamic size without any static sizes (4GiB, 3%)",
+			totalMemory: 4 * GiB,
+			ratio:       0.03,
+			want: sizes{
+				CTMapSizeTCP:  545970,
+				CTMapSizeAny:  272985,
+				NATMapSize:    545970,
+				PolicyMapSize: 17061,
+			},
+		},
+		{
+			name:        "dynamic size without any static sizes (16GiB, 3%)",
+			totalMemory: 16 * GiB,
+			ratio:       0.03,
+			want: sizes{
+				CTMapSizeTCP:  2183881,
+				CTMapSizeAny:  1091940,
+				NATMapSize:    2183881,
+				PolicyMapSize: PolicyMapMax,
+			},
+		},
+		{
+			name:        "dynamic size with static CT TCP size (4GiB, 2.5%)",
+			totalMemory: 4 * GiB,
+			ratio:       0.025,
+			want: sizes{
+				CTMapSizeTCP:  CTMapEntriesGlobalTCPDefault + 1024,
+				CTMapSizeAny:  227487,
+				NATMapSize:    454975,
+				PolicyMapSize: 14217,
+			},
+			preTestRun: func() {
+				viper.Set(CTMapEntriesGlobalTCPName, CTMapEntriesGlobalTCPDefault+1024)
+			},
+		},
+		{
+			name:        "huge dynamic size ratio gets clamped (8GiB, 98%)",
+			totalMemory: 16 * GiB,
+			ratio:       0.98,
+			want: sizes{
+				CTMapSizeTCP:  LimitTableMax,
+				CTMapSizeAny:  LimitTableMax,
+				NATMapSize:    LimitTableMax,
+				PolicyMapSize: PolicyMapMax,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			viper.Reset()
+
+			if tt.preTestRun != nil {
+				tt.preTestRun()
+			}
+
+			d := &DaemonConfig{
+				CTMapEntriesGlobalTCP: viper.GetInt(CTMapEntriesGlobalTCPName),
+				CTMapEntriesGlobalAny: viper.GetInt(CTMapEntriesGlobalAnyName),
+				NATMapEntriesGlobal:   viper.GetInt(NATMapEntriesGlobalName),
+				PolicyMapEntries:      viper.GetInt(PolicyMapEntriesName),
+			}
+			// cannot set these from the Sizeof* consts from
+			// pkg/maps/* due to circular dependencies.
+			d.SetMapElementSizes(
+				94, // ctmap.SizeofCTKey + policymap.SizeofCTEntry
+				94, // nat.SizeofNATKey + nat.SizeofNATEntry
+				32, // policymap.SizeofPolicyKey + policymap.SizeofPolicyEntry
+			)
+
+			if tt.totalMemory > 0 && tt.ratio > 0.0 {
+				d.calculateDynamicBPFMapSizes(tt.totalMemory, tt.ratio)
+			}
+
+			got := sizes{
+				d.CTMapEntriesGlobalTCP,
+				d.CTMapEntriesGlobalAny,
+				d.NATMapEntriesGlobal,
+				d.PolicyMapEntries,
+			}
+
+			if diff := cmp.Diff(tt.want, got); diff != "" {
+				t.Errorf("DaemonConfig.calculateDynamicBPFMapSize (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
-- 
2.26.2

