[ip-pool] simplify range overlap-check query 🤖#10757
Open
jam-mad wants to merge 2 commits into
Open
Conversation
FilterOverlappingIpRanges checked ip_pool_range and subnet_pool_member with four NOT EXISTS subqueries per table (eight total), each shaped to hit one of ip_pool_range's two single-column indexes. subnet_pool_member was added to the same check later using the same pattern, but it only has a composite index over virtual columns, so the "one subquery per index" reasoning never applied to it. Collapsed this to the standard interval-overlap test, one subquery per table (two total): first_address <= candidate_last AND last_address >= candidate_first. Measured with EXPLAIN ANALYZE against a seeded CockroachDB instance at 100k and 1M rows per table. The collapsed query is 7-18x faster whenever a full scan is needed, and never slower. Two things explain this: - CockroachDB does not short-circuit an AND chain of independently run NOT EXISTS subqueries; all four clauses always run to completion. Confirmed in CockroachDB's own docs (subqueries are always evaluated eagerly). The old query pays for four scans where the new one pays for one. - The old split-by-index design only avoids a full scan when the candidate is appended past all existing addresses. Any candidate landing among already-used addresses (a normal case, not an edge case) forces a full scan for both versions anyway. Also closed a gap in existing test coverage: nothing tested that a range merely adjacent to another (no shared address) gets accepted, only that overlapping ranges get rejected. Added test_ip_pool_range_adjacent_ranges_succeed, can_insert_ip_pool_range_adjacent_to_subnet_pool_member, cannot_insert_ip_pool_range_which_contains_subnet_pool_member, and can_explain_filter_overlapping_ip_ranges_query. No schema, index, or API change. Confirmed with cargo xtask openapi check (122 documents checked, 0 stale). Also fixed a stale file path in a dbinit.sql comment while in the area. Closes oxidecomputer#9283
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Simplify the IP Pool / Subnet Pool overlap-check query
Fixes #9283.
Summary
FilterOverlappingIpRanges(the query that stops two IP Pool ranges orSubnet Pool members from overlapping) checked each table with four
separate
NOT EXISTSsubqueries instead of one. This collapses it to onesubquery per table, 2 total instead of 8, with no change in behavior.
Measured with
EXPLAIN ANALYZEagainst a seeded CockroachDB instance at100k and 1M rows per table: the collapsed version is 7-18x faster
whenever a full table scan is needed, and never slower. Pure query
rewrite, no schema or API impact.
The problem
Every time Nexus adds a new IP Pool range or Subnet Pool member, it needs
to check that the new range doesn't overlap anything that already
exists. Both tables share one address space, so the check has to look at
both. That check lives in
nexus/db-queries/src/db/queries/ip_pool.rs.The existing code wrote that check as eight separate
NOT EXISTSsubqueries, four per table, each built to hit one of
ip_pool_range'stwo single-column indexes
(
lookup_pool_range_by_first_address,lookup_pool_range_by_last_address).The reasoning in the doc comment: one
ORcondition would stop theplanner from using those indexes and force a full scan, so the check got
split into index-friendly pieces instead.
The linked issue proposed collapsing this down to the standard
interval-overlap test. Two intervals
[a, b]and[c, d]overlapexactly when
a <= d AND c <= b, applied once per table:A comment on the issue benchmarked both versions and found the collapsed
one only a bit faster, with both falling back to full scans around 1M
rows. I re-benchmarked it on a machine less powerful than most dev
setups.
Also, the code has grown since the issue was filed. It originally only
checked
ip_pool_range. A later change addedsubnet_pool_membertothe same check, using the same 4-subqueries-per-table pattern, which
brought the total to 8 clauses.
subnet_pool_memberdoesn't have thepair of single-column indexes the original reasoning depends on. Its
first_addressandlast_addressare virtual (computed) columnsderived from a
subnetCIDR, backed by only one composite index(
lookup_subnet_pool_member_by_first_and_last_address). So the "4subqueries hit 4 indexes" argument never applied to half of what this
query checks.
Investigation
I set up a CockroachDB instance running the real schema from
schema/crdb/dbinit.sql.I seeded non-overlapping rows into
ip_pool_rangeandsubnet_pool_member, ranANALYZEon both, then ranEXPLAIN ANALYZEon both versions at 100k and 1M rowsper table (1M to match the scale in the issue's benchmark comment).
Ran this directly on a mini computer: an Intel NUC, i5-8365U (4 cores
/ 8 threads @ 1.6GHz), 7.4GB RAM. My poor NUC didn't have enough swap by default
to survive the build and test runs, so I bumped the swapfile to 24GB partway
through. Treat the absolute times as a data point, not a lab result. Both ran
on the same machine under the same load, so the relative gap is what matters:
ip_pool_rangeip_pool_rangesubnet_pool_membersubnet_pool_memberThe gap grows with table size. The results differ from what the earlier
comment on the issue found. Reason: candidate placement, not table size.
Cheap for both versions near the end of the address space. Expensive,
and growing, once placed among existing data.
Two things you can't tell from the SQL alone:
NOT EXISTSsubquery in theANDchain, evenafter an earlier one already proves overlap. CockroachDB's own docs
confirm this: subqueries are always evaluated eagerly, not skipped
like a normal
AND/ORcan be. Old version: pays for four scans.New version: pays for one.
all existing addresses. Any candidate among already-used addresses
(normal, not an edge case) forces a full scan either way. The old
version just pays for that scan four times over.
The collapsed version was never worse in anything tested, and was
consistently faster whenever a full scan was needed.
The change
nexus/db-queries/src/db/queries/ip_pool.rs: collapsed the six helperfunctions building the eight subqueries down to one shared
push_overlap_subqueryplus two thin per-table wrappers.walk_astnow builds two
NOT EXISTSblocks instead of eight. Doc commentrewritten to state the interval-overlap formula and the measured
reasoning above, instead of the now-outdated "split for index use"
justification.
nexus/db-queries/tests/output/filter_overlapping_ip_ranges.sql:regenerated golden SQL fixture to match the new 2-clause version.
schema/crdb/dbinit.sql: fixed a stale path in a comment(
nexus/src/db/queries/ip_pool.rs->nexus/db-queries/src/db/queries/ip_pool.rs),noticed while working in this area, unrelated to the fix itself.
No schema change, no index change, no API/OpenAPI change. Confirmed with
cargo xtask openapi check(122 documents checked, 0 stale).Testing
Existing coverage exercised (no regressions)
cargo nextest run -p nexus-db-queries: all 42 tests indatastore::ip_poolpass, including the pre-existingcannot_insert_ip_pool_range_which_overlaps_subnet_pool_member.cargo nextest run -p omicron-nexus -E 'test(test_ip_pool_range)': thepre-existing
test_ip_pool_range_overlapping_ranges_fails(5 overlapcases: exact duplicate, overlap below, overlap above, candidate
contains existing, existing contains candidate) still passes unchanged.
New tests added
The existing suite only tested that overlapping ranges get rejected,
never that a truly separate, non-overlapping neighbor gets accepted. An
off-by-one in the new check's too-strict direction (e.g.
</>whereit needs
<=/>=) wouldn't be caught by any existing test.test_ip_pool_range_adjacent_ranges_succeed(
nexus/tests/integration_tests/ip_pools.rs): HTTP test. Posts a rangeright below and right above an existing one. Both must succeed.
can_insert_ip_pool_range_adjacent_to_subnet_pool_member(
nexus/db-queries/src/db/datastore/ip_pool.rs): same idea,cross-table. An
ip_pool_rangecandidate one address past an existingsubnet_pool_membersubnet must succeed.cannot_insert_ip_pool_range_which_contains_subnet_pool_member(samefile): the reverse case, a candidate that contains an existing subnet
instead of landing inside one. Covers both halves of the collapsed
check (
first_address <= Xandlast_address >= Y).can_explain_filter_overlapping_ip_ranges_query(same file): smoketest matching this file's
can_explain_*convention. Confirms thequery is valid SQL against the real schema.
How to re-run
To reproduce the
EXPLAIN ANALYZEcomparison end to end:Look for
spans: FULL SCANversus an actual index name in each plan'stable:line, and compare theKV rows read:counts, to see the sameresult shown in the table above.
Risk / non-goals
Performance change only.
either version once a candidate isn't at the tail of the address
space. A new index could help if full scans become a real bottleneck
in production. Out of scope here.
benchmark. Holds at both sizes. Bigger gap at 1M (~18x and ~7x), not
smaller.