Мы делаем суровый DIY для требовательных прорабов. Суровый, потому что он про шестиметровую доску естественной влажности, кольца ЖБИ, трехметровый гипрок и сыпучку палетами. А требовательные, потому что малейшие изменения в UI и UX приводят к «Чего началось-то, нормально же общались».
Интернет-магазин строительных материалов — это когда в каталоге названия товаров от 20 до 140 символов — и в них все важное. Когда 4,8 миллионов записей о связях сопутствующих товаров, когда цена указана за метр, продается в упаковках и купить можно не только integer. Когда порой клиенты не знают, что им нужно и как это называется: нейминг в таких товарах — это богатая тема, достойная отдельной статьи, это вам не трусики: стринги, танго, добрая классика «75+».
Когда товары колеруются, пилятся и поднимаются в грузовом лифте, а если его нет — то пешком. И мы точно должны знать, сколько стоит поднять 1 лист гипсокартона пешком на 25-й этаж в уездном городе N по конкретному адресу.
Когда доставка всего за 2 часа и пересчет доступности товара для продажи и логистического плеча от ХАБа до сборки заказа, а также способа оплаты, должен учитывать сотни параметров. Мы знаем, где сейчас находится каждый товар: город, подразделение, склад, место и ячейка хранения. Мы знаем его каналы поставок и графики перемещений с точностью до минуты. В момент оформления заказа знаем, откуда повезем. Чтобы успеть вовремя, еще в тот момент, когда оплата проходит долгий путь от «одного касания пользователя» до облачной кассы и ОФД, у нас уже запускается машина времени, которая распределяет заказы по рейсам, водителям, машинам, строит маршруты. А на рабочем месте комплектовщика появляется заказ на сборку. И если что-то в этой цепочке изменилось, хотя бы один параметр, то он должен учитываться, пересчитываться и отображаться для пользователя — все должно быть таким реактивненьким, как сейчас модно.
Как все начиналось
Мы осознали, что у нас на проекте 23 разных крестика у модалок, каждый экран — это новая кнопка и новый CSS. Вносится изменение в корзину — ломается каталог. Добавили новый тултип в листинге — сломались шаблоны страниц товара. На складе в Балашихе изменили место хранения у категории — сайт должен это увидеть, чтобы показать информацию о наличии и доступности товара. Только на сайт вместе с ячейкой, пришло название, описание, сопутствующие и аналоги, галерея фото и видосы с бородатым мужиком, монтирующим парашюты, а вслед за видосами пришел админ с вопросом: «WTF, сколько вы будете сервера насиловать, когда перепишете?»
Тогда и решили, что пора все переделать. Монолитность, ненадежность, избыточность, админы опять же, игнорировать это все — плохая примета.
Как декомпозируешь, так и до продакшена понесешь
Начали с плана, разбив блоки на front и back, оценив срок по принципу трех «П». При этом разработку планировали вести параллельно. Вот что мы сделали:
Собрали требования по функционалу от разных подразделений (сходили в гости к маркетингу, SEO, закупкам, контентщикам, контакт-центру, коммерсам и т. д.).
Провели исследование и выбрали технологии.
Спроектировали системы обмена данными.
Спроектировали БД для хранения накопленной информации.
Написали протокол взаимодействия с клиентом (API).
Создали и согласовали контракты по работе с API, выполнили генерацию моков.
Создали дизайн-систему.
Выполнили прототипирование интерфейсов.
Создали дизайн интерфейсов.
Выстроили архитектуру бэкенда и фронтенда.
Выполнили реализацию, в которую вошли:
Реализация обмена данными (базовая информация о товаре: цены, остатки, свойства, сопутка, комплекты и т. д.) в том формате, в котором они хранятся в учетной системе.
Реализация агрегации промежуточных данных. Например, вычисление цен из временного регистра, агрегация остатков по складам, определение активности товаров и т. д.
Сбор всех необходимых данных с учетом нового дизайна.
Реализация серверной части приложения.
Реализация сервисов бэкенд.
Верстка шаблонов.
Реализация клиентской части приложения.
Подготовили тестовое окружение.
Подготовили продакшн окружения.
Выполнили интеграцию (переключение с моков на API).
Провели тестирование на стейдже.
Исправили дефекты и подготовили запуск на продакшн.
Запустили.
Чтобы таска была хороша, она должна вылежаться
Требований по каталогу у нас на годы вперед, поэтому мы отобрали лишь те, что можно было отнести к базовому функционалу. Задача стояла такая: переписать, заложить основы, не пытаться сразу всех удовлетворить. Но все договоренности, планы и согласования оказались бредом для сельской паствы. Все равно документация оказалась сырой, дизайн и решения меняли на ходу. После демо появлялись новые требования, не редкими были и доработки в макетах, обменах данными, структурах данных и API.
Разработка «вид сзади»
Источником данных по товарам, неймингу, остаткам на складах, акциям и т. д. служит у нас учетная система. Интеграция с ней первоначально была реализована через soap-сервис, а при проектировании заложена избыточность, что влекло за собой накладные расходы при изменении части объекта.
Весь код для обмена и работы с данными находился в одном репозитории. Проект развивался, обрастал как новым функционалом, так и избыточностью. И каждая новая фича в каталоге плюсовала время на получение и агрегацию данных. Накопленное легаси сделало управление и доработки обмена данными трудоемкой задачей. Со временем от real-time-чтения мы перешли на агрегацию и хранение информации в специальных таблицах для того, чтобы ускорить чтение, а также приделали горячее кэширование. Проведя анализ накопившегося, мы решили существенно изменить имеющееся решение, чтобы обеспечить высокую скорость обмена информацией, снизить время на доработку и добавление новых обменов, повысить скорость ответа API. В общем, перейти к сервисной архитектуре от монолитной.
Начали с обменов. Необходимо было выбрать брокер очередей. Проведя исследование среди популярных брокеров ActiveMQ, RabbitMQ, Apache Kafka и прочих, остановились на ActiveMQ (так как он был уже в стеке) и Kafka (как потенциально перспективный для компании). Рассматривая инструменты, мы сделали упор на масштабировании и гарантии приема сообщений, по итогам экспериментов, выбрали Kafka. Естественно, при этом пришлось немного поработать с клиентом, так как по идеологии Kafka именно клиент должен быть «умным» и уметь правильно обрабатывать сообщения.
После того как решение созрело и мы научились с ним работать, была выбрана задача на которой можно было бы сделать первый шаг в будущее. Такой задачей стал обмен статическими файлами (изображения, сертификаты, видео) из хранилища учетной системы и загрузка их в облако. В то время на сайте изменение размеров изображений происходило в реальном времени, что отражалось на скорости загрузки страницы. В новых обменах мы решили эту задачу: теперь конвертация изображений и видео во все нужные нам форматы и размеры происходит заранее.
Как мы уже говорили, к началу работы с каталогом текущая версия была реализована в едином приложении, что являлось определенной сложностью, поскольку не было возможности обновить часть приложения. За годы разработки проект накопил достаточное количество легаси, поэтому изменения в одной части приложения приводили к неожиданным ошибкам в другой. Это означало, что и тестирование, в том числе регресс, необходимо было проводить, даже если запятую убрали. В итоге мы выделили все API в независимые сервисы. Для описания документации и введения стандарта взяли за основу спецификацию OpenAPI 3 и правила версионирования SemVer. Описали свой перечень правил и прежде всего разработали контракт данных. Договорились «на берегу».
Для обработки сообщений из очереди kafka использовали демон на php. Для хранения промежуточных данных из обмена создали отдельный instance БД. Полученные данные — сырые, и требуется постобработка, такое решение позволяет разделить нагрузку. В работе новый формат обменов показал себя успешно, поэтому мы продолжили развивать его и масштабировать на весь каталог.
При выборе хранилища БД мы решали поставленную задачу: чтобы API отвечали в пределах 50 – 100 мс. Так же мы планировали использовать в разработке SSR на Node, что тоже наложило требования к скорости ответа API и отказоустойчивости.
Для того чтобы организовать хранение данных каталога, в качестве основной БД мы выбрали Redis, так как за время работы был накоплен достаточный опыт использования и он неплохо организовывается в кластер. Настроили Redis Persistence c записью в один мастер и две географически разнесенные readonly-реплики. В мастер пишут обмены, а реплики используются для чтения в API-сервисах. Для репликации использовали опцию AOF, поскольку это более надежный вариант. Кроме того, провели изучение формата хранения данных и способа сериализации/десериализации данных PHP. По итогам исследования расширение igbinary показало лучшие результаты, чем json, php-сериализатор или msgpack. Для фильтрации и сортировки данных в каталоге выбрали elasticsearch, так как в нашем стеке этот инструмент уже был и его производительность нас устраивала.
При внедрении в продакшн сервисы API развернули на существующей инфраструктуре, а вот для SSR подготовили отдельные серверы с прослойкой кеширования на Varnish.
Per aspera ad astra
Сложности у нас появились на этапе тестирования каталога, мы периодически получали большие задержки в ответах API, причем найти закономерность долгое время не могли. К этому времени мы уже интегрировали новые API для внутренних запросов в сервисах корзины, всех видов калькуляторов, сметах и уже имели немалую нагрузку.
Для поиска проблемы использовали профайлер, дебаг, нагрузочные тесты, читали системные логи, но при этом получали необъяснимые зависания в разных местах кода. В итоге, проводя эксперименты с нагрузочными тестами, заметили совпадение пиков нагрузки с запусками агрегации данных и записи их в redis. Причина была в том, что для оптимизации работы для записи в redis, мы использовали загрузку батчами, но батчи были достаточно большими и запрос на запись в мастер одного батча мог занимать много времени. Но с учетом репликации AOF, которая хранит журнал запросов, все эти запросы выполняются на slave. А так как redis однопоточный, то запросы на получение данных из API сервиса в это время простаивали в очереди. После уменьшения количества объектов в батчах на запись, подобные пики практически перестали воспроизводиться. А поскольку Redis требователен к процессору, CPU были закреплены за виртуальными серверами, обслуживающими кластер Redis.
Фронтмен должен быть красив! Это для начала
За годы работы наследие накопилось и во фронтовой части приложения: скелеты в шкафу, костыли, скелеты на костылях, палки... Поэтому front принял волевое решение, что все-таки нужен свой велосипед и стали пилить свою дизайн-систему.
Мы 11-е в рейтинге среди е-коммов, амбиций дофига, хотим, как у всех, чтобы по-взрослому. Без своей дизайн-системы не продать нам ни галоши, ни обои, ни полы с подогревом. И опять же, 23 крестика у модалок, как тут без стандартизации дизайна, удобства правок, уменьшения time-to-market — вот эти все умные слова. Дизайн-систему начали с малого. Родился UI-kit без документации, молодой и полезный, как микрозелень, это, конечно, усложнило работу над каталогом. Но мы верим, что заложили фундамент, что дьявол в деталях, и наш Kit рассказывает и показывает, как делать хорошо цифровые продукты в DIY для всех: и для суровых строителей, и для девушек с собачками.
На фронте у нас есть устоявшийся стек технологий: React, RxJS, Webpack, SCSS, Typescript. Но помимо дизайн-системы, мы поставили перед собой еще одну задачу: сделать SSR (Server Side Rendering) клиентского кода на Node JS. До этого использовали smarty. Таким образом мы смогли применить один код как на клиенте, так и на сервере.
Еще про амбиции: CustDev явным образом сказал: «Нужна автоподгрузка товаров на странице, потому что тыкать в кнопки «Показать еще», когда ты в варежках и на объекте, — не вариант!» Нужно было решать задачу, при этом сделать удобно и не отрисовывать 1000 товаров.
В общем, кто хоть раз делал каталог, скажет: «В следующей жизни буду разработчиком админок». Потому что он делал SEO, интеграцию с маркетинговыми инструментами. А когда у тебя SPA, а логировать и отправлять события все равно надо, вот тут, кроме как «Всевышний терпел и нам велел», лучше и не скажешь.
Краткая история «ССР»
При работе над SSR тоже не обошлось без геморроя, у нас возникли проблемы с утечкой памяти. Такого рода проблемы обычно не решаются быстро, и поиск утечек занимает достаточно много времени. Но мы справились и для надежности реализовали кластеризацию Node-приложения, которая позволила нам в случае появления новых утечек или иных проблем с падением приложения, переключать пользователей на другую ноду кластера и перезапускать упавшую.
Также большой сложностью была оценка сроков задачи. Много изучали, экспериментировали, перенимали опыт коллег. Была очень высокая неопределенность. Многое не получалось сходу, но мы не опускали руки и продолжали идти вперед.
Мы в ответе за фичу, которую прикрутили
Не так страшны первые 90% проекта, как вторые 90% проекта.
У нас есть отработанная схема запуска такого рода больших фич в продакшн, и каждая крупная фича проходит несколько этапов тестирования и исправления дефектов. Тестирование можно разделить на три блока: тестирование API, верстки, функциональное тестирование GUI.
При тестировании API проверяем бизнес-логику приложения. В итоге было описано и автоматизировано свыше 1600 тест-кейсов согласно тестовой модели. Если говорить про верстку, то нужно понимать, что тестировать нужно полную и мобильную версию, а также разные браузеры и разрешения.
И последний этап — функциональное тестирование графического интерфейса. Но перед этим нужно описать и проревьюить все тестовые сценарии (документация и опыт нам в помощь!). В итоге получилось более 500 тест-кейсов для десктопа и свыше 400 для мобайла.
Когда мы поняли, что все критические дефекты исправлены, все важные для SEO моменты учтены и четко работают, когда мы получили фидбек от отдела маркетинга, что все данные отправляются в разного рода маркетинговые инструменты, мы все равно запустили новый каталог на несколько групп добровольцев из аутсорсингового контакт-центра, которые работают на сайте. И только получив их благословение, начали переключать новый каталог на пользователей. А лучшим доказательством того, что мы молодцы, стала тишина от наших пользователей. Мы все разобрали до основания и собрали иначе, а требовательные прорабы не заметили.
И кстати, фуга — это не про фугу Баха, а про шпаклевку «Фуген». А Антонио — это третий по частотности поисковый запрос. Конечно, ищут не Антонио и не Рикардо. Это автокоррекция «Ветонит» на IOs. А парашют — это не про прыжки, а про теплоизоляцию, но об этом — уже в следующей истории…
Фото парашютов прилагается.