Skip to content

cachedb_perf: high-performance local memory cache built on modern kernel features - #4118

Open
Lt-Flash wants to merge 7 commits into
OpenSIPS:masterfrom
Lt-Flash:feature/cachedb-perf-devel
Open

cachedb_perf: high-performance local memory cache built on modern kernel features#4118
Lt-Flash wants to merge 7 commits into
OpenSIPS:masterfrom
Lt-Flash:feature/cachedb-perf-devel

Conversation

@Lt-Flash

@Lt-Flash Lt-Flash commented Jul 23, 2026

Copy link
Copy Markdown

Summary

cachedb_perf is a local in-memory cachedb backend, selected by URL scheme (perf://). It implements the standard cachedb_funcs vtable, so every module that takes a cachedb_url (topology_hiding, dialog profiles, sql_cacher, ...) and the core cache_* script functions work unchanged — changing the URL is the whole migration.

It exists because cachedb_local's single fixed-size table and per-bucket locking become the bottleneck at high churn: at 50k resident entries and the topology-hiding access pattern, cachedb_perf does 448 ns/op vs 1,013 ns (2.3×; 7.9× vs cachedb_local's 512-bucket default), pure writes are 5.6–9.9× faster and stay flat with concurrency, and end-to-end topology hiding sustains 6,000 CPS where cachedb_local breaks at ~3,900.

How: lock-free reads (per-bucket version + copy-out validation; writers hold a bucket lock for ~hundreds of ns), named collections that grow at runtime, instant expiry with a cheap background sweep, and records in a size-class slab arena — in core shm or a dedicated reservation (memory_backing), with full reclaim in the dedicated modes. Keys hash with MurmurHash3 (the core's hash collides on sequential keys).

Optional, each degrading explicitly when absent: persistence to any db_* backend, cluster warm-up sync, and cross-node pull-on-miss — over the clusterer's bin links, the clusterer_controller plane, or the module's own udp/tcp sockets.

Outside the module, one optional cachedb capability: CACHEDB_CAP_GET_BUF, an allocation-free read into the caller's buffer. Backends and consumers that do not know it are unaffected.

Usage

loadmodule "cachedb_perf.so"
modparam("cachedb_perf", "cache_collections", "th=16")
modparam("cachedb_perf", "cachedb_url", "perf:///th")

loadmodule "topology_hiding.so"
modparam("topology_hiding", "th_state_url", "perf:///th")

From the script the backend id is perf (or perf:<group> when a grouped URL is declared): cache_store("perf", "call-$ci", "$var(state)", 3600);

Complete worked configurations - standalone, clustered over the stock clusterer, and clusterer_controller-managed, with every pull mode and transport explained: Configuration examples.

Parameters

parameter default
cache_collections default=14 collections, name[=log2(buckets)];... — the size is only a starting point, tables grow
cachedb_url perf:// URL(s) to resolve; perf:///name picks the collection; repeatable
memory_backing auto where records live: core shm, own-hg (dedicated huge-page arena), own (dedicated slab in shm, full reclaim)
arena_hugepage_mb 0 huge-page reservation size for the records; 0 = plain shm
arena_hugepage_cap_mb / arena_profile growth cap / sizing profile of the dedicated arena
reclaim_keep, reclaim_quiet_s, reclaim_cooloff_s, reclaim_giveback 1/5/10/1 own-backing reclaim tuning
expiry_sweep_period 1 seconds between reclaim passes (expiry itself is instant)
growth_load_factor / growth_budget 2 / 4096 entries-per-bucket target / max splits per tick
db_url, db_table, db_mode, persist_collections persistence: backend, table, auto load/save policy, which collections
sync_cluster_id, sync_shtag cluster + sharing tag for warm-up sync
replicate_collections, pull_on_miss which collections may pull from peers; repair misses transparently
pull_transport bin how pulls travel: bin, clctr, or module-owned udp/tcp sockets
pull_bind, pull_port the udp/tcp transport's own address
pull_timeout_ms, pull_negative_ms 50/300 pull backstop / negative-answer cache
event_expired_collections, arena_selftest, htable_selftest opt-in events and startup self-tests

MI (perf_stats, introspection, perf_save/perf_load/perf_sync), statistics and events are documented in the module README. All 16 MI commands with real invocation + response examples: MI examples.

Docs, testing

Everything measured, with graphs, lives in the branch: doc/STUDY.md (the study), doc/PR-NOTES.md (progress notes), bench/README.md (the test/benchmark harness). Builds clean with gcc and clang under -Werror; no new dependencies.

Lt-Flash pushed a commit to Lt-Flash/opensips that referenced this pull request Jul 24, 2026
The isolated-cache, 50k end-to-end and 100k three-way benchmarks are all
published in PR OpenSIPS#4118; only the two-socket huge-page-arena end-to-end
number is still outstanding.
@Lt-Flash
Lt-Flash force-pushed the feature/cachedb-perf-devel branch from cf2d1e4 to 642c828 Compare July 25, 2026 00:05
@Lt-Flash
Lt-Flash marked this pull request as ready for review July 26, 2026 07:34
@bogdan-iancu bogdan-iancu self-assigned this Aug 4, 2026
@Lt-Flash
Lt-Flash force-pushed the feature/cachedb-perf-devel branch 3 times, most recently from d51c274 to 2fc31a9 Compare August 10, 2026 09:12
@Lt-Flash
Lt-Flash force-pushed the feature/cachedb-perf-devel branch from 0585839 to 16790b0 Compare August 21, 2026 02:47
Lt-Flash pushed a commit to Lt-Flash/opensips that referenced this pull request Aug 23, 2026
…penSIPS#4118

The upstream PR keeps the module description and the headline numbers;
the index-structure shootout, concurrency and read-protocol
experiments, memory-backing tiers, cachedb_local / cachedb_redis
comparisons, the 50k and 100k topology-hiding runs, the huge-page arena
measurements and the soak findings, with their graphs, live here.
A local cachedb backend (URL scheme perf://) built for the SIP hot path:
lock-free reads (per-bucket seqlock + copy-out validation), short locked
writes, named collections that grow at runtime under a maintenance timer,
instant expiry with cheap background reclaim, and a size-class slab arena
with optional huge-page backing (memory_backing=auto|core|own-hg|own, with
full reclaim - retire, re-cut, give-back - in the own modes).

Optional surfaces, each degrading explicitly when absent: persistence to
any db_* backend (perf_save/perf_load, db_mode), cluster warm-up sync over
a sharing tag, and cross-node pull-on-miss over the clusterer bin links,
the clusterer_controller plane, or the module's own udp/tcp sockets
(pull_transport, with bin-capability HELLO peer discovery). Keys are
hashed with MurmurHash3.

One addition outside the module: an optional CACHEDB_CAP_GET_BUF cachedb
capability - an allocation-free read into the caller's buffer - that
backends and consumers may ignore.

Ships with perf_stats/introspection MI, statistics, events, admin docs,
a benchmark/soak harness (bench/), and the full measurement study
(doc/STUDY.md, doc/PR-NOTES.md).
@Lt-Flash
Lt-Flash force-pushed the feature/cachedb-perf-devel branch from 0fc5ee2 to 1eaa330 Compare August 24, 2026 15:35
@lean1ee

lean1ee commented Aug 25, 2026

Copy link
Copy Markdown

Tested and verified on commit 1eaa330 (branch feature/cachedb-perf-latest).
Environment:

  • Debian 12 (Linux x86_64), GCC 12.2.0
    Verification Summary:
  1. Core Capability (CACHEDB_CAP_GET_BUF):
  • Verified backward compatibility: existing modules (cachedb_sql, db_cachedb, etc.) compile cleanly without modifications.
  • Implemented native get_buf() in the cachedb_tarantool IProto driver. Zero-allocation read path functions exactly according to specification:
    • Returns 0 and populates *vlen on hit.
    • Returns -2 on miss/expiry.
    • Returns -3 and sets *needed when the destination buffer is insufficient.
  1. Module cachedb_perf & Benchmark Suite:
  • Benchmarks (concur, hashtest, lookup, expire2) built and passed.
  • Concurrency throughput measured at 166.5 Mops/s (100% read, 8 threads) and 147.0 Mops/s (95/5 read/write mix).
  • Timer-wheel background expiry verified with O(expired) sweep overhead.
  1. End-to-End SIP Validation:
  • SIP traffic load test executed (100 calls @ 25 cps).
  • Zero packet drops, zero memory leaks, 100% call success rate.
    Conclusion:
    The patch is clean, fully backward-compatible with external backends, and ready for upstream merge into master.

Yury Kirsanov added 6 commits August 25, 2026 21:12
Three complete deployment shapes - standalone, clustered over the stock
clusterer (bin/udp/tcp pull transports), and clusterer_controller-managed
(clctr) - with the pull modes, persistence, memory backings and the
failover hooks explained around each.
The earlier cross-node latency/convergence figures came from three
containers on one host - a single-kernel bridge with no real wire. Re-run
on three physical hosts (one container per host, host networking, same
binary): 30k-key thirds convergence takes 10.7-14.2 s to 99.9% (udp/tcp/
bin), warm p50 is ~197 us including one real LAN round trip, and the
transport differences live entirely in the miss-path tail, where the
module-owned udp/tcp sockets halve bin's p99. Zero failures across 25M
requests. STUDY.md gains the authoritative section + 4 figures; the
archived PR-NOTES numbers are banner-marked as superseded.
Every MI command's exact invocation syntax and response shape, pulled
directly from the mi_export_t recipes and their handlers rather than
paraphrased - all 16 commands across read/introspect, single-key ops,
bulk ops, cross-node pull (CP-15), and persistence/cluster sync (CP-19),
plus the two invocation surfaces and the named-params-only / glob-first
gotchas that cost real debugging time otherwise.
…ot written past

pcache_htable_new() computed nbuckets = 1U << size_log2 and then
allocated nbuckets / PCACHE_SEG_SIZE segments into ht->seg[], a FIXED
array of PCACHE_NSEGS pointers sized for 2^24 buckets.  A size_log2 of
25 asks for 8192 segments and writes past a 4096-entry directory; 32 or
more is not a defined shift at all.

Every caller in the tree is gated below the ceiling today, so this is
latent rather than live - which is exactly the kind of latent a later
caller trips, since nothing in the function's contract says where the
limit is.

Refuse above the ceiling and name it, and derive PCACHE_NSEGS from the
new PCACHE_MAX_SIZE_LOG2 rather than repeating 24 in two places, so the
guard and the directory cannot drift apart.

(cherry picked from commit 58cd08c224c0b4a3204b4676c8b14e3b3248df44)
A table at its target load factor keeps almost nothing in the leg; a
table that has stopped growing puts everything there, and the leg is a
chain per hash bucket under ONE lock, so its occupancy is the difference
between a table that performs and one that does not.

ovf_count was already maintained and reported nowhere, so a table sitting
at 81 entries per bucket looked identical from the outside to one at 4.
An accessor beside pcache_ht_nbuckets(), which is where a caller already
goes for the shape of a table.

(cherry picked from commit 81080fe7c6735fb0542d8b39c0b4de151814d25b)
A key lives in its bucket OR in the overflow leg, never both, so any
operation that misses the bucket must walk the chain for its hash under
the one leg lock - and that includes every store, not only operations on
records that are actually in the leg.  A table whose leg grew during a
write burst therefore taxes all of its subsequent writes.

Profiled on a node applying replicas at about 118,000 records a second:
the leg lookup was 17.0% of the applying thread, the largest cost outside
the cipher, ahead of the store itself at 12.1%.

Chain length is leg size divided by the number of heads, so widening the
head array shortens every chain in proportion.  16,384 heads is 131,072
bytes, the largest power of two that still fits inside the single 256 KB
region slot that 1,024 heads already occupied - sixteen times the chains
for no additional memory.  Nothing else changes: the array is one
allocation, the index is a mask, and both follow the constant.

(cherry picked from commit 20fc9c0b3ae0705a0178a952030a7830900d5a41)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants