Представьте: горячий металл, мощные машины, сотни работников — наше производство постоянно подвержено различным рискам. И как мы превратили эти вызовы в возможности? Этот рассказ будет о том, как мы воплотили в жизнь инновационную для компании систему сбора заявок об опасностях, о наших успехах, о трудностях, с которыми мы столкнулись, и о том, какие уроки мы извлекли из этого опыта. Давайте окунемся в мир цифровых решений и безопасности труда вместе!

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

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

С чего все начиналось

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

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

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

Сперва система была небольшой и состояла из формы для сбора информации о характере опасности и инструментов для обработки заявки ответственными. С течением времени система разрасталась и усложнялась.

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

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

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

  • 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
IBLOCK_PROPERTY_ID
IBLOCK_ELEMENT_ID

VALUE_NUM

decimal

Для хранения чисел

VALUE_NUM
IBLOCK_PROPERTY_ID

VALUE_ENUM

integer

Для хранения ссылок на элементы других таблиц

VALUE_ENUM
IBLOCK_PROPERTY_ID

Сортировка и фильтрация большого количества записей по полю типа text не лучшая идея.

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

Описание таблицы для хранения свойств v2
Описание таблицы для хранения свойств v2

Свойства типа дата основаны на базовом типе «строка», который хранится в колонке с типом text. Для наших нужд это не подходит: мы хотим быстро фильтровать и сортировать.

Подумав над совместимым решением с текущей логикой инфоблоков, мы создали свой тип свойства, который основан на базовом типе «число» (NUMBER). Значения этого типа свойств хранятся в колонке с типом decimal, более подходящим для наших нужд.

Переезд

Определить проблемы и придумать для них решения было не самой сложной задачей. Большим испытанием оказался переезд на вторую версию инфоблоков.

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

Не было возможности приостановить работу сервиса — его непрерывная доступность была нашим приоритетом. Стоит принять во внимание и свойства с новым способом хранения, добавленные для улучшения производительности. Поэтому мы приняли решение о постепенном переходе на новый информационный блок. Мы создали новый инфоблок рядом с существующим и написали агент, который постепенно перемещал элементы из старого инфоблока в новый. Чтобы в будущем иметь возможность найти старые заявки, мы поместили старые идентификаторы в поле EXTERNAL_ID.

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

Последние штрихи

На этот раз мы ответственно подошли к проработке основных сценариев использования страницы списка заявок и выделили наиболее частые сценарии фильтрации заявок:

  • дата выявления

  • статус заявки

  • локация, где выявлена опасность

  • срок устранения

По умолчанию на странице применен фильтр для отображения заявок текущего года. Также сотрудники активно сортировали и фильтровали заявки по другим ключевым полям. Логичным решением было создать индексы по этим полям.

#

Пояснение

Индекс

1

Наиболее частый сценарий и потребность пользователей

Активность,
Дата обнаружения,
Статус

2

Заявки распределены по трехуровневому классификатору локаций.
Это еще один из частых сценариев использования: руководители ориентируются на свои цеха и подразделения, сотрудники просматривают заявки по своему месту работы

Локация 1,
Локация 2,
Локация 3

3

Многие выборки в сервисе построены относительно этого поля

Срок устранения

После добавления индексов скорость отклика страниц в типовых сценариях взлетела.

Итоги оптимизации

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

Выводы

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

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

Есть и другая поговорка, которая гласит: "Лучшее — враг хорошего". Застревание в стремлении к идеальному решению иногда может замедлить процесс и привести к упущенным возможностям. Важно найти баланс между достижением оптимального результата и возможностью быстро адаптироваться к изменяющимся условиям и требованиям.

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

Для тех, кто дочитал эту статью, оставлю несколько важных рекомендаций:

  • Важно ответственно подходить к анализу структур для хранения данных, создаваемым фреймворками и CMS

  • Необходимо учитывать типовые сценарии использования системы при проектировании БД и вносить соответствующие оптимизации при необходимости

  • Не стоит пренебрегать различными инструментами: нагрузочными тестами, аналитикой

  • Дьявол кроется в мелочах: проблемы могут скрываться в тех участках, на которые сходу и не подумаешь

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

Комментарии (6)


  1. FanatPHP
    05.11.2024 10:46

    Свойства типа дата основаны на базовом типе «строка», который хранится в колонке с типом text. Для наших нужд это не подходит: мы хотим быстро фильтровать и сортировать.

    Подумав над совместимым решением с текущей логикой инфоблоков, мы создали свой тип свойства, который основан на базовом типе «число» (NUMBER). Значения этого типа свойств хранятся в колонке с типом decimal, более подходящим для наших нужд.

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


  1. Tippy-Tip
    05.11.2024 10:46

    А какая СУБД используется для данного сервиса?


    1. LaynN Автор
      05.11.2024 10:46

      Используется совместимая с Битрикс СУБД Percona(MySQL)


  1. MIkhail_Loban
    05.11.2024 10:46

    После слова "инфоблоки" (даже не HL) начал читать по диагонали, извиняюсь. Как будто следующим шагом напрашиется кликхаус с большой денормализованной таблицей, проекциями, матвьюхами и словарями (хотя что-то мне подсказывает, что объёмы у вас даже маловаты для матвьюх, хватит и партицирования). Переезд будет безболезненным практически. С битрой дружится очень хорошо. Рекомендую. Есть даже варианты репликаций mysql -> ch. Проект взлетит и вы избавитесь от кучи детских болячек битры.

    Сам всю жизнь в ней варюсь.


    1. LaynN Автор
      05.11.2024 10:46

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

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

      Благодарю за рекомендации и проявленный интерес к публикации.


    1. FanatPHP
      05.11.2024 10:46

      А там точно аналитика нужна? На мой взгляд для описанной задачи куда лучше подойдет Эластик/Сфинкс. И переезжать никуда не понадобится.