Skip to content

app-policy/checker: cache pending-policy verdicts per policy store generation - #13897

Draft
dimitri-nicolo wants to merge 3 commits into
projectcalico:masterfrom
dimitri-nicolo:dimitri-pmreq954-verdict-cache
Draft

app-policy/checker: cache pending-policy verdicts per policy store generation#13897
dimitri-nicolo wants to merge 3 commits into
projectcalico:masterfrom
dimitri-nicolo:dimitri-pmreq954-verdict-cache

Conversation

@dimitri-nicolo

Copy link
Copy Markdown
Contributor

Description

Third of the stack for CORE-13316, stacked on #13896 (which is on #13895); review the last commit only.

A verdict is a function of the endpoint's applicable rules, the store's IP sets and the flow. For a flow with no L7 attributes — every flow the collector evaluates — the criteria in match() reduce to protocol, source and destination address, destination port and, only where a rule looks at it, source port; identity and HTTP criteria match such a flow whatever the rule says. So a store can remember the verdict for that key, and a flow that repeats an earlier flow's endpoints on a new source port (a client reconnecting, which is most of what a node sees) is answered without walking the policy set. On a large set that is the difference between a millisecond and a map lookup.

What changes

  • policystore.PolicyStore gains a Generation that ProcessUpdate moves on every update but InSync, and an optional Verdicts *VerdictCache bound to it. Entries are valid for one generation; the cache starts over when the store moves on, so a cached verdict never outlives the policies, IP sets or endpoints it was computed from. Coarse (an IP set delta anywhere empties it) and safe; finer invalidation is the sweep-invalidation work later in the epic. The cache is bounded and starts over when full rather than keeping an LRU list.
  • checker.Evaluate consults the cache when the store has one. Whether the source port is part of the key is decided once per endpoint, scope, direction and generation by walking the endpoint's applicable rules (SrcPorts, NotSrcPorts, named source ports). Failed evaluations are not cached. Flows carrying identity or HTTP attributes (Dikastes requests) and flows with a nil address bypass it. A cached trace is shared between callers; the collector already copies it.
  • The collector gets the cache through policystore.WithVerdictCache, so every store its manager creates — including on reconnect — carries one, and reports felix_collector_policy_eval_cache_{hits,misses,resets,evictions}_total.
  • FlowLogsPolicyEvaluationCacheSize sizes it (default 65536, 0 disables). It is a config-file / environment-variable parameter with no FelixConfiguration field yet, so that it picks to release branches without an API change (the epic's backport-first rule); the API field can follow on master. Docs regenerated with calico-felix-docgen.

NumbersBenchmarkEvaluateVerdictCache, composite reference set, egress direction (18,662 rules), sampler flow model (10% of flows miss every rule, matching flows aimed uniformly at the walk), -benchtime 100x -cpu 1, one laptop core:

Case ns/op hit ratio
Uncached, 50% repeats 1,002,986
Cached, 0% deliberate repeats 723,532 0.11
Cached, 50% repeats 308,299 0.62
Cached, 90% repeats 78,505 0.92

The real hit ratio depends on the node's traffic; the collector's hits/misses counters report it.

Relation to #13267 / its rebase. Both change Evaluate; the cache wrapper is a dozen lines in front of the walk, so whichever lands second rebases trivially. The cache also helps the compiled engine: a hit skips the walk entirely.

Testing

go test ./app-policy/policystore/ ./app-policy/checker/      # ok, ok (12s: differential corpus + named cases through a cached store, twice, vs uncached; invalidation via ProcessUpdate; source-port keying; L7/nil bypass)
go test ./felix/collector/                                    # ok (wiring test + full suite)
go test ./felix/config/                                       # ok
go test ./app-policy/checker/ -run '^$' -bench BenchmarkEvaluateVerdictCache -benchmem -benchtime 100x -cpu 1
cd felix && go run ./cmd/calico-felix-docgen --format=json > docs/config-params.json && go run ./cmd/calico-felix-docgen --format=md > docs/config-params.md

Release note:

Felix: the flow log collector now caches pending-policy verdicts so that flows repeating an earlier flow's endpoints do not re-walk large policy sets. FlowLogsPolicyEvaluationCacheSize (config file / environment variable) sizes the cache; 0 disables it.

AI assistance: Written with Claude Code (Claude Fable 5.1); the author reviewed the diff and ran the tests and benchmarks above.

By opening this PR you take responsibility for every line in it, and you agree to explain the change yourself during review rather than routing review comments back through an agent. See AI_POLICY.md.

🤖 Generated with Claude Code

…benchmarks

Lift the two synthetic policy sets out of the checker benchmarks into a
package that renders the same set three ways: a policystore.PolicyStore
and endpoint for engine and collector benchmarks, the ToDataplane updates
that load it through ProcessUpdate, and Calico resources (Tier,
GlobalNetworkPolicy, GlobalNetworkSet) for applying it to a cluster. A
number measured at the engine, in the collector and on a node then
describes the same policy set.

The fixture also carries an oracle: Fixture.Expect computes the trace the
engine must report for a flow from the generator's own model of each rule,
independently of the matching code, and MatchingFlow / DeniedFlow /
Sampler produce flows aimed at chosen depths of the walk or following a
flow model (miss fraction, repeat fraction). Later changes to the engine
(compiled policies, a verdict cache, evaluation off the collector's main
loop) are gated on differential tests built on these.

Composite applies both measured shapes to one endpoint: 19,992 ingress
rules, 18,662 egress rules, 7,571 IP sets. BenchmarkEvaluateComposite
measures it in the three cases that matter for the collector; the two
existing benchmarks keep their cases and their numbers.

hack/cmd/policyscale renders a preset as YAML, or prints sampled flows
with their expected verdicts, for node-level runs.

Tracking: CORE-13316.
…yscale oracle

Run Evaluate over a corpus drawn from each policyscale preset (the denied
flow, flows aimed at every 257th rule, sampled flows following a flow
model, and replays as UDP, with an invalid protocol and with nil
addresses) and require the trace to equal the one the fixture's oracle
computes from its own model of the rules, in both policy scopes. The
harness takes two evaluators, so a second implementation of the walk
(compiled policies, a verdict cache, evaluation on another goroutine) is
checked against Evaluate the same way before it is switched on.

The named cases cover what a generated corpus cannot reach: nil
addresses against positive and negated references, named and negated
named ports, IP+port sets, negated and missing sets, protocols out of
range, by name, by number and negated, staged policies in both scopes,
HTTP criteria against an L4 flow, Log rules, Pass into the next tier and
into profiles, and a policy missing from the store. runNamedCases takes
an evaluator for the same reason.

Tracking: CORE-13316.
…neration

A verdict is a function of the endpoint's applicable rules, the store's
IP sets and the flow. For a flow with no L7 attributes (every flow the
collector evaluates) the criteria in match() reduce to protocol, source
and destination address, destination port and, only where a rule looks
at it, source port; identity and HTTP criteria match such a flow
whatever the rule says. So a store can remember the verdict for that
key and answer a flow that repeats an earlier flow's endpoints on a new
source port without walking the policy set, which on a large policy
set is the difference between a millisecond and a map lookup.

PolicyStore gains a Generation that ProcessUpdate moves on every update
but InSync, and an optional VerdictCache bound to it: entries are valid
for one generation and the cache starts over when the store moves on,
so a cached verdict never outlives the policies, IP sets or endpoints it
was computed from. Whether the source port is part of the key is decided
once per endpoint, scope, direction and generation by walking the
endpoint's applicable rules. The cache is bounded and starts over when
full. Evaluate consults it when the store has one; failed evaluations
are not cached; flows carrying identity or HTTP attributes (Dikastes
requests) and flows with a nil address bypass it.

The collector gets the cache through a PolicyStoreManager option so that
every store the manager creates, including on reconnect, carries one,
and reports it as felix_collector_policy_eval_cache_{hits,misses,resets,
evictions}_total. FlowLogsPolicyEvaluationCacheSize sizes it (default
65536; 0 disables). It is a config-file / environment-variable parameter
with no FelixConfiguration field yet, so that it picks to release
branches without an API change; the field can follow on master.

On the composite reference set (18,662 egress rules) with the sampler's
flow model, per evaluation: 1.00 ms uncached; 0.72 ms cached with no
deliberate repeats (11% hit ratio from coincidental ones), 0.31 ms at a
50% repeat rate (62% hits), 0.08 ms at 90% (92% hits).

Tests: the differential corpus and the named cases through a cached
store, twice, against an uncached one; invalidation on IP set delta,
policy update and policy removal through ProcessUpdate; source-port
keying; L7 and nil-address bypass; generation, capacity and option
behaviour in policystore; collector wiring.

Tracking: CORE-13316.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs-not-required Docs not required for this change release-note-required Change has user-facing impact (no matter how small)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants