apiserver: don't query expired decisions for a bouncer that never pulled - #4614
apiserver: don't query expired decisions for a bouncer that never pulled#4614nikosch86 wants to merge 2 commits into
Conversation
|
@nikosch86: There are no 'kind' label on this PR. You need a 'kind' label to generate the release automatically.
DetailsI am a bot created to help the crowdsecurity developers manage community feedback and contributions. You can check out my manifest file to understand my behavior and what I can do. If you want to use this for your project, you can check out the BirthdayResearch/oss-governance-bot repository. |
|
@nikosch86: There are no area labels on this PR. You can add as many areas as you see fit.
DetailsI am a bot created to help the crowdsecurity developers manage community feedback and contributions. You can check out my manifest file to understand my behavior and what I can do. If you want to use this for your project, you can check out the BirthdayResearch/oss-governance-bot repository. |
|
/kind fix |
|
/area local-api |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4614 +/- ##
==========================================
+ Coverage 64.21% 64.27% +0.06%
==========================================
Files 520 520
Lines 39657 39655 -2
==========================================
+ Hits 25465 25490 +25
+ Misses 11805 11786 -19
+ Partials 2387 2379 -8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
/kind fix |
|
Hello @nikosch86 ! Thanks for the PR and the idea is relevant. However :
As any bouncer first's request will have a Did you have a chance to test it in real life ? I think as-is what would happen is:
One thing to consider is how to avoid breaking behavior for k8s and similar environments, where a bouncer changing IP would get a new entry in the database ( I don't have a ideal solution to suggest right now, this might needs further thinking/iteration. PS: Please let's keep the conversation between humans to limit verbosity 😃 |
|
Hi @buixor I reviewed everything again (by hand, not with some AI). I found an issue that is basically the corner stone of why we ran into this, our blocklist import was not differential, so 25k new lines have been added and 25k old lines expired every cycle, that leads to a big delta of blocks that need to be expired when a bouncer connects on the expensive code path and has no last_pull set (because the LAPI DB does not know it yet). To clarify the intention of the fix: when last_pull is null, skip the deleted decisions query, the bouncer has not been seen before, so it has nothing to delete. A different approach that I tested on the cluster would be to not skip the deleted decisions but introduce a time window, eg until > now - W so the scan is cheaper. Apart from code changes I also tried adding an index: baseline first pull: 446s |
|
Hello, I've opened #4619 to add the index you mentioned (plus another one, and a small optimisation to compensate for the write slowdown due to the new index). I'm still trying to understand how this PR had any impact in your environment: in the linked issue, you mention using the firewall bouncer, which will always send Do those bouncers change IP when they query LAPI (proxy, network change at runtime, ...) ? Could you check in the LAPI logs if you have any entries matching |
Fixes #4613
What
A bouncer that has never completed a pull has
bouncers.last_pull = NULL. In the delta branch of the decision stream,expiredSinceis then left nil, andQueryExpiredDecisionsSinceWithFiltersskips itsuntillower bound entirely:so the query degrades from "what expired since your last pull" to "everything that ever expired", with every row passing through the
longestDecisionForScopeTypeValueself-anti-join.Such a bouncer holds no decision from this LAPI, so its deleted set is empty by construction — the whole result is discarded work. This skips the query instead of running it.
Impact
On a production LAPI with ~2.2M rows in
decisions(~50k active, ~25.5k distinct(value, type, scope)), a single delta pull from a bouncer withlast_pull IS NULLtook 26-37 minutes at ~700% CPU, against 1.5-4.5s for the same request from a bouncer withlast_pullpopulated. Run directly against the DB file, one 30,000-row page of the unbounded statement takes ~45s on an i9-12900K and over 120s on a Xeon E3-1245v2, while the bounded form returns in ~1s.EXPLAIN QUERY PLANis identical in both cases andsqlite_stat1is absent, so this is the missing predicate rather than a planner divergence.It is also somewhat self-sustaining:
last_pullis only written on a fully successful response, so a bouncer whose client times out during the multi-minute response can retry into the same query.Regression
Introduced by #3020 (
44a2014f), released in v1.6.3, which turnedsinceinto a pointer and made the predicate conditional in the same commit. That PR did guard the other path —StreamDecisionNonChunkedpassed&time.Time{}rather than nil — but #4413 later removed that path, leaving only the one that passes nil.Test
Adds
TestStreamDeltaFirstPull. Every existing stream test usesstartup=true, so the delta path had no coverage at all; this covers both the first-pull case and the ordinary delta that follows it.The test is red on master and green with the fix:
Locally:
go test ./pkg/apiserver/... ./pkg/database/...passes,go vetandgofmtare clean. I was not able to rungolangci-lint(not installed on this machine), so CI is the first full lint pass.Not in scope
Two related things I deliberately left out, both noted in the issue:
startup=trueruns the same unbounded statement by construction (QueryExpiredDecisionsWithFiltershas nosinceparameter). Whether a client doing a full resync needs a deleted set at all is a contract question I did not want to assume.(value, type, scope)+ a range onuntilwhile the only supporting index is(value). Addingindex.Fields("value", "type", "scope", "until")speeds up every stream query including the bounded ones — applied as a manual index on the affected host it took the unbounded statement from >120s to 12.74s per page, and the end-to-end first sync from 26-37 min to 71s. It is an independent change and belongs in its own PR — happy to open it if you want it.