Memcached, when run in Docker for production, can often be a performance bottleneck disguised as a simple key-value store.
Let’s see it in action. Imagine a simple docker-compose.yml for a web application that uses Memcached for session storage:
version: '3.8'
services:
webapp:
build: .
ports:
- "8000:8000"
depends_on:
- memcached
memcached:
image: memcached:latest
ports:
- "11211:11211"
volumes:
- memcached_data:/var/lib/memcached # Not ideal for production, but for illustration
command: ["memcached", "-m", "1024", "-c", "4096"]
And a basic Python webapp that connects:
from flask import Flask, session
import memcache
app = Flask(__name__)
app.secret_key = 'your_super_secret_key' # In production, use a real secret!
mc = memcache.Client(['memcached:11211'], debug=0)
@app.route('/')
def index():
if 'visit_count' in session:
session['visit_count'] += 1
else:
session['visit_count'] = 1
return f"You have visited this page {session['visit_count']} times."
@app.route('/set/<key>/<value>')
def set_value(key, value):
mc.set(key, value)
return f"Set {key} to {value}"
@app.route('/get/<key>')
def get_value(key):
value = mc.get(key)
return f"Value for {key}: {value}"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
When requests hit /, the webapp tries to read and write to the session object. Flask by default uses a cookie for sessions, but if you configure it to use Memcached (which is common for scalability), the webapp would instead use mc.set and mc.get to interact with the Memcached container.
The core problem Memcached solves is reducing database load by caching frequently accessed data. Instead of hitting your primary database for every user profile, product detail, or session, you hit Memcached. If it’s there, great! Fast response. If not, you fetch from the DB, serve it, and then store it in Memcached for next time. This drastically improves read performance.
Internally, Memcached is a multi-threaded, in-memory, hash-table-based key-value store. It uses a slab allocator to manage its memory, pre-dividing it into chunks (slabs) of different sizes. When you store an item, Memcached tries to find the smallest slab that can accommodate it. This is efficient for memory usage but can lead to fragmentation and wasted space if your data items are highly variable in size and don’t fit neatly into the pre-defined slab classes. The memcached command-line arguments -m (memory in MB) and -c (max concurrent connections) are critical for tuning this.
The command in the docker-compose.yml is where the magic happens. memcached -m 1024 -c 4096 tells Memcached to reserve 1024 megabytes of RAM and to allow up to 4096 simultaneous connections. The -m flag is crucial: if you don’t specify it, or specify too little, Memcached will start evicting items aggressively, negating its caching purpose. Too much, and you starve your host or other containers.
The volumes section, volumes: - memcached_data:/var/lib/memcached, is generally not what you want for production Memcached. Memcached is an in-memory store. Persistence is not its primary goal. If the container restarts, any data in memory is lost. Using a volume here won’t magically persist the memory state. It’s more for potential diagnostic logs or if you were using a persistent version of Memcached (which is rare). For production, you typically don’t want persistence for Memcached.
One of the most common performance pitfalls is not setting the correct memory limit (-m). If Memcached runs out of memory, it will start evicting items according to its LRU (Least Recently Used) policy. This means your cache hit rate plummets, and your application starts hitting the database much more often, leading to a performance degradation that’s hard to diagnose if you’re not monitoring Memcached’s memory usage and eviction statistics. The default memory limit is quite small (e.g., 64MB), which is almost never enough for a production workload.
Another key aspect is the network. By default, Memcached listens on 0.0.0.0, meaning it accepts connections from all interfaces within the Docker network. In our docker-compose.yml, the webapp service can reach Memcached via the hostname memcached on port 11211. If your webapp were running outside of Docker, you’d typically use a docker run command with -p 11211:11211 to expose the port and then connect to your_docker_host_ip:11211. Ensure your firewall rules allow this traffic if connecting externally.
The -c flag, maximum concurrent connections, is also vital. If your application experiences a sudden spike in traffic, and the number of active connections to Memcached exceeds this limit, new connections will be refused. This can manifest as connection refused errors or timeouts in your web application logs. You need to monitor your application’s connection patterns to Memcached and set this appropriately. A common starting point for moderate traffic might be 2048 or 4096, but this is highly workload-dependent.
The slab allocator’s behavior means that storing many very small items can be less efficient than storing fewer, larger items, or items that fit well into the predefined slab classes. If you have a mix of tiny session IDs and larger serialized objects, Memcached might waste space trying to fit them into inappropriate slabs. For extremely diverse item sizes, other caching solutions might be more suitable, but for typical web application caching, Memcached is usually fine.
Finally, remember to monitor Memcached’s statistics. You can connect to it using telnet <container_ip> 11211 and then type stats. Look for curr_connections, limit_maxconn, bytes_used, evictions, and get_misses vs get_hits. A high evictions count indicates you need more memory. A low hit rate (get_hits / (get_hits + get_misses)) means your cache isn’t effective, possibly due to insufficient memory or too short TTLs (Time To Live) on your cached items.
The next common issue you’ll encounter is Memcached becoming a single point of failure.