Виджет на морде

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

Django-Yandex

Обновленная Яндекс.Афиша работает на Джанге. Вот уже третий месяц как. Запустили мы её прямо в канун пятницы тринадцатого в марте!

Проект получился большой, со своими особенностями. Расскажу вам про процесс разработки с допустимой детальностью.

Глобальная цель была - обновить движок Яндекс.Афиши, переписав его на Django.

Что да как

Распил кода КВИ

Ни для кого не секрет, что сервис "Куда все идут" один из сервисов Яндекса написанных на Джанге. Долгое время он был дополнением к Афише и добавлял социальный фан для пользователей. Ему и было суждено дать начало новой Афише.

Мы резонно решили, что, переписывая Афишу на Джанге, нужно опираться уже на имеющийся code base КВИ.

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

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

Конечно, применение наследования таких комплексных сущностей как джанговские модели не могло оказаться бесплатным. Пришлось очень строго следить за тем чтобы родители и наследники были в строго консистентном состоянии - синхронно создавались и умирали. Но зато мы сократили до минимума необходимость переписывать уже имеющийся код КВИ в местах манипуляции с моделями - всё благодаря абсолютно прозрачно работы с атрибутами наследников и родителей, и ORM. Well done Django team!

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

Могучий импорт

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

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

Удачно интегрировать её в проект помогла возможность писать свои команды для manage.py. Рекомендуем.

Первый камень

В начале сентября была написана первая строчка кода непосредственно в самом проекте Афиша. На базе выделенного обобщенного кода КВИ я начал разработку сервиса, постепенно шаг за шагом копируя имеющийся функционал. Не сказал бы, что сразу пришло понимание как должна выглядеть система. Причем что я приметил - в начале придумывал какие-то варианты архитектуры, потом с удивлением замечал как эти варианты хорошо и логично реализуется Джангой. То ли я уже настолько привык к Джанге, что мыслю уже её паттернами, то ли она сама настолько гибкая, что позволяет реализовать практически неограниченные варианты построения сервиса. Загадка:-)

Состояния

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

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

В последствие я уже понял что по сути это другая реинкарнация шаблонного контекста. Поскольку почти весь шаблонный код изобилует конструкциями вида {{state.get_foo}} или {% if state.has_objects %}, то по сути state выполняет роль объекта-контекста на базе которого строиться страница.

Система живет и пока удовлетворяет потребностям. И из-за своей объектной сути позволяет легко наследовать функциональность и избежать дублирования кода.

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

Кеш

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

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

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

Кластер

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

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

Media

Если масштабировать производительность приложения и базы данных вполне легко и очевидно как, то что делать с медиа файлами?

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

С медиа файлами, а в частности с картинками событий, связана ещё одна интересная особенность Афиши - тамбнейлы. Они генерируются по требованию, достаточно в шаблоне правильно позвать image.url.... По поводу этого интерфейса вызова правильной картинки было много споров, но мой вариант победил и прижился. Ваня предлагал сделать шаблонный тег с параметрами нужного изображения, а мне хотелось попробовать передавать эти параметры как часть имени атрибута, т.е. примерно так {{event.image.thumbnail_100x100}}. Благодаря гибкой подсистеме файлов в Джанге это получилось сделать достаточно легко.

pda

Когда уже проект был почти готов, мы поняли, что младшая сестра большой Афиши - pda'шная Афиша, тоже должна быть нами поддержана и реализована на новом движке. Старую оставлять было невозможно из-за слишком больших архитектурных изменений. Да и замкнуть весь проект на себя было в перспективе удобней.

И тут я ощутил всё прелесть MVC фреймворков и Джанги в частности. Вся задача свелась к тому, чтобы написать одну middleware, в которой определять какую афишу мы смотрим, и подсовывать правильный джанговский шаблон. Заняло это, если я не ошибаюсь, всего 2 рабочих дня у верстальщика. И не пришлось менять не строчек кода в других местах. Отлично!

Взаимодействие.

Так сложилось, что от Афиши в Яндексе зависят ещё достаточно много сервисов, в том числе большая и мобильная Мора, страница результатов поиска и другие даже ещё не выпущенные. Для налаживания взаимодействия нового сервиса с ними потребовалось очень много времени.

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

Слава питону и Джанге, что они мне ни разу не помешали реализовать какую-то идею - правильные инструменты:-)

Ссылки

Поскольку предполагалось наиболее плавное и безболезненное переключение со "старой" Афиши на "новую" и чтобы вся информация за время жизни сервиса разлетевшаяся по просторам Интернета не потеряла свою актуальность, то требовалось иметь возможность поддерживать старые ссылки. Причем пришлось это делать именно на уровне приложения. Не долго думая, прикинув требования, я написал ещё один мини-фреймворк для умных редиректов:-) Это совпало с постом Малькольма о продвинутом redirect_to. Пост я сразу не прочитал, а лишь на следующий день, когда мне Ваня сказал, что мы с Малькольмом удивительно в одну сторону мыслим. Конечно у нас всё было гораздо серьезней, и айдишники старых сущностей надо конвертировать и сами урлы по схеме очень сильно отличались (это, кстати, и не позволило разруливать редиректы на уровне веб-сервера- пришлось опускаться до приложения).

Но в итоге всё решение - один view и urls.py с мапингом старых ссылок на новые. Получилось очень просто и в тоже время покрывало требования с лихвой.

Производительность

Последний этап разработки это оптимизации и тестирование производительности. Ваня, как-то уже упоминал наших танкистов, так вот тут они оттянулись по полной. Поскольку мы получаем трафик с большой морды, то должны выдерживать более солидную нагрузку, чем КВИ. Отсюда и более высокие требования к производительности. Проанализировав логи старого сервиса, и поняв предполагаемую нагрузку, мы стали стрелять. Как оказалось, мы по сути сходу перекрыли требования раза этак в два. Что очень всех порадовало.

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

Жизнь

Немного сумбурное описание получилось. Ещё много чего осталось за кадром и по возможности когда-нибудь я ещё расскажу о каких-то интересных деталях разработки и развития Яндекс Афиши.

А пока, спасибо всем кто работал над проектом - вы отличная команда! Вперед к новым целям!

Комментарии 19

  1. Иван Сагалаев написал:

    Ваня отстаивал позицию, что так или иначе "наследование" можно использовать и в процедурном стиле

    Только не в процедурном, а в функциональном :-). Это принципиальная разница.

    Оставлен 15 Май 2009 в 07:26
  2. Виктор Куряшкин написал:

    Наследование в функциональном стиле, как навешивание параметров-функций в рантайме :-)

    Нет, самый правильный подход такой.

    взять obj.__diсt__

    И - ПЕРЕМЕШАТЬ рандомно.

    ага, далее - первая строчка каждой функции должна содержать:

    return reduce(lambda ......

    На крайняк - return HTTPResponse(reduce(lambda...,map(...)))

    Это - очень важно! Ну и конечно же, та же функция должна быть и генератором.

    Тогда все джависты и сишарпники сожрут - галстук, шляпу и моск.

    Параметры реквеста надо перемешивать еще на уровне middleware, чтоб враги не догадались что же там было!

    В urls.py должны быть регвыры посложнее - это очень важно. Вдруг у вас появится перловик и вы не сможете его обрадовать секретаршей с сиськами или достойной зарплатой?

    Оставлен 15 Май 2009 в 13:58
  3. Иван Сагалаев написал:

    Наследование в функциональном стиле, как навешивание параметров-функций в рантайме :-)

    Нет, самый правильный подход такой.

    Не, все не так :-). На самом деле изначально моя фраза была о том, что наследование в объектном стиле -- не единственный способ реализовывать реюз функциональности. Разумеется, имитировать наследование как таковое -- это довольно криво.

    Про то, как сейчас устроен уровень views в Я.Афише, можно целую большую статью написать про то почему оно такое, какое есть, как можно по-другому, и будет ли от этого лучше. Но на большую статью я лично не соберусь :-).

    Оставлен 15 Май 2009 в 15:18
  4. Виктор Куряшкин написал:

    А какой там реальный трафик?

    Оставлен 15 Май 2009 в 16:46
  5. Иван Сагалаев написал:

    А какой там реальный трафик?

    В целом -- NDA :-).

    Можем только сказать, что после размазывания по кластеру "крейсерская" нагрузка получается в районе 7-8 RPS на машину. Это и внешний, и внутренний трафик. И это совсем далеко от обстрельных нагрузок, где сервис держал порядка 100 RPS на одном хосте.

    Оставлен 15 Май 2009 в 16:59
  6. Виктор Куряшкин написал:

    А какой фронтэнд? Что-то типа nginx?

    Оставлен 15 Май 2009 в 17:03
  7. Александр Кошелев написал:

    А какой фронтэнд? Что-то типа nginx?

    Посмотрите Ванин пост

    Оставлен 15 Май 2009 в 17:09
  8. Виктор Куряшкин написал:

    А, ну вообщем-то почти то же самое. Тем паче - в Рамблере любят nginx, Yandex должен выбрать другое )))

    Кстати, а вот более интересный вопрос. Вы как-то шардили базу?

    Django ORM - он как-то не очень такие вещи предполагает.

    Оставлен 15 Май 2009 в 17:17
  9. Александр Кошелев написал:

    Тем паче - в Рамблере любят nginx, Yandex должен выбрать другое )))

    Ну мы не этим, к счастью, руководствуемся;-)

    Кстати, а вот более интересный вопрос. Вы как-то шардили базу?

    Мы базу размазываем с помощью mysql_replicated. Легко и надежно.

    Оставлен 15 Май 2009 в 17:45
  10. Виктор Куряшкин написал:

    О!

    Спасибо, очень полезная штукенция, не знал о такой. Ваше изделие?

    Оставлен 15 Май 2009 в 18:07
  11. Sergey Kishchenko написал:

    Поздравляю! И спасибо за интересный пост :)

    Оставлен 16 Май 2009 в 02:31
  12. Moroz написал:

    Везде обошлись стандартным ОРМ ?

    Оставлен 17 Май 2009 в 02:00
  13. Александр Кошелев написал:

    Везде обошлись стандартным ОРМ ?

    Да. Только ORM. В паре мест чуть-чуть extra и голова на плечах:-)

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

    Оставлен 17 Май 2009 в 12:27
  14. oduvan написал:

    Спасибо. Получилось довольно вдохновляющая статья.

    "вьюхи Афиши это объекты, классов-состояний, которые инстанцируются в момент поступления запроса, делают свои дела и умирают" а можно про это по подробней? И как это реализовано?

    А почему при кешировании вас не устраивала стандартная система таймаутов?

    А что такое "dogpile эффект", а то гугл только на вас и показал. :)

    А что такое PDA и декларативный DSL?

    Оставлен 26 Май 2009 в 17:12
  15. Александр Кошелев написал:

    а можно про это по подробней? И как это реализовано?

    Ой, для этого нужно отдельный пост писать. Если соберусь, то расскажу по-подробней.

    А почему при кешировании вас не устраивала стандартная система таймаутов?

    Специфика юзерского интерфейса - кое-какие элементы протухают в определенное время а не через какое-то время.

    А что такое "dogpile эффект", а то гугл только на вас и показал. :)

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

    А что такое PDA и декларативный DSL?

    PDA (Personal Digital Assistant) - это мобильные устройства, для них у нас своя версия сервиса.

    Ну а DSL это Domain-specific Language. Вот стиль определения джанговских моделей и форм это пример декларативного DSL.

    Оставлен 26 Май 2009 в 23:48
  16. oduvan написал:

    Ой, для этого нужно отдельный пост писать. Если соберусь, то расскажу по-подробней.

    Будем ждать. Заинтриговали вы :)

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

    Хм. Для этого достаточно чтоб они ждали друг друга. Реализовать чтото типа отдельно сокет сервера, и при потере данных из кеша, стучатся в него за новыми. Но я пока не вижу проблемы в том, что несколько процессов одновременно заполнят хеш?

    Оставлен 27 Май 2009 в 01:48
  17. Ринат написал:

    До чего может довести dogpile - можете почитать на softwaremaniacs.org (не помню точно пост), критично это только для высоко нагруженных серверов.

    Для себя я решил эту проблему через CACHE_BACKEND указав свой механизм, отнаследованный от memcached.CacheClass, где реализовал dogpile обработку, http://dklab.ru/chicken/nablas/47.html и парочку своих идей.

    Оставлен 28 Май 2009 в 22:41
  18. Александр Кошелев написал:

    Для себя я решил эту проблему через CACHE_BACKEND указав свой механизм, отнаследованный от memcached.CacheClass, где реализовал dogpile обработку

    Примерно так и я сделал - в виде миксина, который потом к любому бекэнду цепляется. Получается прозрачно для клиентского кода и решает проблему.

    Оставлен 28 Май 2009 в 22:54
  19. adw0rd написал:

    Пост я разу не прочитал

    "сразу"?

    Оставлен 02 Сентябрь 2009 в 15:17