Skip to content

postgres: bound alias memory during vulnerability updates - #2001

Draft
BradLugo wants to merge 4 commits into
quay:mainfrom
BradLugo:blugo/alias-chunking
Draft

postgres: bound alias memory during vulnerability updates#2001
BradLugo wants to merge 4 commits into
quay:mainfrom
BradLugo:blugo/alias-chunking

Conversation

@BradLugo

Copy link
Copy Markdown
Contributor

TL;DR: These changes improve updateVulnerabilities()'s peak-heap-B and B/op for larger inputs, with constant cost to allocs/op, increasing cost to sec/op, and diminishing cost at the lower end to B/op. Overall, I think the tradeoffs are worthwhile for larger vulnerability feeds (read: rhel-vex).

Analysis & Benchmarking

Setup:

tmpdir=$(mktemp -d)

Run benchmarks for baseline:

# Checkout benchmark commit

go test \
    -tags integration \
    -run XXX \
    -bench 'Benchmark_UpdateVulnerabilities/(100|500|1200)[ _]' \
    -benchtime 10x \
    -count 10 ./datastore/postgres/ \
    | tee "$tmpdir/base.txt"

go test \
    -tags integration \
    -run XXX \
    -bench 'Benchmark_UpdateVulnerabilities/50000' \
    -benchtime 1x \
    -count 10 ./datastore/postgres/ \
    | tee -a "$tmpdir/base.txt"

Run benchmarks for chunking change:

# Checkout chunking

go test \
    -tags integration \
    -run XXX \
    -bench 'Benchmark_UpdateVulnerabilities/(100|500|1200)[ _]' \
    -benchtime 10x \
    -count 10 ./datastore/postgres/ \
    | tee "$tmpdir/chunk.txt"

go test \
    -tags integration \
    -run XXX \
    -bench 'Benchmark_UpdateVulnerabilities/50000' \
    -benchtime 1x \
    -count 10 ./datastore/postgres/ \
    | tee -a "$tmpdir/chunk.txt"

Results on my system:

❯ go run golang.org/x/perf/cmd/benchstat@latest old="$tmpdir/base.txt" new="$tmpdir/chunk.txt"
goos: darwin
goarch: arm64
pkg: github.com/quay/claircore/datastore/postgres
cpu: Apple M4 Pro
                                                │     old     │                  new                   │
                                                │   sec/op    │    sec/op      vs base                 │
_UpdateVulnerabilities/100_vulnerabilities-14     9.948m ± 5%    8.949m ±  9%   -10.04% (p=0.002 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     26.63m ± 9%    36.91m ±  3%   +38.60% (p=0.000 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    57.39m ± 3%   173.56m ±  1%  +202.41% (p=0.000 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14    3.414 ± 3%    26.574 ± 50%  +678.27% (p=0.000 n=10)
geomean                                           84.88m         197.6m        +132.75%

                                                │      old      │                  new                  │
                                                │  peak-heap-B  │  peak-heap-B   vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14     1.519Mi ± 50%   1.370Mi ± 56%        ~ (p=0.971 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     3.920Mi ± 23%   4.446Mi ± 25%        ~ (p=0.190 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    5.413Mi ±  9%   5.002Mi ±  9%        ~ (p=0.190 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14   76.19Mi ±  8%   38.91Mi ±  3%  -48.93% (p=0.000 n=10)
geomean                                           7.040Mi         5.867Mi        -16.65%

                                                │     old      │                 new                  │
                                                │     B/op     │     B/op      vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14     654.3Ki ± 0%   664.7Ki ± 0%   +1.59% (p=0.000 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     3.284Mi ± 0%   3.319Mi ± 0%   +1.06% (p=0.000 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    8.026Mi ± 0%   7.307Mi ± 0%   -8.96% (p=0.000 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14   361.3Mi ± 0%   282.4Mi ± 0%  -21.82% (p=0.000 n=10)
geomean                                           8.832Mi        8.166Mi        -7.54%

                                                │     old     │                new                 │
                                                │  allocs/op  │  allocs/op   vs base               │
_UpdateVulnerabilities/100_vulnerabilities-14     5.458k ± 0%   5.464k ± 0%  +0.11% (p=0.001 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     25.92k ± 0%   25.93k ± 0%  +0.04% (p=0.000 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    61.71k ± 0%   61.91k ± 0%  +0.33% (p=0.000 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14   2.554M ± 0%   2.565M ± 0%  +0.44% (p=0.000 n=10)
geomean                                           68.72k        68.87k       +0.23%

We start seeing some real gains at the higher end at the cost of compute.

Run benchmarks for custom plan changes:

# Checkout custom plan changes

go test \
    -tags integration \
    -run XXX \
    -bench 'Benchmark_UpdateVulnerabilities/(100|500|1200)[ _]' \
    -benchtime 10x \
    -count 10 ./datastore/postgres/ \
    | tee "$tmpdir/plan.txt"

go test \
    -tags integration \
    -run XXX \
    -bench 'Benchmark_UpdateVulnerabilities/50000' \
    -benchtime 1x \
    -count 10 ./datastore/postgres/ \
    | tee -a "$tmpdir/plan.txt"

Results:

❯ go run golang.org/x/perf/cmd/benchstat@latest old="$tmpdir/chunk.txt" new="$tmpdir/plan.txt"
goos: darwin
goarch: arm64
pkg: github.com/quay/claircore/datastore/postgres
cpu: Apple M4 Pro
                                                │      old      │                 new                  │
                                                │    sec/op     │    sec/op     vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14      8.949m ±  9%   9.236m ± 12%        ~ (p=0.143 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14      36.91m ±  3%   26.17m ±  5%  -29.10% (p=0.000 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    173.56m ±  1%   74.54m ±  5%  -57.05% (p=0.000 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14    26.574 ± 50%    4.483 ±  2%  -83.13% (p=0.000 n=10)
geomean                                            197.6m         94.80m        -52.02%

                                                │      old      │                  new                  │
                                                │  peak-heap-B  │  peak-heap-B   vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14     1.370Mi ± 56%   1.348Mi ± 40%        ~ (p=0.739 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     4.446Mi ± 25%   4.934Mi ± 12%  +10.97% (p=0.019 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    5.002Mi ±  9%   5.419Mi ± 16%        ~ (p=0.247 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14   38.91Mi ±  3%   39.43Mi ±  4%        ~ (p=0.105 n=10)
geomean                                           5.867Mi         6.140Mi         +4.65%

                                                │     old      │                 new                  │
                                                │     B/op     │     B/op      vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14     664.7Ki ± 0%   779.8Ki ± 0%  +17.32% (p=0.000 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     3.319Mi ± 0%   3.775Mi ± 0%  +13.74% (p=0.000 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    7.307Mi ± 0%   8.413Mi ± 0%  +15.13% (p=0.000 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14   282.4Mi ± 0%   328.0Mi ± 0%  +16.14% (p=0.000 n=10)
geomean                                           8.166Mi        9.437Mi       +15.57%

                                                │     old     │                 new                 │
                                                │  allocs/op  │  allocs/op   vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14     5.464k ± 0%   7.873k ± 0%  +44.09% (p=0.000 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     25.93k ± 0%   37.94k ± 0%  +46.31% (p=0.000 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    61.91k ± 0%   90.74k ± 0%  +46.56% (p=0.000 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14   2.565M ± 0%   3.766M ± 0%  +46.81% (p=0.000 n=10)
geomean                                           68.87k        100.5k       +45.94%

Overall:

❯ go run golang.org/x/perf/cmd/benchstat@latest old="$tmpdir/base.txt" new="$tmpdir/plan.txt"
goos: darwin
goarch: arm64
pkg: github.com/quay/claircore/datastore/postgres
cpu: Apple M4 Pro
                                                │     old     │                 new                  │
                                                │   sec/op    │    sec/op     vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14     9.948m ± 5%   9.236m ± 12%        ~ (p=0.075 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     26.63m ± 9%   26.17m ±  5%        ~ (p=0.247 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    57.39m ± 3%   74.54m ±  5%  +29.87% (p=0.000 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14    3.414 ± 3%    4.483 ±  2%  +31.29% (p=0.000 n=10)
geomean                                           84.88m        94.80m        +11.68%

                                                │      old      │                  new                  │
                                                │  peak-heap-B  │  peak-heap-B   vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14     1.519Mi ± 50%   1.348Mi ± 40%        ~ (p=1.000 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     3.920Mi ± 23%   4.934Mi ± 12%  +25.87% (p=0.005 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    5.413Mi ±  9%   5.419Mi ± 16%        ~ (p=0.912 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14   76.19Mi ±  8%   39.43Mi ±  4%  -48.25% (p=0.000 n=10)
geomean                                           7.040Mi         6.140Mi        -12.78%

                                                │     old      │                 new                  │
                                                │     B/op     │     B/op      vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14     654.3Ki ± 0%   779.8Ki ± 0%  +19.19% (p=0.000 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     3.284Mi ± 0%   3.775Mi ± 0%  +14.94% (p=0.000 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    8.026Mi ± 0%   8.413Mi ± 0%   +4.82% (p=0.000 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14   361.3Mi ± 0%   328.0Mi ± 0%   -9.21% (p=0.000 n=10)
geomean                                           8.832Mi        9.437Mi        +6.85%

                                                │     old     │                 new                 │
                                                │  allocs/op  │  allocs/op   vs base                │
_UpdateVulnerabilities/100_vulnerabilities-14     5.458k ± 0%   7.873k ± 0%  +44.25% (p=0.000 n=10)
_UpdateVulnerabilities/500_vulnerabilities-14     25.92k ± 0%   37.94k ± 0%  +46.37% (p=0.000 n=10)
_UpdateVulnerabilities/1200_vulnerabilities-14    61.71k ± 0%   90.74k ± 0%  +47.04% (p=0.000 n=10)
_UpdateVulnerabilities/50000_vulnerabilities-14   2.554M ± 0%   3.766M ± 0%  +47.45% (p=0.000 n=10)
geomean                                           68.72k        100.5k       +46.27%

Exercise UpdateVulnerabilities with enough alias-carrying
vulnerabilities to cross batch-flush boundaries and verify the link
tables directly. The benchmark reports peak live heap alongside the
usual metrics, since allocation lifetime is invisible to B/op.

Signed-off-by: Brad Lugo <blugo@redhat.com>
Insert and link aliases every 500 vulnerabilities instead of
accumulating bookkeeping for the whole run, halving peak heap for
large updates (82MB to 40MB at 50k vulnerabilities). The out-of-band
alias inserts now happen mid-run, so a failed update can leave alias
rows committed; they are unreachable until a later successful run
links them.

Signed-off-by: Brad Lugo <blugo@redhat.com>
The chunked link statements are INSERT..SELECTs that must see alias
rows committed by concurrent updaters after the transaction began, so
pin read committed instead of inheriting
default_transaction_isolation.

Signed-off-by: Brad Lugo <blugo@redhat.com>
Chunking runs the link statements often enough to cross the
prepared-statement threshold, where the generic plan built for
unnest's default row estimate gets chosen and is disastrous for the
actual array sizes. Executing unprepared keeps per-execution
planning.

Signed-off-by: Brad Lugo <blugo@redhat.com>
@BradLugo BradLugo changed the title Blugo/alias chunking postgres: bound alias memory during vulnerability updates Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant