Hot code upgrade in ACCESS

桐野 俊輔 (skirino) @ ACCESS

Tokyo.ex #8 (2017/11/26)

  1. Hot code upgradeの仕組み
    a. ErlangVMの提供する機能
    b. OTPの提供する機能
  2. ACCESSでの運用状況・設計判断について紹介

Hot code upgradeの仕組み

Hot code upgradeとは

  • ErlangVMの実行を止めずにコードを更新できる
  • Erlang/OTPの強みの1つとされることが多い
  • が、あまり情報が出回ってない
    • 実運用してる人はわりと少ないようだ
  • 「やめておけ」と言われることが多い(こことか)

Pros

  • OSプロセスが生き続けるので、メモリ上のデータ・TCPコネクションなどを維持できる
  • Load balancerの向き先などを変更しなくてよい
  • ErlangVM再起動などの手段よりも速い
  • (なんだかかっこいい)

Cons

  • 正しくやるのは難しい
    • コード変更内容に応じて更新時の処理が違う
    • 特にテストをきっちりやるのは困難
      • 変更内容に応じて毎回状況が違う
      • prod環境のプロセス内部状態(厳密には負荷状況も)を再現しなければならない

Module loading

  • ErlangVMはmodule単位でコードを管理する
  • コンパイラは各moduleのソースコードからbeamファイルを作る
  • ErlangVM起動後、beamファイルを読み込んで実行
    • beamファイルの読み込み操作などのAPIが提供されているので、Erlang/Elixirから挙動をプログラミングできる

How hot code upgrade works (1)

  • ErlangVMは各moduleの中身を2バージョンまで保持する
    • "current""old"
  • currentoldのコードはどちらも実行可能
  • “新規の”関数呼び出しはcurrentを使う
    • モジュール名を指定せずに同一モジュールの関数を呼ぶ場合(local call)は”新規”ではない

How hot code upgrade works (2)

  • 新しいバージョンのmoduleをロードしたとき
    • ロードしたバージョンをcurrent
    • currentoldにする
    • oldを捨てる
      • その際oldを実行中のプロセスはbrutal kill
        • stacktraceが残ってれば”実行中”

注意点

  • killされないよう、oldからcurrentに切り替わるようにしなければならない
    • 普通にGenServerなどを書いてればだいたい大丈夫
  • 一部の操作はoldを消してcurrentだけにしようとする

Demo

Demo (1)

  • 次のスライドのコードをloop.exなどとして保存
  • iexから以下でループ開始
    • > c("loop.ex")
    • > spawn_link(fn -> Loop.loop(0) end)
  • コードを書き換えて、再度 > c("loop.ex")を試す
defmodule Loop do
  def loop(n) do
    Enum.each(1..20, fn i ->
      :timer.sleep(1_000)
      IO.inspect({n, i, str(), __MODULE__.str()}) # local and remote call
    end)
    __MODULE__.loop(n + 1) # remote and tail-recursive call
  end

  def str() do
    "str/0 version1" # change this and reload!
  end
end

Demo (2)

  • 一度新バージョンのコードを実行しても、callstackが残っていると移行しきれない
  • 旧バージョンが完全になくなるのは、loop/1の末尾再帰のとき
    • 末尾再帰がremote callでない場合、oldのままで更新されず、次バージョンのload時にkill
  • 移行しきる前に2回loadするとkill

Hot code upgrade and OTP

OTP special processes (GenServerなど)

  • module差し替えだけで済まない場合がある
    • プロセスの持つデータの移行
  • code_changeの仕組み
    • 更新対象のcallback moduleを使っている全プロセスで処理を中断させる
    • code_change/3 callbackでデータ移行
    • 通常の処理を再開

code_change/3のデメリット

  • 一連の挙動はsystem messageのやり取りで実現されている
    • けっこう無駄なやり取りが多い
    • system messageへの応答が通常処理でブロックされうる。5秒タイムアウトで強制kill
    • 一部のライブラリが定義するプロセスでは、system messageをちゃんとhandleしてくれない場合がある
  • suspendしている間は処理が滞る
    • 対象プロセス群の内部状態をatomicに更新するが、こんな重量級の仕組みはいらない場合も多い

OTP application/release

  • OTP application:
    • beamファイルと設定ファイルからなるパッケージングの単位
    • appupファイル: バージョン更新処理を定義する
      • code_change/3でデータ移行”もここで指定
  • OTP release:
    • 複数のOTP applicationsをErlangVMとセットにしたもの
    • relupファイル: appupファイル群をまとめたもの
    • release単位で更新を適用する方法が定義されている(:release_handler)

release一式を生成するツールたち

  • systools: Erlang同梱(詳細不明)
  • reltool: Erlang同梱(詳細不明)
  • relx: 起動スクリプトもセットで便利、systoolsに依存(使っている)
  • exrm: Elixir用、relxベース(一時使っていたが今は使ってない)
  • distillery: pure Elixir(使ってない)

ACCESSにおける運用状況

antikytheraについて

  • OSSのPaaS framework
  • AWSで稼働中
  • 社内の各チームがそれぞれOTP applicationを作る
  • antikythera coreチームがまとめて運用
  • (いろいろしゃべりたいが、時間の都合で略)

自動デプロイ方針

  • antikythera本体の更新はreleaseを作って:release_handler.install_release/2で取り込む(hot code upgrade)
  • 各チームのOTP appの更新は:release_handler.upgrade_app/2で取り込む(hot code upgrade)
  • “ややこしい”更新は特別扱いして、EC2インスタンス入れ替えで適用する
  • hot code upgradeの際にはcode_change/3しない
    • updateではなくload_moduleを使う単純なappupを生成するツールを自作

“ややこしい”更新について

  • 以下のような更新はgitコミットメッセージに固定文字列を入れておく
    • プロセス内部状態の変更
    • supervision treeの構造変更など、初期化処理の変更
  • デプロイの際、変更分のコミットメッセージを見てデプロイ方式を切り替え

code_change/3を使わない理由

  • (デメリットについては前述)
  • cowboy websocketプロセスがsystem messageに応答しない
    • suspendのためsystem messageが送られると、websocketプロセスがcrashして:init.restart/0になる
  • 動作検証が割に合わない
    • 1回きりのcode_change/3のためにテスト書きたくない
      • 更新内容に応じて状況を再現するのもしんどい
    • インスタンス追加なら更新内容に依らず、同じようにクラスタに参加するだけ

code_change/3の代替案

  • code_change/3に相当するコードをhandle_*の最初に呼び出してデータ移行することもある
  • 単にプロセス内部状態を切り替えるだけ(同じmoduleを使う全プロセスでsyncする必要が無い)ならこれで良い
  • ただし以下は問題になりうる
    • 移行処理が何度も呼ばれる。idempotentにして、余計な処理はしない
    • 一度もmessageが来ないプロセスでは移行処理も走らない
  • pros/consを比べて、インスタンス入れ替えと使い分ける

hot code upgradeの意義

  • インスタンス入れ替えだけでもまあまあOKだが、以下のためなるべくhot code upgradeを使いたい
    • 1日に何十回もデプロイしようと思うと、インスタンス入れ替えは遅すぎる
    • websocket connectionをそのままキープしたい
    • raft_fleetのprocess rebalancingは比較的重いので避けたい

Summary

  • hot code upgradeそのものは有用だが、OTPにあまり頼り過ぎると良くない場合もある
    • GenServerばかり書いているとcode_change/3が目立つが、これが役に立つのは限られた状況のみ
  • hot code upgradeだけでやろうとせず、インスタンス入れ替え方式にfallbackできるように用意しておくといい