Настройка Memcached для кэширования веб-приложения
Memcached — это распределённый кэш в памяти с предельно простой моделью: ключ, значение, TTL. Никаких транзакций, персистентности, pub/sub. Именно эта простота делает его быстрее Redis в сценариях read-heavy кэширования — задержка при попадании в кэш составляет 0.1–0.5 мс против 1–3 мс у Redis с AOF-персистентностью.
Используется там, где нужно кэшировать большие объёмы однородных данных: результаты SQL-запросов, сериализованные объекты, HTML-фрагменты, API-ответы.
Установка и базовая конфигурация
# Ubuntu/Debian
apt install memcached libmemcached-dev
# Редактируем /etc/memcached.conf
-d # daemon mode
-m 2048 # 2GB RAM
-p 11211 # порт
-u memcache # пользователь
-l 127.0.0.1 # только localhost (не выставлять наружу!)
-c 2048 # max connections
-t 8 # threads (= кол-во CPU ядер)
-I 10m # max item size (дефолт 1MB, увеличиваем до 10MB)
-o modern # современные опции slab allocator
Перезапуск и проверка:
systemctl restart memcached
echo "stats" | nc 127.0.0.1 11211 | grep -E "curr_items|bytes|hit_rate|evictions"
PHP-интеграция через php-memcached
pecl install memcached
echo "extension=memcached.so" > /etc/php/8.2/mods-available/memcached.ini
phpenmod memcached
Базовое использование:
$mc = new Memcached();
$mc->addServer('127.0.0.1', 11211);
// Настройки клиента
$mc->setOptions([
Memcached::OPT_CONNECT_TIMEOUT => 50, // ms
Memcached::OPT_RETRY_TIMEOUT => 300,
Memcached::OPT_SEND_TIMEOUT => 100,
Memcached::OPT_RECV_TIMEOUT => 100,
Memcached::OPT_POLL_TIMEOUT => 100,
Memcached::OPT_COMPRESSION => true,
Memcached::OPT_SERIALIZER => Memcached::SERIALIZER_IGBINARY,
Memcached::OPT_TCP_NODELAY => true,
Memcached::OPT_NO_BLOCK => true, // async I/O
]);
Кэширование SQL-запросов
Паттерн cache-aside — самый распространённый:
class ProductRepository
{
private Memcached $cache;
private PDO $db;
private int $defaultTtl = 300; // 5 минут
public function findById(int $id): ?array
{
$key = "product:v2:{$id}";
$product = $this->cache->get($key);
if ($this->cache->getResultCode() === Memcached::RES_SUCCESS) {
return $product;
}
$stmt = $this->db->prepare('SELECT * FROM products WHERE id = ? AND active = 1');
$stmt->execute([$id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
if ($product !== null) {
$this->cache->set($key, $product, $this->defaultTtl);
}
return $product;
}
public function findByCategoryWithPagination(int $categoryId, int $page, int $perPage): array
{
$offset = ($page - 1) * $perPage;
$key = "products:cat:{$categoryId}:p{$page}:pp{$perPage}";
$result = $this->cache->get($key);
if ($this->cache->getResultCode() === Memcached::RES_SUCCESS) {
return $result;
}
$stmt = $this->db->prepare('
SELECT p.*, c.name as category_name
FROM products p
JOIN categories c ON c.id = p.category_id
WHERE p.category_id = ? AND p.active = 1
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
');
$stmt->execute([$categoryId, $perPage, $offset]);
$result = [
'items' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'page' => $page,
];
$this->cache->set($key, $result, 120);
return $result;
}
public function invalidateProduct(int $id): void
{
$this->cache->delete("product:v2:{$id}");
// Инвалидация пагинации по категории — через паттерн тегов
}
}
Инвалидация через теги (эмуляция)
Memcached не поддерживает теги нативно. Стандартный приём — версионированные namespace:
class CacheTagManager
{
private Memcached $mc;
public function getTagVersion(string $tag): int
{
$version = $this->mc->get("tag_version:{$tag}");
if ($this->mc->getResultCode() !== Memcached::RES_SUCCESS) {
$version = time();
$this->mc->set("tag_version:{$tag}", $version, 0); // no expiry
}
return (int)$version;
}
public function buildKey(string $base, array $tags): string
{
$versions = array_map(
fn($tag) => $this->getTagVersion($tag),
$tags
);
return $base . ':' . implode(':', $versions);
}
public function invalidateTag(string $tag): bool
{
return $this->mc->increment("tag_version:{$tag}", 1, time()) !== false;
}
}
// Использование
$tagManager = new CacheTagManager($mc);
// Ключ зависит от версии тега категории
$key = $tagManager->buildKey("products:cat:5:p1", ['category:5', 'products']);
$data = $mc->get($key);
// При изменении категории — все зависимые кэши становятся "несуществующими"
$tagManager->invalidateTag('category:5');
Распределённый кэш — consistent hashing
При нескольких серверах критично использовать consistent hashing, чтобы при добавлении/удалении узла инвалидировалось минимум ключей:
$mc = new Memcached('persistent_pool'); // persistent connection pool
$mc->addServers([
['memcached-1.internal', 11211, 40], // weight 40
['memcached-2.internal', 11211, 40],
['memcached-3.internal', 11211, 20], // меньший вес — меньше трафика
]);
$mc->setOption(Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT);
$mc->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
$mc->setOption(Memcached::OPT_REMOVE_FAILED_SERVERS, true);
$mc->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 3);
$mc->setOption(Memcached::OPT_RETRY_TIMEOUT, 2);
Мониторинг и диагностика
# Статистика сервера
echo "stats" | nc 127.0.0.1 11211
# Важные метрики:
# get_hits / (get_hits + get_misses) = hit rate (цель > 90%)
# evictions > 0 = не хватает памяти, нужно увеличить -m
# curr_connections — текущие соединения
# Смотрим все ключи (только для дебага, не на проде)
echo "stats cachedump 1 100" | nc 127.0.0.1 11211
Prometheus + memcached_exporter:
docker run -d --name memcached-exporter \
-p 9150:9150 \
prom/memcached-exporter:latest \
--memcached.address=127.0.0.1:11211
Grafana dashboard id: 7603 — готовый дашборд для Memcached.
Типичный таймлайн
День 1 — установка, конфигурация размера памяти и thread count, настройка firewall (порт 11211 должен быть закрыт снаружи).
День 2 — интеграция с приложением, реализация cache-aside для тяжёлых SQL-запросов, инвалидация при записи.
День 3 — проверка hit rate, настройка мониторинга, тюнинг TTL по типам данных. Если hit rate ниже 80% — анализ промахов и исправление стратегии кэширования.







