Pull to refresh
0

Готовим кеш правильно

Reading time5 min
Views13K

Многие верят что кеш, да еще и распределенный, — это такой простой способ сделать всё быстрее и круче. Но, как показывает практика, неправильное применение кеша часто, если не всегда, делает всё только хуже.


Под катом вас ожидает пара историй о том, как неправильно прикрученный кеш убил перфоманс.



Нетрадиционный дисклеймер
  • Все описанные в статье события вымышлены, любые совпадения с реальными деплойментами случайны.
  • Описанная в статье логика работы Apache Ignite намеренно упрощена, важные подробности опущены.
  • Не пытайтесь повторять подобное в продакшене без вдумчивого чтения официальной документации!
  • Некоторые из описанных паттернов проектирования являются антипаттернами, помните об этом перед внедрением :)

Моя база тормозит, а с вашем кешем тормозит еще больше :(


Как выглядит среднестатистический вебсервис?


Сервлет ⟷ База данных

Когда база данных перестает справляться нужно сделать что?
Правильно! Купить новое железо!
А когда не поможет?
Правильно! Настроить кеш, желательно распределенный!


Сервлет ⟷ Кеш ⟷ База данных

Будет быстрее? Не факт!


Почему? Допустим, вы добились невероятного параллелизма и можете принять тысячи запросов одновременно. Но, все они придут по одному ключу — горячему предложению с первой страницы.
Далее, все потоки будут следовать одной и той же логике: «значения в кеше нет, пойду за ним в базу».


Что произойдет в итоге? Каждый поток сходит в базу и обновит значение в кеше. В итоге, система потратит больше времени, чем если бы кеша не было в принципе.


А решение проблемы очень простое — синхронизация запросов по одним и тем же ключам.


Apache Ignite предлагает простое кеширование через Spring Caching с поддержкой синхронизации.


@Cacheable("dynamicCache")
public String cacheable(Integer key) {
   // Каждый поток имеет шанс оказаться здесь :( 
   return longOp(key); 
}

@Cacheable(value = "dynamicCache", sync = true)
public String cacheableSync(Integer key) {
   // Здесь, по одному ключу, окажется максимум один поток
   // Все остальные потоки, по этому же ключу, 
   // дождутся результата от первого, вместо выполнения кода longOp(key)
   return longOp(key);
}

Функционал синхронизации был добавлен в Apache Ignite в версии 2.1.


Теперь быстро! Но всё ещё медленно :(


Эта история является прямым продолжением предыдущей и призвана показать, что разработчики кешей тоже не всегда используют их правильно.


Итак, фикс, добавляющий синхронизацию по кешам, оказался в продакшене и… не помог.


Никто не ломанулся за горячим товаром с первой страницы, а все 1000 потоков пошли по разным ключам одновременно, и узким местом стали механизмы синхронизации.


В предыдущей статье я рассказывал, как работали средства синхронизации в Apache Ignite раньше.


Информация обо всех средствах синхронизации раньше хранилась в кеше ignite-sys-cache, по ключу DATA_STRUCTURES_KEY в виде Map<String, DataStructureInfo> и каждое добавление синхронизатора выглядело как:


// Берем глобальный лок по ключу
lock(cache, DATA_STRUCTURES_KEY);

// Добавляем новый синхронизатор
Map<String, DataStructureInfo> map = cache.get(DATA_STRUCTURES_KEY);
map.put("Lock785", dsInfo);
cache.put(DATA_STRUCTURES_KEY, map);

// Отпускаем лок
unlock(cache, DATA_STRUCTURES_KEY)

Итого при создании необходимых синхронизаторов все потоки стремились изменить значение по одному и тому же ключу.



Снова быстро! Но надежно ли?


Apache Ignite избавился от «самого важного» ключа в версии 2.1, начав сохранять информацию о синхронизаторах раздельно. Пользователи получили прирост больше 9000% в реальном сценарии.



И всё было хорошо до тех пор пока не моргнул свет, бесперебойники подвели, и пользователи потеряли разогретый кеш.



В Apache Ignite, начиная с версии 2.1, появилась собственная реализация Persistence.


Но, сама по себе Persistence не дает гарантий получения консистентного состояния при рестарте, т.к. данные синхронизируются (сбрасываются на диск) периодически, а не в реальном времени. Основной задачей Persistence является возможность хранить и эффективно обрабатывать на одном компьютере больше данных, чем влезает в его память.


Гарантия консистентности достигается использованием подхода Write-ahead logging. Говоря проще, данные сначала записываются на диск как логическая операция, а потом уже сохраняются в распределенном кеше.


При рестарте сервера на диске мы будем иметь состояние, актуальное на какой то момент времени. Достаточно на это состояние накатить WAL, и система снова работоспособна и консистентна (полный ACID). Восстановление занимает секунды, в худшем случае — минуты, но не часы или дни, требуемые для прогрева кеша актуальными запросами.


Теперь надежно! Быстро ли?


Persistence замедляет систему (на запись, не на чтение). Включенный WAL замедляет систему ещё больше, это плата за надежность.


Есть несколько уровней журналирования, дающих разные гарантии:


— DEFAULT — полные гарантии по сохранению данных при любом уровне нагрузки
— LOG_ONLY — полные гарантии, кроме случая выхода из строя операционной системы
— BACKGROUND — гарантий нет, но Apache Ignite будет стараться :)
— NONE — журнал операций не ведется.


Разница в скорости DEFAULT и NONE на средневзвешенной системе достигает 10 раз.


Вернемся к нашей ситуации. Допустим, мы выбрали режим BACKGROUND, который в 3 раза медленней NONE и теперь не боимся потерять разогретый кеш (можем потерять операции из последних нескольких минут перед крашем, но не более того).


В таком режиме мы работали несколько месяцев, всякое случалось и мы легко восстанавливали систему после крашей. Все вокруг довольны и счастливы.


Если бы не одно «но», сегодня, 20-го декабря, в пик продаж, мы поняли, что серверы загружены на 80% и вот-вот рухнут под нагрузкой.


Выключение WAL (перевод в NONE) дало бы нам снижение нагрузки в 3 раза, но для этого нужно рестартовать весь кластер Apache Ignite и потерять возможность восстановиться на таком кластере в случае чего — возвращаемся к пункту «быстро и ненадежно»?


В Apache Ignite, начиная с версии 2.4, появилась возможность отключать WAL без перезапуска кластера, а потом включать его с восстановлением всех гарантий.


SQL


// Выключение
ALTER TABLE tableName NOLOGGING
// Включение
ALTER TABLE tableName LOGGING

Java


// Текущее состояние
ignite.cluster().isWalEnabled(cacheName);
// Выключение
ignite.cluster().disableWal(cacheName);
// Включение
ignite.cluster().enableWal(cacheName);

Теперь и быстро и надежно, но если что — есть варианты ...


Теперь, если нам нужно отключить журналирование (к черту гарантии, главное пережить нагрузку!) мы можем временно отключить WAL и включить его в любой удобный нам момент.


Следует всё же отметить, что фича создавалась совершенно не для этого ...

А для быстрой загрузки огромного набора исторических данных на старте системы.
Также стоит упомянуть, что отключение возможно в рамках только определенных кешей, а не всей системы целиком, т.е. в случае чего данные в кешах, для которых отключение не проводилось, не пострадают.


При этом, после включения WAL, система гарантирует поведение согласно выбранному режиму WAL.


Кеш, даже распределенный, не панацея!


Ни одна технология, какая бы навороченная и продвинутая она не была, не сможет решить все ваши проблемы. Неправильно использованная технология, скорее всего, сделает только хуже, а использованная правильно, вряд ли закроет собой все щели.


Кеш, в том числе распределенный, — это механизм, дающий ускорение только при грамотном использовании и вдумчивой настройке. Помните об этом перед внедрением его в ваш проект, проводите замеры до и после во всех связанных с ним кейсах… и да пребудет с вами перфоманс!

Tags:
Hubs:
+9
Comments0

Articles

Change theme settings

Information

Website
gridgain.com
Registered
Founded
2007
Employees
101–200 employees
Location
США
Representative
Ксения Романова