...

суббота, 7 декабря 2019 г.

[Из песочницы] Как завести pet project и не получить выгоды

Как завести pet project и не получить выгоды


TL;DR

Статья описывает использование pet project как способ поддержания и улучшения навыков. Автор создал PHP библиотеку для установки ФИАС из XML файлов.


Цель

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

После запуска очередного типового сайта, я решил, что пора что-то менять. Работу менять не хотелось, зато я вспомнил как на одной из конференций докладчик рекомендовал использовать собственный факультативный проект, так называемый pet project, для обучения. Метод показался подходящим и я решил попробовать.


Выбор задачи

Выбор задачи оказался самой сложной частью затеи. В голову ничего особенного не приходило: какие-то сервисы, вроде парсера вакансий, которые легко можно реализовать на привычном стеке. Я забросил мысль о проекте на несколько месяцев, пока случайно не увидел новость о хакатоне министерства финансов. В нем предлагалось использовать один из списка источников открытых данных для создания сервиса. Среди прочих была указана и федеральная информационная адресная система (ФИАС). К сожалению, хакатон к тому времени уже закончился.

О ФИАС я узнал впервые, но задача показалась интересной. Судите сами: около 30 Гб XML файлов, порядка 60 миллионов строк в базе данных и, более того, библиотека потом могла оказаться полезной в работе. На Github нашлось несколько готовых решений, но это меня не остановило. Наоборот, на основании их анализа я составил дополнительные требования, которые выгодно выделили бы мою реализацию.

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


Формулировка задачи

90% успеха — это правильная постановка задачи. После нескольких лет шаблонной работы было довольно сложно заставить себя чётко сформулировать задачу. Хотелось просто начать работать, а уже в процессе всё само собой бы прояснилось. Спустя час борьбы с прокрастинацией я, наконец, записал: создать библиотеку на PHP для импорта данных ФИАС.

Позже, войдя во вкус, я добавил несколько дополнительных требований:


  • реализация на PHP без использования сторонних утилит, исключительно код на PHP и расширения из PECL,
  • импорт всех данных из набора ФИАС,
  • полный цикл установки и обновления: поиск нужной версии, получение архива, распаковка, запись в базу,
  • максимальная гибкость: возможность изменять место хранения, модифицировать данные перед записью, фильтровать нужные и т.д.,
  • библиотека должна легко встраиваться в существующие проекты.

ФИАС

У ФИАС есть официальный сайт, который даёт нам определение и цель создания системы


Федеральная информационная адресная система (ФИАС) — федеральная государственная информационная система, обеспечивающая формирование, ведение и использование государственного адресного реестра.

Целью создания ФИАС является формирование единого федерального ресурса, содержащего достоверную, единообразную, общедоступную, структурированную адресную информацию. Благодаря внедрению ФИАС эту информацию можно бесплатно получить через Интернет на официально зарегистрированном портале ФИАС.

Материалов с описанием вполне достаточно как на сайте ФИАС, так и на Хабре, поэтому не буду заострять на этом внимание.

Вкратце. ФИАС поставляется в двух форматах: ФИАС и КЛАДР. Второй устарел и выводится из использования. Информация хранится либо в DBF, либо в XML. Каждое изменение в составе ФИАС помечается новой версией. Можно запросить либо пакет с полными данными, актуальными на текущий момент, либо, содержащий только изменения между двумя версиями. Ссылки предоставляет SOAP сервис. Пакет представляет из себя RAR архив, содержащий файлы со специально сформированными названиями. Они состоят из префикса, имени набора данных и даты формирования. Есть два типа префиксов: AS_ для файлов, данные из которых нужно добавить в базу, и ASDEL для файлов, данные из которых следует удалить из базы.

ФИАС содержит следующие данные:


  • реестр адресообразующих элементов (это и есть граф адресов: областей, городов и улиц),
  • элементы адреса, идентифицирующие адресуемые объекты (номер дома и данные о доме),
  • сведения о земельных участках,
  • сведения о помещениях (квартирах, офисах, комнатах и т.д.),
  • сведения по нормативному документу, являющемуся основанием присвоения адресному элементу наименования.

А также несколько словарей
  • перечень возможных значений интервалов домов (обычный, чётный, нечётный),
  • перечень статусов актуальности записи адресного элемента по классификатору КЛАДР4.0,
  • перечень статусов актуальности записи адресного элемента по ФИАС,
  • перечень полных, сокращённых наименований типов адресных элементов и уровней их классификации,
  • перечень видов строений,
  • перечень возможных видов владений
  • перечень кодов операций над адресными объектами,
  • перечень возможных состояний объектов недвижимости,
  • перечень типов помещения или офиса,
  • перечень типов комнат,
  • перечень возможных статусов (центров) адресных объектов административных единиц,
  • типы нормативных документов.

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

В конечном итоге мы имеем довольно простой и линейный алгоритм установки ФИАС:


  • получить из SOAP сервиса ссылку на архив и номер текущей версии,
  • скачать архив,
  • распаковать,
  • записать в базу все данные из файлов с префиксом AS_,
  • удалить из базы все данные из файлов с префиксом ASDEL (да, именно так, при установке тоже приходится удалять часть данных),
  • записать номер установленной версии.

И не менее простой алгоритм обновления:


  • получить из SOAP сервиса список с номерами версий и ссылками на файлы с изменениями,
  • если текущая версия в локальной базе является последней, то остановить выполнение,
  • получить ссылку на архив с изменениями до следующей версии,
  • скачать архив,
  • распаковать,
  • записать в базу все данные из файлов с префиксом AS_,
  • удалить из базы все данные из файлов с префиксом ASDEL,
  • записать номер обновлённой версии,
  • вернуться на первый шаг.

ФИАС оставляет противоречивые впечатления. С одной стороны: полная автоматизация всего процесса, открытые форматы, хорошая документация. С другой: странное решение использовать проприетарный RAR для открытых данных; отличия документации от реальности (в основном это касается обязательности атрибутов), которые доставляют много небольших, но неприятных проблем; изредка приходят архивы, которые не удаётся распаковать в Linux; некоторые дельты между версиями занимают по 4-5 Гб.


Архитектура

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

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

Как итог, библиотека содержит четыре основных типа объектов, для каждого из которых чётко определена зона ответственности:


  • сервисы — предоставляют инструменты для выполнения низкоуровневых системных задач,
  • объект состояния — хранит информацию для передачи между операциями,
  • операции — при помощи сервисов и состояния реализуют атомарную часть бизнес логики,
  • цепочка операций — выполняет операции и передаёт состояние между ними.

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


Фреймворки

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

Первая относительно стабильная версия была готова за два месяца работы по вечерам. Фактически она могла существовать отдельно от фреймворка и содержала всё необходимое: входной скрипт для запуска в консоли, DI контейнер, надстройку над PDO, собственный логгер и миграции структуры БД — чем я очень гордился.

Конечно же, коллеги её беспощадно забраковали.

Главным аргументом против стало отсутствие поддержки популярных фреймворков. Никто не хотел писать отдельную обёртку для библиотеки. Из-за этого я допустил самую дорогостоящую по времени ошибку: начал поддерживать как standalone версию, так и отдельные обёртки для каждого фреймворка. Реальные файлы ФИАС отличаются от того, что написано в документации. Каждый раз, когда нужно было убрать или добавить, например, not null в описание колонки, приходилось вносить изменения в три репозитория. Из-за утомительности процесса работа застопорилась ещё на полгода.

Ощущение незавершённости терзало меня всё это время и после кровавой схватки с ленью заставило вернуться к проектированию новой версии. Для начала я решил, что standalone библиотека никому не нужна, а, значит, следует выкинуть из пакета все сервисы, которые предоставляют фреймворки, заменив их интерфейсами. Так под нож пошли: входной скрипт для запуска в консоли, DI контейнер, надстройка над PDO, собственный логгер и миграции структуры БД. Следом я решил делать отдельные пакеты для каждого фреймворка, которые будут соединять все части из основного в рабочий скрипт и предоставлять конкретные реализации сервисов.

Ключевым моментом стали модели. Постоянно обновлять разнотипные наборы объектов в нескольких репозиториях не хотелось. В это же время на основной работе мне достался проект на Symfony. После беглого ознакомления с ним я решил, что наиболее полезная функция SF — это кодогенерация и именно она решит все мои проблемы. Я создал yaml файл в основном пакете, который содержит декларативное описание данных ФИАС. Затем я добавил кодогенераторы, которые на основе этого описания создают конкретные классы для моделей: сущности Doctrine для Symfony и объекты Eloquent для Laravel. Во время разработки генераторов я понял, что twig-шаблоны для этого не подходят, и остановился на специализированном решении — Nette PHP Generator.

В качестве proof of concept я создал бандлы для Laravel и Symfony. Поскольку со вторым я работал дольше, то всё последующее буду описывать в его контексте.


Инфраструктура

Большая часть моих боевых проектов была написана на устаревших технологиях, поэтому ни на одном из них я не мог применить современные анализаторы кода. Избавившись от гнёта legacy, я установил и настроил все инструменты контроля качества кода, какие смог:


  • Psalm для проверки типов,
  • Scrutinizer для общей оценки качества (хорошо помогает с цикломатической сложностью),
  • PHP Coding Standards Fixer для проверки стиля кода,
  • PHP Copy/Paste Detector для поиска дубликатов,
  • PHPUnit для запуска unit тестов.

Интегрировал проверки в Github с помощью Travis. В качестве финального штриха добавил Docker файл для создания локального окружения разработчика в комплекте с make файлом, который содержит основные команды для контейнера (запуск проверок, тестов, создание моделей и другие).


Итоги обучения


PHP 7

До начала работы над библиотекой я ни разу по-настоящему не использовал новые возможности PHP 7. Они прекрасны: от строгих типов до значительного роста производительности. Отдельное сердечное спасибо разработчикам за null coalescing operator. Такого серьёзного уменьшения кодовой базы после введения одного оператора я ещё не видел.


RAR

Удивительно, но в PECL нашёлся пакет для работы с RAR. Обычно такие расширения не вызывают доверия и я стараюсь их избегать. Это же оказалось подозрительно стабильным: без проблем установилось в 7.2, относительно быстро и с малым потреблением оперативной памяти смогло распаковывать огромные архивы (6 Гб распаковывается за 10-20 минут в зависимости от доступных ресурсов системы). До сих пор опасаюсь, что это некое проявление закона Мёрфи.


XmlReader

Прочитать гигантские xml файлы — нетривиальная задача. И снова на помощь пришло PECL расширение — XmlReader. Я далеко не сразу осознал всю его мощь, но в несколько подходов приспособил в связке с Symfony serializer для быстрого и экономного получения данных из файлов ФИАС. На стороне библиотеки объект для чтения реализует интерфейс итератора, который последовательно возвращает строки с xml, соответствующие одной записи в файле. С помощью Symfony serializer эти строки преобразовываются в объекты. 20 Гб файл читается за 3-4 минуты при этом используется не более 50 Мб оперативной памяти.


Запись в базу данных

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

Волшебство сущностей Doctrine показало насколько объекты могут быть самоописательными. Я решил использовать тот же подход, а заодно избавиться от собственной реализации записи данных в базу с помощью PDO. Взамен я создал интерфейс Storage, который описывает методы обработки объектов. На основании класса сущности конкретная реализация Storage решает как именно и куда записать данные. Такой подход позволил легко подключать самые разнообразные хранилища: от MySql до csv-файлов.


Оптимизация вставки данных

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

Сначала я перешёл на встроенные в PostgreSql колонки типа uuid для первичных ключей. Запись в uuid колонку с индексом значительно быстрее, чем запись в строку.

Следом я отказался от всех некритичных индексов и внешних ключей, поскольку забота о целостности данных полностью на стороне команды ФИАС.

Затем я переделал интерфейс Storage так, чтобы внешний скрипт мог явно проинформировать его о завершении импорта. Это позволило использовать bulk insert, который в разы ускорил запись. В поисках информации я также наткнулся на команду copy вместе с query_to_xml. У неё было два больших недостатка: во-первых, пользователь PostgreSql должен иметь права на чтение файла, чего я никак не мог гарантировать, а, во-вторых, терялась возможность модифицировать данные внутри скрипта перед записью.

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


Параллельные процессы

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

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

Первым я решил попробовать расширение Parallel. У него есть один фатальный недостаток — интерпретатор должен быть собран с поддержкой ZTS (Zend Thread Safety). Поскольку ZTS не работает в обычных web скриптах, пришлось бы иметь две разных версии интерпретатора. Одну, без ZTS, для web, вторую, с ZTS, для установки ФИАС. Потенциальный прирост производительности перевешивал это неудобство, особенно с учётом того, как легко собрать новый Docker контейнер и использовать его совместно со старым. К сожалению, запуск Symfony внутри нового потока вызывал переполнение стека PHP, а отказываться от DI контейнера и удобной конфигурации я был не готов.

Наконец, я нашёл Symfony process. Фактически он запускает новый процесс для указанной консольной команды и следит за его завершением. Пришлось добавить две дополнительные цепочки. Первая скачивает архив, распаковывает его и инициирует параллельные процессы для обработки данных. Вторая принимает список файлов из аргумента командной строки и записывает их содержимое в базу.

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

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

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

После этих правок я смог добиться импорта полной версии ФИАС за 16-20 часов в зависимости от ресурсов сервера. Не так хорошо, как хотелось бы, но я продолжаю работу над оптимизацией. На очереди полный отказ от PostgreSql в пользу Elasticsearch.


Выводы

Стоило ли оно того? Два года работы над библиотекой, которая так и не попала ни в один боевой проект?

Да, полностью.

Работу я всё же сменил. Во время тура по десятку собеседований я ответил на много каверзных вопросов только благодаря своему pet project.

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

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

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий