Исходный код Настройка и релизы
В этом последнем руководстве мы сделаем таблицу маршрутизации для нашего распределенного хранилища ключей-значений настраиваемой, а затем, наконец, упакуем программное обеспечение для производства.
Давайте сделаем это.
Среда приложения
До сих пор мы жестко закодировали таблицу маршрутизации в модуле KV.Router. Однако мы хотели бы сделать таблицу динамичной. Это позволяет нам не только настроить разработку/тестирование/производство, но также и позволить различным узлам работать с различными записями в таблице маршрутизации. Есть функция OTP, которая делает именно это: среда приложения.
Каждое приложение имеет среду, которая хранит специфическую конфигурацию приложения по ключу. Например, мы можем сохранить таблицу маршрутизации в среде приложения :kv, присвоив ей значение по умолчанию и позволяя другим приложениям изменять таблицу по мере необходимости.
Откройте apps/kv/mix.exs и измените функцию application/0 на возвращение следующего:
def application do
[
extra_applications: [:logger],
env: [routing_table: []],
mod: {KV, []}
]
end
Мы добавили новый ключ :env в приложение. Он возвращает среду по умолчанию приложения, которая имеет запись с ключом :routing_table и значением пустого списка. Имеет смысл, чтобы среда приложения поставлялась с пустой таблицей, так как конкретная таблица маршрутизации зависит от структуры тестирования/развертывания.
Чтобы использовать среду приложения в нашем коде, нам нужно заменить KV.Router.table/0 определением ниже:
@doc """ The routing table. """ def table do Application.fetch_env!(:kv, :routing_table) end
Мы используем Application.fetch_env!/2, чтобы прочитать запись для :routing_table в среде :kv. Вы можете найти дополнительную информацию и другие функции для управления средой приложения в модуле Application.
Поскольку наша таблица маршрутизации теперь пустая, наши распределенные тесты должны завершиться ошибкой. Перезапустите приложения и повторно запустите тесты, чтобы увидеть ошибку:
$ iex --sname bar -S mix $ elixir --sname foo -S mix test --only distributed
Нам нужен способ настройки среды приложения. Вот когда мы используем файлы конфигурации.
Конфигурация
Файлы конфигурации предоставляют механизм для настройки среды любого приложения. Elixir предоставляет две точки входа для конфигурации:
config/config.exs— этот файл читается во время сборки, прежде чем мы скомпилируем наше приложение и загрузим зависимости. Это означает, что мы не можем получить доступ к коду в нашем приложении или зависимостях. Однако это означает, что мы можем контролировать их компиляциюconfig/runtime.exs— этот файл читается после компиляции нашего приложения и зависимостей, и поэтому он может настроить работу нашего приложения во время выполнения. Если вы хотите прочитать системные переменные окружения (черезSystem.get_env/1) или любой вид внешней конфигурации, это подходящее место для этого
Например, мы можем настроить стандартный приглашающий запрос IEx на другое значение. Давайте создадим файл config/runtime.exs со следующим содержимым:
import Config config :iex, default_prompt: ">>>"
Запустите IEx с iex -S mix, и вы увидите, что приглашающий запрос IEx изменился.
Это означает, что мы также можем настроить наше :routing_table непосредственно в файле config/runtime.exs. Однако какое значение конфигурации нам следует использовать?
В настоящее время у нас есть два теста, помеченные как @tag :distributed. Тест «взаимодействие с сервером» в KVServerTest, и «запросы маршрутизации между узлами» в KV.RouterTest. Оба теста завершаются ошибкой, так как они требуют таблицы маршрутизации, которая в настоящее время пуста.
Для простоты мы определим таблицу маршрутизации, которая всегда указывает на текущий узел. Это та таблица, которую мы будем использовать для разработки и большинства наших тестов. Вернитесь в config/runtime.exs и добавьте эту строку:
config :kv, :routing_table, [{?a..?z, node()}]
С такой простой таблицей мы теперь можем удалить @tag :distributed из теста в test/kv_server_test.exs. Если вы запустите весь набор тестов, тест теперь должен пройти.
Однако для тестов в KV.RouterTest нам фактически нужны два узла в нашей таблице маршрутизации. Для этого мы напишем блок настройки, который выполняется перед всеми тестами в этом файле. Блок настройки изменит среду приложения и вернет ее обратно после завершения, как это:
defmodule KV.RouterTest do
use ExUnit.Case
setup_all do
current = Application.get_env(:kv, :routing_table)
Application.put_env(:kv, :routing_table, [
{?a..?m, :"foo@computer-name"},
{?n..?z, :"bar@computer-name"}
])
on_exit fn -> Application.put_env(:kv, :routing_table, current) end
end
@tag :distributed
test "route requests across nodes" do
Обратите внимание, что мы удалили async: true из use ExUnit.Case. Поскольку среда приложения является глобальным хранилищем, тесты, которые изменяют ее, не могут выполняться одновременно. При всех внесенных изменениях все тесты должны пройти, включая распределенный.
Релизы
Теперь, когда наше приложение работает распределенно, вы, возможно, задаетесь вопросом, как мы можем упаковать наше приложение для работы в производстве. В конце концов, весь наш код до сих пор зависит от версий Erlang и Elixir, установленных в вашей текущей системе. Для достижения этой цели Elixir предоставляет релизы.
Релиз — это автономный каталог, который состоит из вашего кода приложения, всех его зависимостей, плюс всей виртуальной машины Erlang (VM) и среды выполнения. После сборки релиза его можно упаковать и развернуть на целевом узле, если целевой узел работает на той же дистрибуции и версии операционной системы (ОС), что и машина, на которой был собран релиз.
В обычном проекте мы можем собрать релиз, просто выполнив mix release. Однако у нас есть проект-«зонт», и в таких случаях Elixir требует от нас дополнительных данных. Давайте посмотрим, что необходимо:
$ MIX_ENV=prod mix release
** (Mix) Umbrella projects require releases to be explicitly defined with a non-empty applications key that chooses which umbrella children should be part of the releases:
releases: [
foo: [
applications: [child_app_foo: :permanent]
],
bar: [
applications: [child_app_bar: :permanent]
]
]
Alternatively you can perform the release from the children applications
Потому что проект-«зонт» предоставляет нам множество вариантов при развертывании программного обеспечения. Мы можем:
развернуть все приложения в «зонте» на узел, который будет работать как сервер TCP, так и хранилищем ключей-значений
развернуть приложение
:kv_serverдля работы только как сервер TCP, при условии, что таблица маршрутизации указывает только на другие узлыразвернуть только приложение
:kvкогда мы хотим, чтобы узел работал только как хранилище (без доступа TCP)
В качестве отправной точки определим релиз, который включает приложения :kv_server и :kv. Мы также добавим к нему версию. Откройте mix.exs в корне проекта-«зонт» и добавьте внутри def project:
releases: [
foo: [
version: "0.0.1",
applications: [kv_server: :permanent, kv: :permanent]
]
]
Это определяет релиз с именем foo с приложениями kv_server и kv. Их режим установлен на :permanent, что означает, что если эти приложения аварийно завершат работу, весь узел завершит работу. Это разумно, так как эти приложения являются основными для нашей системы.
Прежде чем мы соберем релиз, давайте также определим нашу таблицу маршрутизации для производства. Поскольку мы ожидаем иметь два узла, нам нужно обновить config/runtime.exs следующим образом:
import Config
config :kv, :routing_table, [{?a..?z, node()}]
if config_env() == :prod do
config :kv, :routing_table, [
{?a..?m, :"foo@computer-name"},
{?n..?z, :"bar@computer-name"}
]
end
Мы жестко закодировали имена таблицы и узлов, что достаточно для нашего примера, но в реальной производственной настройке вы, вероятно, перенесете это в внешнюю систему конфигурации. Мы также обернули это в проверку config_env() == :prod, чтобы эта конфигурация не применялась к другим средам.
При настройке конфигурации давайте попробуем собрать релиз еще раз:
$ MIX_ENV=prod mix release foo
* assembling foo-0.0.1 on MIX_ENV=prod
* skipping runtime configuration (config/runtime.exs not found)
Release created at _build/prod/rel/foo!
# To start your system
_build/prod/rel/foo/bin/foo start
Once the release is running:
# To connect to it remotely
_build/prod/rel/foo/bin/foo remote
# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/prod/rel/foo/bin/foo stop
To list all commands:
_build/prod/rel/foo/bin/foo
Отлично! Релиз был собран в _build/prod/rel/foo. Внутри релиза будет файл bin/foo, который является точкой входа в вашу систему. Он поддерживает множество команд, таких как:
bin/foo start,bin/foo start_iex,bin/foo restart, иbin/foo stop— для общего управления релизомbin/foo rpc COMMANDиbin/foo remote— для выполнения команд на работающей системе или для подключения к работающей системеbin/foo eval COMMAND— для запуска новой системы, которая выполняет одну команду, а затем завершает работуbin/foo daemonиbin/foo daemon_iex— для запуска системы как демона на системах Unixbin/foo install— для установки системы как службы на машинах Windows
Если вы запустите bin/foo start, это запустит систему с коротким именем (--sname) равным имени релиза, которое в данном случае foo. Следующим шагом является запуск системы с именем bar, чтобы мы могли подключить foo и bar вместе, как мы делали в предыдущей главе. Но прежде чем мы этого добьемся, давайте поговорим о преимуществах релизов.
Зачем нужны релизы?
Релизы позволяют разработчикам предварительно компилировать и упаковывать весь свой код и среду выполнения в единый модуль. Преимущества релизов следующие:
Предварительная загрузка кода. VM имеет два механизма для загрузки кода: интерактивный и встроенный. По умолчанию он работает в интерактивном режиме, динамически загружая модули при первом использовании. В первый раз, когда ваше приложение вызывает
Enum.map/2, VM найдет модульEnumи загрузит его. Есть недостаток. Когда вы запускаете новый сервер в производстве, ему может потребоваться загрузить много других модулей, что приведет к аномальному пику времени отклика первых запросов. Релизы работают во встроенном режиме, который загружает все доступные модули заранее, гарантируя, что ваша система готова обрабатывать запросы после запуска.Настройка и настройка. Релизы предоставляют разработчикам точный контроль над настройками системы и флагами VM, используемыми для запуска системы.
Автономность. Релиз не требует включения исходного кода в ваши артефакты производства. Весь код предварительно скомпилирован и упакован. Релизы даже не требуют Erlang или Elixir на ваших серверах, так как они включают VM Erlang и его среду выполнения по умолчанию. Кроме того, стандартные библиотеки Erlang и Elixir усечены, чтобы включить только части, которые вы фактически используете.
Несколько релизов. Вы можете собрать разные релизы с разной конфигурацией для каждого приложения или даже с разными приложениями в целом.
Мы написали обширную документацию по релизам, поэтому пожалуйста, обратитесь к официальной документации для получения дополнительной информации. Сейчас мы продолжим изучение некоторых из перечисленных выше функций.
Сборка нескольких релизов
До сих пор мы собрали релиз под названием foo, но наша таблица маршрутизации содержит информацию как для foo, так и для bar. Давайте запустим foo:
$ _build/prod/rel/foo/bin/foo start 16:58:58.508 [info] Accepting connections on port 4040
И подключимся к нему и выполним запрос в другом терминале:
$ telnet 127.0.0.1 4040 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. CREATE bitsandpieces OK PUT bitsandpieces sword 1 OK GET bitsandpieces sword 1 OK GET shopping foo Connection closed by foreign host.
Наша программа уже работает, когда мы работаем с ведром с именем «bitsandpieces». Но поскольку ведро «shopping» будет храниться в bar, запрос завершается ошибкой, так как bar недоступен. Если вы вернетесь к терминалу, в котором выполняется foo, вы увидите:
17:16:19.555 [error] Task #PID<0.622.0> started from #PID<0.620.0> terminating
** (stop) exited in: GenServer.call({KV.RouterTasks, :"bar@computer-name"}, {:start_task, [{:"foo@josemac-2", #PID<0.622.0>, #PID<0.622.0>}, [#PID<0.622.0>, #PID<0.620.0>, #PID<0.618.0>], :monitor, {KV.Router, :route, ["shopping", KV.Registry, :lookup, [KV.Registry, "shopping"]]}], :temporary, nil}, :infinity)
** (EXIT) no connection to bar@computer-name
(elixir) lib/gen_server.ex:1010: GenServer.call/3
(elixir) lib/task/supervisor.ex:454: Task.Supervisor.async/6
(kv) lib/kv/router.ex:21: KV.Router.route/4
(kv_server) lib/kv_server/command.ex:74: KVServer.Command.lookup/2
(kv_server) lib/kv_server.ex:29: KVServer.serve/1
(elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Function: #Function<0.128611034/0 in KVServer.loop_acceptor/1>
Args: []
Теперь давайте определим релиз для :bar. Первым шагом может быть определение релиза точно так же, как foo внутри mix.exs. Кроме того, мы установим опцию cookie для обоих релизов в значение weknoweachother, чтобы они могли разрешать соединения друг с другом. Для получения дополнительной информации по этой теме см. Документацию по распределенному Erlang:
releases: [
foo: [
version: "0.0.1",
applications: [kv_server: :permanent, kv: :permanent],
cookie: "weknoweachother"
],
bar: [
version: "0.0.1",
applications: [kv_server: :permanent, kv: :permanent],
cookie: "weknoweachother"
]
]
И теперь давайте соберем оба релиза:
$ MIX_ENV=prod mix release foo $ MIX_ENV=prod mix release bar
Остановите foo, если она всё ещё работает, и перезапустите её, чтобы загрузить cookie:
$ _build/prod/rel/foo/bin/foo start
И запустите bar в другом терминале:
$ _build/prod/rel/bar/bin/bar start
Вы должны увидеть ошибку, подобную ошибке ниже, 5 раз, прежде чем приложение, наконец, завершит работу:
17:21:57.567 [error] Task #PID<0.620.0> started from KVServer.Supervisor terminating
** (MatchError) no match of right hand side value: {:error, :eaddrinuse}
(kv_server) lib/kv_server.ex:12: KVServer.accept/1
(elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Function: #Function<0.98032413/0 in KVServer.Application.start/2>
Args: []
Это происходит потому, что релиз foo уже прослушивает порт 4040, а bar пытается сделать то же самое! Одним из вариантов может быть перемещение конфигурации :port в среду приложения, как мы сделали для таблицы маршрутизации, и настройка разных портов для каждого узла.
Но давайте попробуем что-то другое. Давайте сделаем так, чтобы релиз bar содержал только приложение :kv. Таким образом, он будет работать как хранилище, но не будет иметь пользовательского интерфейса. Измените информацию :bar на следующую:
releases: [
foo: [
version: "0.0.1",
applications: [kv_server: :permanent, kv: :permanent],
cookie: "weknoweachother"
],
bar: [
version: "0.0.1",
applications: [kv: :permanent],
cookie: "weknoweachother"
]
]
И теперь давайте соберем bar ещё раз:
$ MIX_ENV=prod mix release bar
И, наконец, успешно запустите его:
$ _build/prod/rel/bar/bin/bar start
Если вы подключитесь к localhost ещё раз и выполните другой запрос, теперь всё должно работать, при условии, что таблица маршрутизации содержит правильные имена узлов. Отлично!
С помощью релизов мы смогли «вырезать разные куски» нашего проекта и подготовить их к работе в производстве, все упакованные в один каталог.
Настройка релизов
Релизы также предоставляют встроенные хуки для настройки практически всех потребностей производственной системы:
config/config.exs— предоставляет конфигурацию приложения на этапе сборки, которая выполняется до компиляции нашего приложения. Этот файл часто импортирует файлы конфигурации, основанные на среде, такие какconfig/dev.exsиconfig/prod.exs.config/runtime.exs— предоставляет конфигурацию приложения во время выполнения. Она выполняется каждый раз при запуске релиза и далее расширяется с помощью поставщиков конфигурации.rel/env.sh.eexиrel/env.bat.eex— шаблоны файлов, которые копируются в каждый релиз и выполняются при каждой команде для настройки переменных среды, включая переменные, специфичные для виртуальной машины, и общие переменные среды.rel/vm.args.eex— шаблон файла, который копируется в каждый релиз и предоставляет статическую конфигурацию виртуальной машины Erlang и других флагов во время выполнения.
Как мы видим, config/config.exs и config/runtime.exs загружаются во время релизов и обычных команд Mix. С другой стороны, rel/env.sh.eex и rel/vm.args.eex специфичны для релизов. Давайте посмотрим.
Конфигурация среды операционной системы
Каждый релиз содержит файл среды, названный env.sh в системах Unix-подобных и env.bat в системах Windows, который выполняется до запуска системы Elixir. В этом файле вы можете выполнить любой код уровня ОС, например, вызвать другие приложения, установить переменные среды и так далее. Некоторые из этих переменных среды даже могут настроить, как сам релиз будет выполняться.
Например, релизы выполняются с использованием коротких имён (--sname). Однако, если вы хотите фактически запустить распределённое хранилище ключей-значений в производстве, вам потребуются несколько узлов, и запустить релиз с опцией --name. Мы можем добиться этого, установив переменную среды RELEASE_DISTRIBUTION внутри файлов env.sh и env.bat. Mix уже имеет шаблон для этих файлов, который мы можем настроить, поэтому давайте попросим Mix скопировать их в наше приложение:
$ mix release.init * creating rel/vm.args.eex * creating rel/remote.vm.args.eex * creating rel/env.sh.eex * creating rel/env.bat.eex
Если вы откроете rel/env.sh.eex, вы увидите:
#!/bin/sh # # Sets and enables heart (recommended only in daemon mode) # case $RELEASE_COMMAND in # daemon*) # HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" # export HEART_COMMAND # export ELIXIR_ERL_OPTIONS="-heart" # ;; # *) # ;; # esac # # Set the release to load code on demand (interactive) instead of preloading (embedded). # export RELEASE_MODE=interactive # # Set the release to work across nodes. # # RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". # export RELEASE_DISTRIBUTION=name # export RELEASE_NODE=<%= @release.name %>
Необходимые шаги для работы через узлы уже закомментированы в качестве примера. Вы можете включить полное распределение, раскомментировав последние две строки, удалив ведущие #.
Если вы работаете в Windows, вам нужно открыть rel/env.bat.eex, где вы найдёте это:
@echo off rem Set the release to load code on demand (interactive) instead of preloading (embedded). rem set RELEASE_MODE=interactive rem Set the release to work across nodes. rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". rem set RELEASE_DISTRIBUTION=name rem set RELEASE_NODE=<%= @release.name %>
Ещё раз, раскомментируйте последние две строки, удалив ведущие rem, чтобы включить полное распределение. И всё!
Аргументы виртуальной машины
rel/vm.args.eex позволяет указать низкоуровневые флаги, которые контролируют работу виртуальной машины Erlang и её среды выполнения. Вы указываете записи так, как будто вы указываете аргументы в командной строке, а также поддерживаются комментарии в коде. Вот сгенерированный по умолчанию файл:
## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here ## Increase number of concurrent ports/sockets ##+Q 65536 ## Tweak GC to run more often ##-env ERL_FULLSWEEP_AFTER 10
Вы можете ознакомиться с полным списком аргументов и флагов виртуальной машины в документации Erlang.
Подводя итоги
На протяжении всего руководства мы создали очень простое распределённое хранилище ключей-значений как возможность изучить множество конструкций, таких как универсальные серверы, надзиратели, задачи, агенты, приложения и многое другое. Не только это, мы написали тесты для всего приложения, ознакомились с ExUnit и узнали, как использовать инструмент сборки Mix для выполнения широкого круга задач.
Если вы ищете распределённое хранилище ключей-значений для использования в производстве, обязательно изучите Riak, которое также работает в виртуальной машине Erlang. В Riak ведра реплицируются для предотвращения потери данных, а вместо маршрутизатора они используют консистентное хеширование для сопоставления ведра с узлом. Алгоритм консистентного хеширования помогает уменьшить количество данных, которые нужно мигрировать, когда к вашей рабочей системе добавляются новые узлы хранилища.
Конечно, Elixir можно использовать для гораздо большего, чем распределённые хранилища ключей-значений. Встроенные системы, обработка данных и загрузка данных, веб-приложения, системы потоковой передачи аудио/видео и другие — это лишь некоторые из различных областей, в которых Elixir превосходит. Мы надеемся, что это руководство подготовило вас к изучению любой из этих областей или любой будущей области, в которую вы можете захотеть интегрировать Elixir.
Хорошего кодирования!
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/config-and-releases.html