Представьте: горячий металл, мощные машины, сотни работников — наше производство постоянно подвержено различным рискам. И как мы превратили эти вызовы в возможности? Этот рассказ будет о том, как мы воплотили в жизнь инновационную для компании систему сбора заявок об опасностях, о наших успехах, о трудностях, с которыми мы столкнулись, и о том, какие уроки мы извлекли из этого опыта. Давайте окунемся в мир цифровых решений и безопасности труда вместе!
Далее рассказ пойдет об одном из наших сервисов, но опыт и выводы будут полезны и при решении других задач.
Статья будет полезна архитекторам, руководителям и разработчикам.
С чего все начиналось
Приветствую всех читателей! Меня зовут Илья, и я с вами сегодня, чтобы поделиться историей о том, как мы, команда ИТ-специалистов в металлургической компании, воплотили в жизнь инновационную систему сбора заявок об опасностях. На большом производстве критически важно не только быстро реагировать на различного рода нарушения и опасности, но и контролировать своевременное их устранение. Было принято решение о разработке системы, которая решала бы такую задачу.
Система предоставляет возможность любому сотруднику оставить заявку с указанием вида опасности и места ее обнаружения, а также позволяет руководителям на местах получать релевантную информацию и обратную связь.
Разрабатываемая система создана на основе Битрикс, и так как мы построили там ни одну систему, можно с уверенностью сказать, что мы обладаем необходимым опытом, наработками и кадрами.
Сперва система была небольшой и состояла из формы для сбора информации о характере опасности и инструментов для обработки заявки ответственными. С течением времени система разрасталась и усложнялась.
Мы добавляли новые поля в форму заявок, расширяли функциональность системы, внедряли новые страницы для более детального контроля показателей обработки заявок и устранения опасностей. Важным этапом стало введение матрицы ответственных за обработку заявок, которая позволила нам оптимизировать назначение задач и оперативно информировать руководителей участков о возможных задержках в принятии решений.
Помимо этого мы активно использовали графическое представление данных. Статистика по заявкам отражается на гистограммах и круговых диаграммах, что позволяет нам не только видеть общую картину, но и выявлять тенденции и проблемные зоны для дальнейшего улучшения процесса.
Для контроля за устранением опасностей также был реализован функционал эскалации, который уведомлял руководителей о просрочках и количестве отложенных заявок. Наша система активно использовалась сотрудниками, и количество ежедневно добавленных заявок только росло. К моменту окончания активной разработки система сильно выросла:
20 различных страниц
Более 10 гистограмм
Более 1000 типов опасностей
Более 1400 организационных единиц
Более 500 000 заявок
Мы заметили как с каждым днем рос интерес сотрудников к нашей системе. Однако не у всех из них всегда есть доступ к компьютеру, многие работают за пределами офиса. Для обеспечения максимального удобства использования мы внедрили необходимые интерфейсы в мобильное приложение. Этот шаг значительно повысил мобильность и оперативность при фиксации заявок об опасностях: теперь каждый сотрудник мог сделать фотографию и мгновенно регистрировать заявку или факт нарушения, что существенно упростило взаимодействие с сервисом для большого количества людей.
Система была реализована с помощью простых инструментов, которые присутствуют в коробке Битрикс. Для хранения данных использовались инфоблоки, для работы с ними — стандартные инструменты старого ядра.
В мире разработки любое решение является компромиссом, который может изменяться с течением времени. Это правило распространяется и на IT-сферу, где принятие решений, включая проработку архитектуры и прочее, играет ключевую роль.
Реализованное решение справлялось с нагрузкой, заложенной при создании. Однако спрос на наш сервис оказался выше, чем мы ожидали. Уже после первого месяца использования сервиса и анализа его производительности мы заметили неблагоприятную тенденцию, которая с увеличением числа запросов и функций начала принимать экспоненциальный характер. Было очевидно, что с увеличением нагрузки система нуждается в корректировках, мы приступили к анализу возможных узких мест.
Быстрый анализ
В первую очередь мы подумали про кеширование или предварительное создание кешей.
Но нет, такой вариант не подойдет. Где было возможно что-то закешировать, уже все закешировано.
Следом мы обратили внимание на сам инфоблок с заявками, ведь основные выборки были связаны именно с заявками. Как оказалось, свойства инфоблока хранились в общей таблице. Такое хранение свойств точно не подходит для больших объемов данных, производить фильтрации по таким структурам более затратно.
Решено! Изменим место хранения свойств, и сервис будет работать как ракета лучше.
Задача — проще не бывает: зайди в админку битрикс, нажми на кнопку «изменить место хранения свойств» и вуаля — все готово.
Даже на такой простой задаче мы сходу споткнулись.
Свойства, повсюду свойства
Количество свойств в инфоблоке уже перевалило за пятьдесят. Этот факт влиял на наши планы. По реализации битрикс мы не можем конвертировать инфоблоки, в которых более чем 50 свойств. Очевидно, что часть из них не использовалась:
Были пустые
Заполненные однотипными значениями
Заполненные значениями, которые можно вычислить на основе других
Свойства заводились в инфоблоки. По мере развития они становились не нужны, но не удалялись из инфоблоков. После удаления ненужных свойств у нас осталось всего 42.
Некоторые из свойств были заведены как списочные, хотя обозначали флаг (0/1).
Мы заменили такие свойства на свойство типа число, чтобы упростить запросы, и логику построения запросов в коде. Теперь не нужно было получать идентификатор элемента списка и передавать его в запрос, достаточно просто передать 0 или 1.
Джоины, они во всем виноваты
Посмотрев еще раз на запросы, которые формировались при получении заявок, мы пришли к выводу, что они ужасны. Запросы пестрили джоинами из-за способа хранения свойств: чем больше условий по свойствам, тем больше джоинов.
Основной особенностью инфоблока первой версии является хранение значений свойств в общей таблице. Примерно таким образом могла бы выглядеть часть свойств одной заявки, которые хранятся в общей таблице свойств.
Общее число доступных для пользователя фильтров весьма внушительное.
Представьте как мог бы выглядеть запрос при использовании нескольких фильтров с учетом такого хранения данных. Ниже приведен пример запроса, в котором использовано всего лишь несколько условий по свойствам.
SELECT `iblock_element`.`ID` AS `ID`
FROM `b_iblock_element` `iblock_element`
LEFT JOIN `b_iblock_element_property` `detection_date_prop`
ON `iblock_element`.`ID` = `detection_date_prop`.`IBLOCK_ELEMENT_ID` AND
`detection_date_prop`.`IBLOCK_PROPERTY_ID` = 3186
LEFT JOIN `b_iblock_element_property` `status_prop`
ON `iblock_element`.`ID` = `status_prop`.`IBLOCK_ELEMENT_ID` AND
`status_prop`.`IBLOCK_PROPERTY_ID` = 3187
LEFT JOIN `b_iblock_element_property` `hazard_type_prop`
ON `iblock_element`.`ID` = `hazard_type_prop`.`IBLOCK_ELEMENT_ID` AND
`hazard_type_prop`.`IBLOCK_PROPERTY_ID` = 1
LEFT JOIN `b_iblock_element_property` `author_prop`
ON `iblock_element`.`ID` = `author_prop`.`IBLOCK_ELEMENT_ID` AND
`author_prop`.`IBLOCK_PROPERTY_ID` = 1
LEFT JOIN `b_iblock_element_property` `location_1_prop`
ON `iblock_element`.`ID` = `location_1_prop`.`IBLOCK_ELEMENT_ID` AND
`location_1_prop`.`IBLOCK_PROPERTY_ID` = 1
LEFT JOIN `b_iblock_element_property` `location_2_prop`
ON `iblock_element`.`ID` = `location_2_prop`.`IBLOCK_ELEMENT_ID` AND
`location_2_prop`.`IBLOCK_PROPERTY_ID` = 1
WHERE `iblock_element`.`ACTIVE` = 'Y'
AND `iblock_element`.`IBLOCK_ID` = 218
AND `detection_date_prop`.`VALUE` >= '2022-09-13'
AND `hazard_type_prop`.`VALUE` = 2771327
AND `author_prop`.`VALUE` = 116433
AND `location_1_prop`.`VALUE` = 52583
AND `location_2_prop`.`VALUE` = 52584
GROUP BY `iblock_element`.`ID`
ORDER BY `ID` DESC
LIMIT 0, 100
Конвертировали инфоблок во вторую версию (хранение свойств в отдельной таблице), стало работать заметно быстрее, но не всегда. Запросы после конвертации стали выглядеть аккуратнее.
SELECT
BE.ID as ID,
BE.NAME as NAME,
FPS0.PROPERTY_2413 as PROPERTY_CLASSIFIER_TYPE_IBLOCK_VALUE,
FPEN0.VALUE as PROPERTY_STATUS_VALUE
FPS0.PROPERTY_2387 as PROPERTY_DETECTION_DATE_VALUE,
FPS0.PROPERTY_2388 as PROPERTY_USER_CREATOR_VALUE,
FPS0.PROPERTY_2424 as PROPERTY_LOCATION_DEPTH_1_VALUE
FROM
b_iblock B
INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
INNER JOIN b_iblock_element_prop_s416 FPS0 ON FPS0.IBLOCK_ELEMENT_ID = BE.ID
LEFT JOIN b_iblock_property_enum FPEN0 ON FPEN0.PROPERTY_ID = 2386 AND FPS0.PROPERTY_2386 = FPEN0.ID
WHERE
BE.IBLOCK_ID = '416'
AND BE.ACTIVE='Y'
AND FPS0.PROPERTY_2413 IS NOT NULL
AND FPS0.PROPERTY_2387 >= '2771327'
AND FPS0.PROPERTY_2388 = '116433'
AND FPS0.PROPERTY_2424 = '52583'
AND FPS0.PROPERTY_2425 = '52584'
LIMIT 20
Мы получили результат, но не тот, который хотели. Сервис стал работать быстрее, но страницы все еще грузились более одной секунды, а в некоторых случаях запросы выполнялись все также долго.
Проведя несколько экспериментов с запросами, мы увидели, что запросы к таблице со свойствами без использования таблицы b_iblock_element выполняются быстрее.
Поразмыслив над тем, какие данные мы берем из таблицы b_iblock_element для отображения или обработки заявки, мы пришли к выводу, что никакие… Кроме поля ACTIVE.
Во многих частях сервиса использовались orm для инфоблоков, самым логичным решением было отказаться от классической orm, которая предоставляется модулем «Инфоблоки», и реализовать свой аналог, который бы не обращался к таблице b_iblock_element. Так мы и поступили, поле активность завели как числовое свойство.
SELECT `properties`.`IBLOCK_ELEMENT_ID` AS `IBLOCK_ELEMENT_ID`,
`properties`.`PROPERTY_2413` AS `CLASSIFIER_TYPE_IBLOCK`,
`properties`.`PROPERTY_2386` AS `STATUS`,
`properties`.`PROPERTY_2387` AS `DETECTION_DATE`,
`properties`.`PROPERTY_2388` AS `USER_CREATOR`,
`properties`.`PROPERTY_2424` AS `LOCATION_DEPTH_1`,
`properties`.`PROPERTY_2425` AS `LOCATION_DEPTH_2`
FROM `b_iblock_element_prop_s416` `properties`
WHERE (`properties`.`PROPERTY_2413` IS NOT NULL AND `properties`.`PROPERTY_2413` <> 0)
AND (`properties`.`PROPERTY_2427` IS NOT NULL AND `properties`.`PROPERTY_2427` <> 0)
AND `properties`.`PROPERTY_2387` > 2771327
AND `properties`.`PROPERTY_2388` = '116433'
AND `properties`.`PROPERTY_2424` = 52583
AND `properties`.`PROPERTY_2425` = 52584
LIMIT 0, 20
Запросы стали еще лучше, ведь теперь мы практически всегда работали с одной таблицей.
По-прежнему наблюдались проблемы с сортировкой и фильтрацией заявок, но на этот раз проблема была не в таблицах, а в конкретных свойствах.
Нюансы хранения дат в инфоблоках
Слабым местом оказались свойства типа “дата и время”. Посмотрим подробнее, как битрикс хранит значения свойств этого типа.
Обратим внимание, что значение записано в колонке VALUE. Битрикс хранит значения свойств в колонках трех типов:
Ключ | Тип | Назначение | Составной индекс |
VALUE | text | Для хранения строковых значений | VALUE |
VALUE_NUM | decimal | Для хранения чисел | VALUE_NUM |
VALUE_ENUM | integer | Для хранения ссылок на элементы других таблиц | VALUE_ENUM |
Сортировка и фильтрация большого количества записей по полю типа text не лучшая идея.
При изменении места хранения свойств картина не меняется, значения свойств типа дата все также хранятся в поле с типом text, лишь уменьшается количество строк для перебора.
Свойства типа дата основаны на базовом типе «строка», который хранится в колонке с типом text. Для наших нужд это не подходит: мы хотим быстро фильтровать и сортировать.
Подумав над совместимым решением с текущей логикой инфоблоков, мы создали свой тип свойства, который основан на базовом типе «число» (NUMBER). Значения этого типа свойств хранятся в колонке с типом decimal, более подходящим для наших нужд.
Переезд
Определить проблемы и придумать для них решения было не самой сложной задачей. Большим испытанием оказался переезд на вторую версию инфоблоков.
Стандартными средствами конвертировать инфоблок с первой на вторую версию не представлялось возможным из-за большого количества элементов.
Не было возможности приостановить работу сервиса — его непрерывная доступность была нашим приоритетом. Стоит принять во внимание и свойства с новым способом хранения, добавленные для улучшения производительности. Поэтому мы приняли решение о постепенном переходе на новый информационный блок. Мы создали новый инфоблок рядом с существующим и написали агент, который постепенно перемещал элементы из старого инфоблока в новый. Чтобы в будущем иметь возможность найти старые заявки, мы поместили старые идентификаторы в поле EXTERNAL_ID.
Как только все элементы были перенесены, мы провели релиз ранее подготовленной ветки с оставшимися доработками.
Последние штрихи
На этот раз мы ответственно подошли к проработке основных сценариев использования страницы списка заявок и выделили наиболее частые сценарии фильтрации заявок:
дата выявления
статус заявки
локация, где выявлена опасность
срок устранения
По умолчанию на странице применен фильтр для отображения заявок текущего года. Также сотрудники активно сортировали и фильтровали заявки по другим ключевым полям. Логичным решением было создать индексы по этим полям.
# | Пояснение | Индекс |
1 | Наиболее частый сценарий и потребность пользователей | Активность, |
2 | Заявки распределены по трехуровневому классификатору локаций. | Локация 1, |
3 | Многие выборки в сервисе построены относительно этого поля | Срок устранения |
После добавления индексов скорость отклика страниц в типовых сценариях взлетела.
Итоги оптимизации
В результате выполнения всех работ удалось увеличить скорость отклика страниц более чем в 10 раз. Ранее страница со списком заявок могла открываться более двадцати секунд, а теперь она загружается менее чем за секунду.
Выводы
Подводя итоги, можно с уверенностью сказать, что архитектура в области IT действительно является компромиссом. Возможно, некоторые скажут, что определенные проблемы могли быть предусмотрены заранее, и, возможно, в чем-то они будут правы. Однако я придерживаюсь другого мнения.
Как говорится, "знал бы, где упасть, соломки бы подстелил". Мы часто сталкиваемся с неожиданными вызовами и проблемами, которые невозможно полностью предвидеть. В таких ситуациях важно гибко реагировать и быстро находить решения.
Есть и другая поговорка, которая гласит: "Лучшее — враг хорошего". Застревание в стремлении к идеальному решению иногда может замедлить процесс и привести к упущенным возможностям. Важно найти баланс между достижением оптимального результата и возможностью быстро адаптироваться к изменяющимся условиям и требованиям.
Таким образом, хотя некоторые аспекты архитектуры могли быть улучшены заранее, невозможно избежать всех потенциальных проблем. Важно быть готовым к изменениям и обучаться на ошибках, чтобы постоянно совершенствовать свои проекты и процессы.
Для тех, кто дочитал эту статью, оставлю несколько важных рекомендаций:
Важно ответственно подходить к анализу структур для хранения данных, создаваемым фреймворками и CMS
Необходимо учитывать типовые сценарии использования системы при проектировании БД и вносить соответствующие оптимизации при необходимости
Не стоит пренебрегать различными инструментами: нагрузочными тестами, аналитикой
Дьявол кроется в мелочах: проблемы могут скрываться в тех участках, на которые сходу и не подумаешь
Спасибо за внимание!