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できるように用意しておくといい
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy