О насБлогКонтакты
Backend18 апреля 2011 г. 4 мин 14

Оптимизация MySQL: переход с MyISAM на InnoDB и кэширование через Memcached

AunimedaAunimeda
📋 Содержание

Оптимизация MySQL: переход с MyISAM на InnoDB и кэширование через Memcached

В начале 2011 года у нас упал продакшен-сервер под нагрузкой. Интернет-магазин на PHP 5.3 и MySQL 5.1 работал нормально при 50 одновременных пользователях, но при 200+ сервер переставал отвечать. SHOW PROCESSLIST показывал десятки запросов в состоянии Waiting for table lock.

Причина оказалась в MyISAM.


Почему MyISAM виновен

MyISAM - движок по умолчанию в MySQL 5.1 - использует блокировку на уровне таблицы. Это значит: пока идёт один UPDATE, все SELECT на эту таблицу ждут.

-- Такой запрос блокировал всю таблицу orders на 2-5 секунд
UPDATE orders SET status = 'processing' WHERE id = 12345;

-- Все эти запросы висели в очереди:
SELECT * FROM orders WHERE user_id = 678;
SELECT COUNT(*) FROM orders WHERE status = 'new';

При 200 пользователях очередь блокировок росла быстрее, чем разбиралась.

InnoDB использует блокировку на уровне строки. UPDATE одной записи не мешает SELECT других записей той же таблицы.


Миграция на InnoDB

-- Проверить текущие движки таблиц
SELECT table_name, engine 
FROM information_schema.tables 
WHERE table_schema = 'myshop';

-- Конвертировать таблицу (занимает время на больших таблицах - делать в ночное время)
ALTER TABLE orders ENGINE = InnoDB;
ALTER TABLE products ENGINE = InnoDB;
ALTER TABLE users ENGINE = InnoDB;

-- Проверить результат
SHOW TABLE STATUS FROM myshop;

Конвертация таблицы на 2 миллиона строк заняла около 8 минут. Делали поочерёдно, без остановки сервиса - MyISAM и InnoDB таблицы спокойно сосуществуют в одной базе.


Настройка InnoDB Buffer Pool

Самая важная настройка InnoDB - innodb_buffer_pool_size. Это объём RAM, который InnoDB использует для кэширования данных и индексов. Чем больше - тем меньше обращений к диску.

# /etc/mysql/my.cnf

[mysqld]
# На сервере с 4GB RAM - выделяем 70-80% под буферный пул
innodb_buffer_pool_size = 3G

# Включить несколько пулов (уменьшает конкуренцию за мьютексы при высокой нагрузке)
innodb_buffer_pool_instances = 4

# Размер лога транзакций - увеличить для write-heavy нагрузки
innodb_log_file_size = 256M

# Синхронизация на диск: 2 = быстрее, но теряем ~1 секунду данных при краше
# 1 = безопасно, но медленнее. Для магазина выбрали 1.
innodb_flush_log_at_trx_commit = 1

После перезапуска MySQL и прогрева кэша (несколько минут под нагрузкой) количество обращений к диску упало на 80%.


EXPLAIN: находим медленные запросы

Включили slow query log для запросов дольше 1 секунды:

slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

Нашли виновника - запрос без индекса:

-- Этот запрос делал полный скан 2M строк
SELECT * FROM orders 
WHERE status = 'new' AND created_at > '2011-01-01'
ORDER BY created_at DESC;

-- EXPLAIN показал: type = ALL, rows = 1987432
EXPLAIN SELECT * FROM orders 
WHERE status = 'new' AND created_at > '2011-01-01';

Добавили составной индекс:

-- Составной индекс: сначала по status (низкая кардинальность), потом по дате
CREATE INDEX idx_status_created ON orders (status, created_at);

-- После индекса: type = range, rows = 847 (сканируем только нужные строки)

Время запроса: с 4.2 секунды до 12 миллисекунд.


Memcached: кэшируем тяжёлые запросы

Некоторые данные меняются редко, но запрашиваются часто. Список категорий, топ товаров, настройки магазина - идеальные кандидаты для кэша.

# Установка на Ubuntu
apt-get install memcached php5-memcache

Паттерн "cache-aside" на PHP 5.3:

<?php

class Cache {
    private $memcache;
    
    public function __construct() {
        $this->memcache = new Memcache();
        $this->memcache->connect('localhost', 11211);
    }
    
    public function get($key) {
        return $this->memcache->get($key);
    }
    
    public function set($key, $value, $ttl = 300) {
        $this->memcache->set($key, $value, 0, $ttl);
    }
    
    public function delete($key) {
        $this->memcache->delete($key);
    }
}

// Использование
function getCategories(Cache $cache, PDO $db) {
    $cacheKey = 'categories:all';
    
    // Пробуем кэш
    $categories = $cache->get($cacheKey);
    if ($categories !== false) {
        return $categories;  // Возвращаем из кэша
    }
    
    // Промах - идём в базу
    $stmt = $db->query('SELECT * FROM categories WHERE active = 1 ORDER BY sort_order');
    $categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
    
    // Сохраняем в кэш на 10 минут
    $cache->set($cacheKey, $categories, 600);
    
    return $categories;
}

// При изменении категории - инвалидируем кэш
function updateCategory(Cache $cache, PDO $db, $id, $data) {
    $stmt = $db->prepare('UPDATE categories SET name = ? WHERE id = ?');
    $stmt->execute([$data['name'], $id]);
    
    // Сбрасываем кэш
    $cache->delete('categories:all');
}

Результат

Метрика До После
Запросы в состоянии Waiting for lock 30-50 0-2
Среднее время ответа страницы 1.8 сек 0.4 сек
Максимальная нагрузка без деградации 200 RPS 800 RPS
Обращений к MySQL 100% ~20% (остальное из Memcached)

Сервер перестал падать. При этом железо не меняли - только настройки и архитектура.


Что мы вынесли

InnoDB - это не просто замена MyISAM. Это другая парадигма: транзакции, внешние ключи, row-level locking. Если ваш MySQL ещё на MyISAM - миграция обязательна.

Memcached решает проблему повторяющихся тяжёлых запросов. Но кэш - это не замена оптимизации запросов. Сначала индексы и EXPLAIN, потом кэш.

В 2013 году MySQL 5.6 принёс значительные улучшения в InnoDB. В 2015-м появился Percona Server с лучшей статистикой. Сегодня PostgreSQL занял нишу для сложных аналитических запросов. Но базовые принципы - индексы, правильный движок, кэширование - не изменились.

Читайте также

Как перейти с PHP 5.6 на PHP 7.0 без поломки приложения (2016)aunimeda
Backend

Как перейти с PHP 5.6 на PHP 7.0 без поломки приложения (2016)

PHP 7.0 давал в 2 раза более высокую производительность по сравнению с PHP 5.6. Но миграция ломала старый код: mysql_* функции удалены, изменилась обработка ошибок, несовместимые изменения в типах. Мы мигрировали 4 проекта без простоя — вот точный чеклист.

REST API на PHP: уроки первого мобильного бэкендаaunimeda
Backend

REST API на PHP: уроки первого мобильного бэкенда

Мы писали первый REST API для iOS-приложения в 2013 году. Что такое настоящий REST, почему HTTP-методы важны, как сделать аутентификацию без сессий - и ошибки, которые мы совершили.

Почему мы выбрали Node.js в 2011 годуaunimeda
Backend

Почему мы выбрали Node.js в 2011 году

В 2011 году Node.js было 2 года, у него не было LTS, и большинство PHP-разработчиков считали его игрушкой. Мы всё равно перенесли на него реал-тайм дашборд. Вот точное рассуждение, принятые риски и что получилось.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Кыргызстане. Бесплатная консультация.

Получить консультацию Все статьи