Distribunomicon erlang
use many cpu in erlang
erlangは黙っていても、プロセスをちゃんと各CPUに割り当ててくれるか、検証してみる。 テストスクリプトは、前回やったfibを計算するやつ。中々終わらないように、引数を大きくした。
2> test:calc_fibs([100,101,102]). timeout 3> i(). : <0.88.0> test:fib_send/2 233 51843320 0 test:fib/1 174 <0.89.0> test:fib_send/2 233 51095000 0 test:fib/1 186 <0.90.0> test:fib_send/2 233 52452320 0 test:fib/1 184
メインスレッドは、1秒で終了しちゃったけど、その裏で、黙々と計算は行われている。 fib_send
が起動スクリプトで、最終的にはfibを実行してる。
このerlangのプロセスが、CPUに割り振られているか、topで監視してみる。topの表示画面を少しカスタマイズした。u sakae で、自分のプロセスのみ表示。1で各CPU毎の負荷を表示。d 10 で、10秒毎に画面を更新。fで、表示内容を変更(矢印キーで移動して、spaceで選択/非選択、Escで抜ける)。こうして、画面をコピー(コピー結果を編集してる)。
top - 07:54:31 up 2:09, 3 users, load average: 2.69, 1.10, 0.41 Tasks: 148 total, 1 running, 147 sleeping, 0 stopped, 0 zombie %Cpu0 : 0.0 us, 0.2 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu1 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu2 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu3 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st MiB Mem : 2438.9 total, 1130.6 free, 260.7 used, 1047.6 buff/cache MiB Swap: 2045.0 total, 2045.0 free, 0.0 used. 1985.7 avail Mem PID USER VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3155 sakae 2503396 35416 5816 S 300.4 1.4 6:53.26 beam.smp 3162 sakae 2280 740 676 S 0.0 0.0 0:00.00 erl_child_setup :
4個あるCPUのうち3個が忙しく動いている事が観測出来る。各CPUを合計した使用率も300%になってる。最初は、 erl_child_setup
側で、実行されてると予想してたんだけど、そちらはdebianOSレベルではお休みモード。このプロセスは何のためにあるの? しばし疑問だ。
epmd
erlangは分散が得意だそうだ。上で見たように、一つのerlangの中で、プロセスが幾つものCPUに分散出来るんだから、複数のerlangを動かす必要もなかろうに、と思うのは、現場を知らないからだな。
何でもerlangでやりたくなったら、目的毎にerlangを立ち上げたくなるぞ。例えば、携帯屋なら、本業の携帯での通信コントロール、課金システム、請求システム、ウザイinfoと称する広告メール発送、等々。
これらを全部ひっくるめて、一つのerlangでやろう、なんて普通は考えない。それぞれの目的別にerlangを立ち上げて、その間をメッセージで繋げばよい。
複数erlangの連携だな。過去に発掘しておいた、無料で読める本を参照してみる。 Distribunomicon 丁寧過ぎて、本でじっくり読みたい所だ。このセクションでは、複数のerlangに名前を付けてあげる所から話は始まる。
debian:~$ erl -sname z3 Erlang/OTP 23 [erts-11.2] [source] [smp:1:1] [ds:1:1:10] [async-threads:1] [hipe] Eshell V11.2 (abort with ^G) (z3@debian)1>
このように、erlを立ち上げる時に、そのerlに名前を付けてあげる。超合金製のerlangだから、マジンガーZより文字を頂きz3とした。z1,z2号機は既に稼働中。
で、説明では、これらが連携する為の司令塔も同時に立ち上がるとな。その名は、 Command (or daemon) epmd だ。
この司令塔との交信は、世界的な共通番号で行われるそうだ。debianでは、下記のようにその番号(正確にはポート番号)が登録されていた。
debian:~$ grep 4369 /etc/services epmd 4369/tcp # Erlang Port Mapper Daemon epmd 4369/udp
通常、このepmdは一度立ち上がると、意識的に殺さない限り居座るようになっている。ダエモン君だな。サーバーである。けど、クライアントとしても振る舞う事が出来る。
debian:~$ epmd -names epmd: up and running on port 4369 with data: name z3 at port 40065 name z2 at port 34111 name z1 at port 34441
これは、erlが起動した時、それぞれのerlへの通信番号を保持してて、その内容を確認するコマンドだ。多分、それぞれの号機と通信する時は、相手の番号を知った上で、直接やりとりするんだろうね。
debian:~$ epmd -dump epmd: up and running on port 4369 with data: active name <z3> at port 40065, fd = 6 active name <z2> at port 34111, fd = 5 active name <z1> at port 34441, fd = 4
ちまちまとepmdのソースを見てたら、上記のような隠しパラメータを知った。こういうのOSSの楽しみの一つである。
debian:tmp$ echo -ne "\x00\x01\x6e" | nc localhost 4369 name z3 at port 38783
debianだと、こんな事も出来るとな。
それから、epmdを破壊するコマンドも用意されてる。-killだ。これで、司令塔が居なくなる。
下記は、epmd -names をした時のパケットの流れをwiresharkで追ってみた。 解析したパケットをASCIIで保存するには、File -> Export Packet Dissections -> As Plain Textを選んで保存すれば良い(見たい部分をあらかじめ展開しておく事)。インターフェースは lo0 を選んだけどIPv6が使われていたよ。
Internet Protocol Version 6, Src: ::1, Dst: ::1 Transmission Control Protocol, Src Port: 41334, Dst Port: 4369, Seq: 1, Ack: 1, Len: 3 Source Port: 41334 Destination Port: 4369 : TCP payload (3 bytes) Erlang Port Mapper Daemon Length: 1 Type: EPMD_NAMES_REQ (110) Internet Protocol Version 6, Src: ::1, Dst: ::1 Transmission Control Protocol, Src Port: 4369, Dst Port: 41334, Seq: 5, Ack: 4, Len: 66 Source Port: 4369 Destination Port: 41334 : TCP payload (66 bytes) Data (66 bytes) 0000 6e 61 6d 65 20 7a 33 20 61 74 20 70 6f 72 74 20 name z3 at port 0010 34 30 30 36 35 0a 6e 61 6d 65 20 7a 32 20 61 74 40065.name z2 at 0020 20 70 6f 72 74 20 33 34 31 31 31 0a 6e 61 6d 65 port 34111.name 0030 20 7a 31 20 61 74 20 70 6f 72 74 20 33 34 34 34 z1 at port 3444 0040 31 0a 1. Data: 6e616d65207a3320617420706f72742034303036350a6e61... [Length: 66]
名前を表示してねってリクエストで、返答が返ってきてきる。分かり易い例だな。
(z3@debian)1> net_kernel:connect_node( z1@debian ). true (z3@debian)2> nodes(). [z1@debian] (z3@debian)3> net_kernel:connect_node( z2@debian ). true (z3@debian)4> nodes(). [z1@debian,z2@debian]
erl的には、相手と通信出来るように、接続出来るようにリクエストを出す。nodes()で、繋がった相手がリストアップされる。
接続要求に対してtrueが返ってきてる。了解しました、無線用語ではラジャーだ。相手側でも、nodes()すれば、対抗erlが登録されてる事を見て取れる。
debian:tmp$ epmd -kill Killing not allowed - living nodes in database. debian:tmp$ epmd -kill Killed
一台でもerlが生きていると、司令塔を削除出来ない。全部のマジンガーZが死ぬと、司令塔を保持しておく意味が無くなるので、任意に破壊できる(ずっと残しておいても良い)。
立ち上がるerlに -sname で名前を付けた。-nameってのもあるんで、やってみると
(z3@debian)6> debian:~$ erl -name z3 2021-05-27 08:14:30.243593 args: [] format: "Can't set long node name!\nPlease check your configuration\n" label: {error_logger,info_msg} 2021-05-27 08:14:30.255697 supervisor_report ..... : Crash dump is being written to: erl_crash.dump...done
資料で、恐ろしい事が起きるって言ってたのは、この事か。
debian:~$ head erl_crash.dump =erl_crash_dump:0.5 Thu May 27 08:14:30 2021 Slogan: Kernel pid terminated (application_controller) ({application_start_failure,kernel,{{shutdown,.... System version: Erlang/OTP 23 [erts-11.2] [source] [smp:1:1] [ds:1:1:10] [async-threads:1] [hipe] :
理由を探ってみたよ。自主的に落ちたんだね。-snameってのは、簡単に言うと1つのホスト(パソコン)内での名前付けなんだな。それに対して、-nameの方は、FQDNでの指定なのかな。いわゆる、正式なホスト名の指定。@の右側は、DNSサーバーで名前解決してねって事だな。
debian:~$ erl -name z3@10.0.2.15 Erlang/OTP 23 [erts-11.2] [source] [smp:1:1] [ds:1:1:10] [async-threads:1] [hipe] Eshell V11.2 (abort with ^G) (z3@10.0.2.15)1>
ob$ erl -name ob@aa.bb.cc.128 -setcookie NTTxKDDIxSB Erlang/OTP 23 [erts-11.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] Eshell V11.2 (abort with ^G) (ob@aa.bb.cc.128)1> net_kernel:connect_node( 'pen@aa.bb.cc.129' ). true (ob@aa.bb.cc.128)2> nodes(). ['pen@aa.bb.cc.129']
それから、異なるhost(違うパソコン)と接続したい場合、-setcookie は必須である。これを設定(勿論、両host共、同一な文字列)しておかないと、どう頑張っても接続は失敗する。いわば、合言葉だな。
上の例は、同一サブネット上にあるOpenBSDなhostとDebian機を接続した例である。こういう実験にはVMwareは最適である。尚、epmdは自hostのport番号しかハンドリングしない。
そういう意味では、ダイナミックDNSサーバーもどき(但し、提供するのは、host内にたむろするerlの待ち受けポートだけど)。
ob$ epmd -d epmd: Thu May 27 15:50:58 2021: epmd running - daemon = 0 epmd: Thu May 27 15:50:58 2021: there is already a epmd running at port 4369 on ipaddr 0.0.0.0
このepmdは、広く世界中からの質問に答えるやつだ。ここを攻撃されて、毒素を注入されたらどうしよう? 設定で何とかなるのかな?
with ruby
debian:tmp$ irb irb(main):001:0> require_relative 'erlang' => true irb(main):002:0> erl = Erlang::Erl.new('node1@debian', "testtest") Traceback (most recent call last): 10: from /usr/local/bin/irb:23:in `<main>' 9: from /usr/local/bin/irb:23:in `load' 8: from /usr/local/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>' 7: from (irb):2:in `<main>' 6: from (irb):2:in `new' 5: from /tmp/erlang.rb:289:in `initialize' 4: from /tmp/erlang.rb:289:in `each' 3: from /tmp/erlang.rb:290:in `block in initialize' 2: from /tmp/erlang.rb:302:in `connect' 1: from /tmp/erlang.rb:253:in `connect' RuntimeError (handshake error (status: snot_allowed))
debian:~$ erl -sname node1 -setcookie testtest Erlang/OTP 23 [erts-11.2] [source] [smp:1:1] [ds:1:1:10] [async-threads:1] [hipe] Eshell V11.2 (abort with ^G) (node1@debian)1> =ERROR REPORT==== 27-May-2021::09:06:57.008559 === ** node1@debian: Connection attempt from node node1@debian rejected since it cannot handle ["BIG_CREATION", "UTF8_ATOMS"].** (node1@debian)1>
これ、erl側が期待してる特性をruby側が持っていないって事だな。足りない特性BITを、 Distribution Protocol を見て宣言。そして、接続時のflagsにも追加したよ。 そしたら、状況が変わって
(fst@pen)1> =ERROR REPORT==== 28-May-2021::06:25:24.595486 === ** Cannot get connection id for node fst@pen
idが取れないと言ってきた。この状態の時、ruby側のerlもどきは
sakae@pen:/tmp$ ./con.rb ./con.rb:256:in `connect': handshake error (status: snok) (RuntimeError) from ./con.rb:305:in `connect' from ./con.rb:293:in `block in initialize' from ./con.rb:292:in `each' from ./con.rb:292:in `initialize' from ./con.rb:360:in `new' from ./con.rb:360:in `<main>'
こんなエラーが出てる。
247 # handshake 248 version = 5 249 flags = DFLAG_EXTENDED_REFERENCES | DFLAG_EXTENDED_PIDS_PORTS | DFLAG_\ NEW_FUN_TAGS | DFLAG_NEW_FLOATS | DFLAG_MAP_TAG | DFLAG_BIG_CREATION | DFLAG\ _UTF8_ATOMS 250 msg = 'n' + [version].pack("n*") + [flags].pack("N*") + selfnode.to_s 251 write_msg(soc, msg) 252 253 status = read_msg(soc) 254 if status != 's' + 'ok' 255 soc.close() 256 raise("handshake error (status: #{status})") 257 end 258 259 challenge_msg = read_msg(soc)
該当箇所は、本当に接続の一番最初の所。チャレンジも始まる前段階だ。ちょいと気になるversionだけど、資料によれば、
HighestVersion The highest distribution protocol version this node can handle. The value in OTP 23 and later is 6. Older nodes only support version 5. LowestVersion The lowest distribution version that this node can handle. Should be 5 to support connections to nodes older than OTP 23.
OTP 23 を堺に変わっているんですなあ。5と言う事は古いOTPだな。それに合わせてみるか。 OTP 21 でも同様のエラーだったよ。はて、どうしたものか? 将棋名人、米長さんみたいに、長考してみるか(いえね、今彼の伝記を読んでいるもので。。。。。。長考してます、AIよりも考えてる時間が長いぞ)。散歩中に思い付いた。
ruby側はerlの真似をするんだから、本物のerl側のsnameを指定するのは、論理的に破綻してるよ。と言う事で、再実験。
ob$ erl -sname fst -setcookie KDDI Erlang/OTP 23 [erts-11.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] Eshell V11.2 (abort with ^G) (fst@ob)1> Hello! (fst@ob)1> =ERROR REPORT==== 28-May-2021::16:13:31.385263 === ** Node ruby@ob not responding ** ** Removing (timedout) connection **
接続出来て、Helloまでは、送り込めた。その後はruby側のトラブルにより通信が途絶したって言ってる。まあ、少しは進んだな。
ob$ irb irb(main):001:0> require_relative 'con' => true irb(main):002:0> erl = Erlang::Erl.new('ruby@ob', "KDDI") => #<Erlang::Erl:0x000008a18271b138 ... irb(main):003:0> erl.nodes => ["fst@ob"] irb(main):004:0> erl.rpc_call(erl.nodes[0], :io, :format, ["Hello!\n"]) unknown tag 119 unknown tag 0 #<Thread:0x000008a182719a18 /tmp/con.rb:307 run> terminated with exception (report_on_exception is true): /tmp/con.rb:316:in `block in connect': undefined method `id' for #<StringIO:0x000008a0ad8a0470> (NoMethodError) /tmp/con.rb:340:in `pop': execution expired (Timeout::Error) from /tmp/con.rb:340:in `block in rpc_call' from /usr/local/lib/ruby/3.0/timeout.rb:112:in `timeout' from /tmp/con.rb:339:in `rpc_call' from (irb):4:in `<main>' from /usr/local/lib/ruby/gems/3.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>' from /usr/local/bin/irb:23:in `load' from /usr/local/bin/irb:23:in `<main>'
erl.nodesまでは成功してる。epmdとのセッションは上手くいった。次の処理がまずいのだな。
意味もなく、今度はdebian機で再確認。まずはepmdの状態
debian:tmp$ epmd -names epmd: up and running on port 4369 with data: name snd at port 42383 name fst at port 42979
それから、irb
debian:tmp$ irb irb(main):001:0> require_relative 'con' => true irb(main):002:0> erl = Erlang::Erl.new('ruby@debian', "KDDI") => #<Erlang::Erl:0x029771a4 @node=:"ruby@debian", @cookie="KDDI", @nodes={"... irb(main):003:0> erl.nodes => ["snd@debian", "fst@debian"] irb(main):004:0> pp erl #<Erlang::Erl:0x029771a4 @cookie="KDDI", @msgqueue={}, @node=:"ruby@debian", @nodes= {"snd@debian"=> #<Erlang::Node:0x02976d58 @name="snd@debian", @port=42383, @soc=#<TCPSocket:fd 6, AF_INET, 127.0.0.1, 36420>>, "fst@debian"=> #<Erlang::Node:0x02976628 @name="fst@debian", @port=42979, @soc=#<TCPSocket:fd 7, AF_INET, 127.0.0.1, 47268>>}>
対応が、ここまでの部は、よく分かったよ。
byebug and tcpdump
思い出したようにrubyのdebuggerであるbyebugなんてのを取り出してみる(今年の2月にやったな)。
sakae@pen:/tmp$ byebug con.rb [1, 20] in /tmp/con.rb : (byebug) c unknown tag 119 unknown tag 0 Stopped by breakpoint 1 at /tmp/con.rb:316 : (byebug) p m "p\x83h\x03a\x02w\x00Xw\bruby@pen\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x83h\x02Z\x00\x01w\bruby@pen\x00\x00\x00\x01\x00\x00\x00\x01w\x02ok"
こんな返事が返ってきてるんだけど、プチ見難いな。この際、行き来してるパケットをキャプチャーしてみるか。
root@pen:/tmp# tcpdump -i lo -s 300 -X port 39559 >LOG tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on lo, link-type EN10MB (Ethernet), capture size 300 bytes ^C19 packets captured 38 packets received by filter 0 packets dropped by kernel
久しぶりにtcpdumの登場です。-s0にして、全パケットをHex化した方が良かったかな。突然出て来たport 39559 は、対抗するerlが待ち構えているやつだ。
16:15:19.781822 IP localhost.47996 > localhost.39559: Flags [P.], seq 41:199, ac k 45, win 512, options [nop,nop,TS val 3044367889 ecr 3044367888], length 158 0x0000: 4500 00d2 8ad9 4000 4006 b14a 7f00 0001 E.....@.@..J.... 0x0010: 7f00 0001 bb7c 9a87 a35d 3d0b 86c2 4796 .....|...]=...G. 0x0020: 8018 0200 fec6 0000 0101 080a b575 5e11 .............u^. 0x0030: b575 5e10 0000 009a 7083 6804 6f00 0000 .u^.....p.h.o... 0x0040: 0100 0667 6400 0872 7562 7940 7065 6e00 ...gd..ruby@pen. 0x0050: 0000 0100 0000 0000 6400 0064 0003 7265 ........d..d..re 0x0060: 7883 6803 6400 0924 6765 6e5f 6361 6c6c x.h.d..$gen_call 0x0070: 6802 6764 0008 7275 6279 4070 656e 0000 h.gd..ruby@pen.. 0x0080: 0001 0000 0000 0072 0001 6400 0872 7562 .......r..d..rub 0x0090: 7940 7065 6e01 0000 0001 6805 6400 0463 y@pen.....h.d..c 0x00a0: 616c 6c64 0002 696f 6400 0666 6f72 6d61 alld..iod..forma 0x00b0: 746c 0000 0001 6b00 1148 656c 6c6f 2c20 tl....k..Hello,. 0x00c0: 4920 616d 2072 7562 790a 6a64 0004 7573 I.am.ruby.jd..us 0x00d0: 6572 er 16:15:19.782199 IP localhost.39559 > localhost.47996: Flags [P.], seq 45:108, ac k 199, win 512, options [nop,nop,TS val 3044367889 ecr 3044367889], length 63 0x0000: 4500 0073 d586 4000 4006 66fc 7f00 0001 E..s..@.@.f..... 0x0010: 7f00 0001 9a87 bb7c 86c2 4796 a35d 3da9 .......|..G..]=. 0x0020: 8018 0200 fe67 0000 0101 080a b575 5e11 .....g.......u^. 0x0030: b575 5e11 0000 003b 7083 6803 6102 7700 .u^....;p.h.a.w. 0x0040: 5877 0872 7562 7940 7065 6e00 0000 0100 Xw.ruby@pen..... 0x0050: 0000 0000 0000 0083 6802 5a00 0177 0872 ........h.Z..w.r 0x0060: 7562 7940 7065 6e00 0000 0100 0000 0177 uby@pen........w 0x0070: 026f 6b .ok
後のパケットがerlからruby側への返答だ。このパケットを解析する所でtagの119が意味不って事で落ちている(ruby側で解析ルーチンが組み込まれていないんだから当然の事)。
12.30 SMALLATOMUTF8EXT がそれっぽい。119, 88, 90 のtagを追加した。
--- /home/sakae/con.rb Fri May 28 16:51:05 2021 +++ con.rb Sun May 30 07:35:31 2021 @@ -83,8 +83,10 @@ TYPE_INTEGER = 98 TYPE_FLOAT = 99 TYPE_ATOM = 100 + TYPE_ATOM_UTF = 119 TYPE_PORT = 102 TYPE_PID = 103 + TYPE_PID_EXT = 88 TYPE_SMALL_TUPLE = 104 TYPE_LARGE_TUPLE = 105 TYPE_NIL = 106 @@ -95,6 +97,7 @@ TYPE_LARGE_BIG = 111 TYPE_FUN = 112 TYPE_NEW_REF = 114 + TYPE_NEW2_REF = 90 TYPE_MAP = 116 @@decoder = { @@ -102,6 +105,7 @@ TYPE_INTEGER => lambda{|io| io.read(4).unpack("l>")[0]}, TYPE_NEW_FLOAT => lambda{|io| io.read(8).unpack("G")[0]}, TYPE_ATOM => lambda{|io| io.read(r_int16(io)).to_sym}, + TYPE_ATOM_UTF => lambda{|io| io.read(r_int8(io)).to_sym}, TYPE_SMALL_TUPLE => lambda{|io| Tuple.new((1..r_int8(io)).map{from_binary(io)})}, TYPE_LARGE_TUPLE => lambda{|io| Tuple.new((1..r_int32(io)).map{from_binary(io)})}, TYPE_NIL => lambda{|io| [] }, @@ -114,8 +118,12 @@ TYPE_MAP => lambda{|io| (1..r_int32(io)).reduce({}){|acc| acc[from_binary(io)] = from_binary(io); acc} }, TYPE_PID => lambda{|io| node = from_binary(io); (id,b,c) = io.read(9).unpack("NNc") Pid.new(node, id, b, c) }, + TYPE_PID_EXT => lambda{|io| node = from_binary(io); (id,b,c) = io.read(12).unpack("NNI") + Pid.new(node, id, b, c) }, TYPE_NEW_REF => lambda{|io| l = r_int16(io); node = from_binary(io); c = r_int8(io) Ref.new(node, c, (1..l).map{ r_int32(io)}) }, + TYPE_NEW2_REF => lambda{|io| l = r_int16(io); node = from_binary(io); c = r_int32(io) + Ref.new(node, c, (1..l).map{ r_int32(io)}) }, TYPE_FUN => lambda{|io| Fun.new(io.read(r_int32(io)-4))} } @@encoder = {
動作確認は、下記のようにする。
# erl -sname fst -setcookie KDDI # run erl as above, then irb # irb # require_relative 'con' # erl = Erlang::Erl.new('ruby@ob', "KDDI") # adj @ob for your ENV # erl.nodes # erl.rpc_call(erl.nodes[0], :io, :format, ["Hello, I am ruby\n"]) # erl.eval(erl.nodes[0], "1 + 1.")
OTPも版を重ねる毎に、どんどん新しく変化してくのね。真似するのは大変だあ。でも、楽しめたよ。元ネタを提供して下さった方に感謝です。
etc
Erlangのアセンブラと仲良くなる
otp_src_23.3/erts/emulator/internal_doc
を見ておくと吉
ErlangとGolangを比較してみる なかなか興味深い
コンカレントプログラミング Concurrent Programming (原文) 最新版