Запись опубликована
Блог Андрея Смирнова. Пожалуйста, оставляйте
комментарии там.
Начало серии постов
здесь. Продолжаем разбираться с этим вопросом.
Проблема №3. Одного мемкеша мало
Если сайт большой, данных много, кешей тоже много, в один memcached физически не умещаемся. Пожалуйста: создаем кластер мемкешовых серверов, хешируем ключи для определения номера сервера, на котором ключ должен храниться. Всё вроде бы хорошо до тех пор, пока эти сервера не начинают падать или с ними не начинает теряться сетевая connectivity (что легко происходит, т.к. архитекутра кластерная, место на площадках ограничено, сервера физически разнесены, каналы забиты, ненадежны и т.п.)
Самый простой алгоритм хеширования вида:
$memcache_server_id = crc32($key) % count($memcache_servers);
начинает “веселиться”, когда обнаруживает, что сервер упал. Когда обнаруживается проблема сетевого доступа к серверу,
драйвер memcachа убирает его из списка $memcacheservers. При этом count($memcacheservers) изменяется, какая-то часть ключей “сдвигается” на другие сервера, при этом “пропадает” куча кешей (хорошо, если кешей, которые не так жалко потерять, а если речь идёт о сессиях пользователей?).
Необходимо отметить, что та же самая проблема будет, когда мы вводим в строй или выводим из пула сервера мемкеша. Интересная математическая задача: если раньше было N серверов мемкеша и схема распределения ключей по серверам взятием остатка от деления на N (как написано выше), а после каких-то событий стало K серверов, то какой процент ключей останется на том же сервере, на котором они были до изменения количества серверов?
Когда куча ключей теряется, резко начинают перестраиваться кеши, вырастает нагрузка на БД, и т.д., и т.п., то есть падение одного сервера мемкеша заваливает весь “отказоустойчивый” кластер.
Что делать? Есть
другие алгоритмы хеширования ключей, устойчивые к удалению/добавлению серверов в пул. Для
PHP-шного модуля Memcache это:
ini_set('memcache.hash_strategy', 'consistent');
Проблема №4. Одновременное перестроение кеша несколькими мордами
Если мы выставляем ключу (кешу) некоторое время жизни, рано или поздно, memcache скажет, что такого ключа на сервере нет. А если это “популярный” кеш, например, использующийся на главной странице, то такую ситуацию могут “обнаружить” одновременно несколько морд. И они все попытаются построить этот кеш, то есть они все одновременно отправят запрос в БД, то есть они отправят много запросов в БД, причём за короткий промежуток времени. Конечно, как только первый запрос завершится, морда запишет новое значение ключа и больше новых запросов за этой выборкой не будет. Но мы уже подвергли БД высокой нагрузке, хотя хотели этого избежать при помощи кеширования.
Напрашивается решение: пока одна морда строит кеш, все остальные должны её “подождать”. Как это реализовать? Например, с помощью блокировок в том же мемкеше. Перед тем, как начать строить кеш для ключа mykey, проверяем, не выставлен ли уже ключ mykey_lock, если он выставлен, засыпаем и ждём некоторое время, что ключ исчезнет, если он исчез - пробуем снова прочитать mykey, если он появился (другая морда его построила), то берем готовое значение. Не появился за какой-то разумный промежуток времени - начинаем строить сами (значит, никто другой не смог построить значение этого кеша).
Перед тем, как сами начинаем строить кеш (например, если блокировки не было обнаружено), обязательно создаем блокировку: выставляем ключ блокировки mykey_lock, например, на 10 секунд, тем самым не подпуская другие морды к построению такой же выборки. Как выборку построили - записываем её в мемкеш, ключ блокировки удаляем.
Такая схема позволяет избежать части коллизий, который возникают при одновременном построении кешей.