LoginSignup
12
10

More than 5 years have passed since last update.

appupから見る安全なホットコードアップデート

Last updated at Posted at 2016-04-22

Erlangの一つの特徴として, ホットコードアップデート1を標準でサポートしていることが上げられます.
これはrebar2やrebar3, relxといった各種ビルド/リリースツールによって比較的簡単に行うことができますが, それは安全に (processがクラッシュすることなく) ホットコードアップデートできることを意味しません.

appupの書式と, 安全なホットコードアップデートを行う為に考えるべきことを紹介します.

0. 前提知識

appupとは何者なのでしょうか. これを説明する為に, ホットコードアップデートの仕組みを簡単に説明します.

myapp 0.0.1
   |- myapp.rel
   |- lib
       |- libx 1.0.0
       |   |- libx.app (1.0.0)
       |- liby 2.0.0
           |- liby.app (2.0.0)

myapp 0.0.2
   |- myapp.rel
   |- lib
       |- libx 1.0.1
       |   |- libx.app (1.0.1)
       |   |- libx.appup (1.0.0 <--> 1.0.1)
       |- liby 2.0.0
           |- liby.app (2.0.0)
           |- liby.appup (empty)

※ 実際のディレクトリ構成とは異なります.
  1. myapp 0.0.1myapp 0.0.2それぞれにapplicationに関する情報を記したファイルが存在
    • libx 1.0.0に依存するapplicationを記述したlibx.app
    • myapp 0.0.1に含まれるapplicationとertsのバージョンを記述したmyapp.rel
  2. libx.app 1.0.0 <--> libx.app 1.0.1の際に行う動作をlibx.appupに記載
  3. *.appupmyapp.relからmyapp 0.0.1 <--> myapp 0.0.2の際に行う動作relupを生成
  4. relupを元にupgrade/downgradeを行う

ここで重要なのはrelupの元になるappupです. appupがupgrade/downgradeの際の挙動を決めると思って概ね間違いでないでしょう. appupは以下のような書式で書かれます.

{"0.0.3",
 [{"0.0.1", InstructionsUp1},
  {"0.0.2", InstructionsUp2}],
 [{"0.0.1", InstructionsDown1},
  {"0.0.2", InstructionsDown2}]}.

より詳しい仕組みについては, Release Handlingを参照してください.

1. appup の生成方法

appupはおよそ2通りの方法によって生成されます.
ここではその方法について紹介します.

1.1 .appup.src による生成 (静的生成)

身近な例として, OTPがこれにあたります.

といっても, 難しいことをしている訳ではありません. ertsをホットコードで上げたいという要求はないのでしょう. applicationごと再起動してしまうようでした.

ビルドツールとしては, rebar_otp_appuprebar3_appup_compileでサポートされています.2

1.2 beamから自動生成

実際にToVsnFromVsnのbeamファイルがあれば, ある程度の自動生成が可能です. ほとんどの場合は, これで足りるでしょう.
実際に, Appup Cookbookにパターンが記載されています. これに則って自動生成をするだけです.

ビルドツールとしては, rebar_appupsrebar3_appup_generate でサポートされています. また, 独自規則を組み込んだrelflowも存在します.3

2. ホットコードでクラッシュしないコード

ある程度の知見を得た上で, appup.srcから静的生成を行えば, ほとんどのケースでホットコードアップデートを成功させることは可能でしょう.
しかし, それをやりたがるエンジニアはいないでしょう. ミスの許されない作業な上に, 修正の内容を全て知らなければ書けないからです.
その為, 現実には自動生成のappupでホットコードアップデートを成功させる必要があります.

では, クラッシュすることのないコードとはどういうコードでしょうか.

2.1 Functional Module

Release Handling Instructionsにmoduleは2種類に分かれると記載されています.
プロセスが滞留する Residence module と, 単純な関数のみで成り立つ Functional moduleです.

最も簡単な後者から考えていきます.

2.1-1. Code Loadingのタイミングとコードの世代

{"2",
 [{"1", [{load_module, module_a}]}],
 [{"1", [{load_module, module_a}]}]
}.

Functional moduleは上記のように記述するだけで良いが, 果たしてどのタイミングでCode Loadingは走るのでしょうか.
公式ドキュメントから以下のことが分かります.

  • Code Replacement時には, OldとCurrentの世代が存在
  • 内部関数呼び出しの場合は同じ世代の関数を実行する
  • 外部関数呼び出しの場合はCurrentを呼び出す

つまり, モジュール内の関数であっても外部関数呼び出しをしている場合は, そこで世代が変わり, クラッシュする可能性があります.

module_a
-module(module_a).
-export([do/1, do_1/2]).
-record(r1, {a, b, c}).

do(X) ->
    ?MODULE:do_1(X, #r1{a = X}).

do_1(X, #r1{}) -> X.

例えばこのようなコードを書いていた場合, recordのサイズを変えてしまうと?MODULE:do_1/2でクラッシュしてしまう可能性があるということです.

例外として, proc_libを使ったloopに関してはこのような書き方をしなければなりません. これはCurrentに変わるタイミングを作らなければならない為です.
Residence moduleのホットコードアップデートについては後述します.

2.1-2. 依存モジュールの変更

module_b
% 0.0.1
-spec do(binary()) -> binary().

%% 0.0.2
-spec do(iodata()) -> binary().

%% 0.0.3
-spec do(iodata()) -> iodata().

module_bが0.0.1 -> 0.0.2, 0.0.2 -> 0.0.3と上げる際に, これを使う箇所でクラッシュしないことを保証する為に依存モジュールを定義することができます.

%% 0.0.1 -> 0.0.2
{load_module, my_module, [module_b]}. % module_b -> my_moodule の順場で更新

%% 0.0.2 -> 0.0.3
{load_module, module_b, [my_module]}.

循環参照になっていない限り, この順番が保証されます.
ただ, I/Fに後方互換性のある0.0.1 -> 0.0.2ならまだしも, 0.0.2 -> 0.0.3を保証するのは現実的ではないでしょう.
使っているモジュール(module_c)に自分自身(module_b)に対する依存を記載する必要があるのですから.

この機能は多くの自動生成ツールでサポートされていないことに注意しなければなりません.

2.1-3. 後方互換性

後方互換性が担保されている場合は, 必ずしも依存モジュールを記載する必要がありません.
後方互換性とは, 同じ引数を取る場合, 返り値が変わらないことが保証されているということです.

その場合は, 引数が変わらない限りはCurrentとOldのどちらのコード上でも正しく動くため, 依存モジュールの記述は不要です.

2.2 Special Process

gen_serverの始めとするspeciall process にはホットコードアップデート時にStateをマイグレーションする為の仕組みが存在します.

これらのCallback関数がこれに当たります.

2.2-1. code_changeが実行される条件

これらのCallbackを定義するだけで呼ばれるという都合の良いことはありません.
appupに以下の定義を追加した上で, application_masterから連なるsupervision treeに属する必要があります.

{update, my_module, {advanced, []}}

release_handlerのsupervision treeを辿る部分のコードを見てみましょう.
application_masterから typesupervisorとなっているプロセスを辿っていくことが分かります.
また, modulesも適切に設定されている必要があります.

modules is used by the release handler during code replacement to determine which processes are using a certain module. As a rule of thumb, if the child process is a supervisor, gen_server, or gen_fsm, this should be a list with one element [Module], where Module is the callback module. If the child process is an event manager (gen_event) with a dynamic set of callback modules, the value dynamic shall be used. See OTP Design Principles for more information about release handling.

The modules key is optional. If it is not given, it defaults to [M], where M comes from the child's start {M,F,A}

2.2-2. proc_libを使ったsystem_code_change

前述の条件を守り, system_code_change/4を定義すれば良い訳ではありません.
加えて, 公式のExampleに則って実装している必要があります.

より具体的には, sys:handle_system_msg/6を使ってシステムメッセージを処理できるようにしておく必要があります.
その手間を考えると, ホットコードアップデートをしたい多くの場合でproc_libを使うよりも, gen_serverを使った方が良いでしょう.

なお, rebar2を始めとする多くのツールでsystem_code_change/4に対応していないので, そもそもappupを生成するツールが対応しているかを確認する必要があります.

2.2-3. code_changeが実行されるタイミング

code_changeのトリガーはシステムメッセージです.
これは特別な形式のメッセージという訳ではありません. erlang:send/2で送るのと同様のメッセージです.

つまり, handle_call/3などの関数が返る前に実行される心配をする必要はありません.

より具体的な実行のされ方を説明しましょう.

  1. sys:suspend/2でプロセスを止める
  2. moduleおよび依存のあるmoduleのロード, code_changeの実行を行う
    • upgradeの場合は前者, downgradeの場合は後者から実行します.
    • code_changeは新しいバージョンのコードのものが実行されます
  3. sys:resume/2でプロセスを再開する

これらの処理は1 process毎に実行される訳ではありません. そのmoduleに関する全プロセスに対し, 一括で処理を行います.
simple_one_for_oneで多数のプロセスを有していた場合, メッセージキューが貯まっていた場合4にはtimeoutになる可能性があります.
この場合でも見た目上はupgradeが成功したように見えるでしょう. しかし, 実際にはプロセスの再起動などが行われてしまう点に注意が必要です.

2.2-4. code_changeの使い方

code_changeは自動生成されるものだと勘違いされる方もいますが, そんな賢いことはしてくれません.
関数の中身を実装すべきは人間です.

-spec code_change(OldVsn, State, Extra) -> {ok, NewState} | {error, Reason}
  • OldVsn : Moduleのattributesに含まれるVsnもしくは, {down, Vsn}
    • downgradeの際も古い方のVsnである点に注意が必要です.
  • Extra : appupの{update, my_module, {advanced, Extra}}Extra

Vsnは自分で設定しない限りはチェックサムが採用されてしまう為, human readableではありません.
簡単の為に, ExtraにRelease Applicationのバージョンを入れてしまう場合5もあるようですが, Vsnを正しく定義するのが良いでしょう.

-module(module_c).
-vsn("0.0.2").

-spec migration(FromVsn, ToVsn, State) -> NewState.

code_change({down, Vsn}, State, _Extra) ->
     {ok, migration("0.0.2", Vsn, State)};
code_change(Vsn, State, _Extra) ->
     {ok, migration(Vsn, "0.0.2", State)}.

なお, code_changeでerrorを返した場合は回復不能のエラーであると認識され, 前のバージョンで再起動がかかるので注意が必要です.6 何ともErlangらしい挙動ですね.

2.2-5. 依存モジュールの同時更新

gen_serverで扱うメッセージの種類が増えていった場合, 処理する為の関数を別モジュールに置く場合があるでしょう.
その場合, CodeLoadingのタイミングの観点から 同時に更新したいという要求があります.

{update, my_module, {advanced, []}, [my_module_2]}

とすることで可能です.
my_module_2my_moduleのプロセスのsuspend中に更新されます.
しかし, これは自動生成が難しい為, 考えるべきではないでしょう.

2.3 supervisor

supervisorはホットコードアップデートにおいても特別な存在です.
また, 挙動に一番癖があるので特に注意が必要な箇所でもあります.

2.3-1. 既存の子に関する変更

{update, supervisor, my_sup_module}

これを記述することで, strategy, intensity, period, start, restart, type, modules....とほぼ全ての設定を変更可能です.
例外として, 以下を変更することはできません

  • Processが存在しないChild spec
  • Child id

2.3-2. 子の追加/削除

{"2",
 [{"1",
   [{update, ch_sup, supervisor},
    {apply, {supervisor, restart_child, [ch_sup, m1]}}
   ]}],
 [{"1",
   [{apply, {supervisor, terminate_child, [ch_sup, m1]}},
    {apply, {supervisor, delete_child, [ch_sup, m1]}},
    {update, ch_sup, supervisor}
   ]}]
}.

このように全て手動で行う必要があります.

3. まとめ

随所で各種ツールの実装について触れましたが, ホットコードデプロイ周りは思っているよりも使われていません. (もしくは最低限の範囲でしか使われていません)
「downgrade何それ美味しいの?」です.7

つまるところ, Learn You Some Erlang (イカ本) でも述べられていますが, 選択肢がない場合にのみ行うものなのです. しかし, 軽微な修正を行う為には非常に有用なツールなのも事実です.

個人的な見解8としては, 以下のことを守った上で自動生成のappupを使って行うのが現実的な落としどころだと思っています.

  • exportしている関数は, 後方互換性が損なわれていない
  • exportする関数の引数に使用されるレコード定義を変更していない
  • 変更のあった関数に入れる引数を変えない
  • supervisorの子は変更しない. 追加削除をしない.

この記事でホットコードアップデートに興味を持ったのであれば, 実際に運用している方のスライドも合わせて読むと良いでしょう.
最後に, 実際に試しながら/コードを読みながら書きましたが誤りがあればご指摘頂ければ幸いです.



  1. ホットコードアップデート/ローディング/スワッピングと様々な呼び方がありますが,公式ドキュメントにはそれらしき単語が見つからなかったので,ここではホットコードアップデートとします. 

  2. とはいえ, 形式チェックのみ行い, appup.srcからコピーしてくるだけです. 

  3. appup_upgrade_hook/2とsup_upgrade_notify/2がexportされていると自動的にupgrade/downgradeの際に呼び出すように実装されています 

  4. システムメッセージを優先する処理は通常書かれません.つまり,メッセージキューに貯まっている場合は処理を待たされることに注意が必要です. 

  5. 毎度俺俺でお馴染みのRelflowのことです.  

  6. 詳しくは, http://erlang.org/doc/man/release_handler.html#install_release-1 

  7. dowgradeも可能なappupを生成するツールをご存じの方はご一報ください.(私がcommitしている場合を除く) 

  8. 私が所属する会社の見解ではありません. 

12
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10