Comments 22
Не так важно, какие именно слоты переносятся между серверами, главное добиться правильного соотношения между количеством слотов у работающих серверов (если веса равные, то и количество слотов на каждом должно быть примерно равно друг другу).
Если сервер отвалился, то его слоты должны делиться поровну между оставшимися. Если появился один или несколько новых, то нужно взять с каждого из старых по равному числу слотов, но так, чтобы количество слотов после перераспределения было примерно равно.
Если веса у серверов различны, то отношение количества слотов на этих серверах должно примерно соответствовать соотношению их весов.
Посмотрите на такой простой кусочек кода:
Если это продакшен-код (или создан из такового), тогда он очень, очень плох… И дело не в неправильной работе с кешом, а в самом коде. Да даже и пример можно было сделать, используя нормальные практики программирования, заодно выиграв в читаемости.
Мы в Badoo решаем эту проблему тем, что пишем данные во все кэширующие серверы сразу. Благодаря этому при чтении мы можем использовать общий механизм выбора сервера — в коде можно использовать обычный механизм шардирования по User ID, и при чтении не нужно ничего знать про специфику этого «горячего» ключа. В нашем случае это работает, поскольку у нас сравнительно немного серверов (примерно десять на площадку).
Если бы кэширующих серверов было намного больше, то этот способ мог бы быть не самым удобным — просто нет смысла дублировать сотни раз одни и те же данные. В таком случае можно было бы дублировать ключ не на все серверы, а только на их часть, но такой вариант требует чуть больше усилий.
А вы полностью сами написали распределенный кэш? Почему индустриальный лидеры: hazelcast и ignite — не подошли? Там и шардирование есть, и репликация.
Записывать кэш на все 10 серверов — это, кажется, супер дорого, поправьте меня, пожалуйста. Особенно, если вы ждёте Аков от всех нод, а не какого-то кворума.
Да, писать в 10 кэширующих серверов долго и дорого, но мы используем это крайне редко. Обычно мы пишем ключ и читаем его только из одного сервера.
Я нашёл меньше 20 групп ключей, использующих схему со всеми серверами. Обычно это какие-то счётчики или кэши конфигурации по странам/городам, которые обновляются только фоновыми скриптами (т.е. мы исключаем параллельные обновления). В онлайне эти ключи только читаются и не отличаются от остальных.
Честно говоря, я не знаю, рассматривались ли решения, про которые вы говорите, но кажется, что проблема не такая большая, чтобы использовать для неё специализированное решение. Memcached у нас уже был, так что мы использовали его.
Вынос обновлений в фон
А если проекты маленькие, но проблема с параллельными вычислениями все же присутствует? Выносить в фон выглядит немного дороговато- переписывать чужой код, кто-то должен
поддерживать, помнить про это, убедить остальную команду, клиента…
Тоже есть небольшой опыт с метками (boolean), метками времени, но если что перегружается, то все остается в «воздухе». Что если на момент вычисления блокировать какой-нибудь файл? Если основной процесс с вычислениями помрет, то файл будет автоматом разблокирован(?) и другой процесс в очереди сможет подхватить работу, при этом основной процесс может записывать какие-то промежуточные вычисления в этот файл, что позволит следующему начать уже не с нуля. Это конечно все нагрузка на диск, но опять же это не для крупных проектов.
Дальше можно нарастить более сложную логику: если на «холодный старт» много что нужно вычислить, то может не ставить в очередь другие процессы, а дать им что «повычислять»? Т.е. по кол-ву «вычислений» появятся что-то вроде «мастер» процессов, а остальные останутся в очереди, когда «мастер» процесс завершает свой кусочек вычислений для кэша он становится в очередь к остальным, как-то только все «мастер» процессы выполнят свои задачи, то все процессы отдадут ответы из кэша. -Пока лишь фантазии…
Честно говоря, вариант с мастер-процессом не выглядит проще в реализации, чем фоновое обновление (хотя сложно "лечить по фотографии").
Если у вас есть много вычислений, которые можно разбить на отдельные части, то их можно обновлять по частям, используя для каждой части вероятностные обновления до истечения времени жизни основного кэша. В этом случае можно и избежать фонового процесса и иметь часть готовых результатов на момент "протухания" кэша итогового результата.
Что будет, если бд по какой то причине не сможет обновлять кеш некоторое время? Ведь в этом случае практически все клиенты вместо null будут получать «протухшие» значения, до тех пор пока не возобновится обновление. Возможно это допустимое поведение, но его нужно учитывать.
По описанию это очень похоже на вероятностные методы, только вместо случайного числа тут тут время обращения
Есть ли готовая библиотека для PHP, которая реализует кэш, инкапсулирует решение проблем, описанных в статье (cache stampede и обработка сбоев при обновлении кэша), и подходит хотя бы для небольших проектов?
function getContactsCountCached1(int $user_id) : ?int
{
$contacts_count = \Contacts\Cache::getContactsCount($user_id);
if ($contacts_count !== false) {
return $contacts_count;
}
if ($lock->get() === true) {
while($lock->get() === true){
sleep(DB_TIME_OUT / 10); //wait n/10 sec
}
return getContactsCountCached($user_id);
}
$lock->lock(DB_TIMEOUT); // set on n sec
$contacts_count = $this->getContactsCount($user_id, DB_TIMEOUT);
$lock->unlock();
if (is_null($contacts_count)) {
return null;
}
\Contacts\Cache::setContactsCount($user_id, $contacts_count);
return $contacts_count;
}
Было бы здорово, если бы вы прогнали тест с XFetch и дали бы возможность сравнить с остальными.
Что-то подобное есть в дополнительных материалах, которые я приводил:
Репозиторий на GitHub с описанием и тестами разных способов
Там в директории samples/ есть log-файлы с результатами теста разных способов (правда никакой визуализации нет и не очень удобно сравнивать способы друг с другом).
Проблемы при работе с кэшем и способы их решения