この記事は、#NervesJP Advent Calendar 2019 の16日目です。昨日は @y_jono さんの NervesでEV3(lego mindstorms)を走行させよう(NervesでのEV3ファームウェア開発) でした。
今日は複数の Nerves マシン同士を通信させる方法を考えてみたいと思います。本当にこんなことをしないとならないのか未だに半信半疑なので、もしこんな方法があるよという話があればぜひお知らせください。
概要
Nervesに限らず、ElixirでもErlangでも「複数ノードで並行プログラミング」の話はいくつもあります。これは単一マシン・単一OS上での複数ノードのことが多く、「複数マシンでネットワーク的に分散している環境で並行プログラミング」の話はあまり転がっていません。今回はこれについて考えてみました。以下「ノード」というのはElixir用語でのnodeを指すことにします。個体として独立したハードウェアやOSについて「マシン」という名前で統一します。
複数マシンでのElixirマシンの接続
異なるマシン上でElixirを稼働してそれらのプロセス同士で通信して動かすことが可能です。このときElixirが以下の条件で稼働していることが必要です。
- ノード名の
@
以降にマシンを識別するIPアドレスが表記されている - クッキーが使用されている
例えば iex を起動するなら以下のように起動します。
$ iex --name alice@192.168.55.3 --cookie comecomeeverybody
$ iex --name bob@192.168.55.7 --cookie comecomeeverybody
すると Node.connect/1
関数 1 で互いを結びつけることが出来ます。
iex(alice@192.168.55.3)1> Node.connect(:"bob@192.168.55.7")
true
iex(alice@192.168.55.3)2> Node.list
[:"bob@192.168.55.7"]
これは Nerves であっても(きっとおそらく)同様です。Nerves を使うときに固定IPアドレスが使える環境ならネットワークインタフェースにアドレスを指定して、ノード名の @
以下にアドレスを書いて渡せばできるに違いありません。Nerves でのアドレスの設定の仕方はNerves: Connecting to your Nerves target を御覧ください。
Virtual Ethernet を使う場合の問題
ただしこの「ノード名の @
以下にアドレスを書いて」が曲者です。PCで遊んでいるときは(自由に書ける場合は)良いのです。
しかしながら、Nerves の場合はちょっと違います。ターゲットマシンをUSBでホストPCにつないで、その上でVirtual Ethernetを使うことがほとんどでしょう。このときがちょっと面倒になります。
@
から後ろが nerves.local
になってますね。これの .local
はホストPCからみると mDNS (multicast DNS) で名前解決をしてターゲットのVirtual Ethernet上のIPアドレスを発見するのに使います。なのでここに IP アドレスを持ってくるとホストPCで ssh nerves.local
と書いてもアクセスできなくなります。USB の口の Virtual Ethernet 側の IP アドレスが解決できなくなるので、ここにはIPアドレスを書けないのです。
とりあえずサーバを介して接続できれば良しとする
この問題は、例えば Node.connect/2
関数があって、第1引数が node
第2引数がIPアドレスを指定できたりすると一気に解決なのですが、そういう関数はありません。なので2つのNervesマシンをいきなり接続するのが難しそうです。しょうがないので仲介するサーバがあればよいということに今回は落ち着きました(落ち着かせました)。仮定としてサーバは固定のIPを持つものとします。
今回のネットワーク
ラズパイ0Wが2つあって、それぞれ Alice と Bob という名前にします。それぞれ Virtual Ethenet 経由でホストPCにつながってます。また Alice と Bob は WiFi 経由で IP で到達可能なネットワーク上にある Server マシンにつなげることが可能です。
よくある構図と思います。Nerves トレーニングを受けた方は、トレーニングのセットが2つあると思ってください。トレーニングのときはserverの方には NervesHub がありましたね。
基本方針
このネットワークでAliceとBobが間接的にでもつながれば良しとします。すなわち「Alice と Bob は Server と通信できる」こと、さらに言い換えると「それぞれが Server に対して Node.connect/1
関数で接続できる」ことが出来れば今回はOKとします。このために以下をします。
- ノード名の
@
より前を変える- 全部が プロジェクト名 @nerves.local だと、server から見て何がどれなんだか区別できないので
- クッキーを設定する
- クッキーを設定しないと
Node.connect/1
が接続してくれないので
- クッキーを設定しないと
設定
では具体的な設定を行っていきます。
基本設定
まずは Nerves の基本的な設定をしてください。ラズパイ0Wの USB ケーブルで Virtual Ethernet を使うことを前提にしています。あと WiFi の設定は以下のようにします。
config :default,
wlan0: [
networks: [
[
ssid: "お手元のWiFiのSSID",
psk: "お手元のWiFiのパスワード",
key_mgmt: :"WPA-PSK" # WPA-PSK2の場合でもこのままで繋がりました
]
]
]
あとは以下でターゲットの Nerves マシンを作ります。おそらくこれだけだと思うのですが、色々やってるうちに失念したかもしれません。何かおかしかったら教えて下さいませ。
$ export MIX_TARGET=rpi0
$ mix deps.get
$ mix firmware
$ mix firmware.get.script
$ ./upload.sh
第1行目の環境変数 MIX_TERGET
はラズパイ0系用にしてあります。マシンの種類によって
Nerves: Supported Targets and Systems
から選んでください。
ノード名を変える
Nerves というか Elixir というか Erlang のノード名を変更するにはいくつか方法があります。ここに見つけた方法をあげておきます。今回は最初の方法で実施しました。
rel/vm.args を変更する
Nerves のターゲットの開発環境ディレクトリ(トレーニングで _target
となるディレクトリ)の下に rel/vm.args.eex
というファイルがあります。
## Add custom options here
## Distributed Erlang Options
## The cookie needs to be configured prior to vm boot for
## for read only filesystem.
これに以下の行を加えるとノード名が変更できます。alice
の場合は以下です。
-name alice@nerves.local
詳しくは Erlang: vm.args を読んでください。
config/target.exs を変更する
Nerves のファイルを変更することでもノード名を変更できます。
config :nerves_init_gadget,
ifname: "usb0",
address_method: :dhcpd,
mdns_domain: "nerves.local",
# node_name: node_name, # これをコメントアウト
node_name: :alice, # これを追加
node_host: :mdns_domain
ノード名を変更するだけならこれでも良かったのですが、以下に続くクッキーの変更もあるので、今回はこちらは使いませんでした。
秘密の :erlang.setnode/2 :erlang.setnode/3 を使う
動いているノードの名前を変更できる 隠しコマンド らしいです。ネットで拾いました。なにせ公式ドキュメントに何も書いてないので引数がよく分かりません。ただし以下のように関数が存在するのは確かです。ここのエラーは「そんな関数は存在しない」ではなく「引数が誤ってる」ですから。
iex(afo@10.0.1.6)6> :erlang.setnode(1,2,3)
** (ArgumentError) argument error
:erlang.setnode(1, 2, 3)
iex(afo@10.0.1.6)6> :erlang.setnode(0,1)
** (ArgumentError) argument error
:erlang.setnode(0, 1)
クッキーを指定する
異なるノード間でやりとりしようとするとクッキーの指定が必要になります。これも複数の方法があります。静的に指定するのが前者ですので、今回は前者を使いました。
rel/vm.args に記述する
先程のノード名を指定するのに使った rel/vm.args.eex
に以下の行を付け加えてください。
-setcookie comecomeeverybody
:erlang.set_cookie/2 を使う。
これは動いているノードにクッキーを指定するコマンドです。
iex(alice@nerves.local)1> :erlang.set_cookie(node(), :comecomeeverybody)
true
これは隠しコマンドでもなんでもなくて公式ドキュメント Erlang: set_cookie/2 に説明があります。
実行してみる
ラズパイ0Wを2つ用意して以下をやってみました。
- Nerves をインストールする
- それぞれのノード名を変更する
- それぞれのクッキーを設定する
これとは別にサーバに相当する PC で iex を起動しました。
Alice の起動
$ ssh nerves.local
...snip # たくさんメッセージが出るので省略
iex(alice@nerves.local)1>
ちょっとたくさん出てきますが alice@nerves.local
で立ち上がります。ネットワークインタフェースを見てみましょう。
iex(alice@nerves.local)1> ifconfig
lo: flags=[:up, :loopback, :running]
inet 127.0.0.1 netmask 255.0.0.0
inet ::1 netmask ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
hwaddr 00:00:00:00:00:00
wlan0: flags=[:up, :broadcast, :running, :multicast]
inet 10.0.1.11 netmask 255.255.255.0 broadcast 10.0.1.255
inet fe80::ba27:ebff:fe1d:bd23 netmask ffff:ffff:ffff:ffff::
hwaddr b8:27:eb:1d:bd:23
usb0: flags=[:up, :broadcast, :running, :multicast]
inet 172.31.140.149 netmask 255.255.255.252 broadcast 172.31.140.151
inet fe80::e096:3ff:fe90:eb43 netmask ffff:ffff:ffff:ffff::
hwaddr e2:96:03:90:eb:43
このように、ループバック、WiFi、USB、のネットワークインタフェースが活きているのが分かります。この usb0
はホストPCとの接続用に、wlan0
をサーバとの接続用にします。
Bob の起動
$ ssh nerves.local
...snip # たくさんメッセージが出るので省略
iex(bob@nerves.local)1>
同様に bob@nerves.local
も立ち上がります。ifconfig
すると alice 同様にネットワークインタフェースが3つできてるのが分かります。
Server の準備
サーバは MacOS 上の iex です。固定のIPアドレスがありますので、それをノード名に含めて server@10.0.1.6
としてクッキーと一緒に立ち上げます。
$ iex --name server@10.0.1.6 --cookie comecomeeverybody
Erlang/OTP 22 [erts-10.5.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.9.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(server@10.0.1.6)1>
念のため接続しているノードを確認しておきます。
iex(server@10.0.1.6)1> Node.list
[]
Alice と BoB とをサーバに接続する
ではNervesマシンをサーバに接続してみます。
iex(alice@nerves.local)2> Node.list
[]
iex(alice@nerves.local)3> Node.connect(:"server@10.0.1.6")
true
iex(alice@nerves.local)4> Node.list
[:"server@10.0.1.6"]
とうまいこと接続できました。Bob もやってみましょう。
iex(bob@nerves.local)2> Node.list
[]
iex(bob@nerves.local)3> Node.connect(:"server@10.0.1.6")
true
iex(bob@nerves.local)4> Node.list
[:"server@10.0.1.6"]
と接続できます。サーバを見てみましょう。
iex(server@10.0.1.6)2> Node.list
[:"alice@nerves.local", :"bob@nerves.local"]
と両方の Nerves マシンとの接続が確立しているのが分かります。こうなれば Elixir の並行プログラミングはこれまで同様にできそうです。
全部のノードが完全グラフでつながるのではないことに注意
上で「同様にできそうです」と書いていますが、あくまでサーバとNervesノードとでの通信においてです。
同一マシン上で3つ以上のノードに Node.connect/1
した場合は、明示的に接続を指定してないノードであっても全てのノードで全てのノードを Node.list/0
で見ることができました。実際、全てのノードにおける任意の2ノード間で直接通信するような並行プログラミングが可能になります。ところが今回の場合はそうはなりません。
3地点で同時に Node.list/0
を実行した結果が以下です。
iex(server@10.0.1.6)3> Node.list
[:"alice@nerves.local", :"bob@nerves.local"]
iex(alice@nerves.local)5> Node.list
[:"server@10.0.1.6"]
iex(bob@nerves.local)5> Node.list
[:"server@10.0.1.6"]
このように、alice と bob は直接通信ができないのです。今回 IP で届くところと言いつつ、実は alice と bob と server とは全く同一の LAN セグメント 10.0.1.0/24 上にあります。ですのでルータの経路制御とかは関係なく alice と bob を直接通信させるこが可能な環境です。これはそもそも Elixir/Erlang のノード接続の方式だとは思うのですが、もう少し時間をかけて確認したいところです。
まとめ
広域分散環境にシステムを構築することを念頭に、Nerves の複数のマシン間での通信をさせてみました。このためにノード名の変更とクッキーの追加を行う方法を探しました。実際に実験してみて、サーバとの接続はできましたが、Nerves マシン間の直接の接続はできませんでした。
ちなみに、今回使ったような環境はたまたまそうだったと言うより、よくある話なのかと思います。一つのマシンに対して保守・管理のIFと運用のIFとがあるというのは比較的自然だからです。このような場合にでもちゃんとノードの接続が出来ないといけません。今回、ラズパイ0WのNervesを対象にしたので時間を喰いましたが、割と使い道があるのかもという気がしてきてます。
さて、明日の#NervesJP Advent Calendar 2019 の記事は @gokkozemisei さんの
エリジョになってElixirでお天気情報を取得してみた です。こちらもお楽しみに!
参考文献
-
おそらくErlangの :net_kernel.connect_node/1 関数へのラッパ。 ↩