S5-nobook-preview.png
Slide Show
Автор

Виталий Филиппов
Нижний колонтитул
Кэширование в веб-приложениях - что, где, когда
Дополнительный нижний колонтитул

Виталий Филиппов, 14:42, 21 июня 2013

JetSnail.svg

Содержание

Аннотация

Кэширование — базовый пример компромисса между временем выполнения и памятью, используемый повсеместно и в больших масштабах. Доклад будет являть собой общий обзор приёмов, а также некоторых АНТИ-приёмов кэширования, применяемых в веб-разработке.

А именно:

Кэширование в веб-приложениях — что, где, когда @@

JetSnail.svg

Виталий Филиппов

Кто я? @@

Vitalif.jpg

В CUSTIS — ведущий веб-разработчик

Мои доклады и контакты: http://yourcmc.ru/wiki/User:VitaliyFilippov

Поддерживаю сборку MediaWiki-notext.svg MediaWiki4Intranet: http://wiki.4intra.net/ Wiki4intranet-logo.svg

(«И давно вы страдаете программизмом?»)

Адрес этого доклада: http://lib.custis.ru/WebAppCaching

О чём доклад?! @@

Веб-приложения @@ %%

Clouds.svg

Веб-приложение ≈ примерно САЙТ! @@

  • Сетевое, клиент-серверное
  • Открытые стандарты, протокол HTTP

На клиенте (клиент = браузер):

  • Основное: HTML+CSS+JavaScript

На сервере:

  • Очень популярен LAMP
  • Разное

LAMP.svg

Схема веб-приложения @@ %%

Webapp-nocache.svg

Кэши есть везде! @@ %%

Webapp-withcache.svg

Краткий экскурс в архитектуру веб-приложений

Что такое веб-приложение, сейчас объяснить довольно просто, ибо это просто то, что обеспечивает работу веб-сайтов, а ими мы все пользуемся постоянно.

Однако, некоторые особенности хотелось бы повторить:

Для написания собственно веб-приложений на серверной стороне используются абсолютно всевозможные языки программирования — вплоть до функциональных (Haskell, Erlang), низкоуровневых (C/C++), а также всяких Go. Однако наибольшую популярность имеют высокоуровневые, обычно скриптовые, языки — PHP, Java, Perl, Python, Ruby, и в последнее время — JavaScript. ПО, обеспечивающее выполнение приложения, написанного на соответствующем языке, обычно называют сервером приложений.

При этом данные обычно хранятся в реляционной СУБД (а возможно, в НЕреляционной, типа MongoDB). Наибольшее распространение имеют свободные MySQL/MariaDB и PostgreSQL. Данных обычно довольно много, запросы по большей части OLTP’шные, то есть их много, но все относительное лёгкие — это диктуется необходимостью отображать страницы за вменяемое время. Общая же черта всех баз данных в том, что они работают медленно относительно самого языка программирования, а также в том, что сетевое взаимодействие с ними порождает накладные расходы.

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

Далее — на сервере:

Об этом всём мы и поговорим далее.

Кэширование @@ %%

JetSnail-grayed.svg

Кэширование @@ %%

Обмен вычислений на память!

ScaleCpuRam.png

То есть, сохранение и повторное использование чего-нибудь

Кэшировать можно почти всё, что угодно — данные, код, соединения, адреса…

Определение кэширования

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

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

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

Кстати, кэшировать можно не только результаты вычислений. Например, можно кэшировать соединения, или потоки/процессы в операционной системе. И то, и то применяется повсеместно, так как создание потока или соединения — небыстрая операция. Есть даже отдельный класс приложений — connection pooler’ы для СУБД, почтовых серверов и так далее.

В то же время некоторые системы без кэша бы, можно сказать, не работали вообще. Простой пример — это жёсткие диски. Если бы не было кэша, от них было бы очень сложно добиться нормальной скорости даже последовательного чтения/записи, не говоря уже о случайном: на аппаратном уровне диск вынужден всё время писать и читать большими кусками через кэш-буфер — иначе, условно говоря, нужно было бы точно синхронизировать момент чтения/записи следующего сектора с программным обеспечением.

Кэш vs БД @@

База заранее вычисленных данных — не кэш! Ибо:

Например, memcached — кэш, а Redis — БД.

Отличия кэша от БД

Кэш — не база данных. Если вы заранее вычислите все возможные варианты выходных данных, сохраните их и потом просто будете использовать их в готовом виде — это не кэш (хотя иногда это им называют). Кэш отличается от БД тем, что, как правило, строится на лету, динамически, а также тем, что записи в нём непостоянны, и приложение не должно полагаться на их наличие. То есть, при использовании кэширования всегда существует ветвление в коде: «есть сохранённая запись — используем, нет — вычисляем и сохраняем».

Кэш-память, не только очевидно конечна (всё конечно :)), но часто и на порядок/порядки меньше, чем основная — просто потому, что для кэша используется быстрая память, а быстрая память дорога. Поэтому кэш обычно не вмещает все данные приложения (хотя может и вмещать, если приложение маленькое). Поэтому цель системы кэширования в том, чтобы в каждый момент сохранять в кэше наиболее используемые элементы. Если память иссякает, при добавлении элемента обычно определяется элемент или элементы для удаления из кэша и освобождения памяти, с помощью стратегии замещении. Это тоже не всегда так — существуют реализации, которые в этой ситуации просто отказываются добавлять новый элемент. Это не очень хорошо (потенциально снижает эффективность), однако так ведёт себя, например, весьма популярный кэш APC для языка PHP.

Кроме того, из-за требования быстрого доступа к кэшу он почти всегда имеет очень простую структуру: таблица вида ключ-значение, из которой можно быстро читать и в которую можно быстро писать по ключу. И ключ, и значение — произвольные строки, а детали реализации (деревья и прочие структуры данных) обычно не видны вообще.

Примеры кэшей и Key-Value БД

Чтобы можно было использовать memcached как key-value базу данных, люди придумали memcacheDB — патченый memcached без вытеснения элементов. Сейчас memcacheDB уже, правда, не развивается, и уже больше используется Redis, не в последнюю очередь засчёт его продвинутых возможностей.

Где кэшировать данные? @@

Where to cache.svg

Память процесса @@

GC1.png

Больше проблем, чем плюсов ☹. Самая главная:

Такой кэш не масштабируется!

Управляемая память:

☺ Накладные расходы = 0, объект живой
☹ Живые объекты — толстые
☹ Может прийти GC

Неуправляемая/разделяемая (например, так делают Одноклассники)

☹ Те же накладные расходы на сериализацию

Память процесса

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

Однако на деле у такого подхода оказывается больше проблем, чем плюсов. Начать следует с того, что в Некоторых Языках (а именно, в PHP и только в PHP) при обычном использовании, без сильных извращений, сохранить что-то живым — будь то объект или загруженный в память класс — до следующего запроса невозможно: при каждом запросе состояние интерпретатора очищается (да-да, зато писать легко). То есть максимум, что можно (но не нужно) сделать в PHP для кэширования в памяти процесса — это использовать разделяемую память (shared memory). Но так делать не надо, так как работать оно будет хуже, чем банальный memcached, а масштабироваться не сможет.

Собственно, невозможность масштабирования — то есть, невозможность использования кэша на другом сервере/серверах — верна для любого внутрипроцессного кэша, а не только для PHP, и является основным недостатком.

Ещё минусы:

ОК, «живые» объекты не кэшируем. Второй вариант — использовать неуправляемую или разделяемую (с помощью C/C++) память как внешний кэш — для кэширования сериализованных представлений объектов. Теоретически даже можно сделать реализацию, позволяющую размазывать такой кэш по нескольким серверам. Но опять встаёт вопрос — а зачем? Выигрыш в производительности будет невелик, а в остальном это то же самое, что и использование обычного memcached (только писать надо руками).

Внешний кэш @@

Cat shared cache.jpg

Просто и популярно — memcached ± redis

☺ Может быть разделяемый, распределённый

☹ Накладные расходы на сериализацию и сеть

Локально используйте UNIX сокеты
PHP: ставьте igbinary
Java, C++, Python: protobuf от google
Consistent hashing @@ %%

Для быстрого добавления/удаления узлов

ConsistentHashing.png

Внешний кэш

Простое и популярное, хотя и чуть более медленное, решение — внешний кэш. Обычно для этого используется популярное решение — memcached и/или redis. Также существуют разные альтернативные решения — например, Bullet Cache, которые могут быть несколько быстрее, чем memcached (но несильно, поэтому, может, и не нужно морочиться).

Минус такого решения — накладные расходы на сетевое взаимодействие и сериализацию объектов. Посему:

Зато внешний кэш элементарно масштабируется путём выноса на отдельный сервер или сервера.

Для удобства добавления/удаления кэш-серверов можно даже использовать консистентное хеширование, доступное «из коробки» в большинстве клиентских библиотек. Смысл в том, что всё пространство ключей отражается на целые числа, а они представляются в виде кольца и разбиваются на интервалы, соответствующие серверам. После этого при добавлении/удалении серверов расположение меняется не у почти всех элементов, как в обычных хеш-таблицах, а в среднем лишь у 1/N элементов, где N — новое число серверов. Ну а для обеспечения лучшей равномерности этого распределения одному серверу, как правило, отводится не один большой интервал, а много маленьких. Всё это показано на иллюстрации ниже.

ConsistentHashingH.png

@@ %%

OkayFace.svg

А если данные меняются?

Инвалидация кэша @@

⇒ Кэш нужно обновлять (сбрасывать).

Простейшие варианты:

Инвалидация @@ %%

А если у объектов сложные зависимости?

BigGraph.jpg

Что такое инвалидация

Инвалидация — это обновление элементов в кэше с целью обеспечения актуальности используемых данных. Казалось бы, в чём тут проблема? Если нечто закэшировано по известному ключу, приложение может просто отправить запрос на удаление этого элемента из кэша. Это простейший метод инвалидации — по ключу. Нормально работает в простейших случаях, когда зависимостей между элементами либо нет, либо почти нет.

Второй простейший вариант — при любом обновлении делать полный сброс кэша. Опять-таки, если известно, что данные меняются только 1 раз в день, но ВСЕ целиком — это вполне нормальное решение.

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

Не сбрасывать кэш вообще — пояснение

Иногда кэш на самом деле можно и не сбрасывать. Иногда, в кривых системах, это даже происходит неумышленно — разработчики просто не пишут код для сброса кэша. Например, такое встречается во множестве сайтов, построенных на готовых CMS типа 1с битрикса или вордпресса. Кстати, в тему — в плагине WP Super Cache недавно обнаружили уязвимость, позволяющую через этот кэш взломать сервер, навставлять редиректов, изнасиловать женщин и всё такое прочее…

Не сбрасывать кэшированные элементы — это более-менее нормальная стратегия, если элементы данных никогда не изменяются, а только добавляются новые. Кэшировать, соответственно, можно страницы объектов — выборки нельзя. Кроме того, нужно помнить, что кэш всё равно нужно будет сбрасывать при обновлении кода системы.

По времени (TTL) @@

TTL.jpg

☺ Просто, но не оперативно ☹

Кэширование по времени

Если оперативность (мгновенная доступность обновлённой информации) не важна — можно просто задавать определённое время жизни (TTL, Time To Live) для кэшируемых элементов. Отслеживать зависимости тогда не нужно — вы просто знаете, что ваше приложение всегда отдаёт элементы, устаревшие не более, чем на заданное время. Системы кэширования обычно сами поддерживают установку TTL при записи в кэш, но если даже нет — не страшно: TTL можно приписывать к записываемым данным, проверять программно при чтении ключа, и при истечении программно же удалять элемент. В этом случае кэш, правда, должен поддерживать вытеснение, иначе память кончится очень быстро.

Ещё существует такой способ, как микрокэширование — кэширование с TTL, равным, например, 1 секунде или даже меньше. В высоконагруженной среде даже такой способ может серьёзно повысить производительность засчёт снижения конкуренции между параллельными процессами, при практически неизменной оперативности информации. В вебе это применяют обычно на самом внешнем уровне (например, nginx) для кэширования готовых страниц. Однако следует помнить, что это всё ещё кэш, и что бездумное применение (например, кэширование ВСЕХ выборок из базы) может привести к очень весёлым глюкам.

Наиболее гибко — По тегам @@

TagCloud.png

Теги без поддержки тегов @@

Списочный метод:

Версионный метод :

Но: нужен Redis!

Инвалидация по тегам

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

При этом, в принципе, можно сделать, чтобы и сам тег мог зависеть от других тегов — это и будет отражать весь граф зависимостей между объектами.

Теги являются более продвинутым функционалом, чем TTL, и доступны далеко не везде. Однако их тоже можно реализовать поверх существующего кэша, с одной оговоркой — метаданные очень желательно хранить не в самом кэша, а в перманентном key-value хранилище — например, в Redis. Причина всё в том же — записанный в кэш ключ может исчезнуть в любой момент, и если там хранить используемые для сброса метаданные — ключи кэша могут не сброситься, и опять-таки привести к прикольным ошибкам.

Варианта реализации два:

  1. Простой и очевидный — на каждый тег хранить отдельным элементом список ключей, им помеченных, и дополнять его при записи каждого нового элемента в кэш. Имеет следующие проблемы:
  2. На основе версий тегов. С каждым тегом ассоциируется версия — обычно просто целое число. Для примера можно даже взять просто текущее время, но только для примера — реализация на времени ненадёжна, чуть больше грузит систему засчёт постоянных проверок текущего времени и слабо пригодна для разделяемого кэша засчёт разницы в установке системного времени между серверами.
    Смысл следующий — при сбросе тега его номер версии увеличивается, а при чтении каждого элемента текущая версия тега сверяется с сохранённой в данных элемента. Таким образом, сброс происходит мгновенно (O(1)), но зато и при записи, и при чтении нужно узнавать версию каждого тега.

Версионный вариант существует в виде путешествующей по просторам интернета реализации тегов от Дмитрия Котерова, но она, увы, не совсем корректна, так как использует для хранения метаданных сам кэш, а не перманентное хранилище.

Оценка эффективности кэша @@

AraGlushak.jpg

Главное — ВЫИГРЫШ В ПРОИЗВОДИТЕЛЬНОСТИ

Оценка эффективности кэширования

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

Естественно, при снижении процента попаданий эффективность кэша тоже снижается; также это может означать, что кэшируется что-то не то, например, слишком «горячие» (часто меняющиеся) элементы. Однако, % попаданий — не единственный и на самом деле не главный показатель. А кроме того, низкий % попаданий может сигнализировать не об одной конкретной, а о разных проблемах:

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

Кроме того, можно вспомнить о размере кэша и количестве вытеснений:

Ещё один показатель — это среднее число попаданий на элемент, так сказать, «средняя температура по больнице». Смысл следующий — если в среднем после записи в кэш ключ за всё время жизни считывается один раз, по-видимому, это очень редко используемый элемент, и его можно не кэшировать вообще. Этот показатель вычислить сложнее: с момента запуска до момента удаления из кэша первого элемента это просто отношение общего числа попаданий к суммарному количеству элементов; но вот когда элементы начинают удаляться или вытесняться — без дополнительных телодвижений его уже не вычислишь.

Типичные фейлы @@ %%

(Анти-паттерны кэширования)

Слишком мало Слишком много
Пустая миска.jpg Тоша объелся.jpg

Fail № 1 @@ %%

«Положил и точно заберу»

Например, сессии в memcached

Кэш как база

Наиболее простой пример этого неправильного использования — хранение пользовательских сессий в memcached.

Системы кэширования обычно полагаются на то, что кэшируемые элементы не первичны, и считают нормальным удалить из кэша любой элемент в любой момент. С этим связана довольно популярная ошибка использования — расчёт приложения на наличие элемента в кэше в каких-то условиях, даже если это условие заключается, например, в наличии в кэше другого элемента. Например, такая ошибка есть в коде MediaWiki :) авторы сначала сохраняют в кэш отдельными элементами все сообщения локализации, плюс дополнительный элемент с индексом, и думают, что если прочитали индекс, то прочитают и сами сообщения — а это, вообще говоря, совсем не так!

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

Возвращаясь к хранению сессий в memcached, можно сказать, что оно, вероятно, приведёт к тому, что в боевом окружении, где пользователей много, сессии будут постоянно «слетать».

Fail № 2 @@ %%

Кэширование авторизованных страниц

Или одного и того же списка с выбранным элементом

(итог — комбинаторный взрыв)

Комбинаторный взрыв

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

Fail № 3 @@ %%

Аппарат искусственного дыхания

Будет очень грустно его отключать (сбрасывать кэш)

Аппарат искусственного дыхания

Часто вместо того, чтобы хоть как-то оптимизировать работу системы, её просто накрывают кэшированием — например, это относится ко ВСЕМ PHP’шным «коробочным» CMS — в их неэффективности лидирует главный отстой под названием 1С-Битрикс. Так тоже делать не надо, ибо:

  1. Оно просто будет медленнее, даже с кэшем.
  2. При повышении оперативности данных производительность системы будет сильно проседать.
  3. При вынужденном сбросе кэша (например, после обновления кода) система будет «ложиться» от нагрузки полностью.

Справедливости ради можно отметить, что при действительно огромной нагрузке (уровня яндекса? гугла? социальных сетей?) при сбросе кэша лечь может и оптимизированная система. Тогда, если вы действительно уверены, что оптимизировать больше просто нечего — ну что ж, тогда нужно пытаться двигаться в сторону «плавного прогрева» кэша (разрешения сосуществования старого и нового кода, и постепенного ввода в строй серверов с новой версией).

Но сначала — оптимизация работы.

Fail № 4 @@ %%

Cache hit под 100 %, а всё тормозит!

Кэшировали яро, но не то, что надо

Не то кэшируем

См. выше — #Оценка эффективности кэширования.

Заключеньице @@

Клиентское кэширование @@ %%

Http.jpg

HTTP @@

Запрос Ответ

МЕТОД /адрес/?параметры HTTP/1.1
Host: домен.сайта
Заголовок: Значение

Тело запроса (при загрузке файлов)

HTTP/1.1 000=код_статуса Статус Ответа
Content-Type: text/html; charset=UTF-8
Заголовок: Значение

Тело запроса (текст страницы)

Особенности HTTP

HTTP — это простой текстовый протокол для работы в стиле «запрос-ответ» (браузер посылает запрос, сервер отвечает).

Запрос состоит из метода, адреса (URI), заголовков и иногда — тела запроса (обычно при загрузке файлов на сервер). Ответ — из статуса, заголовков и тела. Заголовки — пары вида «Ключ: значение»; различных HTTP-заголовков существует много.

После ответа на запрос по желанию клиента и сервера (управляется заголовками) соединение может не закрываться, а оставаться открытым. Это первый пример кэша — кэш соединений, называемый «Keepalive». Работу он ускоряет весьма прилично, особенно, при использовании HTTPS, так как обмен ключами и проверка сертификатов — относительно нетривиальная операция.

HTTP «почти» не имеют состояния, то есть каждый запрос выполняется независимо от предыдущего, а ресурсы при этом описываются в RESTоподобном виде с помощью адресов (URI). Однако совсем без состояний многое бы не реализовывалось, поэтому некоторая поддержка всё-таки есть, в виде Cookies. Кроме того, существуют «не совсем веб»-приложения, написанные по аналогии с обычными, и использующие HTTP как просто транспортный протокол. Но таких, к счастью, довольно мало — было бы много, так хорошо бы Web не развился.

Блин! Что ещё за кэш? @@

google://php отключить кэш

Пацаны, у меня фаервол @@

VeryNewAlgo.jpg
ChallengeAccepted.svg

Cache-Control: no-cache, no-store, must-revalidate, max-age=0 Pragma: no-cache Vary: * Expires: Thu, 01 Jan 1970 00:00:00 GMT

@@ %%

GodKillsKitten.jpg

HTTP-кэш любят все @@

HTTP-кэш @@

Прокси-сервер…

HTTP-кэш

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

Основа — это заголовки запроса и ответа для управления кэшированием, с помощью которых клиент и сервер сообщают друг другу, как кэшировать ответ и можно ли это делать вообще, и статус ответа 304 («содержимое не менялось»), с помощью которого сервер сообщает клиенту, что тот может использовать предыдущий сохранённый ответ. Об этом мы и поговорим далее.

HTTP-кэш: схема @@ %%

HTTP Caches.svg

Управление HTTP-кэшированием @@

HTTP 1.0: (по времени)

HTTP 1.1: (по времени и значениям)

HTTP-заголовки для управления кэшем

HTTP 1.0

Кэширование появилось ещё в версии протокола HTTP 1.0 (в которой ещё даже не было Keepalive). Там оно имело относительно простой вид (тем не менее, зачастую достаточный) — только по времени последней модификации и сроку годности, плюс была возможность полного запрета кэширования путём отправки заголовка Pragma: no-cache. Эту прагму все, как правило, включают до сих пор, в расчёте на клиентов, поддерживающих только HTTP 1.0 и не поддерживающих 1.1. Полный запрет кэширования на самом деле означает не то, что клиент вообще не может сохранять ответ, а то, что использовать его без повторного запроса на сервер (валидации) нельзя.

Кэширование по времени работает так:

HTTP 1.1

Новая версия протокола вместо заголовка Pragma вводит заголовок Cache-Control (причём и в запрос, и в ответ), который даёт более расширенное управление кэшированием. Кроме того, в HTTP 1.1 к поддержке кэширования по времени добавляется возможность кэширования по значениям. А именно, заголовки ETag, If-None-Match и Vary.

ETag (entity tag, «тег сущности») и If-None-Match работают аналогично Last-Modified и If-Modified-Since, только значения этих заголовков сравниваются не как даты, и проверяется не то, что дата последней модификации не больше If-Modified-Since, а просто как строки, и проверяется точное соответствие сохранённого на клиенте ETag’а (If-None-Match) актуальному. Эту проверку нужно делать на сервере, включая в ETag версии элементов, использованных на странице.

Все кэши в HTTP 1.1 делятся на две категории: «общие» и «личные». Общий кэш — это кэширующий прокси-сервер, который может использоваться многими людьми параллельно и не должен сохранять ответы, содержащие конфиденциальные данные. Личный кэш — это кэш браузера, используемый только одним человеком и, соответственно, пригодный для кэширования таких ответов.

Заголовок Vary служит для того, чтобы сказать прокси-серверу, что по одному и тому же адресу (URL) может отдаваться разная страница в зависимости от заголовков запроса, перечисленных в значении Vary. Например, страница может отличаться в зависимости от языка, запрошенного браузером, тогда можно указать Vary: Accept-Language, и прокси-серверы смогут корректно кэшировать ответ. Специальное значение Vary: * действует как запрет кэширования на прокси-серверах — логика * здесь означает «ответ зависит от дополнительных параметров, не включённых в запрос». Например, от IP-адреса клиента или от фазы луны. Соответственно, сами прокси-серверы заголовок Vary: * добавлять не могут. Однако считается, что любой HTTP 1.1 совместимый сервер-источник должен генерировать заголовок Vary с любым кэшируемым ответом, чтобы помочь работать проксям.

Cache-Control @@

В Cache-Control… @@ %%

WAT Owl.jpg

…Есть странные опции

Странные опции @@

О Cache-Control

Теперь о Cache-Control. Значение заголовка состоит из перечисления директив через запятую.

Основные используемые в ответе директивы:

Однако, кроме этого есть и довольно странные и почти не используемые параметры:

В запросе Cache-Control тоже может использоваться, но веб-приложениями этот заголовок запроса обычно не обрабатывается вообще, а браузеры реально используют только один вариант:

Остальные варианты использования весьма странны:

В общем, в протоколе HTTP есть куча фич для обеспечения функционирования прокси-серверов. Только вот текущие тенденции заключаются в том, что прокси-серверов становится всё меньше — они используются в основном в офисах для фильтрации трафика, и мало кто использует их как средство ускорения работы в сети. А HTTPS (HTTP через SSL-шифрованное соединение) возможность кэширования на прокси-сервере вообще убивает на корню, иначе бы нарушалась секретность и прокси-сервер перехватывал бы страницы; а сайтов, доступных только через HTTPS, становится всё больше и больше. Любопытно также, что в новом протоколе SPDY, созданном для ускорения сетевого взаимодействия HTTP, шифрование вообще обязательно ⇒ с ним прокси вообще ничего кэшировать не может. Хотя безопасность самого HTTPS при этом — вещь достаточно специфичная засчёт того, что сертификаты предоставляются коммерческими компаниями, а эти компании совершенно спокойно продают дочерние корневые сертификаты, с помощью которых можно незаметно перехватить трафик любых сайтов, использующих любые их сертификаты.

Так что возможно, про прокси-кэши и многие фичи HTTP, с ними связанные, в будущем вообще можно будет забыть. :)

Тем не менее, полезное применение у всех этих директив есть — во-первых, для управления кэшем браузера, а во-вторых, с их помощью можно управлять кэшем обратного прокси — вашего личного прокси-сервера, который обычно стоит «перед» серверами приложений и может кэшировать их ответы, что снижает нагрузку на backend’ы.

Long Poll @@

Kotiki.jpg

(как пример кэша соединений)

Задача: твиттер/вконтактик, показывать новых котиков в реальном времени.

При ожидании ответа сервер подвешивает соединение клиента на N секунд.

Заключение @@

Для содержимого достаточно отслеживать даты изменений:

Для статики:

Заключение

Авторами мелких приложений HTTP-кэширование часто рассматривается как нечто вредное и «лишь ведущее к глюкам», и его стараются просто отключить, для чего во все ответы включают полный набор запрещающих заголовков. Получается этакий «вечный Ctrl-Shift-R» (это сочетания клавиш для сброса кэша в Firefox).

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

Кроме того, браузеры, соблюдающие спецификацию HTTP (Firefox, Chrome) при переходе «Назад», если кэширование запрещено, пытаются перезагрузить страницу. Таким образом, заголовки кэширования ещё и помогают пользователю быстрее ходить по истории посещений.

И ещё один важный момент — корректные заголовки для управления кэшем любят поисковики. Заголовки позволяют им корректнее и быстрее индексировать сайт и отслеживать даты изменений материалов. Так что использовать кэширование нужно, хотя бы на основе Last-Modified.

Кэш приложения @@ %%

Gepard.jpg

(основное, на что мы можем повлиять!)

Что кэшировать? @@

Как можно бОльшие куски информации:

страницы (если можно; обычно — нельзя)
→ блоки (побить на них всю страницу; обычно — можно)
→ выборки (только тяжёлые)
→ объекты (только очень тяжёлые)

Приёмы @@ %%

Что делать с макаронами? [плохим кодом]

UdarNogoi.svg

Приёмы кэширования

При кэшировании задача — как можно сильнее сократить объём работы. Следовательно, при реализации нужно стараться кэшировать как можно бОльшие куски информации. В случае веб-приложения сначала надо постараться закэшировать страницу целиком. Если в соответствии с требованиями актуальности в вашем случае так делать можно, а комбинаторного взрыва при этом не происходит — супер, делайте так. Однако такое счастье бывает редко — почти всегда есть как минимум авторизация пользователя, а пользователей много, и это не даёт эффективно кэшировать страницы целиком. Однако всё равно не стоит упускать это из виду — наиболее «горячие» страницы (главную?) можно попробовать, например, отдавать кэшированные, и потом уже на клиенте динамически подставлять в них блок с авторизацией.

MVC @@ %%

Нет понятия «объект»? ⇒ Модель

Не можем кэшировать шаблоны, так как непонятно, где шаблоны? ⇒ View

Stash

При построении страницы сначала выполняется либо берётся из кэша вывод каждого блока, а потом блоки подставляются в layout и, возможно, друг в друга. Проблемой могут оказаться взаимные зависимости («побочные эффекты») выполнения блоков. Простой пример — заголовок страницы. Вставлять его нужно в layout, но определяется он, скорее всего, контроллером основного блока. Для решения нужно разрешить каждому View, кроме просто HTML-кода, выдавать на выходе ещё и небольшой ассоциативный массив — «Stash» («заначку») с данными, предназначенными для других шаблонов и кэшировать их вместе.

Остальные побочные эффекты — жестоко удалить.

Побочные эффекты? @@ %%

⇒ Инкапсулировать их в Stash

Связанные сущности

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

Под такими понимаются объекты, подгружаемые в зависимости от основных, например, любые наборы картинок, атрибутов и т. п. Это вообще довольно интересная тема. Проблема связанных сущностей в том, что какие из них действительно нужны для отображения, обычно знает View, но не знает контроллер. Но при этом основную выборку строит контроллер, а не View. Обращение к связанным сущностям стараются сделать максимально простым — таким же, как просто чтение свойства объекта. В итоге дополнительные данные загружаются в цикле для каждого элемента, а не все разом (что медленно).

Решение проблемы:

  1. Либо перейти от паттернга MVC к паттерну MVP, запретить общение View и модели и заставить контроллер (становящийся Presenter’ом) отдавать во View в точности те данные, которые нужны тому для отрисовки.
  2. Либо сделать автоматическую массовую загрузку связанных сущностей. Каждый объект, полученный как часть какой-то выборки, запоминает ссылку на всю выборку. При необходимости прочитать связанную сущность из любого одиночного объекта он читает и прописывает ссылки на эти же связанные сущности для всех объектов, являющихся частями «родной» выборки. Идея основана на том предположении, что выборки обычно обрабатываются целиком, и идентичным образом.

Всё описанное, разумеется, реализуемо только при наличии модели в целом. Если код представляет собой макароны, в которых получение данных из БД перемешано с их выводом и никак не обёрнуто в объекты, так красиво приёмы не применишь. Придётся либо структурировать код и вводить модель, либо забить и оптимизировать всё взаимодействие каждый раз по месту.

Lambda-Walk по связанным объектам? @@ %%

Либо M-V-Presenter

Либо массовая автозагрузка

Если шаблон читает из БД связанные объекты…

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

Решение — кэшировать основную выборку после view. То есть, при кэш-промахе сначала читать выборку, потом запускать шаблон, а уже потом сохранять дополненную данными выборку в кэш целиком. Когда в следующий раз такая выборка загрузится из кэша, дочитывать из БД уже ничего не будет нужно.

Шаблон читает из БД… @@ %%

…как кэшировать?

⇒ Кэшировать после шаблона

HMVC

Обычно вполне нормальный подход — это разбить страницу на отдельные блоки и кэширование их отдельно. Это можно назвать HMVC, просто потому, что с выделением каждого блока в свой маленький контроллер любой MVC превращается в H (Hierarchical, иерархический). Такое разбиение достаточно естественное, так как структура сайтов обычно блочная сама по себе. Обычный набор блоков — это:

HMVC @@ %%

HMVC.svg CachedSiteBlocks.svg

Иерархический MVC @@

Об HMVC в Kohana

Любой PHP-фреймворк — вещь достаточно кривая и состоящая из бесполезных обёрток чуть менее, чем полностью. Это верно и для Zend, и для Yii, и для всяких Kohana. В первую очередь потому, что PHP изначально веб-ориентированный язык и сам по себе содержит практически весь нужный функционал, и единственное, что остаётся фреймворкам — это заворачивать простые процедурные интерфейсы, предоставленные языком, в чуть менее простые объектно-ориентированные обёртки. В языках, более-менее являющихся языками общего назначения (Python, Java, Perl, Ruby) это не совсем так — там фреймворки обычно как минимум выполняют полезную функцию абстрагирования от реализации веб-сервера.

Но, например, во всём Zend Framework’е я лично могу назвать 1 полезный кусок — это то, чего не хватает PHP’шному SoapServer’у: автоматический генератор WSDL. Если же учесть, что фреймворки, как правило, задают довольно жёсткие рамки для разработки (шаг в сторону — побег, прыжок на месте — попытка улететь) — получается, что они не только не расширяют возможности языка, но и сужают их.

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

Главное преимущество HMVC — это как раз кэшируемость! То есть, тот факт, что при разбиении страницы на блоки каждый блок можно кэшировать отдельно, а потом собирать из них страницу. И то, что при минимальных дополнительных усилиях вдобавок к серверному (на уровне приложения) кэшированию можно легко прикрутить ещё и клиентское (на уровне HTTP). Для этого надо всего лишь вычислять время модификации каждого блока (легко делается по тегам, если отслеживать время сброса каждого тега) и разбить обработку запроса на две стадии:

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

Так что это ещё один аргумент за то, чтобы писать самому и не использовать готовые фреймворки.

На что ещё можно влиять @@

Веб — не низкий уровень, до кэша CPU не спустишься :)

JavaScript:

На что ещё можно влиять при веб-разработке

В теории, при разработке пытаться повлиять можно на всё, вплоть до кэшей процессора (которых у него много — L1/L2/L3, кэш TLB…). При низкоуровневом программировании или, например, программировании сложной математики так и стараются делать — засчёт лучшей кэш-локализации можно получить серьёзный выигрыш в производительности.

Однако, при веб-разработке спуститься до такого уровня оптимизации трудно, а большого выигрыша это не даст. Разве что можно попытаться оптимизировать работу дисков, например, на уровне размера блока файловой системы — это может повысить производительность при раздаче больших объёмов статических файлов.

Кроме этого остаётся лишь:

Кстати, о JavaScript, выполняющемся на клиенте, и его кэше.

Сколько бы ни оптимизировали его реализации авторы браузеров, а фреймворками типа ExtJS и jQuery производительность убить всё равно легко. Как минимум, из-за того, что какие-то действия, которые браузер бы в простом виде закэшировал, при использовании фреймворка закэшировать уже не получится. Например, в прототипной объектной ориентированности многие используют всякие $.extend(), и JS движку, вместо того, чтобы спокойно взять из кэша уже разобранное представление вашего класса с функциями, приходится выполнять все эти extend()'ы и динамически формировать прототипы. Не надо так делать — пишите на «голых» прототипах: просто function Cl() {}, а потом Cl.prototype.fn = function() { ... }.

Аналогично с обработчиками — если они навешиваются кодом после загрузки страницы, да ещё и на jQuery’вские селекторы — кэшировать это браузер не может, код приходится выполнять. А вот если их inline’ить — то есть, писать на странице всякие onchange() и onclick(), браузер спокойно достанет это из кэша, изначально даже не смотря, что там написано.

Аналогично и с просто динамически генерируемыми элементами, особенно, когда их много. Сюда подпадает, например, весь ExtJS, а из примеров помельче сюда попадает WikiEditor для MediaWiki, инициализация которого происходит через заметное время уже после загрузки страницы (что прилично бесит).

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

Кэш СУБД — примеры @@

MySQL:

PostgreSQL:

Резюмируем @@

@@ %%

ThatsAllFolksCut.svg

http://lib.custis.ru/WebAppCaching

vfilippov d0g custis d0t ru
vitalif d0g mail d0t ru


Любые правки этой статьи будут перезаписаны при следующем сеансе репликации. Если у вас есть серьезное замечание по тексту статьи, запишите его в раздел «discussion».

Репликация: База Знаний «Заказных Информ Систем» → «Кэширование в веб-приложениях - что, где, когда»

  1. Алгоритмы кэширования — http://en.wikipedia.org/wiki/Cache_algorithms