diff options
| author | Nataliia Solomko <natalirs1985@gmail.com> | 2026-08-04 17:40:11 +0300 |
|---|---|---|
| committer | Nataliia Solomko <natalirs1985@gmail.com> | 2026-08-31 13:45:52 +0300 |
| commit | aa1eb26e371e14dad0df002bb8ba784e7c7f6f82 (patch) | |
| tree | cc19b600492eec95c3676fe813af9e9aa888b5d7 /src | |
| parent | 2c269ef450f88690d03970fcafb1dbaa8b3b7f54 (diff) | |
| download | vyos-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-x | src/conf_mode/interfaces_ethernet.py | 58 | ||||
| -rwxr-xr-x | src/conf_mode/vpp.py | 41 |
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 |
