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

Материал из YourcmcWiki
Версия от 23:30, 9 ноября 2014; VitaliyFilippov (обсуждение | вклад)

(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск

Содержание

Аннотация

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

А именно:

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

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

JetSnail.svg

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

Кто я? @@

Vitalif.jpg

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

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

Поддерживаю сборку MediaWiki-notext.svg MediaWiki: Wiki4intranet-logo.svg Mediawiki4Intranet

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

  • Кодинг лет с 11 :)
  • Linux’оид ± веб-разработчик то ли с 16, то ли с 17, [сайтиков понаписал]
  • Языки: в основном PHP, Perl, Python
  • Win&Mac must die! Only GPL! Only Free Software! Only hardcore :)

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

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

  • Веб-приложения
  • Кэширование
    • Где? Внешний кэш vs память процесса
    • Как надо? Инвалидация
    • Оценка эффективности
    • Как НЕ надо? Фейлы
  • Client-side кэширование (HTTP)
  • Приёмы server-side кэширования
  • Дополнительные меры

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

Clouds.svg

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

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

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

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

На сервере:

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

LAMP.svg

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

Webapp-nocache.svg

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

Webapp-withcache.svg

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

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

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

  • Приложение сетевое, клиент-серверное. Сервер отдаёт и обрабатывает данные, клиент отображает. В большей или меньшей степени это разделение существует всегда.
  • Клиентами обычно выступают браузеры, а протоколы взаимодействия и форматы отображаемых данных весьма неплохо стандартизированы организацией W3C (World Wide Web Consortium), что позволяет работать с вебом с разных платформ и устройств.
  • Используемые протоколы — TCP/IP на сетевом уровне; HTTP поверх него для собственно передачи страниц; TLS/SSL для зашифрованных соединений; SOAP поверх HTTP для веб-сервисов.
  • Основные используемые на клиентской стороне форматы — HTML, CSS, JavaScript (ECMAScript); JPEG, PNG и GIF для изображений; SVG для векторных изображений. Различных стилевых элементов на страницах, как правило, используется довольно много. Есть и другие поддерживаемые браузерами форматы, например, XSLT, но они используются реже.

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

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

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

  • На уровне аппаратных компонентов вашего компьютера.
  • На уровне вашей ОС.
  • На уровне HTTP: в нём есть поддержка кэширования ресурсов. Кэшируют их и браузеры, и прокси-серверы, и обратные прокси.

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

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

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

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

JetSnail-grayed.svg

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

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

ScaleCpuRam.png

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

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

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

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

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

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

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

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

Кэш vs БД @@

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

  • Кэш обычно строится на лету, динамически.
  • Записи в кэше обычно непостоянны.
    (с этим связан Fail №1 — использование кэша как БД)
  • Обычно есть вытеснение
    LRU / FIFO / прочие[1]

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

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

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

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

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

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

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

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

Where to cache.svg
  • Внешний кэш + сериализация
    PHP — без вариантов, только внешний
  • Память процесса
    Управляемая? Неуправляемая?
  • Распределённый кэш

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

GC1.png

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

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

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

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

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

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

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

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

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

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

Ещё минусы:

  • «Живые» инициализированные объекты, как правило, весят больше, чем их сериализованное представление — следовательно, при использовании кэша внутри процесса в том же объёме памяти можно сохранить меньшее число объектов.
  • При хранении большого числа мелких объектов в управляемой памяти в какой-то момент может «прийти сборщик мусора» (GC, Garbage Collector). То есть, например, в Java может запуститься не «быстрый» инкрементальный алгоритм сборки мусора, а «медленный» и останавливающий выполнение. А при большом числе мелких объектов сборка мусора и/или дефрагментация кучи может занять приличное время, в течение которого ваше приложение отвечать не будет.
  • Кэшировать «живые» объекты в разделяемой памяти можно вообще только в неуправляемых языках (C/C++) — в управляемых и скриптовых отсутствует способ поместить объект языка в «отдельную» кучу в разделяемой памяти. Соответственно, закэшированные «живые» объекты будут дублироваться в каждом процессе — тоже плохо. Либо придётся использовать многопоточное однопроцессное приложение — но с многопоточностью в условиях сильного параллелизма легко забраться в очень глубокий лес с очень толстыми партизанами.

ОК, «живые» объекты не кэшируем. Второй вариант — использовать неуправляемую или разделяемую (с помощью 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 (но несильно, поэтому, может, и не нужно морочиться).

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

  • Если сервер у вас один — используйте локальные UNIX сокеты вместо TCP на 127.0.0.1.
  • Ставьте правильный (более быстрый и более компактный) сериализатор — igbinary для PHP; для C++ и Java можно использовать protobuf от google.

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

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

ConsistentHashingH.png

@@ %%

OkayFace.svg

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

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

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

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

  • Не сбрасывать вообще (только добавление)
  • Сбрасывать по ключу (если нет зависимостей)
  • Сбрасывать ВСЁ (если меняется всё 1 раз в день)

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

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

BigGraph.jpg

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

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

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

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

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

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

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

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

TTL.jpg

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

  • Удаление через заданное время жизни (TTL)
  • TTL без поддержки TTL: при чтении сверять срок годности
  • Микрокэширование: TTL = 1 секунда

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

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

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

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

TagCloud.png
  • Тег = зависимость
  • На элементы кэша ставятся теги
  • Сброс всех ключей по тегу = обновление зависимости
  • Иерархия: если тег сам зависит от других

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

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

  • Хранить списки ключей для каждого тега
  • Медленный сброс

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

  • Сброс = инкремент версии тега
  • При чтении сверяем версию

Но: нужен Redis!

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

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

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

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

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

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

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

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

AraGlushak.jpg

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

  • Профилирование С кэшем и БЕЗ кэша
  • Hit/Miss (попадания/промахи)
    Низкие hit: горячие/взрыв/дублирование/размер
  • Размер кэша, количество вытеснений

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

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

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

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

В целом, главный показатель эффективности кэширования — это просто выигрыш в производительности, который оно даёт. Его можно выяснить путём профилирования или мониторинга приложения с использованием кэша и без, желательно — под нагрузкой. Мониторинг можно делать даже на боевых серверах с помощью легковесных инструментов — например, для 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 @@

  • Простой текстовый протокол
  • Почти Stateless (почти REST)
  • Keepalive — кэш соединений
Запрос Ответ

МЕТОД /адрес/?параметры 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 отключить кэш

  • Как отключить кэширование на PHP — Создание и Продвижение Сайтов
  • Запрет кэширования посредством PHP — Справочник веб-языков
  • Записки программиста PHP — Как отключить кэширование страниц
  • 100%-ное отключение кэширования — Форум програмистов
  • How to Remove Cache in PHP | eHow

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

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-кэш любят все @@

  • браузеры — быстрее открывают страницу (повторно, «Назад»)
  • поисковики — быстрее индексируют
  • прокси — лучше работают
  • А нагрузка — снижается…
    Защита от умника с кнопкой F5

HTTP-кэш @@

Прокси-сервер…
  • Браузеры/прокси могут сохранять HTTP-ответы
  • Есть статус ответа HTTP 304 Not Modified
  • Есть заголовки для управления кэшированием
    Причём частично в довольно диких комбинациях
  • Куча костылей для проксей

HTTP-кэш

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

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

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

HTTP Caches.svg

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

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

  • Last-Modified, If-Modified-Since
  • Expires, Pragma: no-cache

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

  • ETag, If-None-Match
  • Vary
  • Cache-Control

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

HTTP 1.0

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

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

  • С нормальным ответом сервер отдаёт дату последней модификации (Last-Modified: дата в RFC822) и/или дату, до которой ответ можно смело кэшировать (Expires: дата в RFC822)
  • При повторном запросе того же ресурса клиент смотрит на Expires и Pragma: no-cache. Если срок действия не истёк, а кэширование разрешено, клиент использует сохранённый ответ.
  • Если срок действия истёк — клиент отправляет серверу обычный запрос, одновременно передавая значение, бывшее в поле Last-Modified, в заголовке If-Modified-Since («если менялось с тех пор»).
  • Сервер может ответить либо обычным ответом со статусом 200 OK, либо ответом, означающим «возьми из кэша» со статусом 304 Not Modified и пустым телом. Вместе с 304 сервер опять может передать дату последнего изменения в заголовке Date — это будет иметь тот же эффект, что Last-Modified.

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 @@

  • no-cache (запрет кэширования)
  • must-revalidate (костыль)
  • private, public (личное/публичное)
  • срок кэширования: max-age, s-maxage (для прокси)
    Warning icon.svg (осторожно!)

В Cache-Control… @@ %%

WAT Owl.jpg

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

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

  • no-transform
    OperaTurbo.png
  • no-store (отвернись и не смотри)
  • proxy-revalidate, community=…
  • в запросе: max-age, only-if-cached, min-fresh, max-stale

О Cache-Control

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

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

  • no-cache: ответ вообще нельзя использовать без валидации (повторного запроса на сервер).
  • must-revalidate: невалидные ответы (с истекшим сроком действия) использовать нельзя, их нужно сначала валидировать (без must-revalidate это может быть не всегда так).
  • max-age=X: срок годности ответа в секундах. max-age=0 — очевидно, запрет кэширования.
  • s-maxage=X: он же, но действует только на кэш прокси (типа, «shared-maxage»).
  • private: ответ содержит личные данные, кэшировать его на прокси-серверах нельзя.
  • public: наоборот, ответ можно кэшировать на прокси-серверах. Естественно, с учётом всех правил валидации.

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

  • no-transform: прокси-серверу запрещено преобразовывать элементы (например, нельзя пережимать BMP в JPEG). А никто так и не делает, кроме Opera Turbo. Но это же опера, поэтому стандарт она не соблюдает и на no-transform всё равно кладёт.
  • no-store: ответ содержит «совсем личные» данные и прокси-сервер должен «отвернуться и не смотреть», как можно быстрее удалив ответ из оперативной памяти :).
  • proxy-revalidate: то же, что must-revalidate, но действует только на прокси. Зачем оно надо — непонятно.
  • community=XXX: нечто вроде Vary, дополнительное расширение, позволяющее разбить ответы по одному и тому же адресу на «группы» и давать соответствующий ответ, если клиент запросит конкретную группу. Это лучше вообще не использовать, для этих целей лучше подходит хотя бы Vary: Cookie.

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

  • max-age=0, что означает «эй, прокси-серверы, обновите эту страницу все, и отдайте её мне свежую».

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

  • max-age=X: нужно дать ответ, устаревший не более чем на X секунд. При этом нельзя давать ответ, устаревший в соответствии с его заголовками.
  • only-if-cached: тоже чудо. Прокси-сервер должен дать ответ, только если он уже сохранён у него локально, и не дай бог не должен лезть в интернет. Тоже странная идея: типа, до прокси канал быстрый, а от прокси до целевого ресурса — очень медленный, и клиент ждать, пока прокси его обновит, совсем-совсем не хочет.
  • min-fresh=X: клиент хочет увидеть ответ, который будет валиден не менее, чем ещё X секунд.
  • max-stale=X: клиент разрешает прокси-серверу отдать устаревший в соответствии с заголовками кэширования ответ, но не более, чем на X секунд (можно использовать вместе с max-age).

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

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

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

Long Poll @@

Kotiki.jpg

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

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

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

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

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

  • no-cache + Last-Modified: ...
  • 304 Not Modified + Date: ...

Для статики:

  • Большой max-age

Заключение

Авторами мелких приложений 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, иерархический). Такое разбиение достаточно естественное, так как структура сайтов обычно блочная сама по себе. Обычный набор блоков — это:

  • Layout — обрамление страницы, заголовок и подвал. Может делать «подзапросы» к вспомогательным блокам.
  • Основной блок — обычно один. Бывает больше, но тогда они обычно отображаются друг под другом. Может делать «подзапросы» к вспомогательным блокам.
  • Вспомогательные блоки с различными относительно мелкими информационными «вставками».

HMVC @@ %%

HMVC.svg CachedSiteBlocks.svg

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

  • Блочная структура естественна
  • Удобно кэшировать!
  • Юзают авторы поделия под названием Kohana Framework Thumbs down.svg
  • Однако они о кэшировании НЕ ЗНАЮТ! :D и поэтому его там правильного нет

Об 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 не спустишься :)

  • Кэш ЯП — заюзать (PHP: APC/XCache, остальные: предзагрузка)
  • Правильный сериализатор — поставить
  • Кэш СУБД

JavaScript:

  • Поменьше фреймворков Thumbs down.svg
  • Писать ручками в прототипах :)
  • Обработчики inline

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

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

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

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

  • Настроить использование кэша языка программирования. Исходя из того факта, что загрузка кода обычно занимает какое-то время, а в скриптовых языках время занимает ещё и компиляция скрипта в промежуточное представление, лучше эти однообразные действия выполнять пореже. В PHP для этого служат Opcode Cacher’ы — APC, XCache и аналоги, а в прочих языках нужно просто загрузить все используемые модули сразу и не выгружать их долгое время (иногда перезапуск рабочего процесса всё-таки оказывается необходим по причине утечек памяти).
  • Использовать быстрый сериализатор (igbinary для PHP).
  • Настроить кэш СУБД — об этом далее.

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

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

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

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

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

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

MySQL:

  • Запросы: query-cache-limit, query-cache-size
  • Таблицы: table-open-cache, table-definition-cache
  • Дисковый: innodb-buffer-pool-size
  • Бинлог: binlog-cache-size
  • Потоки: thread-cache-size

PostgreSQL:

  • Дисковый: shared_buffers
  • Остальное (планы, таблицы): work_mem
  • Размер кэша ОС: effective_cache_size

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

  • Стараться кэшировать целые страницы
  • HMVC
  • memcached, redis
  • Теги, Last-Modified
  • PHP: обязательны APC/XCache, igbinary
  • Тюнить кэш СУБД
  • Поменьше фреймворков, побольше разума

@@ %%

ThatsAllFolksCut.svg

http://yourcmc.ru/wiki/WebAppCaching

vfilippov d0g custis d0t ru

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