Много месяцев у меня ушло на разработку этого модуля — CHI::Cascade. Теперь решил рассказать о нём здесь, в блоге, и о его концепции, чтобы те, кто испытывает в нём потребность, но ещё не знают о его существовании, подумали об его использовании. Итак, кэширование на основе зависимостей.
Концепция make
Вообщем, если вы программер, вам навярняка известна такая полезная утилита, как make. Вкратце — make получает файл с описаниями зависимостей и их командами, и выполняет только те команды shell, которые вычислены «для выполнения» на основе этих зависимостей. Это очень классная концепция, которую можно использовать как в программировании, так и для автоматизации работы в Unix. То есть, эта концепция зависимостей сводит к минимуму исполнение каких либо кодов/команд/скриптов для получения результата. Вообще, идеология «дерева» замечательная вещь, а если к этому приделать зависимости — что от чего зависит и как — то вообще это сила. Если что то мы изменили где-то в дереве, то перестраиваются те узлы, которые как либо зависимы, а остальное не трогается.
Концепция — кэш (cache)
Теперь о второй полезной вещи — кэше. Для сайтов с крупной посещаемостью, перед программером стоит задачи оптимизации. Один из лучших способов оптимизации — что либо кэшировать. Но всё кэширование всегда сводится к простым понятиям: если нет в кеше — мы вычисляем это нечто (возможно, уходят секунды, а могут и минуты), а если есть в кеше — отдаём из него (почти моментально). А чтобы была динамика сайта (информация была свежей), мы также при сохранении в кеш ставим время expires, например пять минут, или час. То есть через пять минут то, что есть в кеше — исчезнет из него автоматически и значит снова будет пересчитано при необходимости. Такой схемой пользуется практически все, но у нее есть изъяны.
Изъяны
Использование кэширования не практике происходит в многопроцессорном окружении. И вот тут могут быть следуюшие проблемы. Например, вы вычисляете что либо скриптами, чтобы выдать www страничку пользователя. На это, скажем требуется, например, 5 секунд. Например, строится дерево категорий из базы, затем сортировка и т.п.. Пока длятся эти 5 сек, на сайт заходит второй пользователь. В кеше ещё нет результата, так как он вычисляется и процесс, обрабатывающий запрос второго пользователя начинает также вычислять всё тоже самое. И вот система трудится над тем же самым, уже работает два ядра процессора, два коннекта к базе и т.п.. А потом заходит третий за эти 5 сек. Всё тоже самое. И вот всё это будет длится, пока кто либо из процессов не закинет первым значение в кеш с этим ключём. Если успеет. А может и получиться ситуация «снежного кома» — всё новые и новые коннекты не только будут отнимать ресурсы, но и тормозить существующие вычисления. И тогда первый запросивший пользователь получит результат не через 5 сек, а уже через 30-40 секунд. Есть простое решение — первый запрос должен засунуть заглушку перед вычислениями, что-то типа страницы «Подождите, запрос обрабатывается», или данные (триггер какой нибудь, например). В таком случае, второй, третий пользователи сразу получат это значение из кеша и аналогичный код для расчёта исполняться уже не будет. Но и тут есть проблемы — а как дальше быть, если периодически данные устаревают. Опять же, когда то вычисленное значение устареет и удалится из кеша. И тогда снова всё повторится — первый запросивший пользователь будет ждать и дождётся новой страницы, а другие в этот момент опять будут получать заглушку. То есть мы будем терять доверие к посетителям. А ведь можно было бы им показать пусть и старые страницы, пока идёт расчёт новых.
А теперь присовокупим к этому то, что некоторые данные могут зависеть то других и как бы смысла вычислять некоторые данные нет, если что-то меняется в другом месте. Например, главная страница сайта может иметь превью, составленное из топовых разделов сайта, и иметь кусочки из каждого подраздела. А сам каждый подраздел иметь свою страницу. И то и другое — динамические фрагменты с данными, которые зависят друг от друга — то есть главная страница зависит от, скажем, топа десяти популярных разделов и их данных внутри (чтобы сделать превью). А если, скажем, ещё сам раздел разбит на страницы, то можно сказать, что раздел А со страницей 1, зависит от всех данных всего раздела А, но «отпиленным» куском только для страницы 1, а страница 2 раздела А также зависит от того самого полного куска раздела А, но только отпиленным куском для страницы 2. То есть, в идеале по правильному, мы можем один раз расчитать содержимое (только данные) раздела А полностью (назовём это «полным полотном»), а потом отдельные страницы расчитывать, обратившись к этим полным данным, но выполнив над ними простую функцию — «распилки». При этом, если каким либо образом изменится «полное полотно» раздела А, нам в идееле надо моментально выдавать пользователям новые страницы 1,2,3 и т.д.. для раздела А. А ещё вот бы учесть помимо зависимостей то, чтобы выдавать старые данные из кеша, если идёт расчёт новых — чтобы свести к минимуму разочарование пользователей.
Не тревиальная задача, не правда ли? Что делать?
Модуль CHI::Cascade
Для подобных почти real time ситуаций, когда есть крупные, и возможно долгие вычисления, я и написал этот модуль. Он объединяет в себе преимущества кэширования, преимущества иерархического дерева зависимостей, в котором каждые узлы — это правила с кодом для вычисления (при необходимости), где нисходящие (childs) узлы автоматически получают вычисленные (точнее актуальные) данные восходящих узлов (parent). Модуль сам контролирует что от чего зависит на основе ваших правил, сам решает, что надо пересчитать, а что взять из кеша, сам подсовывает прозрачно старые данные, если в данный момент происходит пересчёт этой же цели кем либо ещё. Также, как я уже сказал, одни куски кода автоматически получают актуальные данные тех зависимостей, от которых они зависят — вам не надо беспокоится чтобы каким либо образом получить их! Например, код распилки страниц для раздела будет автоматически получать данные «полного полотна» раздела, если вы указали, что он зависит от этой цели.
Также можно, косвенно, использовать зависимости для других задач. Например, если вы определите самый верхний узел, от которого всё зависит, вы можете, так же, как и в make, удалить target метку на него, и тогда все данные, зависимые от него, на любом уровне в дереве зависимостей, будут автоматически пересчитаны в тот момент, когда они понадобятся (когда поступит www запрос, например). При этом сайт будет функционировать, так как даже если что-то не успеет быть пересчитанным — пользователи получат в крайнем случае старые данные, но не надолго. Очень удобно это делать для «ресета» сайта, например.
Неправда ли, здорово?
Где взять?
Если вас заинтересовал мой модуль, я сейчас могу отослать вас к разным источникам, которые есть. Я не в силах сейчас в блоге расписать все детали использования этого модуля. Но многое есть в документации, которую я написал к нему, на английском языке. Где-то мой английский кривой, не обессудьте. Но я думаю, вполне понятен как для технической документации. Я постараюсь периодически писать статейки с примерами для этого модуля.
Скажу ещё от себя. Перед тем как писать этот модуль, я долго проводил разведку на предмет наличия подобного и ничего не нашёл. Поэтому стал его писать. Сейчас этот модуль я использую в своей работе и могу сказать, что сейчас этот код оправдал мои ожидания и работает так как надо мне. Я испытывал его под большим трафиком и благодаря этому выловил очень много багов и проблем. Сейчас он очень стабилен и вылизан (ветка devel). К добавлению новых фич я буду подходить очень аккуратно и взвешено. Лично у меня есть потребность в маленьких дополнениях к нему. Но в общем всё сделано так, как я бы хотел.
Сам модуль доступен через CPAN: http://search.cpan.org/dist/CHI-Cascade/
Его легко можно поставить обычным способом.
Но, на CPAN лежит довольно не свежая версия. У меня есть свежая с улучшениями. Но я их пока держу в статусе devel (разработки), так как не до конца написана документация. Если вас интересует моя последняя версия, то вот ссылки:
https://github.com/Perlover/CHI-Cascade
Но в devel ветке (линк выше) не вся документация на последние изменения. Поэтому, чтобы не смешивать всё, я сделал ветку с последней дкументацией тут:
https://github.com/Perlover/CHI-Cascade/tree/documentation
Как только я допишу документацию и проверю, я соединю это с devel веткой, а потом всё будет опубликовано на CPAN
Если вас принципиально интересует этот модуль, пишите в комментарии здесь. Также, конечно, разные запросы на фичи, баги — лучше слать в раздел «Issues» на github. Но, как я написал выше, новое я буду добавлять очень аккуратно. Большинство возможностей уже сделаны и многое можно сделать с уже существующей версией.
Спасибо что дочитали до конца!
Идея с зависимостями очень плодотворна.
В своё время, когда вёл проект по интернет-магазину для страховой компании, встала задача описывать сложные страховые продукты.
В результате по моему требованию подрядчиком был разработан мета-язык для описания страховых калькуляторов.
Страховой продукт — это дерево с зависимостями, так что — это история про то же самое. Годы 2006-2007-2008.
Мета-язык был компилируемым, с XML-синтаксисом, ant-подобным.
Вобщем-то, тяжело было на нём писать, но зато легко было хранить спецификации на страховые продукты и слать спецификации другим отделам.
Поздравляю вас с модулем CHI::cascade. Оценил, позавидовал. Эту задачу надо было УВИДЕТЬ и СФОРМУЛИРОВАТЬ: это половина работы. У вас хватило терпения и сил и на вторую половину. Поздравляю.
Предполагаю, что вы активно будете развивать эту идею на клиентской стороне, желаю вам удачи и в этом.
Спасибо, Дмитрий! 🙂
Хм… Круто… Спасибо за работу!