Elixir (Erlang/OTP) には,プロセスを終了させることなく新しいコードをデプロイするHot Code Reloadingという仕組みが存在する.これは,Hot Code SwapとかHot Code Deployとか呼ばれることもある.
稼働中のアプリケーションの,プロセスを止めずにコードだけを新しいものに差し替え,状態すらも継続させるという,まるで魔法のようなことが可能になるのがHot Code Reloadingだ.ただし,ちょっとだけ注意点があるので,それを紹介していく.
Hot Code Reloadingで何が起こっているのか?
その前に,まずHot Code Reloadingで何が行われているのかというのをちょっとだけ紹介しておく.基本的には,Erlang VM (BEAM) が relup
ファイルに従って
{"1.0.1",
[{"1.0.0",[],
[{load_object_code,
{my_app,"1.0.1",
['Elixir.MyApp.Hoge']}},
point_of_no_return,
{suspend,['Elixir.MyApp.Hoge']},
{load,
{'Elixir.MyApp.Hoge',brutal_purge,
brutal_purge}},
{code_change,up,[{'Elixir.MyApp.Hoge',[]}]},
{resume,['Elixir.MyApp.Hoge']}]}],
プロセスの一時停止,新しいコードをロード,状態の更新,プロセス再スタートと順番に行っているに過ぎない.ここで出てくる code_change
については後述.Erlangはここに出てきている操作を順番にやっているだけなので,ここに記述されないモジュールについては何も発生しない.そのため,もしHot Code Reloadingで思う通りに新しいモジュールがデプロイされない場合は,ここをチェックしてみると良い.
vsn
まず最初に紹介するのは vsn
というキーワード.これをmoduleに指定することで,各モジュールのバージョンを明示することができる.
ただし, vsn
はオプショナルである.Hot Code Reloadingをやるからと言って,必ず指定しなければいけないものではない.後述する Transform stateをやるときに便利なので,そのときに使うことをおすすめする.
例えば,
defmodule MyModule do
@vsn "2" def init() do
end
#...
end
とすることで MyModule
に "2"
というバージョンが指定される.これを指定しない場合,ファイルのmd5ハッシュから自動的にバージョンが算出される.ただし,先程述べたTransform stateをやる場合に code_change
というメソッドを書くのだが,そこでこのバージョンを使うので,これをやる場合は指定しておくと便利である.
結局どんな場合に使えば良い?
gen_server
もしくはgen_statem
を使う場合- コード変更なし(md5ハッシュが更新されない)だがモジュールをリロードしたい場合.例えばライブラリのみを更新しているが,モジュールでそのライブラリを使っている場合,
vsn
が更新されないとモジュールがリロードされないため,更新されたライブラリが利用されない場合がある.
Transform state
前述した code_change
を使う話がこれ.通常,ErlangはHot Code Reloadingの際にプロセスの一時停止・再開は行うが,Stateを変更したりはしてくれない.そのため,例えばモジュールのstructを変更するようなものは,Hot Code Reloadingできないということになる.
しかし,Erlangのspecial processと呼ばれるもの(gen_serverとかgen_statem)は,Hot Code Reloading時にStateを変更する方法を提供している.これが code_change
メソッドである.このメソッドは,Hot Code Reloadingの際に呼ばれ,このメソッド内部でStateを古いバージョンから新しいバージョンに変換することができる.
基本
defmodule MyApp.Hoge do
@vsn "1"
use GenServer defstruct [:hoge]
def init(state) do
{:ok, state}
end def handle_call(_, _from, state) do
## Some codes
end
end
MyApp.Hoge
のstructを以下のように変更するとする.
defmodule MyApp.Hoge do
@vsn "2"
use GenServer defstruct [:hoge, :fuga]
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 | fuga: "fuga" }}
end
end
vsn
を更新し, code_change
メソッドを定義し,内部で新しいStateを返す.こうすることで,Hot Code Reloadingされたあとの MyApp.Hoge
はStateに fuga
というメンバを持ち,値も入った状態でプロセスが再開することになる.
code_changeが実行される条件
便利な code_change
だが,すべてのプロセスで,どんな状況でも呼ばれるわけではない.
- プロセスはアプリケーションのSupervisorによって起動されているか,もしくはそのSupervison treeによって起動されている必要がある³
- プロセスはErlangのspecial processである必要がある
1は非常に重要な点で,先程の例であれば MyApp.Hoge
は application.ex
で以下のように起動される必要がある.
defmodule MyApp.Application do
use Application @impl true
def start(_type, _args) do
children = [
MyApp.Hoge
] opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
もしくはSupervision treeの下で,
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
enddefmodule 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.Hoge
] Supervisor.init(children, strategy: :one_for_one)
end
end
のように起動される必要がある.
code_changeが呼ばれない例
special processではない
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
gen_serverがApplicationのSupervisorで起動されていない
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
enddefmodule MyApp.MyServer do
@vsn "1"
use GenServer
defstruct [:hoge] def init(_state) do
{:ok, pid} = GenServer.start_link(MyApp.Hoge, %MyApp.Hoge{})
{:ok, %{hoge: pid}}
end
# Some methods
enddefmodule MyApp.Hoge do
@vsn "2"
use GenServer defstruct [:hoge, :fuga]
# Some methods # Will not be called
def code_change("1" = vsn, state, _extra) do
{:ok, %{ state | fuga: "fuga" }}
end
end
この例では, MyApp.MyServer
はApplicationのSupervisorによって起動されているが, MyApp.Hoge
はそうではないし,Supervision treeにも所属していない.そのため code_change
メソッドは呼び出されない.
モジュールの名前変更
Hot Code ReloadingにおいてRenameのイベントは検知されない.そのためモジュールの名前を変えるのは十分に気をつける必要がある.
何が起こるのか
例えば, Hoge
モジュールを Fuga
に変更する場合,
- 古いプロセスは
Hoge
を利用し続けているが,新しいプロセスにはHoge
が存在しない.そのため,古いプロセスでHoge
を呼び出した時点でクラッシュする.もし,このプロセスがSupervisorで管理されている場合,再起動される. Hoge
とFuga
がgen_serverの場合はもう少し複雑なので以下を参照
アプリケーションのSupervisorで起動されたgen_serverの場合
defmodule MyApp.Application do
use Application @impl true
def start(_type, _args) do
children = [
MyApp.Hoge
] opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
このような場合,Supervisorは Myapp.Hoge
を再起動しない.そのため,もし
children = [
MyApp.Fuga
]
のような書き換えをしたとしても, Hot Code Reloadingによって Fuga
は起動しない.つまり,仮に新しいバージョンで Fuga
が起動されることを期待したコードを書けば,クラッシュする.
defmodule MyApp.SomeModule do
def init() do
MyApp.Fuga.fugafuga() #=> (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
そして,放置したところで Fuga
は起動しないので,クラッシュし続けることになる.
つまり,このケースにおいて Hot Code Reloadingを使うことはできない.Erlang VMを再起動するしかない.
gen_serverが他のプロセスによって起動された場合
defmodule MyApp.SomeModule do
use GenServer
def init(state) do
{:ok, pid} = MyApp.Hoge.start_link()
{:ok, %{hoge: pid}}
end def handle_info(_, state) do
MyApp.Hoge.hogehoge()
end
end
このようにして起動した Hoge
を Fuga
に変更するとする.
defmodule MyApp.SomeModule do
use GenServer
def init(state) do
{:ok, pid} = MyApp.Fuga.start_link()
{:ok, %{hoge: pid}}
end def handle_info(_, state) do
MyApp.Fuga.hogehoge() #=> (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
Hot Code Reloading後,MyApp.Fuga.hogehoge()
の呼び出しはクラッシュする.なぜなら Hot Code Reloadingにおいて, MyApp.SomeModule
の init
は誰からも呼ばれず, MyApp.Fuga
は起動していないため.しかし, MyApp.SomeModule
をどこかのSupervisorで
children = [
MyApp.SomeModule
]
として起動していた場合,クラッシュしたプロセスがSupervisorにより再起動される.その際には init
を通過するため, Fuga
が起動するので,これ以降 MyApp.Fuga.hogehoge()
の呼び出しは成功するようになる.
つまり,このケースにおいては,1回のクラッシュを許容するのであれば Hot Code Reloading可能である.もちろん, MyApp.SomeModule
をSupervisorで管理していない場合,これはうまくいかない.
メソッドの呼び出し方
Local call vs Full qualified call
Local call
defmodule MyModule do
def hoge() do
end def fuga() do
hoge() # local call
end
end
Full qualified call
defmodule MyModule do
def hoge() do
end def fuga() do
MyModule.hoge() # full qualified call
end
end
Full qualified callの場合,常に最新バージョンのモジュールメソッドを呼び出す.対してLocal callの場合,同じバージョンのモジュールメソッドを呼び出す.詳しくは以下のスライドを参照.
https://www.slideshare.net/Elixir-Meetup/hot-code-replacement-alexei-sholik/19
どちらを使うかはケースバイケースだが,自分がどちらを使っているかは意識しておいたほうが良い.
Configの変更
Elixirでは通常, Mix.Config
や Config
を config/${mix_env}.exs
内で利用していると思う.ここの設定値を増やしたり,変更した場合,Hot Code Reloadingでは設定の再読込は行われない.そのため,新しい設定値を利用することはできない.もちろん,これは rel/config.exs
や rel/vm.args
も同じである.
この場合も,Hot Code Reloadingを使うことはできないということになる.
心配しなくていいこと
- メソッドの引数や戻り値を変更する場合
- ファイル名を変更する場合
- ライブラリの更新
これらにおいては,ほぼ問題が発生しないので特に心配する必要はない.
何が嬉しいの?
ここまで注意点を書いてきたが,ここまでしてHot Code Reloadingする旨味はなんだろう?
- プロセスが停止・再起動することがないので,サーバはリクエストを受け続けられる
- WebSocketのプロセスも停止・再起動することがないので,デプロイ時にコネクションが切れない
- Stateが保持し続けられる
これらはoViceにとってはかなり嬉しい.oViceはもちろんWebSocketを使っており,WebRTCも使っている.これらがデプロイ時に途切れないというのは,Webアプリケーション開発をしていく上で重要だ(これがないと,例えばユーザが少ない深夜帯にしかデプロイできない,というような状況になる).
もちろん,フロントエンドのJavaScriptでWebSocketやWebRTCの再接続処理は行われる.しかし,デプロイのたびに切断され再接続するのと,デプロイによって切断されないとでは,大きな違いだ.ビデオ通話しているときに切断されるのは嫌であろう…….
まとめ
Hot Code Reloadingするアプリケーションを開発する上での注意点をいくつか紹介しました.これらを検証するために,テスト用のアプリケーションをひとつ作りました.
Hot Code Reloadingが提供する利点はかなり大きいので(特にWebSocketを使うアプリケーションでは),必要な場面では恐れずに使っていきたいですね.