Исходный код Настройка и релизы
В этом последнем руководстве мы сделаем таблицу маршрутизации для нашего распределенного хранилища ключей-значений настраиваемой, а затем, наконец, упакуем программное обеспечение для производства.
Давайте сделаем это.
Среда приложения
До сих пор мы жестко закодировали таблицу маршрутизации в модуле 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 на ваших серверах, поскольку они включают в себя виртуальную машину 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, чтобы включить полную распределенность. И это всё!
Аргументы VM
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.18.1/config-and-releases.html