Skip to content

fix: N+1 node queries in dhcp, api and websocket handlers#439

Open
xtrusia wants to merge 5 commits into
canonical:masterfrom
xtrusia:fix/node-query-n-plus-one
Open

fix: N+1 node queries in dhcp, api and websocket handlers#439
xtrusia wants to merge 5 commits into
canonical:masterfrom
xtrusia:fix/node-query-n-plus-one

Conversation

@xtrusia

@xtrusia xtrusia commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

On large subnets several code paths run one or more node queries per object.
On a /19 subnet with about 6000 allocated IPs, one DHCP configuration rebuild runs about 18000 queries.
The ipaddresses API read has the same problem.
On one deployment at this scale, database statistics showed billions of reads on maasserver_nodeconfig and maasserver_node.
The same deployment had slow API responses and deploy timeouts.

The problem is worse on versions that also have LP:2130237.
That bug makes the region unresponsive during rack registration, so rack controllers time out and reconnect.
Reconnects trigger new DHCP configuration pushes.
On a large subnet every push runs the N+1 queries again on the same database thread pool.

Each commit fixes one path and adds a regression test for it.

  • DHCP host generation (make_hosts_for_subnets)
  • IPAddressesHandler.read
  • the interface listing in Fabric.delete
  • tracked VMs in the vmcluster handler
  • Pod.host in the pod websocket handler

Each path now loads the node together with the main query, so the query count stays constant when the object count grows.
The tests check that the query count does not grow with the number of objects.
The prefetches keep the related rows in memory and use an IN clause with one entry per IP, the normal prefetch_related tradeoff.

We plan to backport these fixes to the stable branches down to 3.4, where the same code paths run in production.

@xtrusia xtrusia requested a review from a team July 8, 2026 04:19
@maas-lander

Copy link
Copy Markdown
Collaborator

Check where you would like a Mattermost message to be sent to when CI completes and this PR is merged

  • Direct message
  • ~maas

@xtrusia xtrusia force-pushed the fix/node-query-n-plus-one branch from 7d6b12c to 0d0e9ce Compare July 8, 2026 04:20
make_hosts_for_subnets iterated sip.interface_set.order_by("id") for
every allocated IP and then read each interface's node config, node
and parents -- several database queries per host, i.e. O(number of
hosts) round-trips (~18000 for a subnet with 6000 static IPs).
The whole configuration is regenerated on every change and runs in a
database thread, so this scales badly on large subnets.

Move the ordering into a Prefetch and load node_config/node and
parents alongside, so the query count stays constant regardless of
host count.

Signed-off-by: Seyeong Kim <seyeong.kim@canonical.com>
@xtrusia xtrusia force-pushed the fix/node-query-n-plus-one branch from 0d0e9ce to 8fdaf87 Compare July 8, 2026 04:46
xtrusia added 4 commits July 8, 2026 14:19
IPAddressesHandler.read serialized each IP's interface_set, whose
system_id/resource_uri call Interface.get_node() (node_config.node),
so it ran a query per IP against maasserver_nodeconfig and
maasserver_node. interface_set is a M2M, so those relations are not
covered by a plain prefetch and must be named explicitly.

Prefetch interface_set with the node selected in the same query so
the node query count stays constant regardless of the number of IPs
returned.

Signed-off-by: Seyeong Kim <seyeong.kim@canonical.com>
Fabric.delete lists the still-connected interfaces via
get_log_string(), which calls get_node() per interface.
select_related node_config__node so building that error message does
not query the node per interface.

Signed-off-by: Seyeong Kim <seyeong.kim@canonical.com>
The vmcluster handler reads vm.machine (system_id/hostname) for each
tracked VM, querying the node per VM. select_related("machine") in
tracked_virtual_machines so the query count does not grow with the
number of VMs.

Signed-off-by: Seyeong Kim <seyeong.kim@canonical.com>
Pod.host calls hints.nodes.first(), which queried the node once per
pod in the handler's list. Prefetch hints__nodes ordered by id in the
handler queryset so first() is served from the cache.

Signed-off-by: Seyeong Kim <seyeong.kim@canonical.com>
@xtrusia xtrusia force-pushed the fix/node-query-n-plus-one branch from 8fdaf87 to 239efcc Compare July 8, 2026 05:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants