Repository navigation
Releases: Lt-Flash/perfcached
Release list
perfcached 0.6.6
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
This page covers 0.6.3 to 0.6.6: 0.6.3, 0.6.4 and 0.6.5 were not published and have no pages of their own. Coming from 0.6.2, read all of it. The ChangeLog has the detail.
The first release published only after it was proven. 0.6.6 is the commit v0.6.6-rc3 (7d28c44), green on both CI systems and run for 72 hours on a three-node test fleet under staging traffic before this tag was put on it. Nothing was rebuilt for this page: the signed tarball below is the build that ran there. On that fleet it rolled in one node at a time over 0.6.4, which had rolled in over 0.6.2.
The dashboard leaves the daemon
perfcached-dashboardserves the status page from its own process: a client of the fleet's native door through libperfd, one encrypted connection per member, so a dashboard under attack stalls no daemon. The page is the daemon's, byte for byte, under the same script-hash CSP.- Its HTTP layer is libmicrohttpd. TLS and HTTP/2 belong to a reverse proxy in front;
contrib/carries HAProxy and nginx front ends for several dashboards, andtrusted_proxynames the proxies whoseX-Forwarded-Foris believed. - A POST needs a custom request header, a JSON body under
body_maxand the session token; each action asks for the fleet's enable secret once per browser page; wrong secrets lock the address out. Several dashboards sharing asession_keyhonour each other's session tokens. - Its own package, with its own user and a disabled unit. The daemon's own page stays, deprecated, and goes in 0.7.0.
- Wrong enable secrets are counted per source address on the native door: after
enable_lockout_afterof them withinenable_lockout_s(5 in 60 s by default) that address's enable is refused for the window.
Signed builds and the node's half of an upgrade (0.6.5)
- A release tarball comes with a detached ed25519 signature.
perfcached -K <tarball>checks it against the release key built into the binary, or[upgrade] trusted_keysfor builds you sign yourself. upgrade_stagereceives a signed build over a privileged connection (perfcli upgrade-stage <tarball> <version>), verifies it, and passes it to every member that can take one;upgrade_statuslists what is staged.- An optional root helper,
perfcached-upgrade, with its systemd units installed but not enabled, switches a node to a staged build and switches it back if the new one does not report ready. The Debian packages do not carry it. - A node refuses to start on state a newer build wrote - a newer snapshot format, a WAL record of an unknown type, a newer cluster-id file - and says so, instead of reading it as damage.
Settings a fleet must agree on, compared (0.6.5)
- Each member's keepalive carries the settings the fleet must share:
[wal] fsync, whether an HTTP token is set, the[alerts]thresholds, the tombstone limits, and each read-through collection's write settings. A member that differs from the most common value is named on every node as aconfig.<setting>alert. - Every member reports its build; the status page shows each node's version and the header says "mixed versions" during a roll, where the cards of the members still on the older build blink amber.
- The tombstone table's limits are alerts:
cluster.tombstoneswhen deletes are dropped beforetombstone_retain_sis up,cluster.peer_absentwhen a member returns after an absence longer than deletes are kept.
The restart reconcile has a fast path (0.6.4)
- A node back from a restart asks a peer that was up throughout for every delete and expiry made since its snapshot began; when the peer can vouch for that window, the pass ends in seconds instead of asking about every replayed key. Otherwise it asks key by key as before, and
stats.cluster.reconcile_fast_whysays why. - An eagerly replicated key that expires is remembered with the retained deletes, so an older, longer-lived copy cannot come back. Size
tombstone_retain_maxfor deletes plus expiries. - Asking key by key is paced to the free pending-request slots, between 64 and
reconcile_rate_maxa second; stats,/metricsand the page show an open pass's progress.
Write-through to Redis, hardened (0.6.3 - 0.6.5)
- Under
upstream_writes = throughevery node follows every change Redis makes under the collection's prefixes (broadcast tracking): a write another Redis client makes to a key written through here drops the node's copy, and the next read fetches Redis's value. - A node's restart no longer empties the fleet's write-through cache, and a node no longer answers nil for a key Redis holds when an invalidation lands between a record and its no-fall mark.
- The write queue is bounded (
upstream_write_queue, 10,000): past it a write is refused at once, and one with under a quarter of its timeout left is answered "not applied" without being sent. - The writer has its own credential (
upstream_write_user,upstream_write_password), sends plain writes pipelined, and the native door forwards add, sub, hash writes and mset under through. upfillfills the keys a collection owned before the switch to through, compares each with what Redis already holds, and says what Redis's values replaced - never a value, only lengths and expiries. Sweeps are paced by bytes as well as keys (upstream_drain_mb_rate), and the writer thread's CPU share is on the page.
libperfd 0.6.6
- The admin verbs, typed:
perfd_stats,perfd_membersandperfd_clientsanswer the trees the status page reads,perfd_enableraises privilege and raises it again by itself after a re-dial, andperfd_upfill/perfd_updrainrun, check or stop a sweep.
Files
The signed tarball: perfcached-0.6.6.tar.gz with perfcached-0.6.6.tar.gz.sig - the exact build that soaked on the test fleet, x86_64, built on Debian 13 (glibc 2.41) with libcurl mail. It unpacks to one prefix directory (bin/, lib/, include/, share/) for /opt, and it is what upgrade_stage and the perfcached-upgrade helper take. Check it with the binary you already run (0.6.5 or later):
perfcached -K perfcached-0.6.6.tar.gz # GOOD, the release key 5646e8e4 (built in)
Debian 12 and 13, Ubuntu 20.04 and 24.04, amd64:
perfcached: the clustered daemon, every cluster mode.perfcached-standalone: the single-node edition, built from the same tree without the cluster code. A[cluster]section in its config is refused rather than ignored.perfcached-dashboard(new): the status page's own process, unit installed disabled. Not for Ubuntu 20.04: its libmicrohttpd (0.9.66) predates the API (0.9.71) the dashboard is written for. Run the dashboard on another machine for a 20.04 fleet - it reaches the members over the native door.libperfd0: the client library,libperfd.so.0.libperfd-dev: its header, static archive,libperfd.solink andlibperfd.pc.
The two daemon packages install the same files and replace each other. They do not need libperfd0: install it, or libperfd-dev, for your own programs.
sudo apt install ./perfcached_0.6.6-1+deb13_amd64.deb
sudo apt install ./perfcached-dashboard_0.6.6-1+deb13_amd64.deb # on the machine that shows the page
sudo apt install ./libperfd0_0.6.6-1+deb13_amd64.deb ./libperfd-dev_0.6.6-1+deb13_amd64.deb # to build against libperfd
Each distribution's packages were built for it: Debian 13 natively (gcc 14.2, glibc 2.41), Debian 12 (gcc 12.2), Ubuntu 24.04 (gcc 13.3) and Ubuntu 20.04 (gcc 9.4, debhelper from focal-backports) each in a container of that distribution. Every build ran noisetest, the selftest and a deliberately broken-lock build that must fail. Each set was then checked in a clean container of its distribution:
- the version and revision
perfcached -Vreports (0.6.6 (7d28c44)), and the systemd unit; - the daemon starting and answering ping, set and get over its encrypted native door from
perfcli(on epoll: the containers' default seccomp profile refuses io_uring); - a program built against
libperfd-devthrough pkg-config, linkinglibperfd.so.0and reporting 0.6.6; - the shipped config passing
perfcached -C, a[cluster]section refused by the standalone edition, and the two editions replacing each other; - the dashboard package: its binary's libraries, its unit file and the example config;
- an upgrade over the published 0.6.0 packages.
Alpine 3.24 (musl), x86_64, both editions, as tarballs of the tree's make install layout: binaries in usr/bin (the dashboard among them), libperfd (shared and static) with perfd.h and libperfd.pc, the example config, and the docs. No systemd unit or OpenRC service is shipped; run the daemon under your own supervisor.
apk add libsodium libcurl libmicrohttpd
tar -xzf perfcached-0.6.6-alpine3.24-x86_64.tar.gz
cp -a perfcached-0.6.6-alpine3.24-x86_64/usr perfcached-0.6.6-alpine3.24-x86_64/etc /
Built with gcc 15.2 in a clean Alpine container behind the same three gates, then checked in a second clean container: -V and the musl interpreter, the dashboard's libraries, the round trip, the libperfd program, the shipped config and the [cluster] refusal.
libperfd for Alpine: libperfd and libperfd-dev as .apk, signed with the perfcached release...
perfcached 0.6.2
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
This page covers 0.6.2 and 0.6.1, which has no page of its own. The ChangeLog has the detail.
No packages are attached to this release. Build it from the source archive below (README, Install: a C compiler, make and libsodium), or stay on the 0.6.0 packages.
A rolling restart from 0.4.5. One thing to do first, from 0.6.0 or earlier: a read-through collection's password moved from [secrets] upstream into the collection, as upstream_password. 0.6.1 and later refuse the old key, and perfcached -C says where it goes.
0.6.2
Write-through to Redis
upstream_writes = throughsends every client write on a read-through collection to the upstream's master first: the listed upstream whoseROLEsays master, or, when two say so, the one the serving replicas follow. The written keys are read back in the same round trip, so the fleet then holds what Redis holds: a value with Redis's expiry, a deleted key as gone, and a list, set, sorted set or stream left to Redis. The client gets Redis's own reply.- With no master reachable every write is refused (the critical alert
write.<collection>) and reads go on. The master is looked for at start and after a failure, backing off to every 10 s;ROLEis asked every 10 s, and RedisJSON every 60 s while it is missing - never once per key. - At the RESP door every write command is forwarded as sent; perfcached's
EXonJSON.SETbecomes anEXPIREafter it.EVAL,EVALSHAandRL.HITwrite only this node and are refused. At the native door:set,setnx,setxx,del,expire,jsetandjdel. - JSON writes need RedisJSON on the master. Without it they are refused here, with no round trip, and a warning alert says so.
- Pub/sub is not bridged: Redis's channels and the fleet's stay separate.
Commands perfcached does not know
- Under
through,upstream_unknown = pass(the default) lets Redis classify a command perfcached does not implement (COMMAND INFO,COMMAND GETKEYS): a write goes to the master with its keys read back, a read to a serving replica. The reply is relayed byte for byte, up toupstream_pass_max_kb(1024); a larger one is refused, never cut. - Refused whatever Redis says: admin, dangerous, blocking, pub/sub, transaction and script commands, another database, writes that name no key, and keys outside
upstream_prefixes.refusekeeps "unknown command".
Stats and the status page
/stats upstream.write_throughhas the target, its state, RedisJSON, and the writes by outcome and by command as sent to Redis, with the last eight errors in Redis's own words and the slowest write in five minutes. The page shows them under the collection, grouped with its upstreams.- A command that waited for an upstream (a read-through miss, a write-through write) is counted once in the commands table; it was counted twice.
- The slow log and the alerts fold like the commands table, and the commands table sorts by calls or by its slowest call. Every card holds its content at desktop widths.
The HTTP door
- A peer refused by
http_allowcan no longer stall a worker. A silent refused connection held a worker for 200 ms, and one sending a byte every 150 ms held it indefinitely; the 403 is now sent and the socket drained without waiting. Every release from 0.3.0 has this, so update a node whose HTTP port is reachable from outside its allow-list. http_rate(20 requests a second per client address, loopback exempt) answers a faster address429 Too Many Requests.http_cache_ms(1000) builds/statsand/metricsat most once in that long, however many pages are open.- A browser can open a page behind
[secrets] http: the 401 asks for its login, and Basic with the token as the password (any user name) is accepted beside Bearer.
0.6.1
Alert mail a person can read
- The subject is
<STATE>: <title> on <host>- CRITICAL, WARNING, INFO or RESOLVED - in at most 78 characters, and From names the host. Each mail has a plain and an HTML part: per alert what happened, its status, when it started, what the node does by itself and what to do. A resolve answers the problem's first mail. - Levels say how soon to act: a clean start is info (not mailed at the default level), an unclean start and a WAL loss are warnings, a third loss within an hour and a heal stuck for ten minutes are critical.
Several Redis upstreams for one collection
- Up to eight.
upstream_role = replicareads only from servers whoseROLEsays replica (upstream_master_fallback = yeslets the master serve while none does).upstream_mode = balancesends each fetch to the one with the fewest in flight;failoverkeeps one active, the next taking over when it stops and the primary returning afterupstream_preempt_hold_s. Each upstream has its own tracking. - The upstream password belongs to the collection,
upstream_password, so one perfcached can front several Redis servers. - No ring of read-throughs: an upstream that is itself a perfcached is refused unless
upstream_allow_perfcached = yes, and then a key crosses one perfcached hop at most.
I/O
- The page has an io card: the backend running (io_uring or epoll), a fallback in amber with its reason, and the busiest worker's share of the last five minutes.
- A node on epoll whose busiest worker stays over
[alerts] epoll_busy_pct(80) for five minutes is warned, with the change to make. - A pub/sub subscriber that keeps up is no longer closed as slow when its worker drains a large backlog of big messages on io_uring: a drain stops at 1 MB as well as at 1,024 messages.
perfcached 0.6.0
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
This page covers 0.6.0. 0.5.7.1, 0.5.8 and 0.5.9 have pages of their own without packages: coming from 0.5.6.6, the last release with packages, read those too. The ChangeLog has the detail.
A rolling restart. From 0.4.5, 0.6.0 joins a fleet one node at a time; use SET IFEQ/IFNE and DELEX once every node runs 0.5.0 or later. A config that uses [mail] (0.5.9) is refused by an older release: to roll back, put the old config back with the old binary.
io_uring by default
- The workers receive into provided buffers, with one multishot receive per connection, and send their replies with the turn's single
io_uring_enter(): noread()and nowrite()a request. - At start the daemon tries io_uring for real and falls back to epoll where the kernel (before 6.0),
kernel.io_uring_disabledor a container's seccomp profile refuses it. The reason is logged and shown in/stats(io.backend), andperfcached -Csays so beforehand.[daemon] io = epollforces epoll. - On 16-vCPU VMs with multiqueue NICs, against epoll: 41% more RESP and 63% more native requests a second one at a time, 12% more pipelined, and a third less CPU a request.
- On epoll, a short read ends a wakeup's reads: one
read()a request instead of two. - PRODUCTION.md section 7: one NIC queue per CPU on the nodes and the clients, irqbalance, and the daemon unpinned.
A fast writer with a slow reader is throttled, not closed
- At 4 MB of replies unread, a client's further requests wait and its socket is not read, until the backlog drains below 1 MB. One that reads nothing for
output_stall_s(default 30) meanwhile is closed as a slow consumer. - The log says
throttling client <ip:port>the first time, and the disconnect line counts the throttles./stats(clients.output) and/metricscount them. A slow consumer's close now says so; it used to read "peer closed".
Busy polling, opt-in
busy_poll_usandbusy_poll_prefer, per worker (kernel 6.9+), off by default. Its effect on perfcached is not measured yet: measure it on your own NIC before you keep it.
Stats
/stats ioand/metricscount the system calls against the work they moved: waits, reads and writes, io_uring enters, completions, receives and sends, and UDP datagrams a call.perfcached_io_worker_ops_totalhas each worker's I/O.
Packages
Debian 12 and 13, Ubuntu 20.04 and 24.04, amd64:
perfcached: the clustered daemon, every cluster mode.perfcached-standalone: the single-node edition, built from the same tree without the cluster code. A[cluster]section in its config is refused rather than ignored.libperfd0: the client library,libperfd.so.0.libperfd-dev: its header, static archive,libperfd.solink andlibperfd.pc. A program built against it links the shared library by default.
The two daemon packages install the same files and replace each other. They do not need libperfd0: install it, or libperfd-dev, for your own programs.
sudo apt install ./perfcached_0.6.0-1+deb13_amd64.deb
sudo apt install ./libperfd0_0.6.0-1+deb13_amd64.deb ./libperfd-dev_0.6.0-1+deb13_amd64.deb # to build against libperfd
Each distribution's packages were built on that distribution: Debian 13 (gcc 14.2), Debian 12 (gcc 12.2, in a container), Ubuntu 20.04 (gcc 9.4) and Ubuntu 24.04 (gcc 13.3). Every build ran noisetest, the selftest and a deliberately broken-lock build that must fail. Each set was then checked in a clean container of its distribution:
- the version and revision
perfcached -Vreports, and the systemd unit; - the daemon starting and answering ping, set and get over its encrypted native door from
perfcli, on epoll under the container's default seccomp profile and on io_uring without it; - a program built against
libperfd-devthrough pkg-config, linkinglibperfd.so.0; - the shipped config passing
perfcached -C, a[cluster]section refused by the standalone edition, and the two editions replacing each other; - an upgrade over the published 0.5.6.6
perfcachedpackage.
Alpine 3.24 (musl), x86_64, both editions, as tarballs of the tree's make install layout: binaries in usr/bin, libperfd (shared and static) with perfd.h and libperfd.pc, the example config, and the docs. No systemd unit or OpenRC service is shipped; run the daemon under your own supervisor.
apk add libsodium libcurl
tar -xzf perfcached-0.6.0-alpine3.24-x86_64.tar.gz
cp -a perfcached-0.6.0-alpine3.24-x86_64/usr perfcached-0.6.0-alpine3.24-x86_64/etc /
Built with gcc 15.2 in a clean Alpine container behind the same three gates, then checked in a second clean container: -V and the musl interpreter, the round trip on epoll and on io_uring, the libperfd program, the shipped config and the [cluster] refusal.
libperfd for Alpine: libperfd and libperfd-dev as .apk, signed with the perfcached release key. Trust the key once (its public half is attached here and in contrib/alpine/keys), then install:
cp yury@kfi.net.au-6aca2bb6.rsa.pub /etc/apk/keys/
apk add ./libperfd-0.6.0-r0.apk ./libperfd-dev-0.6.0-r0.apk
Checked in a clean container that trusts only that key: both install, pkg-config reports 0.6.0, a program links the shared or the static library and pings a 0.6.0 daemon, and without the key apk refuses them.
SHA256SUMS covers every file on this page.
Known
- In a container under Docker's or podman's default seccomp profile, io_uring is refused and the daemon runs on epoll.
perfcached -Cand the log say which. - Busy polling's effect is not measured yet.
- No lists, sets, sorted sets or streams yet. Keys hold strings, counters, JSON documents or hashes (a hash is one value, under 58,000 bytes).
discovery = unicast(clouds, Kubernetes) is tested in lab network namespaces that drop IP fragments and block UDP, not yet in a real cloud deployment.spreadmode has not yet carried production traffic.- In
eagermode a delete made on one side of a network partition can be undone when the partition heals.
perfcached 0.5.9
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
Superseded by 0.6.0. This page was added afterwards for the record: no packages are attached - install the latest release. The source archives below are this tag's tree.
This page covers 0.5.9. The ChangeLog has the detail.
A rolling restart. From 0.4.5, 0.5.9 joins a fleet one node at a time; use SET IFEQ/IFNE and DELEX once every node runs 0.5.0 or later. A config that uses [mail] is refused by an older release: to roll back, put the old config back with the old binary.
A node raises its own alerts, and mails them
- Failed or healing, records the WAL lost, a slow fsync, writes refused for want of memory, an arena near its ceiling, a read-through upstream unreachable or its tracking off, its own start (critical after an unclean stop). Each is logged, kept - the last 100 in
/stats alertsand on the status page's alerts card - and counted on/metrics. - With
[mail]and a build with an SMTP client (PC_MAIL=libcurl; the release packages have one), alerts are mailed: STARTTLS or TLS, AUTH with the password in[secrets] mail, the same alert at most once permin_interval_sunless it got worse, everything withindigest_sin one mail, and the clear when it recovers.perfcli -E ... mailtestsends a test mail and prints the server's answer.tls_verify = nokeeps TLS for a relay whose certificate cannot be verified, with a warning at every config check. - In a fleet,
[mail]on one node is enough: the nodes elect the lowest-numbered one that can mail and relay their alerts to it, and what happens to the fleet itself - a member silent, failed, gone or missing, the fleet below a majority - is mailed once, by that node.
libperfd 0.6.0
perfd_get_buf,perfd_jget_bufandperfd_hget_bufread into the caller's buffer and allocate nothing. The SONAME stayslibperfd.so.0.
Build flavors
make menuconfigchooses release, debug or asan (sanitizers linked statically); a non-release build says so in-V,/statsand the page header.contrib/systemd/cores.confkeeps a crashed node's core;asan.confroutes a sanitizer's report beside it.- The perfcached packages depend on libcurl (the alert mail).
perfcached 0.5.8
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
Superseded by 0.6.0. This page was added afterwards for the record: no packages are attached - install the latest release. The source archives below are this tag's tree.
This page covers 0.5.8. The ChangeLog has the detail.
A rolling restart. From 0.4.5, 0.5.8 joins a fleet one node at a time; use SET IFEQ/IFNE and DELEX once every node runs 0.5.0 or later.
libperfd 0.5.0: a shared library
libperfd.so.0beside the static archive: an update with the same SONAME replaces it in place. The ABI carries what that needs -perfd_optsstarts with its size (PERFD_OPTS_INIT,perfd_opts_init()), errors carry a kind (perfd_error_kind()), values are released withperfd_free_value(), the library logs throughperfd_set_log(), and onlyperfd_*is exported. A pkg-config file,libperfd.pc;make install-lib.- The archive is position-independent, so it links into shared objects such as plugins and modules.
test/abitest.shholds the library to its SONAME: a caller built against the frozen 0.5.0 header runs against the new library, and no exported symbol disappears.
Packages and the licence boundary
libperfd0andlibperfd-dev.deb;libperfdandlibperfd-dev.apk for Alpine, signed with the release key.- libperfd's archive no longer carries a GPL object from the daemon: its MIT boundary is now the set of files the compiler reads to build it, checked on every push.
Asterisk
func_perfdlinks the installed libperfd (found through pkg-config, or--with-perfd=DIR) instead of compiling a copy: the patch goes from 22 files to 8, and the module compiles under developer mode.
perfcached 0.5.7.1
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
Superseded by 0.6.0. This page was added afterwards for the record: no packages are attached - install the latest release. The source archives below are this tag's tree.
This page covers everything since 0.5.6.6: 0.5.7 and 0.5.7.1, which replaced it the same day. The ChangeLog has both.
A rolling restart. From 0.4.5, 0.5.7.1 joins a fleet one node at a time; use SET IFEQ/IFNE and DELEX once every node runs 0.5.0 or later.
A rolling restart waits for the witness a reconcile needs
- A node does not call itself safe to restart while it is the last live witness of a peer's reconcile:
stats.cluster.restart.waits_fornames the peers. (0.5.7 waited while any peer reconciled; 0.5.7.1 waits only where its restart would leave the reconcile without a witness.) - A reconcile left with no witness at all - every peer restarted after the node went down - says so once, after 30 seconds, with the remedy, and counts the time in
stats.cluster.reconcile_no_witness_s. A rolling restart that went on regardless used to leave the first node's reconcile waiting for good. - The status page's header says when a reconcile is in trouble: red "reconcile has no witness - restart this node", amber "do not restart: node N needs it to finish its reconcile".
/memberscarries each member'sreconcilingandsole_witness.
The memory guard and huge pages
- The guard no longer counts a huge-page arena twice. On a 2 GB node with
arena_mb = 1024on a 512-page pool it refused every new client once the arena had given memory back. Boot says when a pool is outside the limit;/stats clients.memory.hugetlb_pool.
Read-through probes without a fetch
EXISTS,TTL,PTTLandTYPEof a key the collection does not hold answer from one round trip of TYPE and PTTL, and a hash or JSON document is remembered as present for the next ones - they used to fetch and store the whole document./stats upstream.fill_present,present_hits.
Connect storms
- A worker turn runs at most 8 Noise handshakes and parks the rest, and accepts at most 64 connections: at ~6,000 encrypted connects a second, an established client's p99 went from 92-107 ms to 33-37 ms.
/stats clients.handshakes.
perfcached 0.5.6.6
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
This page covers everything since 0.5.5: 0.5.6 and its point releases up to 0.5.6.6. The ChangeLog has each one.
A rolling restart. 0.5.6.6 joins a 0.4.5 or later fleet one node at a time: restart the members first and the master last, and read the fleet between nodes. The storage formats and the wire epoch are unchanged, and clients built on libperfd 0.4.0 keep working. Two things to know before you start:
- Check the memory guard on huge-page nodes first - see Known below. A node whose arena lives in a reserved hugetlb pool may need
memory_floor = off. - A config that uses a new key is refused by an older release (
memory_floor,stats_reset,upstream_writes). To roll back, put the old config back with the old binary.
The daemon protects itself from the out-of-memory killer
On a 1 GB node, 50,000 idle RESP connections used to get the daemon killed. Now:
- A memory guard reads, once a second, the memory the node may still use: MemAvailable, or the headroom of a memory-limited cgroup (v2 or v1), whichever is tighter, less the arena's uncommitted part. Below the floor, new clients are refused exactly as at
max_clients. Open connections and/statsare untouched, and refusing stops once a further 5% of the limit is free.A 300 MB cgroup now holds 24,457 of 25,000 connections and refuses the rest.[daemon] memory_floor = 15% # the default; kept within 128 MB .. 4 GB. Or: 512 MB | 2 GB | off max_clients, when unset, counts memory as well as descriptors: the smaller of the two wins. The boot line and/stats clients.max_basissay which. A configuredmax_clientsabove what memory holds stands, with a warning; the guard protects.- An idle connection holds no buffers. After two quiet seconds its input and output buffers are released (14 KB down to 2.2 KB per idle client), and they grow back on its next request. Once a burst of closed or idle connections settles, the freed heap is handed back to the system, so the daemon's memory falls back after a mass disconnect.
- A busy client is no longer slowed by idle ones. Every request used to pay a walk over all of its worker's connections (with 20,000 idle: 60 -> 750 us a PING). Now it does not.
- Encrypted native clients past the limit are refused before the Noise handshake, with the RESP door's
-ERR max number of clients reached. A storm of refused clients no longer costs a handshake each: 12,000 clients against a limit of 2,000 take a third less daemon CPU. A library that does not read the line sees its handshake fail, as on any close. A plaintext native door (loopback, unix socket) still answers an error frame to the first request. - New counters:
/stats clients.memoryandclients.buffers. New/metrics:perfcached_memory_limit_bytes,_available_bytes,_floor_bytes,perfcached_memory_refusing,perfcached_clients_refused_memory_total,perfcached_conn_buffers_released_total,perfcached_heap_trims_total. The status page shows a red "memory floor" chip while the node is refusing.
libperfd 0.4.2
- 0.4.1: a blocking connect takes the key from the process-wide cache. It used to derive it on every connect: 150 ms and 64 MB each. The derive is single-flight across threads, and
perfd_psk_prepare()derives beforefork()so a forking server's children inherit it. Rebuild clients that connect often. - 0.4.2: fits a musl thread's 128 KB default stack. A connect from a thread on Alpine overflowed it.
Fixes
- A crash on the metrics door (0.5.6 - 0.5.6.2): a connection that reached
http_timeoutin the very moment its request or close arrived was freed and then handed its event, a use-after-free ending in SIGSEGV. Seen under a monitor plus a Prometheus scraper. - WAL, standalone edition (every standalone release since 0.4.0): a key that expired, was reaped and was written again on another thread could come back after a crash with its expiry instead of the new value. The clustered edition is not affected.
- WAL: an empty node restarted more times than its WAL has segments came back FAILED ("wal: FULL at startup").
- "perfcached ready" is logged once every worker's listeners are open; a client that connected the moment it appeared could be refused.
- A record moved back from the overflow chain expires on time.
- A native client with no secret is told so at once, instead of being taken for a long handshake.
- The per-door connection and request totals are counted to 64 bits.
Read-through
- Tracking stays on one server. Behind a round-robin proxy over several replicas, a fetching connection could land on another server than the tracking one, and its copies would never see a change. Each connection now reads its server's
run_id. One that reached another server is not tracked; its copies live at mostupstream_negative_ms, andstatscounts them (upstream.tracking_other_server). The upstream user needs+info; the GUIDE's ACL line has it. upstream_writes = refuse: a read-through collection clients can only read. Every write to it, on every door, is refused with one message, and so are a drain sweep and a restore into it.- Read-through works on the RESP door under a mapped db index (
resp_collectionsmappingSELECT nonto a named collection). - It counts what clients got: a read answered with a value is a hit, an absent answer a miss. The status page adds a line per read-through collection: fetched from the upstream (found / absent, average fetch time), answered from fetched copies, and answered absent from the negative cache.
- The drain sweep reads untracked, so the upstream no longer remembers every swept key.
- The
<name>.nofallcompanion follows its parent's autoscale. tools/redis-replica-interop.shruns read-through against a real Redis replica in throwaway containers.
Stats and the status page
- Every count on the status page is the last five minutes, kept by the daemon (
win_<field>in/stats), so the window is true from the moment the page opens. Gauges stay current; maxima are the last five minutes'. /statsresets on a schedule: daily at local midnight by default (stats_reset = off | 1d .. 7d,stats_reset_at = HH:MM). The page's "reset stats" resets the whole fleet at one moment./metricsis never reset, so Prometheus sees true counters across resets and collection resizes.perfcached_command_latency_secondshas one bucket per octave (3 us .. 262 ms): 18 rows per command instead of 129. Every bound is an edge of the daemon's own histogram, so the counts are exact.- The header names hosts in trouble: it blinks red (failed) or amber (any other state but ready) and counts the fleet ("fleet 3/3 ready").
- One rule for collection names wherever one is made: 1-32 characters of
A-Z a-z 0-9 _ -, not starting with-. A name from before the rule is kept and named in the log.
Known
- The memory guard counts a hugetlb arena twice. With the arena on MAP_HUGETLB huge pages, the pool is already outside MemAvailable, but the guard also subtracts the arena's uncommitted part. A node whose arena is large against its RAM can refuse every new client once the arena has handed memory back. For example: 2 GB of RAM,
arena_mb = 1024on a 512-page hugetlb pool. At a fresh start the arena commits the whole pool, so it shows up later, not at boot.- Check the boot line
arena: ... reserved on MAP_HUGETLB huge pagesand/stats clients.memory.budget. - On such nodes set
memory_floor = off(the 0.5.6.4 behaviour) until the fix. - THP and 4K arenas are counted correctly.
- Check the boot line
- Read-through:
KEYSandSCANsee only what the collection holds.proxy,shardandspreadcollections refuse an upstream. In a cluster, a read on another node in the instant before a change reaches it may answer the upstream's older value. - No sorted sets, lists or streams yet. Keys hold strings, counters, JSON documents or hashes (a hash is one value, under 58,000 bytes).
discovery = unicast(clouds, Kubernetes) is tested in lab network namespaces that drop IP fragments and block UDP, not yet in a real cloud deployment.spreadmode has not yet carried production traffic.- In
eagermode a delete made on one side of a network partition can be undone when the partition heals.
Packages
Debian 12 and 13, Ubuntu 20.04 and 24.04, amd64, in both editions:
perfcached: the clustered daemon, every cluster mode.perfcached-standalone: the single-node edition, built from the same tree without the cluster code. A[cluster]section in its config is refused rather than ignored.
The two install the same files under the same names and replace each other, so moving between editions is one apt install. Each package was built in a clean container and install-checked in a second one: the version and revision, the edition perfcached -V reports, libperfd 0.4.2, the shipped config passing perfcached -C (also with memory_floor set), and the systemd unit. For the standalone package it also checked that a [cluster] section is refused and that installing perfcached over it swaps the edition.
Alpine 3.24 (musl), x86_64, both editions, as tarball...
perfcached 0.5.5
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
A rolling restart. 0.5.5 joins a 0.4.5 or later fleet one node at a time: restart the members first and the master last, and read the fleet between nodes. The storage formats, the wire epoch and libperfd (0.4.0) are unchanged. Read-through is off unless a collection names an upstream, so a fleet that does not use it runs exactly as before.
Read-through: take a keyspace over from Redis
A collection can name an UPSTREAM - Redis, Valkey or another perfcached - that a node reads on a local miss and never writes. Move the clients to perfcached and it answers from its own collection; a key it does not hold yet is read from the upstream the first time anyone asks. No big-bang copy, and no way back into Redis. The GUIDE has the whole story; in short:
[secrets]
upstream = <the password perfcached sends to the upstream>
[collection 0]
upstream = redis://reader@10.0.0.12:6379/0
upstream_prefixes = tok:*, cid:*
- One way only. perfcached sends the upstream the handshake and reads -
TYPE,PTTL,GET,HGETALL,JSON.GET, andSCANfor the drain sweep - and nothing else. Point it at a replica, as a user that can only read; the GUIDE has the ACL line. - Only the keys you list fall through:
upstream_prefixesis required (Redis glob patterns,*for all of them). - Strings, hashes and RedisJSON documents are carried, with the upstream's TTL. A list, set, sorted set or stream answers
WRONGTYPEand is not stored. - A read waits for one fetch and answers from a node-local copy; concurrent misses for one key cause one fetch. A change -
INCR,HSET,JSON.*path writes,DEL,EXPIREand the rest, on either door - first takes the upstream's value over as a real write (logged, replicated), then runs:INCRof an upstream41answers42. A plain overwrite (SET) does not wait. - A key the fleet has written or deleted never reads through again. perfcached cannot delete the upstream's copy, so a companion collection,
<name>.nofall, created and replicated with the collection, remembers it for as long as the upstream's copy may live. - The upstream's own changes reach perfcached through Redis client tracking (RESP3, Redis 6 or later): a key the upstream changes, deletes or expires drops out of the fetched copies at once. Losing a tracking connection drops every fetched copy.
- When the upstream fails, a read answers an error (or a miss, with
upstream_on_error = miss); a change always answers the error - it never runs on a guess. - The drain sweep finishes the move:
perfcli -E <enable> updrain <collection>scans the upstream and takes over every key the fleet does not already hold, has not written and has not deleted, at mostupstream_drain_ratekeys a second (5000). Run it on one node; the writes replicate. Then removeupstreamandupstream_prefixeson every node and restart them. statscarries anupstreamblock per collection - fetches and fills by type, promotions, invalidations, errors, timeouts, fetch time, and the drain sweep's progress.
Tested against a scripted upstream that fails the run on any write - on one node and in a three-node eager fleet - and against redis 8.10.2 as a read-only ACL user, where MONITOR shows perfcached sending only the handshake and reads and ACL LOG stays empty (tools/redis-interop.sh, a throwaway container).
Known
- Read-through:
KEYSandSCANsee only what the collection holds;proxy,shardandspreadcollections refuse an upstream; and in a cluster, a read on another node in the instant before a change reaches it may answer the upstream's older value - the same window in which an eager fleet without an upstream would answer a miss. - No sorted sets, lists or streams yet. Keys hold strings, counters, JSON documents or hashes (a hash is one value, under 58,000 bytes).
discovery = unicast(clouds, Kubernetes) is tested in lab network namespaces that drop IP fragments and block UDP, not yet in a real cloud deployment.spreadmode has not yet carried production traffic.- In
eagermode a delete made on one side of a network partition can be undone when the partition heals.
Packages
Debian 12 and 13, Ubuntu 20.04 and 24.04, amd64, in both editions:
perfcached- the clustered daemon, every cluster mode.perfcached-standalone- the single-node edition, built from the same tree without the cluster code; a[cluster]section in its config is refused rather than ignored. Read-through works in both.
The two install the same files under the same names and replace each other, so moving between editions is one apt install. Each package was built in a clean container and install-checked in a second one: the version and revision, the edition perfcached -V reports, the shipped config passing perfcached -C, the systemd unit, and for the standalone package that a [cluster] section is refused and that installing perfcached over it swaps the edition.
perfcached 0.5.1
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
A rolling restart. 0.5.1 joins a 0.4.5, 0.4.6 or 0.5.0 fleet one node at a time: restart the members first and the master last, and read the fleet between nodes. The storage formats, the wire epoch and libperfd (0.4.0) are unchanged; an older node ignores the two new lane messages. Every node keeps the identity it has - see below for switching to one derived from the machine.
A node's identity comes from the machine
- With a state directory and the default
identity_source = auto, the identity is derived from the platform - the VM's DMI product uuid, or inside a container (where that uuid is the host's) a NIC's MAC - keyed by a salt the node mints once into its state directory (node-salt). A disk cloned onto another VM comes up as a new member rather than a twin; two daemons on one machine are two members; a state directory moved to another path keeps its identity. - An identity already on disk with no
node-saltbeside it - one an earlier release minted at random, or derived under asite_saltsince removed - is kept, with a WARNING naming the source the machine offers. To switch, stop the node and deletenode-identity(or setidentity_source = dmi/mac); it then joins as a new member, so on a fleet holding data do it one node at a time. product_uuidis readable only by root and the daemon runs asperfcached: the shipped clustered unit copies it into the service's runtime directory first (RuntimeDirectory=perfcachedand a rootExecStartPre=-+step). A unit written for an earlier release needs those two lines for the DMI source.
autoscale = floor
- A fourth mode beside
auto,warnandoff: the collection grows by key count asautodoes, and shrinks only down to the size it was given - itsbuckets_log2, a create's size, or an explicit resize. An empty collection sized for the keys it expects keeps that size instead of being shrunk a minute after the start./statscarriesautoscale_floor_log2for every collection.
Cluster
- A departing master's goodbye is no longer taken for a UDP block. Its members measured its silence from the machine's boot and, while its TCP lane still answered during its shutdown snapshot, moved all their traffic to it onto TCP - for good.
- A forced lane now comes back: the node asks the peer by UDP every
[cluster] lane_retry_s(default 30) and returns to UDP once the answer proves both directions (/stats cluster.lane.udp_restored). A peer that leaves takes its forced lane with it.
Store
- A SCAN, a snapshot or a resize copy that walked a table's overflow leg while the leg drained back into the buckets could miss a record: the walk found its place again by position after records ahead of it had left. It now resumes after the last record it handed over that is still in the chain - a record may be handed over twice, never skipped.
Known
- No sorted sets, lists or streams yet. Keys hold strings, counters, JSON documents or hashes (a hash is one value, under 58,000 bytes).
discovery = unicast(clouds, Kubernetes) is tested in lab network namespaces that drop IP fragments and block UDP, not yet in a real cloud deployment.spreadmode has not yet carried production traffic.- In
eagermode a delete made on one side of a network partition can be undone when the partition heals.
Packages
Debian 12 and 13, Ubuntu 20.04 and 24.04, amd64, in both editions:
perfcached- the clustered daemon, every cluster mode.perfcached-standalone- the single-node edition, built from the same tree without the cluster code; a[cluster]section in its config is refused rather than ignored.
The two install the same files under the same names and replace each other, so moving between editions is one apt install. Each package was built in a clean container and install-checked in a second one: the version and revision, the edition perfcached -V reports, the shipped config passing perfcached -C, the systemd unit (the clustered one with the runtime directory and the DMI copy step), and for the standalone package that a [cluster] section is refused and that installing perfcached over it swaps the edition.
perfcached 0.5.0
A multithreaded, self-clustering cache for Linux, with its own encrypted protocols and a Redis-compatible door. New here? Start with the README.
A rolling restart. 0.5.0 joins a 0.4.5 or 0.4.6 fleet one node at a time: restart the members first and the master last, and read the fleet between nodes. The storage formats and the wire epoch are unchanged, and libperfd 0.3.0 clients keep working. Use SET ... IFEQ/IFNE and DELEX once every node runs 0.5.0; the Symfony lock scripts are refused until it does.
Alpine Linux
- Alpine (musl) is a supported runtime. On Alpine 3.24 (musl 1.2.6, its own malloc) the daemon serves 0.78-0.95 of the glibc build's rate where the server is the bottleneck, and is ahead of
redis:8-alpineon every measured row -INCR,SET/GETof 256 B and 1 KB values, a 7:1 mix,JSON.SETandJSON.GET, at 4 and 12 workers - by 1.5x to 14.9x. bench/Containerfile.alpinebuilds a 55 MB image. CI builds both editions on Alpine and runs the standalone suites there.- The allocator is a build choice:
make menuconfig, orPC_ALLOC = libc | mimalloc | jemalloc. libc stays the default everywhere (the packages below use it);perfcached -Vnames a non-default allocator, and/statscarriesprocess.allocator.
No heap per request
- The JSON path allocates nothing after warm-up:
JSON.SET,GET,NUMINCRBY,ARRAPPEND,DELand the hash forwards made three to four allocations each. On musl one of them was a 256 KB buffer mapped and unmapped on every command, which heldJSON.SETat 25-30k a second; it now runs at 1.3M at 4 workers. - libperfd 0.4.0 allocates nothing per call on a warm handle. The API and the wire are 0.3.0's, so a rebuild is all an existing client needs;
perfd_getandperfd_jgetstill return a malloc'd result, as documented.
Compare-and-set, and Symfony Lock
SET ... IFEQ <value> | IFNE <value>andDELEX <key> [IFEQ|IFNE <value>], in Redis 8.4's spelling and with its replies.IFDEQ/IFDNEare refused by name: perfcached keeps no value digest. Native methodssetifeq,setifne,delifeq,delifne, and in libperfdperfd_set_ifandperfd_del_if.EVAL,EVALSHAandSCRIPT LOAD | EXISTS | FLUSHfor the lock scripts of Symfony's Lock component (5.4 to 7.4): they are recognised by SHA1 and run natively, as one atomic step on the key's holder. Any other script is refused.
Proxy collections
- A request may ask where its key lives (an opt-in flag), and the reply names the node that holds it. libperfd's
route_hintsoption remembers that per key and sends the next request for the key straight there; a stale entry costs one extra hop, never a wrong answer. Off by default. - A
GET,DELorEXPIREthat a stale locator steered to a node no longer holding the key now asks the fleet once. Before, theGETanswered nil, theDELanswered 0 and left the key alive on its new holder, and theEXPIREleft it without a TTL.
Faster
- A
SETthat rewrites an unchanged value compares the two a word at a time and prefetches the stored one (on musl,SETof 256 B +7%, of 1 KB +15%). - The reply writer's small appends and its numbers no longer go through
memcpy: on musl aGEThit made sevenmemcpycalls, now three.
Known
- No sorted sets, lists or streams yet. Keys hold strings, counters, JSON documents or hashes (a hash is one value, under 58,000 bytes).
discovery = unicast(clouds, Kubernetes) is tested in lab network namespaces that drop IP fragments and block UDP, not yet in a real cloud deployment.spreadmode has not yet carried production traffic.- In
eagermode a delete made on one side of a network partition can be undone when the partition heals.
Packages
Debian 12 and 13, Ubuntu 20.04 and 24.04, amd64, in both editions:
perfcached- the clustered daemon, every cluster mode.perfcached-standalone- the single-node edition, built from the same tree without the cluster code; a[cluster]section in its config is refused rather than ignored.
The two install the same files under the same names and replace each other, so moving between editions is one apt install. Each package was built in a clean container and install-checked in a second one: the version and revision, the edition perfcached -V reports, the shipped config passing perfcached -C, the systemd unit, and for the standalone package that a [cluster] section is refused and that installing perfcached over it swaps the edition.