Исходный код Конфигурация и релизы
В этом последнем руководстве мы сделаем таблицу маршрутизации для нашего распределенного хранилища ключей-значений настраиваемой, а затем, наконец, упакуем программное обеспечение для производства.
Давайте сделаем это.
Среда приложения
До сих пор мы жестко закодировали таблицу маршрутизации в модуле 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— для запуска системы в качестве демона на системах Unix-подобныхbin/foo install— для установки системы как службы на машинах Windows
Если вы запустите bin/foo start, это запустит систему с коротким именем (--sname) равным имени релиза, которое в данном случае foo. Следующим шагом является запуск системы под именем bar, чтобы мы могли подключить foo и bar вместе, как мы делали в предыдущей главе. Но прежде чем мы этого добьёмся, давайте поговорим о преимуществах релизов.
Зачем нужны релизы?
Релизы позволяют разработчикам предварительно скомпилировать и упаковать весь свой код и среду выполнения в единый модуль. Преимущества релизов:
Предварительная загрузка кода. VM имеет два механизма для загрузки кода: интерактивный и встроенный. По умолчанию он работает в интерактивном режиме, динамически загружая модули при первом использовании. В первый раз, когда ваше приложение вызывает
Enum.map/2, VM найдёт модульEnumи загрузит его. Есть недостаток. Когда вы запускаете новый сервер в режиме производства, ему может потребоваться загрузить многие другие модули, что приведет к тому, что первые запросы будут иметь необычный всплеск времени отклика. Релизы работают в встроенном режиме, который загружает все доступные модули заранее, гарантируя, что ваша система готова обрабатывать запросы после запуска.Конфигурация и настройка. Релизы дают разработчикам полный контроль над конфигурацией системы и флагами VM, используемыми для запуска системы.
Самодостаточность. Релиз не требует включения исходного кода в ваши производственные артефакты. Весь код предварительно скомпилирован и упакован. Релизы даже не требуют Erlang или Elixir на ваших серверах, так как они включают виртуальную машину Erlang (VM) и среду выполнения по умолчанию. Кроме того, стандартные библиотеки 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
Вы можете найти полный список аргументов и флагов VM в документации 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.16.3/config-and-releases.html