summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNataliia Solomko <natalirs1985@gmail.com>2026-08-04 17:40:11 +0300
committerNataliia Solomko <natalirs1985@gmail.com>2026-08-31 13:45:52 +0300
commitaa1eb26e371e14dad0df002bb8ba784e7c7f6f82 (patch)
treecc19b600492eec95c3676fe813af9e9aa888b5d7 /src
parent2c269ef450f88690d03970fcafb1dbaa8b3b7f54 (diff)
downloadvyos-1x-aa1eb26e371e14dad0df002bb8ba784e7c7f6f82.tar.gz
vyos-1x-aa1eb26e371e14dad0df002bb8ba784e7c7f6f82.zip
vpp: T9146: Enable DPDK no-multi-seg when a full frame fits the buffer
Disabling DPDK multi-segment buffers (no-multi-seg) lets VPP receive each frame into a single buffer, increasing forwarding throughput. The frame must then fit the buffer 'data-size', so 'no-multi-seg' is computed: it is enabled only while the largest MTU across all VPP-bound interfaces, plus its L2 framing overhead (26 bytes), fits 'data-size'. Otherwise VPP keeps multi-seg and Jumbo frames still work without a larger buffer. Changing the MTU of a VPP-bound interface can flip the flag, so interfaces_ethernet reconfigures VPP when it changes, via an initial dependency (set_dependents_initial) to avoid a cycle with the existing vpp -> ethernet dependency.
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/interfaces_ethernet.py58
-rwxr-xr-xsrc/conf_mode/vpp.py41
2 files changed, 96 insertions, 3 deletions
diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py
index 16ff90300..552137852 100755
--- a/src/conf_mode/interfaces_ethernet.py
+++ b/src/conf_mode/interfaces_ethernet.py
@@ -22,6 +22,8 @@ from vyos.base import Warning
from vyos.config import Config
from vyos.configdep import set_dependents
from vyos.configdep import call_dependents
+from vyos.configdep import set_dependents_initial
+from vyos.configdep import call_dependents_initial
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
from vyos.configdict import is_vrf_changed
@@ -48,7 +50,9 @@ from vyos.utils.dict import dict_set
from vyos.utils.dict import dict_delete
from vyos.utils.network import get_vrf_tableid
from vyos.utils.process import is_systemd_service_running
+from vyos.utils.file import read_file
from vyos.vpp.config_deps import deps_bond_dict
+from vyos.vpp.config_resource_checks.memory import no_multi_seg_fits
from vyos.vpp.config_verify import verify_vpp_remove_interface
from vyos.vpp.config_verify import verify_vpp_mac_change_supported
from vyos.vpp.control_vpp import VPPControl
@@ -140,6 +144,51 @@ def update_bond_options(conf: Config, eth_conf: dict) -> list:
eth_conf['bond_blocked_changes'] = blocked_list
return None
+def _vpp_no_multi_seg_change(conf: Config) -> bool:
+ """Whether VPP must be reconfigured to flip 'no-multi-seg' (T9146)
+
+ True only when the flag the current config requires differs from the one VPP
+ is running with
+ """
+ # VPP is down: nothing to reconfigure now; the flag is recomputed the next
+ # time vpp.py runs (a 'vpp' commit or boot).
+ if not is_systemd_service_running('vpp.service'):
+ return False
+
+ # Largest MTU across all VPP-bound interfaces - the flag depends on the max,
+ # not just the interface being changed, so a change that leaves a bigger
+ # interface in place does not needlessly flip it.
+ vpp_ifaces = conf.list_nodes(['vpp', 'settings', 'interface'], default=[])
+ max_mtu = max(
+ (
+ int(conf.return_value(['interfaces', 'ethernet', iface, 'mtu']) or 1500)
+ for iface in vpp_ifaces
+ ),
+ default=1500,
+ )
+ # buffer data-size (default 2048) that the frame must fit under 'no-multi-seg'
+ data_size = int(
+ conf.return_value(
+ ['vpp', 'settings', 'resource-allocation', 'buffers', 'data-size']
+ )
+ or 2048
+ )
+ # what the new config requires after MTU change
+ desired = no_multi_seg_fits(max_mtu, data_size)
+
+ # read the flag VPP is actually running with from its generated startup.conf
+ try:
+ current = 'no-multi-seg' in read_file('/run/vpp/vpp.conf')
+ except OSError:
+ # Running state unknown: reconfigure only when 'no-multi-seg' must be
+ # off (an oversized frame would drop otherwise). When it should be on,
+ # multi-seg is a safe fallback, so skip the restart.
+ return not desired
+
+ # reconfigure VPP only when the flag actually flips
+ return current != desired
+
+
def get_config(config=None):
"""
Retrieve CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -204,6 +253,14 @@ def get_config(config=None):
if is_vrf_changed(conf, ifname):
set_dependents('firewall', conf)
+ # T9146: An MTU change on a VPP-bound interface can flip 'no-multi-seg';
+ # reconfigure VPP only when it actually flips. An initial dependency is
+ # used because a normal ethernet -> vpp one would cycle with the existing
+ # vpp -> ethernet dependency.
+ if vpp_config and is_node_changed(conf, base + [ifname, 'mtu']):
+ if _vpp_no_multi_seg_change(conf):
+ set_dependents_initial('vpp', conf)
+
return ethernet
def verify_speed_duplex(ethernet: dict, ethtool: Ethtool):
@@ -458,6 +515,7 @@ def apply(ethernet):
# run the dependents
call_dependents()
+ call_dependents_initial()
vpp_iface_config = dict_search(f'vpp.settings.interface.{ifname}', ethernet)
if vpp_iface_config is not None and is_systemd_service_running('vpp.service'):
diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py
index a831ded02..3c62e3c43 100755
--- a/src/conf_mode/vpp.py
+++ b/src/conf_mode/vpp.py
@@ -76,9 +76,16 @@ service_name = 'vpp'
service_conf = Path(f'/run/vpp/{service_name}.conf')
systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf'
-vpp_log = getLogger(
- service_name, format='%(filename)s[%(process)d]: %(message)s', address='/dev/log'
-)
+try:
+ vpp_log = getLogger(
+ service_name,
+ format='%(filename)s[%(process)d]: %(message)s',
+ address='/dev/log',
+ )
+except ValueError:
+ # vpp.py is re-executed when run as a config dependency (T9146); the named
+ # logger is created only once, so reuse the existing instance on re-import.
+ vpp_log = getLogger(service_name)
dependency_interface_type_map = {
'vpp_interfaces_bonding': 'bonding',
@@ -209,6 +216,24 @@ def _normalize_buffers(config: dict):
)
+def _set_no_multi_seg(config: dict):
+ """Enable 'no-multi-seg' when the largest VPP interface MTU fits one buffer.
+
+ Disabling multi-segment buffers boosts throughput, but a received frame must
+ then fit a single buffer - i.e. the largest interface MTU (plus L2 framing)
+ must fit within the buffer 'data-size'. When it does, set the flag so the
+ template emits 'no-multi-seg'; otherwise leave it unset so VPP keeps
+ multi-seg and can still carry Jumbo frames without a bigger buffer (T9146).
+ """
+ max_mtu = max(
+ (int(c['mtu']) for c in config['settings'].get('interface', {}).values()),
+ default=1500,
+ )
+ data_size = config['settings']['resource_allocation']['buffers']['data_size']
+ if memory.no_multi_seg_fits(max_mtu, data_size):
+ config['settings']['no_multi_seg'] = True
+
+
def _get_max_xdp_rx_queues(config: dict):
"""
Count max number of RX queues for XDP driver
@@ -381,6 +406,11 @@ def get_config(config=None):
for iface, iface_config in config['settings']['interface'].items():
iface_config['driver'] = 'dpdk'
+ # configured MTU, used to set no_multi_seg option
+ iface_config['mtu'] = (
+ conf.return_value(['interfaces', 'ethernet', iface, 'mtu'])
+ or '1500'
+ )
# old_driver = leaf_node_changed(
# conf, base_settings + ['interface', iface, 'driver']
@@ -478,6 +508,11 @@ def get_config(config=None):
xdp_api_params['flags'] = 'no_syscall_lock'
iface_config['xdp_api_params'] = xdp_api_params
+ # Enable 'no-multi-seg' only when the buffer data-size can hold the
+ # largest interface MTU in a single segment (T9146); otherwise VPP keeps
+ # multi-seg so Jumbo frames still work.
+ _set_no_multi_seg(config)
+
# Buffer normalization (auto → computed)
_normalize_buffers(config)
# Configure VPP main-core and workers 'cpu-cores' settings