Привет народ. Меня зовут h3poteto, и я работаю в oVice разработчиком на полставки. В этом посте я хотел бы поделиться тем, что я узнал о горячей перезагрузке кода Эликсира.
Elixir (Erlang/OTP) можно развернуть без остановки Erlang VM (BEAM). Эта функция называется Горячая перезагрузка кода / Горячая замена кода / Горячее развертывание кода. В этой статье мы будем называть это перезагрузкой горячего кода.
Какая польза?
Мы используем Elixir для разработки приложения oVice и используем Hot Code Reloading для развертывания кода. Это очень полезно и похоже на волшебство.
- Не останавливает процесс, поэтому сервер продолжает получать запросы
- Не останавливает сервер WebSocket, поэтому соединение не будет разорвано.
- Состояние процессов будет сохранено
Эти моменты важны для нас, потому что в нашем приложении мы используем и WebRTC, и WebSocket. Конечно, соединение WebSocket (используемое для передачи данных между внешним приложением и внутренним сервером) не разрывается во время развертывания, более того, соединение WebRTC (используемое для аудио/видеоданных) не разрывается.
Конечно, наше интерфейсное приложение будет повторно подключаться при отключении соединения. Поскольку наше приложение обеспечивает связь в режиме реального времени, мы не хотим, чтобы пользователи сталкивались с разрывами связи. Но мы также хотим обновить наше программное обеспечение, чтобы исправить ошибки и добавить функции, поэтому мы хотим иметь возможность развертывания, не вызывая разрывов связи.
Тем не менее, мы должны быть осторожны при написании кода Elixir, чтобы воспользоваться преимуществами Hot Code Reloading. Я объясню, как вы должны быть осторожны.
Basic: Что происходит во время перезагрузки горячего кода?
Erlang выполняет процесс перезагрузки в соответствии с relup
(обновление выпуска). Например:
{"1.0.1", [{"1.0.0",[], [{load_object_code, {my_app,"1.0.1", ['Elixir.MyApp.Foo']}}, point_of_no_return, {suspend,['Elixir.MyApp.Foo']}, {load, {'Elixir.MyApp.Foo',brutal_purge, brutal_purge}}, {code_change,up,[{'Elixir.MyApp.Foo',[]}]}, {resume,['Elixir.MyApp.Foo']}]}],
Он приостановит текущий процесс, загрузит новый модуль, вызовет метод code_change
и возобновит процесс. Я объясню code_change
позже.
всн
Это не обязательно, это необязательно, но я рекомендую указать это при преобразовании состояния OTP.
Мы можем предоставить @vsn
версию модуля, и она считывается во время Hot Code Reloading.
Например,
defmodule MyModule do @vsn "2" def init() do end #... end
Если мы не укажем @vsn
, версия будет определена автоматически из хэша MD5 модуля. Поэтому, если код изменится, вам не нужно указывать новый @vsn
, потому что изменится MD5. Но если вы пишете code_change
метод преобразования состояния OTP, необходимо указать @vsn
. См. следующий раздел о преобразовании состояния OTP.
Когда мы должны указать vsn?
- Используя
gen_server
илиgen_statem
. - Вы хотите перезагрузить модуль, не изменяя его. Например, когда вы обновляете библиотеки зависимостей, а библиотеки используются в модуле. Если вы не обновите
@vsn
и не измените модуль, модуль (процесс) не будет перезагружен. - Вы пишете
code_change
метод для преобразования состояния OTP.
Преобразование состояния
Обычно Erlang не изменяет состояние процесса во время Hot Code Reloading. Это означает, что мы не можем использовать горячую перезагрузку кода при изменении структуры модуля.
Но специальные процессы Erlang (например, gen_server и gen_statem) имеют функцию преобразования состояния процесса во время Hot Code Reloading. Вы можете использовать эту функцию, определив метод code_change
. Он будет вызываться при обновлении и преобразовывать состояние вашего модуля из старой версии в новую версию.
Базовый
defmodule MyApp.Foo do @vsn "1" use GenServer defstruct [:foo] def init(state) do {:ok, state} end def handle_call(_, _from, state) do ## Some codes end end
Когда вы изменяете структуру MyApp.Foo
, добавляя :bar
,
defmodule MyApp.Foo do @vsn "2" use GenServer defstruct [:foo, :bar] def init(state) do {:ok, state} end def handle_call(_, _from, state) do ## Some codes end def code_change("1" = vsn, state, _extra) do {:ok, %{ state | bar: "bar" }} end end
обновите @vsn
, определите метод code_change
, и он вернет {:ok, new_state}
.
Условия для выполнения code_change
- Процесс должен выполняться под главным супервизором приложения или в дереве супервизора.
- Это специальный процесс Erlang.
Первое очень важно. В приведенном выше примере вам нужно запустить MyApp.Foo
в application.ex
.
defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ MyApp.Foo ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end
Или вам нужно запустить MyApp.Foo
под деревом наблюдения.
defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ MyApp.MySupervisor ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end defmodule MyApp.Supervisor do use Supervisor def start_link(init_args) do Supervisor.start_link(__MODULE__, init_args, name: __MODULE__) end @impl Supervisor def init(_) do children = [ MyApp.Foo ] Supervisor.init(children, strategy: :one_for_one) end end
Примеры, когда code_change
не будет вызываться
Не специальный процесс
defmodule MyApp.Websocket do @vsn "2" @behaviour :cowboy_websocket defstruct [:username] # Some methods # Will not be called def code_change("1" = vsn, %{username: username} = state, _extra) do {:ok, %{state | username: username <> "-user"}} end end
GenServer не выполняется под управлением приложения
defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ MyApp.MyServer ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end defmodule MyApp.MyServer do @vsn "1" use GenServer defstruct [:foo] def init(_state) do {:ok, pid} = GenServer.start_link(MyApp.Foo, %MyApp.Foo{}) {:ok, %{foo: pid}} end # Some methods end defmodule MyApp.Foo do @vsn "2" use GenServer defstruct [:foo, :bar] # Some methods # Will not be called def code_change("1" = vsn, state, _extra) do {:ok, %{ state | bar: "bar" }} end end
В этом случае MyApp.MyServer
выполняется под супервизором приложений. Но MyApp.Foo
не выполняется супервизором приложений и не принадлежит ни к какому дереву супервизии. Так что метод code_change
вызываться не будет.
Модуль переименования
Будьте осторожны при переименовании модулей. Горячая перезагрузка кода не может обнаружить события переименования, поэтому лучше перезапустить виртуальную машину Erlang без горячей перезагрузки кода.
Что происходит?
Например, я меняю имя модуля Foo
на Bar
.
- Старые процессы вызывают
Foo
, но в новом процессе нет модуляFoo
. Так что не удалось вызватьFoo
, и старые процессы рухнули. Если они являются членами какого-либо супервизора, они перезапускаются. - Если
Foo
иBar
являются GenServer, это сложнее. Пожалуйста, смотрите ниже.
GenServer запущен диспетчером приложений
Если вы начнете Foo
в своем application.ex
,
defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ MyApp.Foo ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end
этот супервизор не будет перезапущен во время перезагрузки горячего кода. Так что, если вы перепишете его,
children = [ MyApp.Bar ]
MyApp.Bar
не будет запускаться после перезагрузки горячего кода. Это означает, что он рухнет, когда вы вызовете его в кодах приложений.
defmodule MyApp.SomeModule do def init() do MyApp.Bar.baz() #=> (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started end end
И MyApp.Bar
после этого не запустится и будет продолжать крашиться.
Таким образом, вы не можете использовать Hot Code Reloading в этом случае. Вам необходимо перезапустить виртуальную машину Erlang, если вы хотите переименовать модуль в диспетчере приложений.
GenServer запущен некоторыми другими процессами
Если вы начнете Foo
в SomeModule
,
defmodule MyApp.SomeModule do use GenServer def init(state) do {:ok, pid} = MyApp.Foo.start_link() {:ok, %{foo: pid}} end def handle_info(_, state) do MyApp.Foo.baz() end end
и переименуйте его в Bar
,
defmodule MyApp.SomeModule do use GenServer def init(state) do {:ok, pid} = MyApp.Bar.start_link() {:ok, %{foo: pid}} end def handle_info(_, state) do MyApp.Bar.baz() #=> (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started end end
MyApp.Bar.baz()
вылетит, потому что Bar
не запускается после Hot Code Reloading. Но если вы зарегистрируете SomeModule
в диспетчере приложений,
children = [ MyApp.SomeModule ]
MyApp.SomeModule
процесс будет перезапущен после сбоя и будет вызван init
. В это время будет вызван MyApp.Bar.start_link()
, поэтому MyApp.Bar.baz()
будет успешно выполнено.
Поэтому, если вы допускаете один сбой, вы можете использовать перезагрузку горячего кода в этом случае. Конечно, если не запускать MyApp.SomeModule
в супервизоре, этот кейс нормально не заработает.
Вызов метода
Местный вызов против полного квалифицированного вызова
Местный звонок:
defmodule MyModule do def foo() do end def bar() do foo() # local call end end
Полный квалифицированный вызов:
defmodule MyModule do def foo() do end def bar() do MyModule.foo() # full qualified call end end
Полный квалифицированный вызов всегда вызывает модуль последней версии, но локальный вызов вызывает тот же самый модуль версии. Подробности см. на следующем слайде.
https://www.slideshare.net/Elixir-Meetup/hot-code-replacement-alexei-sholik/19
Изменение конфигурации
Большинство приложений Elixir используют Mix.Config
или Config
в config/${mix_env}.exs
. Если вы измените эти файлы конфигурации, новая конфигурация не будет загружена после перезагрузки горячего кода. Конечно, у rel/config.exs
и rel/vm.args
такая же проблема.
Таким образом, в этом случае вы не можете использовать перезагрузку горячего кода, используйте чистый перезапуск.
Вещи, которые отлично работают при горячей перезагрузке кода
- Изменить аргументы и возвращаемые значения
- Переименуйте имя файла модуля
- Обновление библиотек
Эти действия не проблема, поэтому вам не нужно о них беспокоиться.
В заключение
Я представил несколько замечаний по написанию кода приложения, если вы используете горячую перезагрузку кода. Особенно сложны Erlang OTP и метод code_change
. Вот репозиторий, который я создал, чтобы проверить и поэкспериментировать с этим поведением.
Горячая перезагрузка кода предоставляет потрясающие функции, подобные магии. Так что давайте наслаждаться Erlang/Elixir и Hot Code Reloading.