Введение
Многие книги по столярному и плотницкому делу начинаются с рассказа о правильной организации рабочего места и инструмента. Мне хочется верить, что навыки разработки – это тоже культура и мастерство. Рациональный подход к рабочему окружению позволяет снизить стоимость разработки и последующей доработки проекта благодаря раннему обнаружению проблем и повышению производительности труда разработчика. Тема, конечно же, обширна, и я планирую написать цикл статей:
- Создание простейшего приложения Erlang (эта статья)
⋅⋅⋅ Использование docker для изоляции рабочей среды
⋅⋅⋅ Управление зависимостями и сборка Erlang проекта
⋅⋅⋅ Борьба за качество результата - Кластерные и распределенные системы. Виртуализация рабочего окружения
⋅⋅⋅ Использование docker-compose для разработки и тестирования на примере приложения, использующего внешние хранилища данных (riak, tarantool).
⋅⋅⋅ Service Discovering используя Consul
⋅⋅⋅ Мониторинг среды разработки. - Комплексное тестирование: от unit-тестов до UI через интеграционное тестирование не прерывая логического контекста.
⋅⋅⋅ Продолжение обзора Common Test
⋅⋅⋅ Введение в Selenium и Selenium Hub
⋅⋅⋅ Интеграция Selenium Python и Common Test в рамках созданной рабочей среды.
После прочтения данного цикла у читателя должно сложиться представление о подходах, применяемых в современной разработке и тестировании проектов различного уровня, от утилит до распределенных кластерных систем. В данной статье разбирается инструментарий и простейшая песочница. Если тема будет интересна, продолжу цикл по кластерным системам на Erlang, Golang, Elixir, Rust.
Замечание: Для успешной работы с данной статьей на вашей машине должен быть установлен docker, docker-compose и GNU Make. Установка docker не занимает много времени, необходимо лишь помнить, что ваш пользователь должен быть добавлен в группу docker.
Данный код проверен только на debian-like дистрибутивах.
Пример приложения
Итак, давайте попробуем создать атомарный счетчик (atomic counter). Приложение должно выполнять следующие функции:
- increment
- decrement
- reset
И соответствовать требованиям:
- целевая система: ubuntu 16.04 LTS
- иметь простейшее HTTP API
- работать на Erlang/OTP >=19.0
- стабильно выдавать больше 1k RPS при смешанной нагрузке на запись и чтение в конкурентной среде от 1 до 100 клиентских потоков.
- иметь начальный интерфейс мониторинга внутренних процессов приложения
В тексте статьи изложены принципы и мотивация принятия тех или иных решений, но нет примеров или цитат кода. Весь код доступен в репозитории: https://github.com/Vonmo/acounter
Изоляция и виртуализация инфраструктуры
В настоящее время существует множество путей решения задачи виртуализации окружения. Все они обладают своими плюсами и минусами, и часто выбрать довольно сложно. Для разработки серверного и распределенного программного обеспечения я предлагаю использовать Docker контейнеризацию, так как docker гибкий и современный инструмент, который позволяет снизить расходы на оборудование на этапе разработки, улучшить процессы тестирования и в некоторых случаях упростить доставку приложений конечным пользователям. Например, нет проблемы запустить на среднем ноутбуке кластер из 12-15 контейнеров с разнообразными сервисами, смоделировать взаимодействие этих сервисов и написать интеграционные тесты в среде приближенной к боевой, а также проверить масштабирование ваших сервисов или обработать и оттестировать отказы, в том числе крупные аварии и восстановление после них.
Замечание: docker и docker-compose предлагаются как решение для этапа разработки: рабочее окружение программистов, staging для тестировщиков. Обоснование основы для боевого окружения выходит за рамки данной статьи.
Поскольку наша среда разделена на 2 уровня, хост и контейнеры, нам необходимо два makefile:
- Makefile отвечает за внешнее управление виртуальной средой: создание, запуск, остановка необходимого набора контейнеров, ручки для запуска задач компиляции и анализа кода
- docker.mk отвечает за все, что происходит с кодом внутри контейнеров.
В docker-compose.yml находится описание всех контейнеров нашего кластера:
- Base. Для выполнения п.2. требований создаем базовый образ. Упаковываем в него Erlang/OTP 19.3 и весь необходимый софт.
- Test. Данный контейнер унаследован от Base и в него прозрачно смонтирован код.
Замечание: если необходимо гарантировать работу вашего приложения на других версиях erlang, базовый образ дополняется этими версиями. В базовом образе уже установлен kerl и все что нам остается сделать, это добавить в базовый образ необходимую версию erlang, а в makefile дополнительные строки для всех версий erlang и запуска тестов на них.
Для управления виртуальной средой в makefile предопределены следующие цели:
$ make build_imgs
– создает необходимые образы docker$ make up
– запускает и настраивает контейнеры$ make down
– очищает тестовую среду
Управление зависимостями и сборка Erlang проекта
Многие программы, которые приходится анализировать и разрабатывать, имеют сторонние зависимости. Это может быть как зависимость от кода в виде сторонней библиотеки, так и зависимость от утилиты, например, утилиты миграции базы данных.
Зависимости от утилит и их версий, а также от бинарных библиотек, мы уже решили в прошлом пункте. Теперь кратко рассмотрим процесс управления зависимостями и сборки в Erlang. Наиболее популярные из де-факто существующих в эрланге способов – это erlang.mk и rebar. Поскольку я в повседневной практике использую rebar, на нем и остановимся.
Основные функции rebar:
- Rebar предлагает решение проблемы зависимостей, сборки релизов, настройки компилятора и окружений. В rebar3 появился lock-файл с версиями всех зависимостей и vendor-плагин. Мы можем выбрать стратегию работы с зависимостями, либо мы при каждой новой сборке (новая сборка – сборка с нуля, т.е зависимости не скачаны) скачиваем все зависимости и раскладываем их в директории сборки, либо мы, используя vendor-плагин, замораживаем все зависимости в локальной директории и держим их в репозитории приложения.
- Также rebar3 предлагает способ упаковки релиза с помощью relx. Сборка релизов в erlang тоже обширная тема и заслуживает отдельной статьи. В данном примере реализован простейший релиз c продакшен окружением, т.е исходные коды не включены, отладочная информация убрана, и релиз готов к запуску на целевой системе без дополнительных манипуляций, т.е Erlang VM и обвязка скриптов для управления сервисом включены в поставку.
- Расширения позволяют запускать различные полезные утилиты (об этом ниже).
Для сборки и тестирования в makefile определены следующие цели:
$ make tests
– собирает тестовый профиль приложения и запускает все тесты.$ make rel
– собирает итоговый релиз
Борьба за качество результата
Пару слов о Common Test Framework
Стандартным подходом в инженерной практике является тестирование. Практически все предметы, окружающие нас, были разработаны с применением тестов в том или ином виде. В мире erlang существует два базовых фреймворка для тестирования: eunit и common test (далее CT). Оба эти инструмента позволяют оттестировать практически все аспекты работы проектируемой системы, вопрос только в сложности самого инструмента и подготовительных манипуляций перед непосредственным запуском тестов. Eunit предлагает путь модульного тестирования, а common test – это более гибкий и разноплановый инструмент с уклоном в интеграционное тестирование.
В CT есть четкая иерархия процесса тестирования. Спецификации позволяют настроить все аспекты запуска тестов. За ними следуют наборы, в которых группы тест-кейсов объединяются в логически завершенный блок. В рамках тестовой группы мы также можем настраивать порядок запуска тестов и параллельность, гибко конфигурировать тестовое окружение.
Гибкость в конфигурировании тестовой среды кроется в трехуровневой модели инициализации и завершения тестовых кейсов:
init_per_suite/end_per_suite
– вызывается один раз при запуске конкретного набораinit_per_group/end_per_group
– вызывается один раз для заданной группыinit_per_testcase/end_per_testcase
– вызывается перед каждым тестом в группе.
Наверняка каждый, кто разрабатывал через тесты и применял eunit, сталкивался с ситуацией, когда плавающий тест отвалился, и из-за этого в тестовой среде остались, например, загруженные приложения, которые ломают инициализацию следующих тестов. Благодаря гибкости CT можно корректно обрабатывать подобные ситуации наряду с многими другими, а также сократить время прогонов всех тестов за счёт продуманной инициализации окружения.
Интеграция Xref
Итак, зачем нам может понадобиться xref? Если отвечать коротко, то для выявления зависимостей между функциями, приложениями и релизами, а также для обнаружения мертвых участков кода.
В крупных проектах часто случается так, что какой-то код становится мертвым. Причин масса: например, мы написали функцию А в модуле X, потом она переехала в модуль Z под названием A2, при этом все тесты успешно прошли, и разработчик забыл об X:A. Так как функция A была экспортирована, то компилятор не подсказал нам о том, что X:A не используется. Конечно, чем раньше мы уберем мертвый код, тем меньше будет кодовая база и, соответственно, затраты на ее поддержание.
Как работает Xref? Он проверяет все вызовы и сопоставляет их с определенными функциями в модулях. Если функция определена, но нигде не задействована, будет предупреждение. Также существует сценарий использования, когда нам нужно узнать все места, где используется тот или иной метод.
Для использования xref в рабочей среде предопределена цель:
$ make xref
Используем Dialyzer
В прошлом пункте мы разобрались, как выявить зависимости и неиспользуемые функции. А что если функция есть, она используется, но арность (количество аргументов) или сами аргументы не соответствуют определению? Или например случаи никогда не исполняемых ветвей в case и if операторах, лишние проверки в охранных выражениях или несоответствие декларации типов. Именно для поиска подобных расхождений служит dialyzer.
Для использования dialyzer в рабочей среде предопределена цель:
$ make dialyzer
Автоматическая проверка стиля оформления кода
Каждая команда решает для себя, каким стандартам оформления следовать, и следовать ли вообще. Большинство крупных проектов стараются придерживаться стандартов оформления, потому что данная практика снимает ряд проблем при поддержке кодовой базы.
Ввиду того, что для Erlang нет одной универсальной IDE, так как кто-то любит emacs, кто-то vim или sublime, то возникает проблема автоматической проверки. К счастью, существует интересный проект elvis, позволяющий без войн внутри команды следовать стандартам оформления.
Например, мы договорились, что перед пушем в репозиторий прогоняем проверку на соответствие стилям.
Для использования elvis предопределена цель:
$ make lint
Разработка приложения-счетчика
- Клонируем репозиторий
$ git clone https://github.com/Vonmo/acounter.git
- Запускаем песочницу
$ make build_imgs
$ make up
- Итерационно разрабатываем и тестируем основной функционал приложения. После каждой итерации запускаем тесты:
$ make tests
- Когда все тесты заработали и код логически завершен, необходимо провести нагрузочное тестирование, чтобы проверить нашу программу на соответствие пункту 4 требований.
В текущей реализации нагрузочное тестирование выполнено не совсем корректно, так как генератор нагрузки и исследуемая система работают на одной машине. Но даже такой код позволяет понять возможности нашей реализации.
Генератор перед запуском основных тестов прогревает систему, а затем выполняет ступенчатое увеличение нагрузки. - На данном этапе мы имеем полностью работоспособное приложение. Его можно упаковать в релиз и доставить конечным пользователям:
$ make rel
- Для проверки работоспособности релиза можно запустить его в режиме консоли
$ ./_build/prod/rel/acounter/bin/acounter console
И зайти на http://localhost:18085/. Если вы видите текст “The little engine that could.”, то релиз запустился и функционирует штатно.
Итоги
В качестве заключения, хотелось бы поблагодарить читателей за терпение и проявленный интерес к теме. Нам с вами удалось за короткий промежуток времени получить рабочую песочницу, позволяющую упростить и стабилизировать процесс разработки. В следующих статьях я постараюсь рассказать, как можно расширить данную песочницу для нужд разработки распределенных и многокомпонентных систем.
Erlang не самый популярный язык, но он отлично подходит для серверного ПО и soft realtime систем. И мне хотелось бы хоть немного оживить данную тему на хабре.