Change to use selectin for DB load strategy

During a mailing list discussion on some OOM issues neutron
has been seeing [0], Mike Bayer recommended we should change
from using subquery to selectin DB load strategy.

A full description of this strategy can be found here [1],
but in short:

- “subquery” loading incurs additional performance / complexity
  issues when used on a many-levels-deep eager load, as
  subqueries will be nested repeatedly.

- "The subqueryload() eager loader is mostly legacy at this
  point, superseded by selectinload()

- "The only scenario in which selectin eager loading is not
  feasible is when the model is using composite primary keys,
  and the backend database does not support tuples with IN,
  which currently includes SQL Server." So that does not
  apply to us.

The plan agreed to at the neutron drivers meeting [2] was to
make this change early in the cycle so we would be able to
see if there were any issues through the D cycle.

Added hacking checks so new code using subquery loads is
not added back.

[0] https://lists.openstack.org/archives/list/openstack-discuss@lists.openstack.org/thread/EHLQQXNG3NLLZYPDGG2ES3DINIJ7YT3N/
[1] https://docs.sqlalchemy.org/en/20/orm/queryguide/relationships.html#selectin-eager-loading
[2] https://meetings.opendev.org/meetings/neutron_drivers/2024/neutron_drivers.2024-05-31-14.00.log.html#l-67

Closes-bug: #2067770
Depends-on: https://review.opendev.org/c/openstack/neutron-lib/+/920936
Change-Id: I6e40a15284da392a3d48d45205a7a5770c14c297
This commit is contained in:
Brian Haley 2024-05-31 16:14:32 -04:00
parent abe8110f53
commit 05fcfef6ce
20 changed files with 59 additions and 30 deletions

View File

@ -42,5 +42,5 @@ class ExtraDhcpOpt(model_base.BASEV2, model_base.HasId):
# eagerly load extra_dhcp_opts bindings
ports = orm.relationship(
models_v2.Port, load_on_pending=True,
backref=orm.backref("dhcp_opts", lazy='subquery', cascade='delete'))
backref=orm.backref("dhcp_opts", lazy='selectin', cascade='delete'))
revises_on_change = ('ports', )

View File

@ -42,7 +42,7 @@ class AddressGroup(standard_attr.HasStandardAttributes,
addresses = orm.relationship(AddressAssociation,
backref=orm.backref('address_groups',
load_on_pending=True),
lazy='subquery',
lazy='selectin',
cascade='all, delete-orphan')
rbac_entries = sa.orm.relationship(rbac_db_models.AddressGroupRBAC,
backref='address_groups',

View File

@ -27,5 +27,5 @@ class AllowedAddressPair(model_base.BASEV2):
port = orm.relationship(
models_v2.Port, load_on_pending=True,
backref=orm.backref("allowed_address_pairs",
lazy="subquery", cascade="delete"))
lazy="selectin", cascade="delete"))
revises_on_change = ('port', )

View File

@ -39,7 +39,7 @@ class ConntrackHelper(model_base.BASEV2, model_base.HasId):
router = orm.relationship(l3.Router, load_on_pending=True,
backref=orm.backref("conntrack_helpers",
lazy='subquery',
lazy='selectin',
uselist=True,
cascade='delete'))
revises_on_change = ('router', )

View File

@ -41,7 +41,7 @@ class FlavorServiceProfileBinding(model_base.BASEV2):
flavor = orm.relationship(Flavor,
backref=orm.backref(
"service_profiles",
lazy='subquery',
lazy='selectin',
cascade="all, delete-orphan"))
service_profile_id = sa.Column(sa.String(36),
sa.ForeignKey("serviceprofiles.id",

View File

@ -58,9 +58,9 @@ class Router(standard_attr.HasStandardAttributes, model_base.BASEV2,
attached_ports = orm.relationship(
RouterPort,
backref=orm.backref('router', load_on_pending=True),
lazy='subquery')
lazy='selectin')
l3_agents = orm.relationship(
'Agent', lazy='subquery', viewonly=True,
'Agent', lazy='selectin', viewonly=True,
secondary=rb_model.RouterL3AgentBinding.__table__)
api_collections = [l3_apidef.ROUTERS]
collection_resource_map = {l3_apidef.ROUTERS: l3_apidef.ROUTER}
@ -120,6 +120,6 @@ class RouterRoute(model_base.BASEV2, models_v2.Route):
router = orm.relationship(Router, load_on_pending=True,
backref=orm.backref("route_list",
lazy='subquery',
lazy='selectin',
cascade='delete'))
revises_on_change = ('router', )

View File

@ -38,11 +38,11 @@ class MeteringLabel(model_base.BASEV2,
name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE))
description = sa.Column(sa.String(db_const.LONG_DESCRIPTION_FIELD_SIZE))
rules = orm.relationship(MeteringLabelRule, backref="label",
cascade="delete", lazy="subquery")
cascade="delete", lazy="selectin")
routers = orm.relationship(
l3_models.Router,
primaryjoin="MeteringLabel.project_id==Router.project_id",
foreign_keys='MeteringLabel.project_id',
lazy='subquery',
lazy='selectin',
uselist=True)
shared = sa.Column(sa.Boolean, default=False, server_default=sql.false())

View File

@ -63,6 +63,6 @@ class RouterNDPProxyState(model_base.BASEV2):
router = orm.relationship(
l3.Router, load_on_pending=True,
backref=orm.backref("ndp_proxy_state",
lazy='subquery', uselist=False,
lazy='selectin', uselist=False,
cascade='delete')
)

View File

@ -58,13 +58,13 @@ class PortForwarding(standard_attr.HasStandardAttributes,
models_v2.Port, load_on_pending=True,
foreign_keys=internal_neutron_port_id,
backref=orm.backref("port_forwardings",
lazy='subquery', uselist=True,
lazy='selectin', uselist=True,
cascade='delete')
)
floating_ip = orm.relationship(
l3.FloatingIP, load_on_pending=True,
backref=orm.backref("port_forwardings",
lazy='subquery', uselist=True,
lazy='selectin', uselist=True,
cascade='delete')
)
revises_on_change = ('floating_ip', 'port',)

View File

@ -49,7 +49,7 @@ class NetworkSegment(standard_attr.HasStandardAttributes,
nullable=True)
network = orm.relationship(models_v2.Network,
backref=orm.backref("segments",
lazy='subquery',
lazy='selectin',
cascade='delete'))
api_collections = [segment.COLLECTION_NAME]
@ -81,6 +81,6 @@ class SegmentHostMapping(model_base.BASEV2):
network_segment = orm.relationship(
NetworkSegment, load_on_pending=True,
backref=orm.backref("segment_host_mapping",
lazy='subquery',
lazy='selectin',
cascade='delete'))
revises_on_change = ('network_segment', )

View File

@ -33,7 +33,7 @@ class SubnetServiceType(model_base.BASEV2):
length=db_const.DEVICE_OWNER_FIELD_SIZE))
subnet = orm.relationship(models_v2.Subnet, load_on_pending=True,
backref=orm.backref('service_types',
lazy='subquery',
lazy='selectin',
cascade='all, delete-orphan',
uselist=True))
__table_args__ = (

View File

@ -26,6 +26,6 @@ class Tag(model_base.BASEV2):
tag = sa.Column(sa.String(255), nullable=False, primary_key=True)
standard_attr = orm.relationship(
'StandardAttribute', load_on_pending=True,
backref=orm.backref('tags', lazy='subquery', viewonly=True),
backref=orm.backref('tags', lazy='selectin', viewonly=True),
sync_backref=False)
revises_on_change = ('standard_attr', )

View File

@ -132,7 +132,7 @@ class Port(standard_attr.HasStandardAttributes, model_base.BASEV2,
fixed_ips = orm.relationship(IPAllocation,
backref=orm.backref('port',
load_on_pending=True),
lazy='subquery',
lazy='selectin',
cascade='all, delete-orphan',
order_by=(IPAllocation.ip_address,
IPAllocation.subnet_id))
@ -217,24 +217,24 @@ class Subnet(standard_attr.HasStandardAttributes, model_base.BASEV2,
cidr = sa.Column(sa.String(64), nullable=False)
gateway_ip = sa.Column(sa.String(64))
network_standard_attr = orm.relationship(
'StandardAttribute', lazy='subquery', viewonly=True,
'StandardAttribute', lazy='selectin', viewonly=True,
secondary='networks', uselist=False,
load_on_pending=True)
revises_on_change = ('network_standard_attr', )
allocation_pools = orm.relationship(IPAllocationPool,
backref='subnet',
lazy="subquery",
lazy="selectin",
cascade='delete')
enable_dhcp = sa.Column(sa.Boolean())
dns_nameservers = orm.relationship(DNSNameServer,
backref='subnet',
cascade='all, delete, delete-orphan',
order_by=DNSNameServer.order,
lazy='subquery')
lazy='selectin')
routes = orm.relationship(SubnetRoute,
backref='subnet',
cascade='all, delete, delete-orphan',
lazy='subquery')
lazy='selectin')
ipv6_ra_mode = sa.Column(sa.Enum(constants.IPV6_SLAAC,
constants.DHCPV6_STATEFUL,
constants.DHCPV6_STATELESS,
@ -299,7 +299,7 @@ class SubnetPool(standard_attr.HasStandardAttributes, model_base.BASEV2,
prefixes = orm.relationship(SubnetPoolPrefix,
backref='subnetpools',
cascade='all, delete, delete-orphan',
lazy='subquery')
lazy='selectin')
rbac_entries = sa.orm.relationship(rbac_db_models.SubnetPoolRBAC,
backref='subnetpools',
lazy='joined',
@ -317,7 +317,7 @@ class Network(standard_attr.HasStandardAttributes, model_base.BASEV2,
name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE))
subnets = orm.relationship(
Subnet,
lazy="subquery")
lazy="selectin")
status = sa.Column(sa.String(16))
admin_state_up = sa.Column(sa.Boolean)
vlan_transparent = sa.Column(sa.Boolean, nullable=True)
@ -331,7 +331,7 @@ class Network(standard_attr.HasStandardAttributes, model_base.BASEV2,
default=constants.DEFAULT_NETWORK_MTU,
server_default=str(constants.DEFAULT_NETWORK_MTU))
dhcp_agents = orm.relationship(
'Agent', lazy='subquery', viewonly=True,
'Agent', lazy='selectin', viewonly=True,
secondary=ndab_model.NetworkDhcpAgentBinding.__table__)
api_collections = [net_def.COLLECTION_NAME]
collection_resource_map = {net_def.COLLECTION_NAME: net_def.RESOURCE_NAME}

View File

@ -31,7 +31,7 @@ class NetworkDhcpAgentBinding(model_base.BASEV2):
network_id = sa.Column(sa.String(36),
sa.ForeignKey("networks.id", ondelete='CASCADE'),
primary_key=True)
dhcp_agent = orm.relationship(agent_model.Agent, lazy='subquery')
dhcp_agent = orm.relationship(agent_model.Agent, lazy='selectin')
dhcp_agent_id = sa.Column(sa.String(36),
sa.ForeignKey("agents.id",
ondelete='CASCADE'),

View File

@ -44,6 +44,9 @@ import_packaging = re.compile(r"\bimport[\s]+packaging\b")
import_version_from_packaging = (
re.compile(r"\bfrom[\s]+packaging[\s]+import[\s]version\b"))
filter_lazy_subquery = re.compile(r".*lazy=.+subquery")
filter_subquery_load = re.compile(r".*subqueryload\(")
@core.flake8ext
def check_assert_called_once_with(logical_line, filename):
@ -263,3 +266,17 @@ def check_no_import_packaging(logical_line, filename, noqa):
for regex in import_packaging, import_version_from_packaging:
if re.match(regex, logical_line):
yield (0, msg)
@core.flake8ext
def check_no_sqlalchemy_lazy_subquery(logical_line):
"""N350 - Use selectin DB load strategy instead of subquery."""
msg = ("N350: Use selectin DB load strategy instead of "
"subquery with sqlalchemy.")
if filter_lazy_subquery.match(logical_line):
yield (0, msg)
if filter_subquery_load.match(logical_line):
yield (0, msg)

View File

@ -79,7 +79,7 @@ def _get_active_network_ports(context, network_id):
agent_model.Agent,
agent_model.Agent.host == ml2_models.PortBinding.host)
query = query.join(models_v2.Port)
query = query.options(orm.subqueryload(ml2_models.PortBinding.port))
query = query.options(orm.selectinload(ml2_models.PortBinding.port))
query = query.filter(models_v2.Port.network_id == network_id,
models_v2.Port.status == const.PORT_STATUS_ACTIVE)
return query
@ -126,7 +126,7 @@ def _get_dvr_active_network_ports(context, network_id):
ml2_models.DistributedPortBinding.host)
query = query.join(models_v2.Port)
query = query.options(
orm.subqueryload(ml2_models.DistributedPortBinding.port))
orm.selectinload(ml2_models.DistributedPortBinding.port))
query = query.filter(models_v2.Port.network_id == network_id,
models_v2.Port.status == const.PORT_STATUS_ACTIVE,
models_v2.Port.device_owner ==

View File

@ -89,7 +89,7 @@ class PortBindingLevel(model_base.BASEV2):
port = orm.relationship(
models_v2.Port,
load_on_pending=True,
backref=orm.backref("binding_levels", lazy='subquery',
backref=orm.backref("binding_levels", lazy='selectin',
cascade='delete'))
segment = orm.relationship(
segment_models.NetworkSegment,

View File

@ -46,7 +46,7 @@ class Trunk(standard_attr.HasStandardAttributes, model_base.BASEV2,
cascade='delete'))
sub_ports = sa.orm.relationship(
'SubPort', lazy='subquery', uselist=True, cascade="all, delete-orphan")
'SubPort', lazy='selectin', uselist=True, cascade="all, delete-orphan")
api_collections = ['trunks']
collection_resource_map = {'trunks': 'trunk'}
tag_support = True

View File

@ -279,3 +279,14 @@ class HackingTestCase(base.BaseTestCase):
_pass(["_('foo')"], "neutron/_i18n.py")
_pass(["_('foo')"], "neutron/i18n.py")
_pass(["_('foo')"], "neutron/foo.py", noqa=True)
def test_check_no_sqlalchemy_lazy_subquery(self):
f = checks.check_no_sqlalchemy_lazy_subquery
self.assertLineFails('N350', f,
"backref=orm.backref('tags', lazy='subquery', viewonly=True),")
self.assertLineFails('N350', f,
"query.options(orm.subqueryload(ml2_models.PortBinding.port))")
self.assertLinePasses(f,
"backref=orm.backref('tags', lazy='selectin', viewonly=True),")
self.assertLinePasses(f,
"query.options(orm.selectinload(ml2_models.PortBinding.port))")

View File

@ -239,6 +239,7 @@ extension =
N346 = neutron.hacking.checks:check_no_sqlalchemy_event_import
N348 = neutron.hacking.checks:check_no_import_six
N349 = neutron.hacking.checks:check_no_import_packaging
N350 = neutron.hacking.checks:check_no_sqlalchemy_lazy_subquery
# Checks from neutron-lib
N521 = neutron_lib.hacking.checks:use_jsonutils
N524 = neutron_lib.hacking.checks:check_no_contextlib_nested