Налаштування Memcached для кеширування веб-застосунку
Memcached — це розподілений кеш у пам'яті з мінімальною моделлю: ключ, значення, TTL. Без транзакцій, персистентності або pub/sub. Саме ця простота робить його швидшим за Redis в сценаріях читання з кешу — затримка при влученні в кеш становить 0,1–0,5 мс проти 1–3 мс у Redis з AOF-персистентністю.
Використовується там, де потрібно кешувати великі обсяги однорідних даних: результати SQL-запитів, серіалізовані об'єкти, HTML-фрагменти, API-відповіді.
Встановлення та базова конфігурація
# Ubuntu/Debian
apt install memcached libmemcached-dev
# Редагуємо /etc/memcached.conf
-d # режим демона
-m 2048 # 2GB ОЗУ
-p 11211 # порт
-u memcache # користувач
-l 127.0.0.1 # тільки localhost (не виставляти назовні!)
-c 2048 # макс з'єднання
-t 8 # потоки (= кількість ядер CPU)
-I 10m # макс розмір елемента (за замовчуванням 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, // мс
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, // асинхронний 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 не підтримує теги нативно. Стандартний прийом — версіоновані простори імен:
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); // без вислідження
}
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');
Розподілений кеш — конгруентне хешування
З кількома серверами критично використовувати конгруентне хешування, щоб при додаванні/видаленні вузла інвалідувалося мінімум ключів:
$mc = new Memcached('persistent_pool'); // постійний пул з'єднань
$mc->addServers([
['memcached-1.internal', 11211, 40], // вага 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) = коефіцієнт влучання (мета > 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 id: 7603 — готовий дашборд для Memcached.
Типовий таймлайн
День 1 — встановлення, конфігурація розміру пам'яті та кількості потоків, налаштування брандмауера (порт 11211 має бути закритий ззовні).
День 2 — інтеграція з застосунком, реалізація cache-aside для важких SQL-запитів, інвалідація при записі.
День 3 — перевірка коефіцієнта влучання, налаштування моніторингу, настройка TTL за типами даних. Якщо коефіцієнт влучання нижче 80% — аналізуємо промахи та виправляємо стратегію кеширування.







