Skip to content

Commit 775dc51

Browse files
committed
wip: memcached
1 parent 4f42ac5 commit 775dc51

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

memcached/README.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Memcached
2+
3+
Source:
4+
5+
- <https://docs.memcached.org/>
6+
- <https://hnasr.substack.com/p/memcached-architecture>
7+
- <https://www.geeksforgeeks.org/system-design/what-is-memcached/>
8+
- <https://deepwiki.com/memcached/memcached>
9+
10+
Table of contents:
11+
- [Memcached](#memcached)
12+
- [1. What is memcached?](#1-what-is-memcached)
13+
- [2. Memcached deep dive](#2-memcached-deep-dive)
14+
- [2.1. Memory Management](#21-memory-management)
15+
- [2.2. Threading](#22-threading)
16+
- [2.3. Least Recently Used (LRU)](#23-least-recently-used-lru)
17+
- [2.4. LRU Locking](#24-lru-locking)
18+
- [2.5. LRU Crawler](#25-lru-crawler)
19+
20+
## 1. What is memcached?
21+
22+
> [!IMPORTANT]
23+
> Memcached is an in-memory key-value store for small chunks of arbitrary data (strings, objects) from results of database calls, API calls, or page rendering. Latest stable: **1.6.42** (2025).
24+
25+
Memcached operates as a high-performance, distributed memory caching system that can significantly improve the speed and scalability of web applications.
26+
27+
Memcached is simple yet powerful. Its simple design promotes quick deployment, ease of development, and solves many problems facing large data caches. The server does not care what your data looks like - items are made up of a key, an expiration time, optional flags, and raw data. Logic is split between client (server selection, routing, failover) and server (storage, eviction, memory management). Servers are disconnected from each other - no crosstalk, no synchronization, no broadcasting, no replication. All commands aim for O(1) performance.
28+
29+
![](https://media.geeksforgeeks.org/wp-content/uploads/20240530170747/memcached-1024.png)
30+
31+
- Keys in Memcached are strings, and they're limited to 250 characters. Values can be any type, but they are limited to 1MB by default.
32+
- Keys also have an expiration date or time to live (TTL). However, this should not be relied on, as the least recently used (LRU) algorithm may remove expired keys before they are accessed.
33+
- Memcached does not persist data. If the process dies, the cache is gone. This is by design - it is a cache, not a database.
34+
35+
## 2. Memcached deep dive
36+
37+
> Shoot out to the great article: <https://hnasr.substack.com/p/memcached-architecture>
38+
> Additional details from [deepwiki.com/memcached](https://deepwiki.com/memcached/memcached) with source references to the actual code.
39+
40+
Memcached follows a client-server architecture. The core components are:
41+
42+
1. **Main thread** — handles initial setup, accepts connections, dispatches to worker threads
43+
2. **Connection handler** — manages client connections via libevent, reads and writes data
44+
3. **Protocol parsing** — processes both ASCII (text) and binary protocol (auto-negotiated)
45+
4. **Command processing** — executes get, set, delete, incr/decr, etc.
46+
5. **Item management** — storage, retrieval, expiration of cached items
47+
6. **Slab allocator** — efficient memory management with size-specific chunks
48+
7. **Hash table** — O(1) lookup by key, dynamically resizable
49+
8. **Optional proxy system** — Lua-configurable request routing to backend servers
50+
9. **Optional TLS** — encrypted client connections with certificate verification
51+
10. **Optional external storage** — keep metadata in memory, large items on disk
52+
53+
### 2.1. Memory Management
54+
55+
When allocating items like arrays, strings or integers, they usually go to random places in the process memory. This leaves small gaps of unused memory scattered across the physical memory, a problem referred to as fragmentation.
56+
57+
![](https://substackcdn.com/image/fetch/$s_!HsMs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff44d352a-5d26-4b48-bc1c-e90179ea4b9c_293x432.png)
58+
59+
Fragmentation occurs when the gaps between allocated items continue to increase. This makes it difficult to find a contiguous block of memory that is large enough to hold new items. Technically, there might be enough memory to hold the item, but the memory is scattered all over the physical space.
60+
61+
Does that mean that the item fails to store if no contiguous memory exists? Not really, with the help of virtual memory, the OS gives the illusion that the app is using a contiguous block of memory. Behind the scenes, this block is mapped to tiny areas in physical memory.
62+
63+
When fragmentation occurs, it can cause a program to run more slowly, as the system assembles the memory fragments. The cost of virtual memory mapping and the cost of multiple I/Os to fetch what could have been a single block of memory is relatively high. That is why we try to avoid memory fragmentation.
64+
65+
Memcached avoids fragmentation by pre-allocating 1 MB-sized memory pages, which is why values are capped to 1 MB by default.
66+
67+
![](https://substackcdn.com/image/fetch/$s_!8ufq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbbeeaca-f865-4fc5-ad89-cb6131ed3105_338x314.png)
68+
69+
The OS thinks that Memcached is using the allocated memory, but Memcached isn't storing anything in it yet. As new items are created, Memcached will write item to the allocated page, forcing the item to be next to each other. This avoids fragmentation by moving memory management to Memcached instead of the OS.
70+
71+
The pages are divided into equal-sized **Chunks**. The chunk has a fixed size determined by the **slab class**. A slab class defines the chunk size, for example, Slab class 1 has a chunk size of 72 bytes while Slab class 43 has a chunk size of a 1MB.
72+
73+
![](https://substackcdn.com/image/fetch/$s_!R8DJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb484e1e0-0fac-40bb-ab0d-d6fc5a68ad94_700x211.png)
74+
75+
![](https://substackcdn.com/image/fetch/$s_!y1jx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd0de457-c490-4abe-9c1b-1e1842ca8c6b_700x227.png)
76+
77+
Items consist of a key, value, and some metadata, and they are stored in chunks. For example, if the item size is 40 bytes in size, a whole chunk is used to store the item. The closest chunk size to the 40-byte item is 72 bytes, which is slab class 1, leaving 32 bytes unused in the chunk. That is why the client should be smart to pick items that fit nicely in chunks, leaving as little unused space as possible.
78+
79+
Memcached tries to minimize the unused space by putting the item in the most appropriate slab class. Each slab class has multiple 1MB pages. In Slab class 1, there are 14,564 chunks per page since each chunk is 72 bytes. Slab classes follow a power-of-N factor (default 1.25) to balance granularity vs waste. If an item is less than or equal to 72 bytes, it'll fit nicely in the chunk. But if the item is larger, say 900 kilobytes, it doesn't fit the slab class 1. So, Memcached finds a slab class appropriate for the item. Slab class 43 of chunk size 1MB is the closest one, and the item will be put in that chunk. The entire item fits on a single page.
80+
81+
![](https://substackcdn.com/image/fetch/$s_!N_Fw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3cdba3-d4cf-45e7-a0e5-2d0cbf9355c7_700x384.png)
82+
83+
But what happens if all allocated pages for this slab class are full?
84+
85+
Slab class 1 is full:
86+
87+
![](https://substackcdn.com/image/fetch/$s_!UPyC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6729b0a9-d595-4cbb-b5b3-68e96d1771ce_700x339.png)
88+
89+
Memcached handles this by allocating a new page and storing the item in a free chunk.
90+
91+
![](https://substackcdn.com/image/fetch/$s_!3kuR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6bb69bc-1086-4333-935b-455b9ffbafcf_700x328.png)
92+
93+
### 2.2. Threading
94+
95+
Memcached uses a multi-threaded architecture to efficiently utilize multiple CPU cores and handle concurrent connections. The number of worker threads defaults to 4, configurable via `-t`.
96+
97+
Memcached accepts remote clients; it has to have networking. Memcached uses TCP as its native transport. UDP is also supported, but was disabled by default because of an attack that happened in [2018 called the reflection attack](https://www.cloudflare.com/learning/ddos/memcached-ddos-attack/).
98+
99+
The Memcached listener thread creates a TCP socket to listen on port 11211. It has one thread that spins up and listens for incoming connections. This thread creates a socket and accepts incoming connections.
100+
101+
Memcached then distributes the connections to a pool of threads. When a new connection is established, Memcached allocates a thread from the pool and gives the connection file descriptor to that thread. That worker thread is now responsible for reading data from the connection.
102+
103+
If a stream of data or a request to get a key is sent to the connection, the thread polls the file descriptor to read the request. Each thread can host one or more connections, and the number of threads in the pool can be configured.
104+
105+
![](https://substackcdn.com/image/fetch/$s_!78h3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F065d679f-dd7b-4725-b226-8572beb026b7_382x417.png)
106+
107+
### 2.3. Least Recently Used (LRU)
108+
109+
Memcached uses a Least Recently Used (LRU) algorithm to manage item eviction when memory is full. Memcached releases anything in memory that hasn't been used for a very long time. That's another reason why Memcached is called transient memory. Even if you set the expiration for an hour, you can't rely on the key being there before the hour expires.
110+
111+
Memcached uses a data structure called a linked list LRU (Least Recently Used) to release items when memory is full.
112+
113+
- Every item in the Memcached key-value store is in a linked list.
114+
- Every slab class has its own LRU.
115+
- If an item is accessed, it is moved from its current position to the head. This process is repeated every time an item is accessed. As a result, items that are not used frequently will be pushed down to the tail of the list and eventually removed if the memory becomes full.
116+
117+
![](https://substackcdn.com/image/fetch/$s_!2gsq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa0d36f7c-4631-4c40-a131-03d52d011d6d_422x211.png)
118+
119+
While the LRU is useful, it can also be quite costly in terms of performance. The locks that are necessary to maintain LRU can slow down throughput and complicate the application.
120+
121+
### 2.4. LRU Locking
122+
123+
No two threads can update the same data structure concurrently. To solve this, the thread that needs to update any data structure in memory must obtain a mutex, and other threads wait for the mutex to be freed. This is the basic locking model, and it is used in all applications. Memcached is no different from the LRU data structures.
124+
125+
In [2018](https://memcached.org/blog/modern-lru/), Memcached completely redesigned the LRU to introduce sub-LRUs per slab class breaking it by temperature - Segmented LRU.
126+
127+
An LRU is split into four sub-LRU's. Each sub-LRU has its own mutex lock. They are all governed by a single background thread called the "LRU maintainer", detailed below.
128+
129+
Each item has two bit flags indicating activity level.
130+
131+
- FETCHED: Set if an item has ever been requested
132+
- ACTIVE: set if an item has been accessed for a second time. Removed when an item is bumped or moved.
133+
134+
![](https://memcached.org/blog/modern-lru/img/state_machine.png)
135+
136+
**HOT** acts as a probationary queue, since items are likely to exhibit strong temporal locality or very short TTLs (time-to-live). As a result, items are never bumped within HOT: once an item reaches the tail of the queue, it will be moved to WARM if the item is active (3), or COLD if it is inactive (5).
137+
138+
**WARM** acts as a buffer for scanning workloads, like web crawlers reading old posts. Items which never get hit twice cannot enter WARM. WARM items are given a greater chance of living out their TTL's, while also reducing lock contention. If a tail item has is active, we bump it back to the head (4). Otherwise, we move the inactive item to COLD (7).
139+
140+
**COLD** contains the least active items. Inactive items will flow from HOT (5) and WARM (7) to COLD. Items are evicted from the tail of COLD once memory is full. If an item becomes active, it will be queued to be asynchronously moved to WARM (6). In the case of a burst or large volume of hits to COLD, the bump queue can overflow, and items will remain inactive. In overload scenarios, bumps from COLD become probabilistic, rather than block worker threads.
141+
142+
**TEMP** acts as a queue for new items with very short TTL's (2) (usually seconds). Items in TEMP are never bumped and never flow to other LRU's, saving CPU and lock contention. It is not currently enabled by default.
143+
144+
HOT and WARM LRU's are limited in size primarily by percentage of memory used, while COLD and TEMP are unlimited. HOT and WARM have a secondary tail age limit, relative to the age of the tail of COLD. This prevents very idle items from persisting in the active queues needlessly.
145+
146+
This is all tied together by the **LRU maintainer background thread**. It has a simple job:
147+
148+
- Iterate over every sub-LRU and peek at the tail item.
149+
- Ensure each sub-LRU is respecting its limits, moving items when necessary.
150+
- Reclaim expired tail items.
151+
- Process any asynchronous bumps from the COLD LRU.
152+
153+
### 2.5. LRU Crawler
154+
155+
This implementation still has some outstanding issues: Sizing the cache is hard. Do I have too much RAM? Too little? With all that waste in the middle, it's hard to tell. Items with inconsistent access patterns (e.g. a user goes out to lunch or to sleep) may cause excessive misses. Larger (multi-kilobyte) expired items could make room for hundreds of smaller items, or allow them to be stored for longer.
156+
157+
Solving these issues lead to the LRU crawler, which is a mechanism for asynchronously walking through items in the cache. It is able to reclaim expired items, and can examine the entire cache or subsets of it.
158+
159+
The LRU crawler also supports an **eviction mode** where it will forcefully evict COLD items when memory is low, going beyond just reclaiming expired items.
160+
161+
![](https://memcached.org/blog/modern-lru/img/concurrentcrawler.png)
162+
163+
The crawler is a single background thread which inserts special crawler items into the _tail_ of each sub-LRU in every slab class. It then concurrently walks each crawler item backwards through the LRU's, from bottom to top. The crawler examines each item it passes to see if it's expired, reclaiming if so.
164+
165+
It will look at one item in class 1, HOT, then one item in class 1 WARM, and so on. If class 5 has ten items and class 1 has a million, it will complete its scan of class 5 quickly, then spend a long time finishing class 1.
166+
167+
A histogram of TTL remaining is built as it scans each sub LRU. It then uses the histogram to decide how aggressively to re-scan each LRU. For example, if class 1 has a million items with a 0 TTL it will scan class 1 at most once an hour. If class 5 has 100000 items and 1% of them will be expired in 5 minutes, it will schedule to re-run in five minutes. It can rescan every few seconds, if necessary.
168+
169+
![](https://memcached.org/blog/modern-lru/img/scheduling.png)
170+
171+
Scheduling is powerful: higher slab classes naturally have fewer items that take a lot more space. It can very quickly scan and re-scan large items to keep a low ratio of dead memory. It can scan class 50 over and over, even if it takes 10 minutes to scan class 1 once.
172+
173+
Combined with segmented LRU, the LRU crawler may learn that "HOT" is never worth scanning, but WARM and COLD give fruitful results. Or the opposite: if HOT has many low TTL items, the crawler can keep it clean while avoiding scanning the relatively large COLD. This helps reduce the amount of scan work even within a single slab class.
174+
175+
This secondary process covers most of the remaining inefficiencies of managing an LRU with TTL'ed data. A pure LRU has no concept of holes or expired items, and filesystem buffer pools often keep data around in similar sizes (say, 8k chunks).
176+
177+
Using a background process to pick at dead data, while self-focusing where it can be the most effective, reclaims almost all of the dead memory. It is now much easier to gauge how much memory a cache is actually taking.

0 commit comments

Comments
 (0)