В предыдущей части мы говорили об основных архитектурных принципах построения масштабируемых порталов. Сегодня поговорим об оптимизации правильно построенного портала. Итак: первый вид оптимизации — локальный кэш.
Часть вторая. Кэширование
Как я уже упоминал в предыдущей части, мы говорим, в первую очередь, о B2C порталах. У этих порталов есть общее свойство: преобладание читающих запросов над пишущими. И нередко, преобладание 10-кратное и более. Понятно, почему кэширование представляется таким многообещающим инструментом.
Кэши — тема интересная и практически бесконечная. Поэтому я попробую обрисовать ее достаточно коротко, для первого раунда оптимизации, не вдаваясь в подробности о кэше 1 и 2 уровня, распределенном кэше (distributed cache) и т. д. Так как нам нужно кэшировать чтение объекта, пока мы пропустим и кэш записи (write cache).
Когда мы начинаем локальное кэширование, перед нами обычно стоит выбор: query или object cache (кэширование вызова или объекта). Query cache (или method cache) — это кэш, который можно использовать «снаружи»: он записывает результаты вызовов методов (query), а мы предполагаем, что одинаковые запросы вызывают одинаковые ответы. При повторении одного и того же запроса несколько раз, можно, начиная со второго раза, пропустить сложную и долгую обработку, и просто вернуть предыдущий результат. Преимущество таких кэшей в том, что они не зависят ни от архитектуры, ни от домена, то есть интегрируются как утилита (например, aspect) в готовый продукт, не сильно его изменяя. В противовес этому преимуществу, у них целый ряд недостатков:
- Они неэффективны, когда у нас богатые интерфейсы, у которых есть несколько методов чтения одного и того же объекта. Причина — они кэшируют путь к объекту, а не сам объект.
- Их размер зависит не от количества возможных объектов (результатов), а от количества возможных запросов, и потому они обычно тяжелее, чем альтернативы. Их размер и количество необходимой памяти тяжело планировать.
- Они некрасивы.
Совсем иначе обстоит дело с объектными кэшами: их тяжелее встроить, но они и гораздо эффективнее. Идеальная имплементация кэша объекта — это коллекция (список или множество зависит от требований), которая содержит все объекты, управляемые этим сервисом, в их объектном виде. В идеале сервис должен быть в состоянии ответить на каждый запрос при помощи кэша и без обращения к внешней персистентности (например, базе данных). К сожалению, такая 100% кэшируемость редко достижима, но там, где ее можно применить, она всегда рентабельна. Лучший пример для 100% кэша — учетные записи пользователей. Они, как правило, маленькие (состоят из id, почты, имени, времени регистрации и т.д.) и постоянно требуются в разных местах приложения.
Кэшируем то, чего нет
Ещё одна полезная форма кэша — так называемый ноль-кэш, или отрицательный кэш. В большинстве случаев объектные кэши наполняются во время работы приложения, для того, чтобы был возможен так называемый «холодный старт» и создание новых объектов в базе, при одновременном существовании нескольких инстанций одного и того же сервиса. В результате, обращения к приложению могут «пробиваться» сквозь кэш и доходить до базы. В случае, если такое «пробивание» будет происходить постоянно, это может привезти к перегрузке базы. Тогда все наши кэши, выстроенные с таким трудом, потеряют всяческую эффективность и защитную функцию. Для борьбы с подобной перегрузкой существуют отрицательные кэши, которые запоминают однажды безуспешно опрошенные объекты, тем самым давая сервису возможность при следующем запросе прекратить их обработку на ранней стадии.
Кэшируем то, что меняется
Другая подкатегория кэшей — ExpiryCaches. Они основаны на постулировании того, что объект или его части не будут менять свое состояние в течение какого-то промежутка времени. Типичный пример такого объекта — профиль пользователя на сайте онлайн знакомств. Если пользователь А редактирует свой профиль, то для пользователя Б, который просматривает профиль пользователя А, не принципиально важно, увидит ли он эти изменения через 5, 30 или 60 секунд (особенно, если эти пользователи не контактируют). Тем самым мы можем зафиксировать состояние профиля пользователя А на время, незначительное для человека, но долгое с точки зрения машины, и работать с зафиксированной версией. Этим мы предотвращаем обработку значительного количества запросов, которые, с большой вероятностью, вернули бы идентичный результат (потому что пользователи меняют профиль гораздо реже, чем, например, ищут или отвечают на сообщения), и экономим ресурсы. Плата за эту экономию — всего лишь риск показать обновление в профиле чуть позже, чем мы могли бы. Эта техника с особой популярностью используется на стороне клиента (веб-сервера) для экономии сетевого траффика (к базе данных или сервису) и, соответственно, времени. Но это не единственный пример, когда использование Expiry Cache целесообразно. Другой сценарий использования — это когда в процессе обработки запроса мы исходим из того, что один и тот же объект будет запрошен много раз, но с настолько удаленных друг от друга мест в коде, что хранить его значение в переменной невозможно или нецелесообразно. Популярная технология реализации таких промежуточных кэшей — ThreadLocal (Java).
ExpiryCache — ни что иное, как обмен ресурсами (trade off): мы жертвуем скоростью отображения изменений ради производительности приложения.
Вообще, масштабирование приложений (и особенно порталов) — это обмен ресурсов, которых у нас много, на те, которых не хватает: например, RAM на CPU, сетевой траффик или IO.
Кэширование — хороший метод для оптимизации одного компонента или сервиса, но потенциал такой оптимизации ограничен. В какой-то момент нам придется признать, что лимит того, что может обработать один инстанс (instance), достигнут, и нам нужно масштабировать компоненты нашей архитектуры (то есть запускать их многочисленные копии). Об этом — третья часть.
Автор: dvayanu