15
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

#NervesJPAdvent Calendar 2020

Day 13

`mix upload.hotswap` (kentaro/mix_tasks_upload_hotswap)の裏側

Last updated at Posted at 2020-12-13

この記事は#NervesJP Advent Calendar 2020の13日目です。

12日目は@torifukukaiouさんの「「kentaro/mix_tasks_upload_hotswap」を試してみる! ご本人が参加していらっしゃるカレンダーにて」でした。さっそく使っていただき、ありがとうございます!この記事では、そのモジュールが何をやっているのかを見ていきたいと思います。


本記事では、mix upload.hotswapというmixタスクを提供する拙作のモジュールkentaro/mix_tasks_upload_hotswapについて、裏側で何が行われているかを見ていきたいと思います。

mix upload.hotswapとは?

mix upload.hotswapとは、いわゆるHot Code Swapping1を行うためのmixタスクです。手元でコードに変更を施した後に、現に動いているElixirのアプリケーションを再起動することなく、その変更をデプロイし適用するという機能を提供しています。

そもそもなぜこれを作ったかということを説明するほうが、やりたいことの理解が進みそうです。このAdvent Calendarのお題であるNervesでIoTデバイスを使ったアプリケーションのコードを書いている時に、ちょっとした変更を適用したいというだけでも、基本的にはファームウェアをビルドしなおして実機にアップロードし、再起動が完了して動作確認ができるようになるまで1分以上かかる2のが面倒になってきました。そこで、Erlang/Elixirの提供するHot Code Swappingを使えばもっと楽できるのではないか?と思って作ったのがこのタスクです。実際、mix upload.hotswapでは、後述する制約はあるものの数秒でデプロイが完了します。

Hot Code Swappingそのものについては、@kikuyutaさんによる「はじめてな Elixir(30) プロセスのホットスワップをする」が、mix upload.hotswapの使い方については前述の@torifukukaiouさんによる「「kentaro/mix_tasks_upload_hotswap」を試してみる! ご本人が参加していらっしゃるカレンダーにて」がわかりやすいので、ぜひご覧ください。

ElixirにおけるHot Code Swapping

そもそもHot Code SwappingはErlang/Elixirそのものが提供する機能です。その機能を利用したデプロイは、Elixir 1.9から提供されているmix releaseというタスクによってデプロイに必要な成果物(これを「リリース」といいます。tar.gzファイルとしてパッケージングされています)を作成し、サーバに配置して実行することで、実現できもします3

ではなぜmix upload.hotswapを作ったのか。前述の通り、そもそもの目的はNervesによるIoTデバイス/アプリケーション開発効率を向上することです4。IoTデバイス上で動くコードを更新するには、一般に、施したコードの変更を元にファームウェアをビルドし直して、デバイス上のファームウェアを新しいもので置き換えて再起動するという工程があります。それはそれでよいのですが、前述の通り1回のデプロイで1分以上時間がかかります。そこで、リリースを用いたりファームウェアの更新をするのではない方法でHot Code Swappingを実現したかったのでした。

kentaro/mix_tasks_upload_hotswapの実装

kentaro/mix_tasks_upload_hotswapの実装は、hotswap.exに含まれるコードが本質的な部分です。

def run(_) do
  app_name = get_config_for(:app_name)
  nodes = get_config_for(:nodes)
  cookie = get_config_for(:cookie)

  System.cmd("epmd", ["-daemon"])
  {:ok, _} = Node.start(:me@localhost)
  Node.set_cookie(cookie)

  for node <- nodes do
    handle_connect(Node.connect(node), node)
  end

  {:ok, modules} = :application.get_key(app_name, :modules)

  for module <- modules do
    for node <- nodes do
      handle_load_module(IEx.Helpers.nl([node], module), module, node)
    end
  end
end

このコードでは、最初に設定ファイルから各種情報を取得します。次に、ノードを起動して、デプロイ先のIoTデバイスと通信できるようにします。また、IoTデバイスの方でも、前もってノードを起動しておく必要があります5。そして、IoTアプリケーションを実装するコードに含まれるモジュールを取得して、IoTデバイスで動いているモジュールをそれらで置き換えるということをしています6

たったこれだけのことで、Hot Code Swappingが実現できているのは驚きですね。ただ、これだけだと何が起きているのかさっぱりわからないので、もう少し深堀りしてみましょう。

:application.get_key(APP_NAME, :modules)

まず、ローカルでコードの変更を施したモジュールをリストアップする必要があります。:application.get_key/27に、Applicationの名前を示すatomと、:modulesを渡してあげると取得できます。IExで試すと、以下のようになります。

$ iex -S mix

iex(1)> :application.get_key(:example, :modules)
{:ok, [Example, Example.Application, Example.Counter]}

lib/ディレクトリ以下のモジュールが取得できているようです。

IEx.Helpers.nl/2

上記で取得したモジュールを、IEx.Helpers.nl/2によってIoTデバイス上で立ち上がっているノードで読み込みます。ところで、このメソッドは何をしているのでしょうか?IEx.Helpers.nl/2のドキュメントには、以下のように書かれています。

Deploys a given module's BEAM code to a list of nodes.

This function is useful for development and debugging when you have code that has been compiled or updated locally that you want to run on other nodes.

受け取ったモジュールのBEAMコード(Erlang VMで実行するための内部表現。Javaのclassファイルみたいなものでしょうか)をノードにデプロイするということをするとのことです。また、開発時やデバッグ次に便利だという旨も書かれています。まさに、今回の目的に沿う機能であるといえます。

さらに深堀りしてみましょう。Elixir v1.11.2におけるIEx.Helpersの実装は以下の通りです。

def nl(nodes \\ Node.list(), module) when is_list(nodes) and is_atom(module) do
  case :code.get_object_code(module) do
    {^module, bin, beam_path} ->
      results =
        for node <- nodes do
          case :rpc.call(node, :code, :load_binary, [module, beam_path, bin]) do
            {:module, _} -> {node, :loaded, module}
            {:badrpc, message} -> {node, :badrpc, message}
            {:error, message} -> {node, :error, message}
            unexpected -> {node, :error, unexpected}
          end
        end

      {:ok, results}

    _otherwise ->
      {:error, :nofile}
  end
end

まず、Erlangのcode.get_object_code/18により、BEAMコードのバイナリを得ています。次に、引数としてわたされたノードたちに対して、これまたErlangのrpc.call/49によりデプロイし、ノードのコンテキストでcode.load_binary/310を実行しています。そうしてモジュールをロードすることにより、Hot Code Swappingを実現していることがわかります。

それにしても、これだけの実装でHot Code Swappingができることには、あらためて驚きです。

Hot Code Swappingの2つの方式

Erlang/Elixirのモジュールには、以下の2種類があります。

Residence module - The module where a process has its tail-recursive loop function(s). If these functions are implemented in several modules, all those modules are residence modules for the process.
Functional module - A module that is not a residence module for any process.

Erlang -- Release Handling > Release Handling Instructionsより

Erlang/Elixirでは、関数の最後で自分自身を呼び出すことで再帰を実現できます11。そして、再帰する関数をKernel.spawn/1Kernel.spawn_link/1でプロセスとして起動することで、並行実行されるプロセスとして処理をバックグラウンドで実行できます12。上記のResidence moduleとは、そうしたモジュールのことをいいます。一方で、Residence moduleでないモジュールのことをFunctional moduleといいます。

また、上記で解説したのは、Hot Code Swappingの2つの方式の内のsimple code replacementと呼ばれるものです。もうひとつは、synchronized code replacementと呼ばれるもので、前述の「はじめてな Elixir(30) プロセスのホットスワップをする」で解説されている方式です。

mix upload.hotswapが対応しているのは、Functional moduleに対するsimple code replacementです13

If a simple extension has been made to a functional module, it is sufficient to load the new version of the module into the system, and remove the old version. This is called simple code replacement and for this the following instruction is used:

Erlang -- Release Handling > Release Handling Instructionsより

一般のHot Code Swappingにおいては、たとえばGenServerで実装したプロセスの状態(state)の構造に変更を加えたときなどに、GenServer.code_change/314を用いてsynchronized code replacementを実現できます。

一方で、mix upload.hotswapでは、単にこのタスクを実行するだけでHot Code Swapingが実現できる利便性とsynchronized code replacementを実現できるうまい方法を思いつけていないために、現状ではsimple code replacementへの対応となっています15

mix upload.hotswapの制約と使いどころ

mix upload.hotswapには、現状では以下の実装の詰めが甘いところや制約があります。

  • lib/以下に含まれるモジュール全部を(更新があろうがなかろうが)デプロイするやり方になっています。本当は更新したコードだけデプロイできるといいのでしょうけれども……。
  • 前述の通り、simple code replacementへの対応のみとなっています。そのため、GenServerで実装されたプロセスが持つ状態の構造を変更した場合は使えません。
  • また、モジュール間で複雑に依存関係がある場合は、デプロイしたあとに不整合が起こる可能性があります16

そのため、mix upload.hotswapは使いどころを定めて利用するのがよいと思います。

  • 依存モジュールを追加したり、GenServerで実装されたプロセスが持つ状態の構造を変更した場合などは、従来どおりmix firmware && mix uploadによりファームウェアそのものを更新してください
  • その他、モジュールの細部を更新したものをIoTデバイスに素早く反映させたい時にmix upload.hotswapを使うとよいでしょう。

というわけで、ふだんの開発時やデバッグ時のように、早いサイクルでPDCAを回したい時に用いていただけるとよいと思います。

おわりに

本記事では、NervesでIoTデバイス/アプリケーションを開発するに際して感じたペインを解決する仕組みとして開発したkentaro/mix_tasks_upload_hotswapについて、それが実際にはどういうことをやっているのかという裏側を解説してみました。

ぜひお使いいただきフィードバックをいただけると幸いです。


#NervesJP Advent Calendar 2020の14日目は、これまた拙文の「『プログラミングElixir 第2版』を読んでいまこそElixirに入門しよう」です。引き続きお楽しみください。

  1. GenServer.code_change/3のドキュメントではHot Code Swappingといわれていますが、ErlangのドキュメントだとCode Loadingだとか、Code Replacementだとかいわれています。

  2. 実際には、必ずしもファームウェアをまるごと更新する必要はなかったりします。Nervesの1.7.0より正式にサポートされたファームウェアパッチによる更新が可能です。

  3. 正確には、Elixirがオフィシャルにサポートしているわけではありません(mix release — Mix v1.11.2を参照のこと)。そのため、Hot Code Swappingの実現には、リリースを作るためのライブラリであるDistilleryの2.0以上のバージョンを使う必要があります。『プログラミングElixir 第2版』の382ページにそのあたりのことが書かれています。また、実際の利用のしかたはHow to perform Hot Code Swapping using Distillery— #2 — A (Live Demo) GenServer State updateを参照してください。

  4. とはいえ、この仕組み自体はNervesでしか使えないというものではなく、汎用的に使えるものではあります。

  5. そのあたりはドキュメントに書いてありますので、ご参照ください。また、この記事ではノード間通信については扱わないので、詳しく知りたい方は「はじめてな Elixir(27) ノード間の通信がどうなってるか調べる」を是非ご覧ください。

  6. ちなみに[node]としている部分はnodesをそのままわたすのでもいいのですが、ノード別・モジュール別にロードしたことを表示したかったので、冗長ですがそのように実装しています。

  7. http://erlang.org/doc/apps/kernel/application.html#get_key-2

  8. https://erlang.org/doc/man/code.html#get_object_code-1

  9. https://erlang.org/doc/man/rpc.html#call-4

  10. https://erlang.org/doc/man/code.html#load_binary-3

  11. Schemeなどのような関数型言語同様に末尾最適化が施されており、再帰呼び出しが関数の先頭へのジャンプ命令に変換されるため、スタックオーバーフローが起きないようになっています。

  12. 具体的には、「ウェブチカでElixir/Nervesに入門する(2020年12月版)」で紹介したウェブチカのループを実装したコードにシンプルな実装例がありますので、参照してください。

  13. Residence moduleでもできそうな気もするけど、できないのだろうか。試してみていないのでわからないです。

  14. https://hexdocs.pm/elixir/GenServer.html#c:code_change/3

  15. つまり、GenServerで実装されたプロセス持つ状態の構造を変更した場合には、従来どおりmix firmware && mix uploadが必要であるということです。うまい実装をご存じの方は、ぜひ教えて下さい。

  16. このあたりの解決が困難なので、mix releaseのドキュメントにある通り、Hot Code SwappingはElixirの公式サポートからは外されてしまったのでした。

15
2
9

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
15
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?