~ HTTP ではないマイクロサービスのすゝめ ~
この記事では、IoT の世界で旬の Raspberry Pi3 上に、ZeroMQ と Python を使った分散システムを構築し、フォグ・コンピューティングを疑似体験してみたいと思います。
長くなってしまったので、記事を分けました。前編は、フォグ・コンピューティングと ZeroMQ の紹介です。
1.フォグ・コンピューティングって何?
昨今、IoTやエッジ・コンピューティングが注目されていますが、一部企業では、さらにその先に、 フォグ・コンピューティング というパラダイムを見据えています。
フォグ・コンピューティングの定義や意義については、既に様々なところに説明がありますので、そちらを御覧ください。
- Definition of Fog Computing (OpenFog)
- IoTの力を引き出すフォグコンピューティングとは (TechCrunch)
- IoT時代に必須のフォグコンピューティングって何? (CHANGE MAKERS)
ざっくりした理解としては、“モノ” と “クラウド” との間に 分散システム を構築しましょうって話ですね。近場のコンピューティング・リソースを束ねて、データの保管や解析ができちゃえば、クラウドまで行く通信時間とコストを削減できるでしょうと。
ソフトウェア的には、多数の IoT ハードウェアを束ねて伸縮自在なサービスを作る感じですね。
2.ZeroMQ 入門
ZeroMQは、非常に高速・軽量で、“組込み型” のメッセージング・ミドルウェアです。
一般にメッセージング・ミドルウェアというのは、プロセス間やノード間で、データの受け渡しを担当してくれるモジュールです。
通信プログラムを書くときには、しばしば、低レベルAPIの仕様を意識したコーディングをしなければいけません。メッセージング・ミドルウェアはそれらの雑事を引き受けてくれるので、マイクロサービスなどの分散システムでよく使われています。RabbitMQ(AMQP)が有名ですね。*1 *2
世の中のメッセージング・ミドルウェアについては、以下に良い比較記事があります。
グラフを注意深く見ると、「ブローカーレス」と「ブローカード」では、 処理できる秒間メッセージ数の桁が違う ことがわかります。ブローカーレスは、場合によってはブローカードの100倍以上高速です。
《グラフ抜粋》
この記事で「ブローカード」といっているものは、メッセージの送信側と受信側の間にメッセージ交換サービス*3を挟む クライアントサーバ・モデル のメッセージング・ミドルウェアです。他方の「ブローカーレス」では、ピアツーピア・モデル が基本となりますので、外部にメッセージ交換サービスは存在しません。
ZeroMQ は「ブローカーレス」タイプの代表格です。アプリケーション組み込み型(Embedded)のライブラリ実装となっており、アプリケーションのスレッドの1つ*4として動作します。
ハードウェア・リソースの乏しい IoT 機器では、データ管理のニーズで組み込み型の SQLite を採用することが多いと思います。ZeroMQ は「メッセージング・ミドルウェア界の SQLite」と言っても良いでしょう。
2.1.ZeroMQ のメリット
ZeroMQ のバイブル的ドキュメント The Guide の “Why We Needed ZeroMQ” を見ると、ZeroMQ で嬉しいことが何なのか、リストアップされています。
以下に意訳を掲載します。どんなライブラリなのか、イメージできますでしょうか?
- バックグラウンド・スレッドで、ロック・フリーの非同期 Network I/O を正しく実装します。
- 通信ピアは、動的に通信への参加や脱退ができ、再接続は ZeroMQ によって自動的に処理されます。サービスの起動順番を気にする必要はありません。
- メッセージは、自動かつインテリジェントに、必要な時のみキューイングされます。
- メッセージ・キュー溢れ状態("High Water Mark"と呼びます)を適切に処理します。ZeroMQ で決められた 通信パターン に従い、送信側をブロック、もしくはメッセージを捨てます。
- 様々なトランスポート層を使って動作できます。この時、コードを書き換える必要がありません: TCP、マルチキャスト、プロセス内通信、プロセス間通信
- 受信側が遅かったり、止まってしまった場合でも、ZeroMQ で決められた 通信パターン に従い、安全に処理できます。
- 開発者の求めるネットワーク・トポロジーに応じて、多様な 通信パターン を利用できます: Request-Reply、Pub-Sub など
- 1行のコードで、メッセージのキューイング、転送、キャプチャなどを目的としたプロキシーを書けます。プロキシーは、ネットワークの複雑さを軽減する効果があります。*5
- メッセージは、送信した“そのままの姿”で受信できます。10KB のメッセージを送信したら、受信側でも 10KB のメッセージとして受け取ることができます。*6
- メッセージは、バイナリ(Blob)として扱われます。0バイト~ギガバイトクラスのメッセージまで、任意のデータを送受信できます。メッセージにフォーマットを規定したい場合、Msgpack や Protocol Buffer などを ZeroMQ に載せることもできます。
- ネットワーク・エラーをインテリジェントに処理します。リトライして意味があるときには、ZeroMQ が自動でリトライします。
- 軽量なので、二酸化炭素の排出量を削減します。
最大の特徴は、実現したいネットワーク処理を、ZeroMQ の考える 通信パターン によって理解、分解して、表現するという手法です。通信パターンには、RPC(Remote Procedure Call)に適した Req-Rep パターンや、データ転送に適した Pub-Sub パターンなどがあります。
一方で、メッセージの保全*7はしてくれません。ここは「ブローカード」タイプのメッセージング・ミドルウェアとの大きな違いです。例えば、ZeroMQ で RPC を実装したとして、ワーカー・プロセスがクラッシュしてリクエストが失われても、ZeroMQ は何もしてくれません。メッセージの損失が許されないケースでは、アプリケーション側で、適切な検知処理やリトライ処理を作り込む必要があります。*8
案ずるより産むが易し。早速、動かしてみましょう。
2.2.ZeroMQ のインストール
メジャーな OS であれば、インストールは基本簡単です。
今回はハードウェアに Raspberry Pi3 を想定しているので、Raspbian OSのイメージを書き込んだ MicroSD カードを指して動作検証します。
以下のコマンドを実行してください。
apt-get update apt-get install libzmq3-dev pip3 install pyzmq
※ 上記作業は、root ユーザにて行ってください。
2.3.Pub-Sub パターンで通信してみる
手始めに、ZeroMQ の Pub-Sub(Publisher-Subscriber)パターンで、Python の通信プログラムを書いてみます。
このパターンは1方向のデータ配信です。しばしば、ラジオ放送に例えられます。
- Publisher は放送局です。聞いている人の有無によらずブロードキャストします。
- Subscriber はリスナーです。聞きたいチャネルにチューニングして受信します。
- 聞かないで既に流れてしまったデータを、あとから受信することはできません。
Publisher側とSubscribe側のコードを説明します。
※ このコードは、The Guide のサンプルコードを元にアレンジしています。
pub_server.py
Publisher をサーバ・プロセスとして実装します。ポート 5556 で、クライアントからのコネクションを待ち受けます。
2重ループになっており、外側ループは1秒1回の sleep ループです。内側ループは1~3チャネル用のデータ(数字)を作成し送信します。チャネルは単なる文字列です。
import zmq import time # ZeroMQ のバックグラウンド・スレッドのコンテキスト context = zmq.Context() # このサーバは、ポート5556で待ちます socket = context.socket(zmq.PUB) socket.bind("tcp://*:5556") i = 0 while True: i += 1 for ch in range(1,4): data = ch * i socket.send_string("{0} {1}".format(ch, data)) print("Ch {0} <- {1} sent".format(ch, data)) time.sleep(1)
sub_client.py
Subscriber はクライアント・プロセスとして実装します。localhost:5556 にいるサーバに接続します。
接続は ZeroMQ のバックグラウンド・スレッドが処理しますので、本当に TCP コネクションが張られているかどうかは、アプリケーション側は意識する必要がありません。
チャネルは、スクリプトの引数で渡します。
import sys import zmq if (len(sys.argv) != 2): print("Usage: # python3 {} <channel>".format(sys.argv[0])) sys.exit(1) ch = sys.argv[1] # ZeroMQ のバックグラウンド・スレッドのコンテキスト context = zmq.Context() # このクライアントは、ポート5556に接続します(バックグラウンドにて) socket = context.socket(zmq.SUB) socket.connect("tcp://localhost:5556") # チャネルの目盛りをあわせる socket.setsockopt_string(zmq.SUBSCRIBE, ch) while True: string = socket.recv_string() ch, data = string.split() print("Ch {0} -> {1} received".format(ch, data))
動かしてみる(その1)
ターミナルを3つの開きます。1つめで pub_server.py を動かしてみましょう。
pi@raspberrypi:~ $ python3 ./pub_server.py Ch 1 <- 1 sent Ch 2 <- 2 sent Ch 3 <- 3 sent Ch 1 <- 2 sent Ch 2 <- 4 sent Ch 3 <- 6 sent Ch 1 <- 3 sent Ch 2 <- 6 sent Ch 3 <- 9 sent Ch 1 <- 4 sent ...
チャネル番号の倍数をブロードキャストしています。リスナーはまだいません。
次に、2つめのターミナルで sub_client.py を実行し、チャネル2を受信してみます。
pi@raspberrypi:~ $ python3 ./sub_client.py 2 Ch 2 -> 66 received Ch 2 -> 68 received Ch 2 -> 70 received Ch 2 -> 72 received Ch 2 -> 74 received ...
2の倍数を受信できていることがわかります。
3つめのターミナルで、再び sub_client.py を実行し、チャネル3を受信してみます。
pi@raspberrypi:~ $ python3 sub_client.py 3 Ch 3 -> 114 received Ch 3 -> 117 received Ch 3 -> 120 received Ch 3 -> 123 received Ch 3 -> 126 received ...
3の倍数を受信できています。
今度は、1つめのターミナルの pub_server.py を Ctrl-C で殺したり、再度スタートしてみましょう。sub_client.py 側では何のエラーもなく、データを受信しなくなったり、再度受信するようになるはずです。*10
構成をまとめると、こんな感じ。1つの Publisher に対して不特定多数の Subscriber が接続するパターンができています。
ZeroMQでは、このような非常に短いプログラムで、1対多のプロセス間通信を簡単かつロバストに実現できます。
2.4.サーバとクライアントを逆にしてみる
ZeroMQ にとって、サーバ側(bind)クライアント側(connect)というのは『窓口どっちにしますか?』という区別に過ぎません。窓口がサーバで、クライアントがサービスを受けに出向くのです。
そもそも TCP は双方向通信ですし、データの流れる方向を決めているのは、PUB
SUB
という ソケットタイプ です。サーバ/クライアントという概念とデータの流れる方向は、独立した話になります。
再びラジオ放送で例えるなら、放送局側がリスナーの送った葉書を読み上げても良いわけです。*11
前回とは逆に、クライアント側を Publisher、サーバ側を Subscriber として、通信プログラムを動かしてみます。
pub_client.py
Publisher がクライアント側なので、socket.connect()
でポート 5556 に接続します。
あとで実行結果をわかりやすくするため、スクリプトの引数に alphabet
が渡されたときは、チャネル1に小文字、チャネル2に大文字の文字を送信するようにしました。
引数が number
なら、チャネル1に自然数、チャネル2に2の倍数を送信します。
import sys import zmq import time if (len(sys.argv) != 2): print("Usage: # python3 {} {{alphabet|number}}".format(sys.argv[0])) sys.exit(1) mode = sys.argv[1] # ZeroMQ のバックグラウンド・スレッドのコンテキスト context = zmq.Context() # このクライアントは、ポート5556に接続します(バックグラウンドにて) socket = context.socket(zmq.PUB) socket.connect("tcp://localhost:5556") i = 0 while True: i += 1 if mode == "alphabet": lower = chr(ord('a') + (i - 1) % 26) socket.send_string("1 " + lower) print("Ch 1 <- {} sent".format(lower)) upper = chr(ord('A') + (i - 1) % 26) socket.send_string("2 " + upper) print("Ch 2 <- {} sent".format(upper)) else: for ch in range(1,3): number = ch * i socket.send_string("{0} {1}".format(ch, number)) print("Ch {0} <- {1} sent".format(ch, number)) time.sleep(1)
sub_server.py
Subscriberはサーバ側です。ポート5556で待受けます。
チャネルは、スクリプトの引数で渡します。
import sys import zmq if (len(sys.argv) != 2): print("Usage: # python3 {} <channel>".format(sys.argv[0])) sys.exit(1) ch = sys.argv[1] # ZeroMQ のバックグラウンド・スレッドのコンテキスト context = zmq.Context() # このサーバは、ポート5556で待ちます socket = context.socket(zmq.SUB) socket.bind("tcp://*:5556") # Channel をサブスクライブ socket.setsockopt_string(zmq.SUBSCRIBE, ch) while True: string = socket.recv_string() ch, data = string.split() print("Ch {0} -> {1} received".format(ch, data))
動かしてみる(その2)
先ほどと同様に、3つのターミナルを開きます。1つめで pub_client.py を動かしてみます。
pi@raspberrypi:~ $ python3 ./pub_client.py number Ch 1 <- 1 sent Ch 2 <- 2 sent Ch 1 <- 2 sent Ch 2 <- 4 sent Ch 1 <- 3 sent Ch 2 <- 6 sent ...
チャネル1と2に、チャネル番号の倍数をブロードキャスト中。リスナーはまだいません。
次に、2つめのターミナルで sub_server.py を動かして、チャネル2のみ受信してみます。
pi@raspberrypi:~ $ python3 sub_server.py 2 Ch 2 -> 262 received Ch 2 -> 264 received Ch 2 -> 266 received Ch 2 -> 268 received Ch 2 -> 270 received ...
2の倍数を受信できています。
ここで、3つめのターミナルでも pub_client.py を動かし、今度は英字をブロードキャストしてみます。
pi@raspberrypi:~ $ python3 ./pub_client.py alphabet Ch 1 <- a sent Ch 2 <- A sent Ch 1 <- b sent Ch 2 <- B sent Ch 1 <- c sent ...
sub_server.py では、別々の pub_client.py がブロードキャストしている数字と英字を、両方同時に受け取れているでしょうか。
... Ch 2 -> 762 received Ch 2 -> 764 received Ch 2 -> 766 received Ch 2 -> B received Ch 2 -> 768 received Ch 2 -> C received Ch 2 -> 770 received Ch 2 -> D received Ch 2 -> 772 received Ch 2 -> E received Ch 2 -> 774 received Ch 2 -> F received ...
2つのクライアントからのメッセージを、チャネル2だけ、受信できています!
途中でクライアント側やサーバ側のいずれかを Ctrl-C で止めたり、再度スタートすると、残りのスクリプトは何の問題も無く動き続けるのがわかると思います。
構成をまとめると、こんな感じ。多数のクライアントが Publish したデータを、1つの Subscriber が集約するパターンになっています。
ZeroMQ が、多対1のメッセージ交換もよろしくハンドリングしてくれました。
2.5.Python vs. バイナリデータ
ZeroMQは、メッセージの中身に関与しないため、どんなバイナリデータ(Blob)でも送受信できますが、ここで言うバイナリデータとは バイト列 のことです。*12
一方、Python の扱うオブジェクトは、メモリ内部では、必ずしもシンプルなバイト列になっていません。
この2つの世界を橋渡しして、Python の内部表現から、ネットワークに送信可能なバイト列を生成することを シリアライゼーション と呼びます。
ZeroMQ の Python バインディングである PyZMQ では、最初から簡単なシリアライゼーションの仕組みを提供しています。
Socketのメソッド | 機能説明 |
---|---|
send_json() | Pythonオブジェクトを JSON 表現のバイト列に変換して送信 |
recv_json() | JSON 表現のバイト列を受信して、Pythonオブジェクトを生成 |
send_pyobj() | Pythonオブジェクトを pickle 表現のバイト列に変換して送信 |
recv_pyobj() | pickle 表現のバイト列を受信して、Pythonオブジェクトを生成 |
send_string() | Pythonのユニコード文字列を、単なるバイト列に変換して送信 |
recv_string() | バイト列を受信して、Pythonのユニコード文字列を生成 |
これまでのサンプルプログラムは、単なる文字列の送受信でしたので send_string()
/ recv_string()
を用いました。
もし Python の整数「7」を、4バイトのバイト列にシリアライズして、ZeroMQ で送信する場合には、以下のコードを用います。
i = 7 data = i.to_bytes(4, 'little') socket.send(data)
バイト列を受信後、Python の整数に戻す場合は以下のコードとなります。
data = socket.recv() i = int.from_bytes(data, 'little')
バイト列リテラルは、b 接頭辞でも作れます。send_string() の代わりに send() を使って文字列を送信するコードは次のようになります。
socket.send(b"Any ASCII string")
日本語など、非ASCII 文字列を送信するときには、encode()メソッドを利用できます。
socket.send("日本語メッセージ".encode('utf-8'))
参考:
- PyZMQ API: socket クラス
- Serializing messages with PyZMQ — PyZMQ 16.1.0.dev documentation
- PyZMQ and Unicode — PyZMQ 16.1.0.dev documentation
- Python言語リファレンス > 3. データモデル > 3.2. 標準型の階層
- Python言語リファレンス > 2. 字句解析 > 2.4. リテラル > 2.4.1. 文字列およびバイト列リテラル
- Pythonオブジェクトをシリアライズする - Dive Into Python 3 日本語版
2.6.マルチパート・メッセージ
Pub-Sub の Python プログラムでは、メッセージは1つの文字列で構成されていました。より複雑なデータをやり取りする場合、データの切れ目 をメッセージに含められると便利です。
例えば、Python の Numpy 配列は、① 型(dtype) ② 形(shape) ③ バイト列で構成できます。この3つのデータを、不可分な単一メッセージにまとめて送受信できると、アプリケーション側の処理を簡単でできます。
ZeroMQ では、このような区切りの入ったメッセージを マルチパート・メッセージ と呼びます。メッセージの区切られた各パートは フレーム と呼びます。
マルチパート・メッセージは、ZeroMQ 内部でのルーティングなどに活用されていますし、アプリケーションから利用する際には Scatter-Gather API のように扱えますので、分割されると意味がなくなるようなデータを、自前で一旦連結してから送信するといった手間がいらなくなります。
ただし、あくまでもメッセージの区切りを入れるだけなので、フレーム単位で送受信している訳ではありません。すべてのフレームがメモリ内で準備できてから、メッセージ全体の送信が行われます。*13
早速、マルチパート・メッセージで Numpy 配列を送受信してみます。メッセージの形式は下図のとおりです。
今回は、Pub-Sub パターンではなく Req-Rep(Request-Reply)パターンを使います。Req-Rep パターンは、データの単純な交換に向くパターンです。Reqソケット側は、必ず先にデータを送信し、その後必ずデータを受信しなければいけません。2回連続で送信することはできません。反対に Rep ソケット側は、必ず先に受信し、その後データを送信します。*14
Pub-Sub パターンと異なり、通信ピアは、互いに準備ができたと認識するまでデータと送信しないため、確実なデータ転送が可能です。
req_server.py
PyZMQ では、socket クラスに send_multipart()
という便利なメソッドが用意されているため、Python リストを渡すだけで、マルチパートのメッセージを送信できます。受信でも recv_multipart()
でリストを受け取れます。
import zmq import numpy as np import json # ZeroMQ のバックグラウンド・スレッドのコンテキスト context = zmq.Context() # このサーバは、ポート5556で待ちます socket = context.socket(zmq.REQ) socket.bind("tcp://*:5556") # 送信するデータの生成:2行3列の整数配列 M = np.asarray([[0,1,2],[2,1,0]]) # M をシリアライズ dtype = str(M.dtype).encode('utf-8') shape = json.dumps(M.shape).encode('utf-8') data = M.tostring('C') # マルチパートメッセージとして送信 socket.send_multipart([dtype, shape, data]) print("Request:\n{} sent".format(M)) # 計算結果を受け取る [dtype2, shape2, data2] = socket.recv_multipart() # Numpy Array にデシリアライズ N = np.frombuffer(data2, dtype=dtype2.decode('utf-8')) N.shape = json.loads(shape2.decode('utf-8')) print("Reply:\n{} received".format(N))
配列のバイト列を得るために tostring()
*15を利用しています。
また、バイト列から Numpy 配列を再構築するために frombuffer()
を利用しました。
rep_client.py
Repソケットから、Numpy 配列を受取り、全要素に +1 して1秒待ってから、結果を送信しています。
import zmq import numpy as np import json import time # ZeroMQ のバックグラウンド・スレッドのコンテキスト context = zmq.Context() # このクライアントは、ポート5556に接続します(バックグラウンドにて) socket = context.socket(zmq.REP) socket.connect("tcp://localhost:5556") # シリアライズされた Numpy Array を受信 [dtype, shape, data] = socket.recv_multipart() # Numpy Array にデシリアライズ M = np.frombuffer(data, dtype=dtype.decode('utf-8')) M.shape = json.loads(shape.decode('utf-8')) print("Request:\n{} received".format(M)) # なにか計算してみる; 各要素に+1 M = M + 1 # ちょっと待ってみる time.sleep(1) # 計算結果を返信 socket.send_multipart([dtype, shape, M.tostring('C')]) print("Reply:\n{} sent".format(M))
動かしてみる(その3)
サーバ側とクライアント側の どちらを先に起動しても結果は同じ です。ZeroMQ が諸々の面倒をみてくれています!
サーバ Req 側
pi@raspberrypi:~ $ python3 req_server.py Request: [[0 1 2] [2 1 0]] sent Reply: [[1 2 3] [3 2 1]] received
クライアント Rep 側
pi@raspberrypi:~ $ python3 rep_client.py Request: [[0 1 2] [2 1 0]] received Reply: [[1 2 3] [3 2 1]] sent
参考:
2.7.様々なソケットタイプ
ZeroMQ には Pub-Sub ソケットや、Req-Rep ソケット以外にも様々なソケットタイプがあります。以下に、ごく簡単にですが、ソケットタイプの紹介をします。
タイプ | 概要 | 詳細 |
---|---|---|
PUB | ブロードキャスト送信側 | SUBと利用。接続中でない時やキュー溢れ時のメッセージは捨てられる。 |
SUB | ブロードキャスト受信側 | PUBと利用。接続中でない時やキュー溢れ時のメッセージは捨てられる。 |
PUSH | データ配布送信側 | PULLと利用。複数接続では Round-robin でメッセージ配信。接続中でない時やキュー溢れ時の送信は待つ。 |
PULL | データ配布受信側 | PUSHと利用。複数接続では Fair-queued でメッセージ受信。接続中でない時やキュー溢れ時の受信は待つ。 |
REQ | RPC 要求側 | REPやROUTERと利用。必ず先に送信してから受信しなければならない。キュー溢れ時は待つ。 |
REP | RPC 返信側 | REQやDEALERと利用。必ず先に受信してから送信しなければならない。キュー溢れ時は待つ。 |
DEALER | 非同期版 REQ | REPやROUTERと利用。返信を待たず連続でデータ送信でき、複数接続で要求をばら撒ける。キュー溢れ時は待つ。 |
ROUTER | 非同期版 REP | REQやDEALERと利用。受信したデータの返信を、バラバラな順番で返せる高度なソケット。キュー溢れ時は待つ。 |
PAIR | スレッド間パイプ | プロセス内スレッドでの利用に特化している。キュー溢れ時は待つ。 |
詳細は ZeroMQ The Guide “Chapter 2 - Sockets and Patterns”を参照してください。
参考:
前編まとめ(つづく)
大分長々と書いてしまいましたが、ZeroMQ の説明は以上です。
次からは応用編に入ります。
*3:すなわちブローカー
*4:スレッド数を複数にして、全コアの性能を引き出すこともできます。
*5:このプロキシーとは、要はブローカーです
*6:メッセージを途中まで受信して残り部分を待つケースを考えないで良いということです
*7:突然の電源断に備えて、未処理のメッセージをローカルディスクに保管するなど
*8:そのための様々なベストプラクティスが The Guide の Chapter 4 で議論されています。
*9:ZeroMQ は多数の言語バインディングを提供していますので、興味のある方は、言語バインディング一覧のページを参考にしてください。
*10:注意深い方は、サーバの送信した最初のメッセージが、クライアントに届いていないことに気づくと思います。これは Slow Joiner 現象と呼ばれ、ZeroMQ の構造上の限界です。メッセージロスが許されないケースでの回避策が The Guide に掲載されています
*11:この時、サーバの役割である放送局は Subscriber、クライアントであるリスナーは Publisher になります。葉書のコーナー名がチャネルといったところでしょうか。
*12:Pythonの言葉で表現するなら、bytes オブジェクトや bytearray オブジェクトです。
*13:受信のときも、すべてのフレームがメモリ内に準備できてから、ZeroMQ からの受信成功の知らせが来ます。
*14:少々制約が厳しいように思いますが、この制約のお陰で Req 側と Rep 側のどちらを先にスタートしても同じ結果が得られます。より高度な通信のユースケースでは、代わりに DEALER ソケットと ROUTER ソケットを使います。
*15:Raspbian の Numpy バージョンが 1.8.2-2 だったためで、Numpy 1.9以降であれば tobytes() を利用できます。意味は同じです。