Erlang Rebar and GProc Tutorial
Использование Rebar
При разработке на Erlang часто приходится собирать зависимости из разных источников, следить за их нужными версиями, создавать OTP-релизы для распространения проектов. Дела достаточно рутинные и неприятные. Для того, чтобы разработка меньше доставляла неприятных моментов, компанией Basho был создан очень удобный инструмент — Rebar. В этой статье я постараюсь раскрыть преимущества от его использования на реальном примере с использованием сторонних зависимостей и созданием конфигурируемых OTP-релизов.
Скачать Rebar можно по адресу http://hg.basho.com/rebar/downloads/rebar. Это один единственный файл, вмещающий в себя несколько beam-модулей. Его надо разместить в любом удобном месте, входящем в путь поиск исполняемых файлов PATH, например,
~/bin/
или/usr/local/bin/
.Приступим к проекту.
Для начала создаём директорию, в которой будет находиться наш проект (
gpt
) и переходим в неё:$ mkdir gpt && cd gpt
В ней создадим поддиректорию, где будем сохранять исходные файлы непосредственно нашего приложения:
$ mkdir -p apps/gpt && cd apps/gpt
Делаем скелет приложения:
$ rebar create-app appid=gpt
Параметр appid определяет имя нашего приложения и соответственно префикс исходных файлов.
$ ls -1 src gpt_app.erl gpt.app.src gpt_sup.erl
Добавляем в заготовку для .app-файла (
src/gpt.app.src
) описание приложения и зависимость от gproc:{description, "GProc tutorial"}, ... {applications, [ kernel, stdlib, gproc %Возвращаемся назад в директорию верхнего уровня, где хранится наш проект, создаём в ней поддиректориюrel
и переходим туда:$ cd ../../ $ mkdir rel && cd relВ rel будут лежать файлы, необходимые для создания релиза — всего того, что требуется для запуска проекта, все его runtime-зависимости.С помощьюrebar
создадим заготовку для ноды, передав её имя в параметреnodeid
:$ rebar create-node nodeid=gptnodeРедактируем файлreltool.config
:... {lib_dirs, ["../deps", "../apps"]}, %Далее можно отредактировать файлfiles/vm.args
, изменив, допустим, имя ноды:-name gptnode@127.0.0.1на-sname gptnode@localhostВернёмся в директорию верхнего уровня:$ cd ../и создадим файлrebar.config
со следующим содержанием:%% Здесь будут лежать зависимости {deps_dir, ["deps"]}. %% Поддиректории, в которые rebar должен заглядывать {sub_dirs, ["rel", "apps/gpt"]}. %% Опции компилятора {erl_opts, [debug_info, fail_on_warning]}. %% Список зависимостей %% В директорию gproc будет клонирована ветка master соответствующего git-репозитория. {deps, [ {gproc, ".*", {git, "http://github.com/esl/gproc.git", "master"}} ]}.Теперь у нас всё готово для создания релиза. Выполним несколько командrebar
(вывод команд опущен):$ rebar get-deps $ rebar compile $ rebar generateКомандаget-deps
скачивает зависимости. В нашем случае, это приложение gproc. Командаcompile
, очевидно, вызывает компиляцию всех исходных файлов, аgenerate
создаёт релиз.Директориюrel/gptnode
можно смело перемещать на другие хосты (разумеется, при условии бинарной совместимости, так как релиз включает в себя и виртуальную машину Erlang). После создания релиза запускаем то, что получилось:(cd rel/gptnode && sh bin/gptnode console)Убедимся в том, что все нужные приложения запущены:(gptnode@localhost)1> application:which_applications(). [{sasl,"SASL CXC 138 11","2.1.9.2"}, {gpt,"GProc tutorial","1"}, {gproc,"GPROC","0.01"}, {stdlib,"ERTS CXC 138 10","1.17.2"}, {kernel,"ERTS CXC 138 10","2.14.2"}]Нас интересуют gpt и gproc. Как видно, они в этом списке присутствуют.Использование gproc
Итак, сrebar
разобрались, научились создавать простенький проект и работать с ним. Приступим к gproc.Как известно, приложения в Erlang, как правило, состоят из множества процессов, которые обмениваются сообщениями.Чтобы процессы знали, кому какое сообщение посылать, необходимо иметь регистратор, преобразующий какие-то координаты в идентификатор процесса. По умолчанию Erlang/OTP предоставляет регистрацию процессов под именем-атомом. Это расточительно, так как атомы не собираются сборщиком мусора, и, создавшись один раз, живут до завершения работы всей ноды, что обязательно приведёт к исчерпанию всей памяти при необходимости регистрировать процессы под уникальными именами. К тому же подобный подход неудобен, так как пришлось бы преобразовывать разные термы в атом, сочинять для этого какие-то правила, кроме того, процесс можно зарегистрировать только под одним именем. Регистрация процессов под именем-атомом с помощью функцииerlang:register/2
допустима лишь для небольшого числа долгоживущих процессов, имя которых не должно меняться, аналог — глобальные переменные в императивных языках программирования.Для обхода этих ограничений зачастую используется следующая схема:
- запускается процесс-регистратор, создающий ets-таблицу и являющийся её владельцем;
- при запуске процессов, нуждающихся в регистрации, они посылают регистратору сообщение, содержащее координаты для регистрации (любой erlang-терм) и свой идентификатор;
- регистратор записывает это сопоставление в ets-таблицу и включает мониторинг процесса с помощью
erlang:monitor/2
;- регистрируемый процесс при завершении либо явно посылает сообщение о своей дерегистрации, либо регистратор получает сообщение
'DOWN'
при падении этого процесса, после чего удаляет его запись из ets-таблицы;Схема эта очень часто используется, почти в каждом приложении есть своя реализация, со своими особенностями и багами. Разумеется, что возникает естественное желание заменить этот регистратор на что-то единственное. И решение проблемы пришло в виде разработчика Ulf Wiger и его приложения gproc (https://github.com/esl/gproc).С API приложения можно ознакомиться на странице https://github.com/esl/gproc/blob/master/doc/gproc.md.Локальная регистрация
Рассмотрим простейший случай — локальная (на текущей ноде) регистрация процесса по произвольному терму.Исходный код для примеров можно взять здесь: http://github.com/Zert/gproc-tutorial.gitКод процессов, которые мы будем регистрировать через gproc, находится в файлеgpt_proc.erl
. Вgpt_sup.erl
находится код супервизора этой группы процессов. При вызове функцииgpt_sup:start_worker/1
будет запускаться наш процесс и регистрироваться под тем именем, которое передаётся в функцию в качестве единственного аргумента. В данном случае, это число.Запустили ноду, используя вышеупомянутую команду, и выполнили в ней серию запусков процессов с разными идентификаторами:(gptnode@localhost)1> [gpt_sup:start_worker(Id) || Id },{ok,},{ok,}]Вызов функцииgproc:add_local_name(Name)
регистрирует процесс, её вызывающий, под именемName
(эта функция является просто обёрткой надgproc:reg({n,l,Name})
, гдеn
—name
,l
—local
). После этого функцияgproc:lookup_local_name(Name)
будет возвращать идентификатор процесса.Теперь скажем одному из процессов, чтобы он начал ждать запуска и регистрации процесса с именем 4. Код, отвечающий за это:handle_info({await, Id}, #state{id = MyId} = State) -> gproc:await({n, l, Id}), ?DBG("MyId: ~p.~nNewId: ~p.", [MyId, Id]), {noreply, State};Здесь функцияgproc:await/1
вызывается с аргументом, имеющим следующий вид:{n, l, Id}
. Почему-то она не имеет обёртки, ну да ладно.(gptnode@localhost)2> gproc:lookup_local_name(1) ! {await, 4}. {await,4}Запустив процесс с идентификатором 4, увидим сначала сообщение от него, а затем от первого ждущего процесса:(gptnode@localhost)3> gpt_sup:start_worker(4). (gpt_proc:29) Start process: 4 (gpt_proc:45) MyId: 1. NewId: 4. {ok,}Сделаем остановку процесса по приёму сообщенияstop
:handle_info(stop, State) -> {stop, normal, State};и остановим его:(gptnode@localhost)4> gproc:lookup_local_name(1) ! stop. stopПосле этого процесс автоматически удаляется из базы данных регистратора:(gptnode@localhost)5> gproc:lookup_local_name(1). undefinedГлобальная регистрация
Общеизвестно, что Erlang является дико распределённым. Под этим подразумевается прозрачный обмен сообщениями между узлами, другими словами, имея идентификатор процесса, можно послать ему сообщение, не зная, на каком узле он находится. Локальная регистрация процесса с помощью gproc позволяет сопоставить произвольный терм идентификатору процесса в пределах одной ерланг-ноды, при этом на другой ноде нельзя получить значение идентификатора, используя этот терм.Для того, чтобы любая нода в кластере имела возможность регистрировать свои процессы так, чтобы они были доступны и с других нод, имеется глобальная регистрация. GProc реализует вызовgproc:add_global_name/1
, позволяющий осуществлять это действие. Рассмотрим на примере.Сначала соорудим две ноды, объединённые в кластер, иrebar
нам в этом поможет, так как имеет возможность создавать конфигурационные файлы по заданному шаблону. При создании кластера необходимо учесть следующие детали:
- Установить одинаковые значения cookie у узлов
- Задать им разные имена
- Передать соответствующие параметры обязательному приложению
kernel
на каждом узле - При использовании GProc, передать ему желаемые роли узлов
Первые два пункта устанавливаются в файле
files/vm.args
:## Name of the node -sname {{node}} ## Cookie for distributed erlang -setcookie gptnode
Здесь
{{node}}
— плейсхолдер, который будет заполняться при создании релиза. Параметр виртуальной машины -setcookie
устанавливает значение cookie для этой ноды, в кластере у всех нод эти значения должны быть одинаковыми.Вторые два пункта устанавливаются в файле
files/app.config
. Здесь тоже будут использоваться плейсхолдеры:%% GProc {gproc, {{ gproc_params }} }, %% Kernel {kernel, {{ kernel_params }} },
Для заполнения плейсхолдеров укажем в файле
reltool.config
, что предыдущие два файла надо обрабатывать как шаблоны:{template, "files/app.config", "etc/app.config"}, {template, "files/vm.args", "etc/vm.args"}
Создаём два конфигурационных файла, по одному на каждую ноду:
vars/dev1_vars.config
и vars/dev2_vars.config
. В файлеdev1_vars.config
будут находиться следующие значения плейсхолдеров:%% etc/app.config {gproc_params, "[ {gproc_dist, {['gpt1@localhost'], [{workers, ['gpt2@localhost']}]}} ]"}. {kernel_params, "[ {sync_nodes_mandatory, ['gpt2@localhost']}, {sync_nodes_timeout, 15000} ]"}. %% etc/vm.args {node, "gpt1@localhost"}.
Для файла
dev2_vars.config
параметры sync_nodes_mandatory
и node
поменяются местами. Разберём их подробнее.Параметр
gproc_dist
относится к приложению gproc, он является кортежом из двух списков. Первый список — узлы, которые способны становиться лидером (master), второй список содержит key-value кортежи, нам пока достаточно лишь одного ключа — workers
, который задаёт список нод, являющихся простыми участниками кластера (slave).К приложению kernel относятся два параметра. Первый,
sync_nodes_mandatory
— список нод, которые обязаны присутствовать в кластере. Второй, sync_nodes_timeout
— время в миллисекундах, которое каждый из узлов будет ожидать появления узлов из предыдущего списка. Если в течение этого времени ноды не появились, то нода остановится. Сделаем его значение 15 секунд, дабы успеть запустить их оба руками.Значение
node
будет записано в параметры запуска виртуальной машины, это её имя.Теперь создадим два релиза с помощью следующего правила из Makefile:
dev1 dev2: mkdir -p dev (cd rel && rebar generate target_dir=../dev/$@ overlay_vars=vars/$@_vars.config)
Переходим в директорию
dev/dev1
, запускаем второе окно терминала (или создаём новое окно в screen), переходим в директорию
dev/dev2. Запускаем в каждом шелле
./bin/gptnode console`. Посмотрим список доступных нод в первом ерланговском шелле:(gpt1@localhost)1> nodes(). [gpt2@localhost]
Видим, что вторая нода нормально запустилась и подключилась к кластеру. Чтобы долго не мудрить, зарегистрируем глобально процесс текущего шелла под каким-нибудь термом:
(gpt1@localhost)2> gproc:add_global_name({shell, 1}). true
В другом окне попробуем запросить идентификатор процесса по этому терму:
(gpt2@localhost)2> gproc:lookup_global_name({shell, 1}).
Как видим, успешно. Послав этому процессу сообщение, мы сможем получить его на первой ноде:
(gpt2@localhost)3> gproc:lookup_global_name({shell, 1}) ! {the,message}. message
Читаем его на первой ноде командой
flush()
:(gpt1@localhost)3> flush(). Shell got {the,message} ok
Комментариев нет:
Отправить комментарий