Note: This article is an AI-generated write-up based on the captions and transcript of the video above. Watch the embedded video for the full visual walk-through and nuances.
How Redis Caps Its Memory Usage: A Deep Dive into Redis Internals
Redis, a high-performance in-memory data store, employs a sophisticated mechanism to manage and cap its memory usage, ensuring stability and predictable performance even under heavy load. Unlike simpler implementations that might cap based on the number of keys, Redis focuses on the total amount of memory consumed. This document explores the core components and design decisions behind Redis’s memory management, primarily focusing on the zmalloc and eviction (evict.c) modules.
The zmalloc Abstraction: Tracking Every Byte
At the heart of Redis’s memory tracking is zmalloc, a custom memory allocator wrapper found in zmalloc.c. zmalloc is not a replacement for the operating system’s malloc; instead, it wraps standard memory allocation functions to provide Redis with a precise, real-time understanding of its memory footprint.
Why zmalloc?
The primary motivation for zmalloc is to maintain an accurate, internal counter of all memory allocated by the Redis process. This counter, used_memory, is crucial because:
- Real-time Usage: It allows Redis to know its exact memory consumption without relying on expensive system calls to query process memory.
- Granular Tracking: Every single allocation, whether for keys, values, internal data structures, or temporary buffers, goes through
zmalloc, ensuring comprehensive tracking.
How zmalloc Works
zmalloc effectively intercepts all memory allocation and deallocation requests within Redis.
- Wrapping Standard Functions:
zmalloc provides its own versions of malloc, free, realloc, and calloc. These functions have the same signatures as their standard C library counterparts but include additional logic.
- Atomic Memory Counter:
- A global atomic counter,
used_memory, is maintained.
- Whenever memory is allocated (via
zmalloc), this counter is atomically incremented by the size of the allocated memory.
- When memory is freed (via
zfree), the counter is atomically decremented.
- The macro
ZmallocStatAlloc is used for incrementing, and ZmallocStatFree for decrementing.
- Prefixing Allocations: Redis often allocates a small prefix before the user-requested memory block. This prefix can store metadata specific to Redis’s memory management, such as the actual size of the allocated block. This is visible in functions like
zmalloc_usable.
- Out-of-Memory Handling:
zmalloc includes a default out-of-memory (OOM) handler, ZmallocDefaultOmHandler. If an underlying malloc call fails (meaning the operating system cannot provide the requested memory), this handler is invoked, typically leading to a server crash and an error message printed to the console.
zmalloc’s Boundary: Tracking vs. Enforcement
A critical design decision in zmalloc is its clear boundary: zmalloc is solely responsible for tracking memory usage. It does not enforce memory limits. If malloc succeeds, zmalloc will allocate the memory and update its used_memory counter, even if this pushes Redis beyond a configured maxmemory limit. The responsibility for checking and acting upon these limits lies with other parts of the Redis codebase, specifically the eviction module.
Memory Eviction: Enforcing Limits with evict.c
While zmalloc provides the raw memory usage data, the evict.c module is responsible for implementing Redis’s memory capping policy. This module uses the used_memory counter from zmalloc to determine when to trigger eviction and how much memory to free.
The maxmemory Configuration
Redis allows users to configure a maxmemory limit. This setting dictates the maximum amount of RAM Redis should consume. When used_memory approaches or exceeds this maxmemory threshold, Redis initiates an eviction process.
Eviction Logic
The eviction process typically involves the following steps:
- Checking Memory Usage: Functions like
zmalloc_get_used_memory() are called to retrieve the current value of the used_memory counter.
- Threshold Check: The
zmalloc_is_used_memory_more_than_max_memory_after_alloc() function (or similar logic) compares the current used_memory against the maxmemory limit. If maxmemory is not set, Redis is allowed to allocate as much as the OS permits.
- Eviction Loop: If the
maxmemory limit is exceeded, Redis enters an eviction loop. This loop continuously selects and removes keys from the dataset based on a configured eviction policy (e.g., LRU, LFU, volatile-LRU) until the used_memory falls below the maxmemory threshold (or a specific target percentage of it).
- Each key eviction involves freeing the memory associated with that key, which in turn causes
zfree to be called and the used_memory counter to be decremented.
- The loop continues until the memory target is reached.
Separation of Concerns
This architecture demonstrates a clear separation of concerns:
zmalloc.c: Provides a low-level, accurate memory tracking utility.
evict.c: Implements the high-level policy for memory management and eviction based on the data provided by zmalloc.
This design offers flexibility. zmalloc can be used by any part of Redis to understand its memory footprint, while the eviction policy can be modified or extended without altering the fundamental memory tracking mechanism.
Design Philosophy and Trade-offs
The decision to separate memory tracking from memory enforcement is a deliberate and powerful one:
- Application Control: It gives Redis (the application) full control over how to react when memory limits are approached or exceeded. Instead of a hard failure from
malloc, Redis can gracefully handle the situation by evicting data.
- Predictability: Eviction policies allow Redis to remain operational and responsive, even when under memory pressure, by shedding less critical data. A direct
malloc failure would lead to an immediate crash.
- Debugging and Monitoring: The
used_memory counter is invaluable for monitoring Redis’s health and debugging memory-related issues.
Comparison with GoLang
Implementing a similar custom memory tracking and capping mechanism in languages like GoLang presents challenges. GoLang relies heavily on its garbage collector (GC) for memory management, abstracting away direct malloc/free calls.
- GoLang’s GC: Go’s runtime manages memory automatically, making it difficult to precisely track every byte allocated by the application in real-time without significant overhead or reliance on internal Go runtime metrics.
- Alternatives in GoLang:
- C Bindings: One could embed C code (using
cgo) to leverage malloc and implement a zmalloc-like wrapper.
- Custom Allocators: Libraries like
jemalloc or tcmalloc (which are C/C++ based) can be integrated, but this often involves more complex setup and management.
- Go’s
runtime.MemStats: Provides statistics about Go’s heap, but not as granular or real-time as zmalloc for every single allocation.
For demonstration purposes or simpler applications, capping by the number of keys might be sufficient in GoLang, but for a high-performance, memory-sensitive system like Redis, the zmalloc approach is essential.
Conclusion
Redis’s memory management strategy, built around the zmalloc wrapper and the evict.c module, is a testament to robust systems design. By meticulously tracking every byte allocated and separating this tracking from the policy enforcement, Redis achieves precise control over its memory footprint. This allows it to implement sophisticated eviction strategies, maintain high availability, and provide predictable performance, making it a cornerstone of modern distributed systems. Understanding these internals provides valuable insights into building resilient and efficient software.