⚱️

OTP distribution with elixirことはじめ(on AWS)

2022/02/16に公開

はじめに

参考

https://elixirschool.com/ja/lessons/advanced/otp_distribution

概要

パブリッククラウドの利用は,コンピュート資源をはじめとした各種リソースの利用に際して,時にHWの存在を透明化します.

OTP (Open Telecom Platform) distributionは,Appの実行に際して,実処理がどのホスト・ノード上で実行されているかを気にせず,複数のノード間通信分散処理を実現することができます.

本記事では,OTP distributionによるノード間分散処理がどのように動作するか,実践的に確認していきたいと思います.

書くこと

  • OTP distributionの実装例
  • 動作検証結果 on AWS

書かないこと

  • OTP distributionの動作原理・詳細
  • 他技術(MPI等)との比較

本文

Elixirについて

Elixirは拡張性と保守性の高いアプリケーションを構築するためにデザインされた、動的で関数型のプログラミング言語です。
Elixirは、低レイテンシで分散型のフォールトトレラントシステムや、Webや組み込みシステムの領域で成功を収めている、Erlang VMを利用します。

https://elixir-lang.jp/

Elixirは,関数型でパイプライン演算子を駆使しながら,極めて軽量・独立したプロセスを持つErlang VM上で実行されます.
高い並列性を簡単に実現できることがErlang系の言語の利点だと思っておりますが,ElixirはかつてのErlangを現代風に置き換えたスーパーセットのようなものなのかもしれません.

OTP (Open Telecom Platform)は,並列処理を行う上で必要なライブラリセットのようなものと理解しています.

ローカル通信

OTP distributionは,異なるノード間で遠隔から処理を実行できる,いわゆるRPCのようなものだと理解しています.

具体例として,まずはローカル通信において異なるターミナル(セッション)間で実行されている異なるプロセス同士が,お互いのモジュールを利用する簡易なAppを,チュートリアルをもとに実装してみましょう.

Elixirは,下記リンクのElixir schoolが充実しているのが採用メリットの一つかもしれません.

https://elixirschool.com/ja/lessons/advanced/otp_distribution

上記で「プロセス」と表現したElixirのランタイムを,Elixirではノードと表現します.
以下の例では,

# terminal ALEX
$ iex --sname alex@localhost

iex(alex@localhost)>
# terminal KATE
$ iex --sname kate@localhost

iex(kate@localhost)>

この状態で,alex@localhostからkate@localhost内で定義されたモジュールの実行を行うため,まずはkate@localhost内でモジュールを定義しましょう.

# terminal KATE

iex(kate@localhost)> defmodule Kate do
...(kate@localhost)>   def say_name do
...(kate@localhost)>     IO.puts "Hi, I'm Kate"
...(kate@localhost)>   end
...(kate@localhost)> end

kate-alex.drawio.png

では実際に,alex@localhostからKate.say_name関数を読んでみます.

iex(alex@localhost)> Node.spawn_link(:kate@localhost, fn -> Kate.say_name end)
Hi, I'm Kate
#PID<11847.129.0>

kate-alex.drawio.png

すごいですね.
対話型のShellから,簡単にノード間通信を実現できました.

チュートリアルに記載がある通り,Kateノード上で実行された実行結果の出力であったとしても,Alexノード上で取得されていることに注意してください.

今回はローカルホスト間の別ノード間での通信であるため,ありがたみに欠けますが,これが異なるホスト間に拡張されても同様に実行できる場合,マイクロサービスの実現や,大規模な並列計算も実現できることが想像にたやすいかと思います.

複数ホスト間通信

AWS上に以下構成のEC2 Instanceを二台建て,SSM Session Managerでアクセスをして操作を行います.検証完了後,全Instanceは終了処理を行っております.

elixirの正規のInstall手順,意外と面倒なのでAMI2上にbrewをインストールしてそれ経由でInstallしていきたいと思います.

https://qiita.com/blackpeach7/items/c61c3e3d1acddbe36114

$ sudo yum install -y git
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

$ export PATH=/home/linuxbrew/.linuxbrew/bin:$PATH
$ brew install gcc

$ brew install elixir
$ elixir -v

Appは先ほど同様.
実行時には, --cookieオプションを使用してリモートノード間を接続します.

# Host A
$ iex --sname kate --cookie sample
iex(kate@ip-10-0-181-45)1> defmodule Kate do
...(kate@ip-10-0-181-45)1>   def say_name do
...(kate@ip-10-0-181-45)1>     IO.puts "Hi, I'm Kate on AWS EC2"
...(kate@ip-10-0-181-45)1>   end
...(kate@ip-10-0-181-45)1> end
# Host B
$ iex --sname alex --cookie sample
iex(alex@ip-10-0-168-214)1> Node.spawn_link(:"kate@ip-10-0-181-45", fn -> Kate.say_name end)
#PID<12300.126.0>
Hi, I'm Kate on AWS EC2

終わりに

ノード間の名前解決等は,今回は静的に指定することで解決したが,実際には各サービスの前段に置いたLoadBalancerや,Ingress Controller,NodeIP等を指定するのが良いと思います.

また, Node.spawn_linkを直接叩いてリモートノードにプロセスを建てるのはあまり良くないパターンらしいです.

けれども、監視ツリーの外にプロセスをつくるのは、できるだけ避けるべきです。今回の実装でNode.spawn_link/2を用いるより望ましいやり方は3つ考えられます。
Erlangの:rpcモジュールを使うと、リモートノードの関数が実行できます。たとえば、シェルから:rpc.call(:"foo@computer-name", Hello, :world, [])とすれば、別ノードの関数Hello.world/0が呼び出せます。
GenServerのAPIにより他のノードでサーバーを起ち上げれば、要求が送れます。たとえば、GenServer.call({name, node}, arg)によりリモートノードのサーバーが呼び出せます。第1引数はリモートプロセスのPIDにしても構いません。
Taskを用いて、ローカルとリモートの両ノードに生成することもできます(「MixとOTP 08: タスクとgen_tcp」参照)。

https://dev.to/gumi/mix-otp-10-3i31

実際には, より高位の関数を用いたりスーパーバイザーによる監視のもとでプロセスを起動するのが良いでしょう.

elixirを使うことで,従来できなかった(難しかった)SIMD並列分散処理が容易に実現できることから,k8s等とまた違うレイヤーでの分散処理基盤を構築することができるようになります.

ここまで書いておいてあれですが,他にわかりやすい記事があったので掲載しておきます.

https://dev.classmethod.jp/articles/classmethod-with-elixir-and-otp/

Discussion