Русский
Русский
English
Статистика
Реклама

Lerna

Организация кодовой базы и тестирования в монорепозиторий

09.10.2020 16:18:18 | Автор: admin

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



Текущее положение дел


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



Главная страница Авито



Страница поисковой выдачи



Страница объявления


Разработка этих страниц вызывала определённые проблемы:


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

Кроме того, главная страница, страницы объявлений и страницы споисковой выдачей позволяют создать непрерывный сценарий покупки дляпользователя. Поэтомув своё время мы приняли решение переписать их слегаси-технологии Twig наReact, чтобы вбудущем перейти наSPA-приложение, а также оптимизировать процесс загрузки компонентов длякаждой изстраниц. Так, кпримеру, сниппет объявления наглавной и странице поиска одинаковый. Тогда зачем грузить его два раза?


В связи сэтим появился npm-пакет, который мы гордо назвали single-page. Задача этого пакета содержать внутри себя верхнеуровневое описание каждой изстраниц. Это описание того, изкаких React-компонентов состоит страница, и вкаком порядке и месте они должны быть расположены. Также пакет призван управлять загрузкой нужных и выгрузкой уже ненужных компонентов, которые представляют собой небольшие React+Redux приложения, вынесенные вотдельные npm-пакеты.


Например, припереходе настраницу срезультатами поиска, пользователю необходимы поисковые фильтры, которые позволят сузить выдачу. Вданном случае single-page определяет, что необходимо запросить пакет сфильтрами и делает соответствующий запрос. Если пользователь уже перешёл настраницу объявления, то поисковые фильтры ему не нужны, а значит и сам пакет можно не использовать, чтобы он не ухудшал производительность страницы.


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


Расскажу, как именно происходила эволюция работы срепозиториями npm-пакетов, и кчему мы пришли.


Отдельные репозитории для npm-пакетов


Как я упоминал ранее, изначально главная страница и страница споисковой выдачей были написаны сиспользованием шаблонизатора Twig. Был один большой шаблон страницы, который импортировал всебя более мелкие шаблоны отдельных компонентов страницы.


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


Стало понятно, что необходимо переходить нановый стек React. Переход проходил поэтапно. Сначала вюните договорились, что все новые компоненты будут написаны сразу наReact-е и будут рендериться настраницу послееё загрузки. Эти компоненты слабо влияли наSEO, поэтому подход имел право нажизнь. Пример такого компонента блок спросмотренными и избранными объявлениями наглавной странице:



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



Сниппет вблоке просмотренных объявлений, который был перенесён сразу нановый стек


А вот как выглядят сниппеты вразличных категориях:



Сниппет вкатегории бытовой электронике



Сниппет вкатегории работа



Сниппет вкатегории недвижимость



Сниппет вкатегории автомобили


Вынос вnpm-пакет это только часть работы. Длятого, чтобы пакет был полезен, он должен быть использован восновной кодовой базе. Поскольку все компоненты это отдельные пакеты, то вмонолите (основной кодовой базе) необходимо подключить их как зависимости, а далее проинициализировать эти пакеты вкоде. Так как блок просмотренных объявлений и сниппет были выделены вотдельные npm-пакеты, то вкодовой базе монолита они были подключены как зависимости. Чтобы увидеть изменения, которые появились сновой версией пакета, достаточно поднять его версию безвнесения каких-либо дополнительных изменений.


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


  • поднять его версию;
  • опубликовать её;
  • подключить новую версию вкомпоненте правого блока;
  • поднять его версию;
  • поднять версию вмонолите.

Это может звучать сложно, но наделе процесс занимал минуты две и не требовал особых навыков.


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


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


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


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


Что получилось наэтом этапе:


  1. Количество связанных пакетов выросло.
  2. Каждый пакет жил вотдельном репозитории.
  3. Вкаждый репозиторий создавался пул-реквест свнесением изменений, связанных споднятием версий зависимости.
  4. Ситуация, когда пакет не имел последнюю актуальную версию зависимости была нормой. Плюс, кэтому моменту уже создавался пакет single-page, который взависимостях имел все ранее созданные пакеты.

Всё это привело к тому, что мы начали искать пути дляулучшения работы срепозиториями и всеми нашими пакетами.


Переход к монорепозиторию


Поиск инструментов привёл нас кLerna. Lerna опенсорсный инструмент дляработы смонорепозиториями. Он позволил бы нам хранить все пакеты вмонорепозитории, и забрал бы часть работы посинхронизации версий насебя. Тогда мы смогли бы не заботиться отом, чтобы поднимать все версии пакетов руками, а главное иметь прозрачную кодовую базу совсеми зависимостями.


После небольшого эксперимента и ресёрча, стало понятно, что Lerna поможет решить часть наших проблем:


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

Мы завели отдельный репозиторий, вкоторый начали переносить все пакеты и связывать их зависимостями черезLerna.


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


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


  • врепозитории обновлялись лишние пакеты;
  • восновной кодовой базе монолита поднято меньше зависимостей, чем должно быть;
  • или наоборот зависимостей поднято больше, чем должно быть.

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


Возникшие проблемы мы решили черезавтоматизацию. Дляэтого написали специальный скрипт, который:


  • находил все зависимые пакеты;
  • поднимал вних dev или latest версию взависимости отпотребности;
  • добавлял changelog всем пакетам;
  • заменял новые версии вpackage.json файле монолита нановые и, вслучае dev пакетов, ещё и публиковал их.

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


Работа CI


Остаётся ещё один вопрос, который хочется подсветить: CI.


Так как репозиторий один, то и все тесты гоняются нанём сразу. Кроме того, появился внутренний инструмент, который позволяет писать компонентные тесты сиспользованием библиотеки Enzyme. Его написали ребята изплатформенной команды Авито и рассказали онём вдокладе Жесть дляJest. Это позволило покрыть сценарии, которые раньше нельзя было проверить. Так, например, появилась возможность проверить вызов попапа приклике на кнопку все характеристики впакете стехническими характеристиками дляавтомобилей.


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


Итоги и планы


Задва года мы прошли процесс перехода отбольшого количества слабо связанных репозиториев доавтоматизации работы смонорепозиторием черезLerna. Приэтом, есть гарантия того, что всё будет работать корректно благодаря прогону тестов накаждом пул-реквесте. Количество самих пул-реквестов уменьшилось с8-9 вхудшем случае додвух: вмонолит и вмонорепозиторий. Приэтом пул-реквест вмонорепозиторий содержит сразу все изменения.


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

Подробнее..

Lerna CI ? Или как не запутаться в трёх соснах

02.01.2021 22:16:45 | Автор: admin

Вместо предисловия

Доброго времени суток! Меня зовут Сергей, и я тимлид в компании Медпоинт24-Лаб. Я занимаюсь разработкой на nodejs чуть больше полутора лет - до этого был C#, ну а ещё до того, всякое разное и не очень серьёзно. Ну то есть, опыта у меня не так чтобы вагон, и иногда приходится серьёзно поломать голову при решении возникающих проблем. Решив такую, всегда хочется поделиться находками с товарищами по команде.

И вот несколько дней назад, они посоветовали мне завести блог... а я подумал, может тогда просто написать на Хабр?

Возможно, примеры практических ситуаций, из которых со скрипом вылезает толковый, но не очень опытный разработчик, будут интересны таким же толковым и неопытным )) А может и ещё кому пригодится.

Рассказывать постараюсь без погружения в теорию, но со ссылками на оную.

О чём пойдёт речь?

Пилот будет посвящён интересной проблеме с которой мы столкнулись при попытке организовать CI/CD для монорепозитория с lerna. Сразу скажу, что этот пост:

  • не про монорепозитории. Плюсы и минусы монорепы, как концепции, уже давно описаны в множестве постов, в том числе на хабре (этот довольно холиварный, кстати)

  • не про инструменты для управления монорепозиториями. Монорепу можно реализовать при помощи Nx, rush, даже просто yarn workspaces. Но так получилось, что мы выбрали lerna и поживём с ней какое то время.

  • не про пакетные менеджеры. Могу порекомендовать хороший видос со сравнением npm, yarn и pnpm и офигенную серию постов в которой работа c npm объясняется с самых азов и очень тщательно. А у нас npm (пока)...

  • не про nestjs. Но он классный!

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

Тогда о чём?

Дано:

Имеется маленький монорепозиторий, в котором лежит сервер на несте и npm-пакет, который содержит всё необходимое для клиентского приложения, которое будет этот сервер вызывать.

packages+-- @contract|+-- src|+-- package.json|   ...|+-- application|   +-- src|   +-- package.json|   ...|+-- package.json+-- lerna.json...
Зачем пакет?

Клиентский пакет или, как мы его называем, "контракт" нужен в первую очередь для обеспечения проверки типов в клиентском коде и ускорении разработки.

То есть, вызывающий код делает не просто axios.post(....) передавая туда никак не проверяемые статически параметры (any), а вызывает метод с типизированным входом и выходом.

import { Client } from '@contract/some-service';const client = new Client(options);const filters: StronglyTypedObject = ...const data = await client.getSomeData(filters)/** И результат у нас тоже типизированный.* А ещё из getSomeData() вылетают типизированные ошибки известного формата,* чем в нас обычно кидается, axios.*/

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

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

const query = new SomeQuery({ ... });const data = await client.call(query);/** Теперь клиент у нас общий на все сервисы, а вот запросы и команды -* волшебные и сами знают, что делать. Это мы используем с rabbitMQ.*/

Такой подход мы используем не только для http-сервисов, но и для сервисов, которые обрабатывают сообщения из RabbitMQ, а также при помощи одного из самописных средств транспорта на основе redis. Но усложнять статью этими деталями не будем.

Так вот, у нас есть монорепа, что она нам даёт? В первую очередь, удобство разработки. Базовая фича для всех релевантных инструментов - это то, что в lerna называется bootstrap.

lerna bootstrap --hoist

Флаг --hoist - самая приятная часть. Он говорит лерне, что все зависимости, если можно, надо ставить в node_modules в корне проекта. Мы на этом экономим место + получаем ещё бонус, который нам пригодится дальше.

Помимо установки пакетов lerna bootstrap создаёт симлинки на пакеты имеющиеся в репозитории. То есть, хотя в application/package.json указано

"dependencies": {"@contract/core": "^1.0.0"}

в действительности, этот пакет не будет установлен из npm-реестра, а просто прилинкуется в node_modules из папки packages. Таким образом, мы его меняем, собираем и сразу используем новую версию в нашем приложении.

Задача

Мы строим систему CI/CD. И нам нужно научиться красиво вписать наш монорепозиторий в конвеер. Казалось бы, задача должна была уже 1000 раз быть решена - настолько она очевидна.

И действительно, есть куча issues на github, посты на Stackoverflow и др. ресурсах. Но нет рецептов.. костыли есть, и то не все рабочие, а нормального "штатного" решения я не нашёл (искал, чесслово).

Так вот, когда наш сервис готов к релизу:

  1. Мы хотим смержить PR и, таким образом, запустить пайплайн.

  2. Мы хотим собрать проект, прогнать линтер, unit-тесты.

  3. Мы хотим поднять версию приложения и пакета (а в чём не было изменений - там не поднимать).

  4. Мы хотим опубликовать пакет @contract в npm registry (в нашем случае, приватном).

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

  6. Ну и мы хотим, чтобы наш артефакт был разумного размера и содержал только то, что ему реально нужно для работы. node_modules по ГБ - не совсем то, что нам нужно.

Поехали!

Первый пункт берут на себя CI/CD системы.

Со вторым проблем быть не должно:

Для третьего lerna нам предлагает две прекрасные команды: lerna version и lerna publish (последняя включает в себя первую и мы будем использовать её). Примерно так:

lerna publish --conventional-commits --yes# На заметку: команда publish принимает все флаги команды version.# В доке это есть, но я с первого раза пропустил.
Чуть подробнее про conventional commits.

Если делать команду lerna publish без указанных ключей, то поднятие версии будет интерактивным,что для CIонвейра не годится. На помощь приходит спецификация Conventional Commits. Соблюдение этого простого и понятного соглашения по структуре commit-сообщений, позволяет lerna автоматически определить, какую как правильно по semver поднять (минор, мажор или патч). Самое милое, что мы можем вынудить наших разработчиков писать правильные коммиты (что само по себе хорошо)! Вот достаточно подробная инструкция.

С пунктом 4 у нас тоже нет проблем.lerna publish нам это уже сделала, а если нас это почему-то не устраивает (ну, к примеру, мы не хотим ставить теги или ещё что), то используем lerna version в сочетании с npm publish из директории пакета. Забавно, что npm publish не имеет ключа --registry, чтобы указать, куда пушить пакет. В случае lerna publish, нас выручит lerna.json (стр. 7):

{  "version": "1.2.2",  "npmClient": "npm",  "command": {    "publish": {      "message": "chore(release): publish",      "registry": ....    }  },  "packages": [    "packages/@contract",    "packages/application"  ]}

Иначе нам понадобится файл .npmrc (файл с настройками npm) в директории пакета.

Первые сложности

Итак, наша CI-машина должна делать примерно следующее (без привязки к конкретной системе CI/CD):

# Pull и checkoutlerna bootsrap --hoist lerna run build # Запустит команду npm run build в каждом пакете.lerna publish --conventional-commits --yescp packages/application/build /tmp/place/for/artifact...

Но для работы нашему приложению нужны ещё node_modules.

Попытка 1. Можно конечно взять да и скопировать папку node_modules из корня проекта в наш /tmp/place/for/artifact. Но тогда:

  • Мы точно получим лишние зависимости (всякие jest, typescript и кучу ещё всего ненужного в рантайме). А если у нас в репозитории не 2 пакета, а 22, то размер node_modules может быть неприличным.

  • Мы, возможно, недополучим нужные зависимости, т. к. lerna поднимает пакеты в корень только если может. Так бывает не всегда - могут быть где-то разные версии, например.

Попытка 2. Не вопрос. У нас же есть package.json внутри packages/application. Там ведь перечисленно всё, что надо! Копируем package.json в папку с артефактом, запускаем npm i - профит! Но:

Дело в том, что для обеспечения повторяемости билда, в CI среде вместо команды npm install принято использовать npm ci. Основное отличие от npm install в том, что пакеты ставятся не из package.json, а из package-lock.json или shrinkwrap.json (смысл тот же). Подробнее о lock-файлах можно почитать в хорошем переводе от Андрея Мелихова.

Для моего рассказа важно понимать следующее:

  • Обойтись без lock-файла никак нельзя. Даже если указать в dependencies точные версии зависимостей без всяких "~" и "^" - это не поможет, т. к. транзитивные зависимости (то есть зависимости ваших зависимостей) вы не контролируете.

  • lock-файл должен быть синхронизирован с package.json. Это значит, что если у нас в package.json появилась новая зависимость (или наоборот), а в package-lock.json её нет, то npm ci будет ругаться:

Приятно, что текст ошибки содержит прямое указание на то, что нам нужно сделать - npm install.

Давайте вспомним, с чего мы начинали нашу сборку: lerna bootstrap --hoist Эта команда на самом деле уже создала нам package-lock.json в корне проекта. Однако, это нам мало помогает.

Можете убедиться в этом сами, скопировав в артефакт package.json из packages/application и lock-файл из корня - получите ошибку. Конечно, ведь там никакого намёка не будет на синхронизацию! А в application lock-файла у нас нет. Поэтому:

Попытка 3. Давайте попробуем обойтись без "всплытия". Да, не супер удобно, зато lock-файл будет там где надо. Делаем просто:

lerna bootstrap

Это действительно даст нам по lock-файлу на каждый проект. Но и тут всё не слава Богу! Потому что при попытке с этим файлом сделать npm ci, нам опять скажут нехорошие слова про синхронизацию. Как так?

Изучаем файл package-lock.json и видим.. что там не хватает пакета @contract/core!Ну и правильно, мы его не устанавливали, а делали симлинк...

Попытка 4. Ок, делаем просто npm install внутри каждого пакета. Тут нам поможет:

lerna exec -- npm i

Ура, теперь lock-файл и манифест внутри пакета синхронизированы! npm ci работает! Победа!

Запускаем наше приложение и при первом же запросе...

Оно падает

Говорит, что с модулем @contractчто-то не то. Конечно не то! Ведь npm i поставил этот модуль из npm registry. Ну тогда понятно - это только формально та же версия. А по факту, мы локально могли внести изменения, в том числе и ломающие, но версию ещё не поднимали и пакет не пушили (напоминаю, что build у нас до publish). Если же никаких изменений в пакете не было, то всё сработает.. и это скорее плохо, чем хорошо. Лучше пусть всегда не работает, чем то так, то сяк.

Думали гадали насчёт того, как бы делать publish сначала. Но это не логично - код надо собрать, потом протестировать, потом только паблишить - а он, зараза, не собирается.

Попытка 4. Ну ок, нам нужен симлинк, давайте его сделаем...

Выполняем:

lerna exec -- npm i      # Создаётся lock-файлы в пакетах.lerna link               # Создаются симлинки.lerna run buildlerna publish --conventional-commits ...cp packages/application/build /path/to/artifact# Можно ещё вместо копирования сделать production сборку# - без sourceMaps и деклараций.cp packages/application/package*.json /path/to/artifact(cd /path/to/artifact && npm ci --production)

И оно даже работает! Только мы кое-что забыли.. Добавим вызов jest где-нибудь между 3-й и 4-й строкой...

И внезапно падают уже тесты

Могут конечно и не упасть... особенно, если их нет. Но приложение может работать некорректно. А точнее, не так, как на машине разработчика, где он делает lerna bootstrap --hoist а потом билд.

До этого момента все проблемы были в общем-то тривиальны. Времени много ушло потому, что не было некоторых базовых знаний платформы и инструментов. Сейчас - после нескольких часов проведённых в гугле (хабре, медиуме, гитхабе...) - уже кажется, что всё просто. А вот новая проблема прямо мистическая. И возможно, вы с ней не столкнётесь. Но в чём тут суть, ИМХО понимать полезно.

Итак, lerna bootstrap --hoist и lerna exec -- npm i && lerna link - в чём может быть разница? Ведь второе - это по сути lerna bootstrap, но без --hoist. Пробуем на машине разработчика убрать флаг hoist... тесты падают. Добавляем - проходят.

На самом деле, когда первый приступ отупения проходит, можно осознать следующее:

packages+-- @contract|   +-- node_modules|       +-- class-transformer|+-- src|+-- package.json|   ...|+-- application|   +-- node_modules|       +-- class-transformer|       +-- @contract -> символическая ссылка|   +-- src|   +-- package.json|   ...|+-- package.json+-- lerna.json...

На схеме подсказка. И application и contract зависят от пакета class-transformer. Вообще-то, там есть и другие общие зависимости, но, к счастью, не все зависимости ломаются, когда в структуре node_modules они присутствуют в двойном экземпляре.

class-transformer - из тех, что ломается.

Подробнее о том, почему

class-transformer - удобная библиотека для преобразования объектов основанная на декораторах. В nestjs она встроена в дефолтную систему валидации (ValidationPipe). Самый простой пример её использования может быть такой:

import { Type } from 'class-transformer';import { IsInt, IsPositive } from 'class-validator';export class Query {  @IsInt()  @IsPositive()  @Type(() => Number)  id: number;}

При этом мы это посылаем в GET запросе (?id=100500) и получается, что nest на вход получит строчку, а не число. И валидатор IsInt() на это ругнётся (может и нет, но IsPositive() ругнётся 100%).

Поэтому мы говорим несту: преобразуй пожалуйста в число. Декоратор @Type() - самый простой способ. Если я не ошибаюсь, то он сделает просто return Number(id) Для более сложных случаев можно использовать декоратор @Transform() в который можно передать функцию преобразования.

Всё это вы можете найти в доке по class-validator и class-transformer.

Но вот только будте осторожны - функция трансформации НИ В КОЕМ СЛУЧАЕ не должна бросить ошибку. Это положит поток (на горьком опыте - потерянные 3 часа жизни)

Так вот:

Что произойдёт в нашем случае. Когда отработает декоратор @Type(), он он запишет в специальный объект в недрах class-transformer метаданные: "у вот этого класса надо преобразовывать входящее значение в число". Потом, когда объект придёт в ваше приложение nest вызовет функцию plainToClass из той же самой библиотеки, передав туда данные и конструктор Query. Она из того же объекта считает метаданные и проведёт преобразование.

В этом "том же" вся проблема. Если копий библиотеки у нас две, то это может быть два разных объекта и когда plainToClass будет работать, метаданных установленных в декораторе @Type() там не окажется!

Это экспериментально подтверждённый факт. Но вот почему это так работает, я не до конца понимаю. Всегда считал, что import повторно одни и те же сущности не загружает, но видимо всё сложнее.


Если кто-то уже достиг дзен - напишите пожалуйста в комментах, кому-то точно пригодится.

А мы как раз поместили наш класс Query в один пакет, а приложение в другой и если внимательно посмотреть на структуру проекта выше, но становится понятно, что у @contractнет никаких шансов найти нужную копию class-transformer.

Забавно, что у class-validator такой проблемы нет. Возможно, они хранят метаданные иначе (в global?). Как именно ещё не успел посмотреть.

Вот собственно ответ и найден. Получается, из-за того, как работает резолв зависимостей в ноде (ищем в node_modules, потом поднимаемся выше, ищем в node_modules... и так до рута) в случае симлинков нам очень выгоден --hoist. В случае установки из registry, пакетный менеджер сделает примерно (меня пугает это примерно...) то же - поднимет всё, что сможет поднять.

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

Что в итоге?

Чуток устаканив в голове новое понимание (и вопросы), я родил примерно следующее:

  • Когда программист начинает работу над репозиторием (только что слонировав его), он действует по плану:

lerna bootstrap --hoist # Не npm i в корне! Это сломает ваш lock-file!lerna run buildjest# ну и работаем...
  • В CI делаем то же самое, потом lerna publish, а затем:

# Makefile# Получаем версию приложения.BUILD:=build.$(shell jq .version packages/application/package.json | sed 's/"//g')artifact:  # Скрипт build/prod убирает sourceMap'ы и декларации, которые не нужны в продакшене(cd packages/application && npm run build:prod -- --outDir ../../deploy/$(BUILD))cp -r packages/application/package*.json deploy/$(BUILD)  # Ставим только рантайм-зависимости из package-lock.json(cd deploy/$(BUILD) && npm ci --production)    # Я вырезал кое-какие несущественные подробности, типа удаления package*.json и  # создания tar.gz архива.rm deploy/$(BUILD)/package*.jsosdf

Мы используем make, но это не суть. В итоге, это всё планируется перенести в Dockerfile, когда наша инфраструктура будет к этому готова.

  • Как создать корректные lock-файлы, если их нет?

lerna exec -- npm ilerna clean --yes# Именно так. Ставим модули, а потом удаляем. Но это единственный способ получить# lock-файлыlerna bootstrap -- hoist
  • Ну и последнее и, пожалуй, самое неприятное. Как установить новый пакет в наш application (или @contract)таким образом, чтобы сохранить консистентность lock-файлов:

# Makefileadd:# (ОЧЕНЬ ВАЖНО) Здесь обновится только package.jsonlerna add --scope=$(scope) $(package) --no-bootstrap# Обновится package-lock.json внутри пакетаlerna exec --scope=$(scope) -- npm i# node_modules внутри units/application нам не нужны!lerna clean --yes# вернёт нам все зависимости в корень и обновит рутовый package-lock.jsonlerna bootstrap --hoist  # Запускаем так (в scope надо передавать имя пакета из package.json):$ make add scope=app_name package left-pad

Почему так сложно? Потому что команда lerna add не обновляет package-lock.json, а только сам манифест. Не понятно почему. Может быть я не нашёл чего-то. Подскажите...

Выводы:

  • Зависимости в ноде - это сложно.

  • Управление зависимостями в монорепозиториях в условиях CI/CD - это ещё сложнее.

  • Но самое главное, свет в конце всегда туннеля есть! И пока решаешь такого рода заморочки, частенько удаётся поднять хороший пласт новых знаний.

Уверен, что это не последняя итерация. Меня не покидает ощущение, что всё можно сделать проще, чище - буду рад мнениям и идеям в комментариях.

Надо ещё поиграться с командой npm shrinkwrap, например...

Большое спасибо тем, кто дочитал до конца... Если здесь ещё кто-нибудь есть?

Если такой формат "история из практики" интересен, напишите пожалуйста, что "так", что "не так". Потому что историй... их есть у меня.

Спасибо за внимание!

Подробнее..
Категории: Node.js , Ci/cd , Nodejs , Nestjs , Monorepo , Lerna

Из песочницы Масштабирование CICD монорепозитория

19.07.2020 18:07:47 | Автор: admin

Lerna


Дано


  1. Монорепозиторий на базе Lerna и Yarn workspaces.
  2. Десяток приложений, и десятки общих пакетов на TypeScript, Angular, NodeJS.
  3. Высокое покрытие тестами самых разных мастей (модульные, интеграционные, e2e).
  4. и Atlassian Bamboo CI/CD.

Задача


Ускорить имеющиеся пайплайны в 2 раза (до, хотя бы, получаса). Попутно повысив стабильность до 90%.


Забегая вперед, скажу что требуемые показатели были достигнуты.


Было



Для инкрементальной сборки lerna filter options:


lerna run build:packages --since --include-merged-tags --include-dependencies

Чтобы попасть в инкремент пакеты должны проходить фазу lerna publish в артифакторий (JFrog):


# Masterlerna publish patch --yes# Featurelerna publish prepatch --yes --no-push --preid "${PREID}"

При такой организации pipeline, возможно только вертикальное масштабирование путём увеличения мощностей elastic агентов.


Этот подход крайне ограничен. И с ростом числа пакетов средняя длительность постепенно росла (~1ч).


Надо заметить, что в силу короткого релизного цикла (сутки), стабильность JFrog и, как следствие, всего pipeline была низка (~70%).


Идея


Собирать каждое приложение независимо от остальных.
На входе монорепозиторий
На выходе production image приложения.


Тестировать тоже независимо от остальных.
На входе production image (зависимости устанвлены, все пакеты собраны)
На выходе отчеты о тестировании и покрытии.


Это позволит собирать и тестировать в параллельном режиме, масштабируясь горизонтально по мере развития линейки продуктов.


Но в таком случае размер node_modules составил бы ~1.5Gb (суммарные зависимости всех пакетов монорепозитория). Что негативно отразилось бы на размере image, времени его загрузки в AWS ECR, и времени развертывания.


Фокусировка


Чтобы "урезать" ("сфокусировать") монорепозиторий для сборки, тестирования и развертывания одного конкретного приложения, достаточно найти подмножество пакетов в общем графе пакетов и переписать декларацию workspaces в корневом package.json непосредственно перед сборкой на CI.


#!/usr/bin/env nodeconst { spawnSync } = require('child_process');const { existsSync, promises: { readFile, writeFile } } = require('fs');const { join, dirname, relative, normalize } = require('path');const PACK_JSON_PATH = './package.json';(async (apps) => {    const packJSON = JSON.parse((await readFile(PACK_JSON_PATH)).toString());    await spawnSync('yarn', ['global', 'add', 'lerna'], { shell: true });    const locations = await listPackages(apps);    const [someLocation] = locations;    const basePath = findBasePath(someLocation);    // All paths should be relative to monorepo root    const workspaces = locations.map((loc) => normalize(relative(basePath, loc)));    packJSON.workspaces.packages = workspaces;    const packJSONStr = JSON.stringify(packJSON, undefined, 2);    await writeFile(PACK_JSON_PATH, `${packJSONStr}\n`);})(    process.argv.slice(2),);async function listPackages(apps = []) {    const filterOptions = apps.flatMap((app) => ['--scope', app]);    const { stdout } = await spawnSync(        'lerna',        ['ls', '-pa', '--include-dependencies', ...filterOptions],        { shell: true },    );    return String(stdout).split(/[\r\n]+/).filter(Boolean);}function findBasePath(packageLocation) {    return existsSync(join(packageLocation, 'lerna.json'))        ? packageLocation        : findBasePath(dirname(packageLocation));}

После "фокусировки" все команды (в том числе и changed) будут относится лишь к подмножетсву пакетов конкретного приложения.


Размер node_modules удалось снизить в среднем в 3 раза.


Fixed mode


Lerna fixed mode, отказ от lerna publish и артифактория позволили повысить стабильность и упростить логику pipeline.


Но как же быть с инкрементальностью сборок?


Инкремент


Для инкремента достаточно отслеживать изменения через команду lerna changed


lerna changed -a --include-merged-tags

Если изменений не обнаружено, то можно переиспользовать latest image приложения для развертывания и тестирования:


#!/usr/bin/env bashAPP=$1lerna-focus.js "${APP}"function nothing_changed() {    [[ -z "$(lerna changed -a --include-merged-tags || true)" ]]}function pull_latest_image() {...}function push_latest_image() {...}function tag_latest_with_version() {...}pull_latest_imageif nothing_changed; then    tag_latest_with_version    exitfibuild-app.sh "${APP}"if is-master.sh; then    push_latest_imagefi

Стало



Что дальше?


Сейчас активно набирают обороты такие решения как Nx: Extensible Dev Tools for Monorepos. Это предмет следующих разборов.


Если эта статья окажется полезной, то в следующей расскажу о горизонтальном масштабировании "на коленке" модульных тестов (Angular, Jest, ElasticSearch, Bamboo CI).

Подробнее..

Бенчмарки VKUI и других ребят из UI-библиотек

26.05.2021 12:10:08 | Автор: admin

Меня зовут Григорий Горбовской, я работаю в Web-команде департамента по экосистемным продуктам ВКонтакте, занимаюсь разработкой VKUI.

Хочу вкратце рассказать, как мы написали 8 тестовых веб-приложений, подключили их к моно-репозиторию, автоматизировали аудит через Google Lighthouse с помощью GitHub Actions и как решали проблемы, с которыми столкнулись.

VKUI это полноценный UI-фреймворк, с помощью которого можно создавать интерфейсы, внешне неотличимые от тех, которые пользователь видит ВКонтакте. Он адаптивный, а это значит, что приложение на нём будет выглядеть хорошо как на смартфонах с iOS или Android, так и на больших экранах планшетах и даже десктопе. Сегодня VKUI используется практически во всех сервисах платформы VK Mini Apps и важных разделах приложения ВКонтакте, которые надо быстро обновлять независимо от магазинов.

VKUI также активно применяется для экранов универсального приложения VK для iPhone и iPad. Это крупное обновление с поддержкой планшетов на iPadOS мы представили 1 апреля.

Адаптивный экран на VKUIАдаптивный экран на VKUI

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

Какие задачи поставили

  1. Выявить главные проблемы производительности VKUI.

  2. Подготовить почву, чтобы развернуть автоматизированные бенчмарки библиотеки и наших веб-приложений на основеVKUI.

  3. Сравнить производительность VKUI и конкурирующих UI-фреймворков.

Технологический стек

Инструменты для организации процессов:

Чтобы проще взаимодействовать с несколькими веб-приложениями, в которых применяются разные UI-библиотеки, здесь используем lerna. Это удобный инструмент, с помощью которого мы объединили в большой проект ряд приложений с отличающимися зависимостями.

Бенчмарки мы проводим через Google Lighthouse официальный инструмент для измерения Web Vitals. Де-факто это стандарт индустрии для оценки производительности в вебе.

Самое важное делает GitHub Actions: связывает воедино сборку и аудит наших приложений.

Библиотеки, взятые для сравнения:

Название

Сайт или репозиторий

VKUI

github.com/VKCOM/VKUI

Material-UI

material-ui.com

Yandex UI

github.com/bem/yandex-ui

Fluent UI

github.com/microsoft/fluentui

Lightning

react.lightningdesignsystem.com

Adobe Spectrum

react-spectrum.adobe.com

Ant Design

ant.design

Framework7

framework7.io


Мы решили сравнить популярные UI-фреймворки, часть из которых основана на собственных дизайн-системах. В качестве базового шаблона на React использовали create-react-app, и на момент написания приложений брали самые актуальные версии библиотек.

Тестируемые приложения

Первым делом мы набросали 8 приложений. В каждом были такие страницы:

  1. Default страница с адаптивной вёрсткой, содержит по 23 подстраницы.

    • На главной находится лента с однотипным контентом и предложением ввести код подтверждения (абстрактный). При его вводе на несколько секунд отображается полноэкранный спиннер загрузки.

    • Страница настроек, в которую входит модальное окно с примитивным редактированием профиля и очередным предложением ввести код.

    • Страница с простым диалогом условно, с техподдержкой.

  2. List (Burn) страница со списком из 500 элементов. Главный аспект, который нам хотелось проверить: как вложенность кликабельных элементов влияет на показатель Performance.

  3. Modals страница с несколькими модальными окнами.

Не у всех UI-фреймворков есть аналогичные компоненты недостающие мы заменяли на равнозначные им по функциональности. Ближе всего к VKUI по компонентам и видам их отображения оказались Material-UI и Framework7.

Сделать 8 таких приложений поначалу кажется простой задачей, но спустя неделю просто упарываешься писать одно и то же, но с разными библиотеками. У каждого UI-фреймворка своя документация, API и особенности. С некоторыми я сталкивался впервые. Особенно запомнился Yandex UI кажется, совсем не предназначенный для использования сторонними разработчиками. Какие-то компоненты и описания параметров к ним удавалось найти, только копаясь в исходном коде. Ещё умилительно было обнаружить в компоненте хедера логотип Яндекса <3

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

Автоматизация

Краткая блок-схема, описывающая процессы в автоматизацииКраткая блок-схема, описывающая процессы в автоматизации

Подготовили два воркфлоу:

  • Build and Deploy здесь в первую очередь автоматизировали процессы сборки и разворачивания. Используем surge, чтобы быстро публиковать статичные приложения. Но постепенно перейдём к их запуску и аудиту внутри GitHub Actions воркеров.

  • Run Benchmarks а здесь создаётся issue-тикет в репозитории со ссылкой на активный воркфлоу, затем запускается Lighthouse CI Action по подготовленным ссылкам.

UI-фреймворк

URL на тестовое приложение

VKUI

vkui-benchmark.surge.sh

Ant Design

ant-benchmark.surge.sh

Material UI

mui-benchmark.surge.sh

Framework7

f7-benchmark.surge.sh

Fluent UI

fluent-benchmark.surge.sh

Lightning

lightning-benchmark.surge.sh

Yandex UI

yandex-benchmark.surge.sh

Adobe Spectrum

spectrum-benchmark.surge.sh


Конфигурация сейчас выглядит так:

{  "ci": {    "collect": {      "settings": {        "preset": "desktop", // Desktop-пресет        "maxWaitForFcp": 60000 // Время ожидания ответа от сервера      }    }  }}

После аудита всех приложений запускается задача finalize-report. Она форматирует полученные данные в удобную сравнительную таблицу (с помощью магии Markdown) и отправляет её комментарием к тикету. Удобненько, да?

Пример подобного репорта от 30 марта 2021 г.Пример подобного репорта от 30 марта 2021 г.

Нестабильность результатов

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

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

Предупреждения из отчётов Google LighthouseПредупреждения из отчётов Google Lighthouse

GitHub Actions по умолчанию выполняет задачи параллельно, как и в нашем воркфлоу с бенчмарками. Решение ограничить максимальное количество выполняющихся одновременно задач:

jobs.<job_id>.strategy.max-parallel: 1

Мы провели бенчмарк дважды, и вуаля результаты стали ощутимо стабильнее. Теперь можно переходить к результатам.

Результаты от 30 марта 2021 г.

VKUI (4.3.0) vs ant:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

ant

default

report

0.99

vkui (4.3.0)

modals

report

1

ant

modals

report

0.99

vkui (4.3.0)

list

report

0.94

ant

list

report

0.89

list - У ant нет схожего по сложности компонента для отрисовки сложных списков, но на 0,05 балла отстали.

VKUI (4.3.0) vs Framework7:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

f7

default

report

0.98

vkui (4.3.0)

modals

report

1

f7

modals

report

0.99

vkui (4.3.0)

list

report

0.94

f7

list

report

0.92

list - Framework7 не позволяет вложить одновременно checkbox и radio в компонент списка (List).

VKUI (4.3.0) vs Fluent:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

fluent

default

report

0.94

vkui (4.3.0)

modals

report

1

fluent

modals

report

0.99

vkui (4.3.0)

list

report

0.94

fluent

list

report

0.97

modals - Разница на уровне погрешности.

list - Fluent не имеет схожего по сложности компонента для отрисовки сложных списков.

VKUI (4.3.0) vs Lightning:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

lightning

default

report

0.95

vkui (4.3.0)

modals

report

1

lightning

modals

report

1

vkui (4.3.0)

list

report

0.94

lightning

list

report

0.99

list - Lightning не имеет схожего по сложности компонента для отрисовки сложных списков.

VKUI (4.3.0) vs mui:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

mui

default

report

0.93

vkui (4.3.0)

modals

report

1

mui

modals

report

0.96

vkui (4.3.0)

list

report

0.94

mui

list

report

0.77

default и modals - Расхождение незначительное, у Material-UI проседает First Contentful Paint.

list - При примерно одинаковой загруженности списков в Material-UI и VKUI выигрываем по Average Render Time почти в три раза (~1328,6 мс в Material-UI vs ~476,4 мс в VKUI).

VKUI (4.3.0) vs spectrum:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

spectrum

default

report

0.99

vkui (4.3.0)

modals

report

1

spectrum

modals

report

1

vkui (4.3.0)

list

report

0.94

spectrum

list

report

1

list - Spectrum не имеет схожего по сложности компонента для отрисовки сложных списков.

VKUI (4.3.0) vs yandex:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

yandex

default

report

1

vkui (4.3.0)

modals

report

1

yandex

modals

report

1

vkui (4.3.0)

list

report

0.94

yandex

list

report

1

default - Разница на уровне погрешности.

list - Yandex-UI не имеет схожего по сложности компонента для отрисовки сложных списков.

modals - Модальные страницы в Yandex UI объективно легче.

Выводы из отчёта Lighthouse о VKUI

  • VKUI по показателям на одном уровне с другими библиотеками. Сильных просадок, как и значительного превосходства, нет.

  • Одно из явных проблемных мест вложенные Tappable протестированы на большом списке. Единственная библиотека, в которой полноценно реализован этот кейс, Material-UI. И VKUI уверенно обходит её по производительности.

  • Lighthouse ругается на стили после сборки много неиспользуемых. Они же замедляют First Contentful Paint. Над этим уже работают.

Два CSS-чанка, один из которых весит 27,6 кибибайт без сжатия в gzДва CSS-чанка, один из которых весит 27,6 кибибайт без сжатия в gz

Планы на будущее vkui-benchmarks

Переход с хостинга статики на локальное тестирование должен сократить погрешность: уменьшится вероятность того, что из-за внешнего фактора станет ниже балл у того или иного веб-приложения. Ещё у нас в репортах есть показатель CPU/Memory Power и он немного отличается в зависимости от воркеров, которые может дать GitHub. Из-за этого результаты в репортах могут разниться в пределах 0,010,03. Это можно решить введением перцентилей.

Также планируем сделать Lighthouse Server для сохранения статистики по бенчмаркам на каждый релиз.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru