(この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar Advent Calendar 2018」の11日目です)
昨日は @curry_on_a_rice さんのElixirでニューラルネットを実装しようとした話でした。
grpc-elixirについて
gRPCはマイクロサービス用の通信プロトコルとして脚光を浴びていますが、今回はそのElixir実装である、gRPC Elixirをご紹介します。C++, Node.js, Python, Ruby, Objective-C, PHP, C#, Java, Goと、様々な言語サポートがある一方で、Elixirは蚊帳の外だったのですが、Tony Han氏によりgRPCのElixir実装が進んでいます。
gRPCに関してはマイクロサービスバックエンドAPIのためのRESTとgRPCをご覧ください。
2018年12月現在はα版ですが、( ⇒ 12/11α版表記取れました ! )
他言語との連携を確認する分には問題ないので、今回ご紹介することにしました。ロードマップを見る限りは、圧縮・ロギング・外部エンコーディング以外の実装は済んでます。
gRPC公式には単純な接続を確認するHello Worldと、双方向ストリーミングを行うRoute Guideというサンプルがあります。elixir-grpcでは両方とも実装されているので、2つのサンプルを使った通信をPlay with Dockerを使用して行います。
いわゆる「体裁」面はほぼ実装済みで、β版への移行は内部で使用しているgunなどのhttp2の関連ライブラリの安定待ちではないかと、私は推測しています。
本コラム投稿直前に正式版となりました!
以降の手順で必用とされるスキル
vimでファイルの編集ができること
Dockerコンテナで中と外の居場所の区別がつくこと
一般的な実装手順
今回は単項目の呼び出しに限定して、解説します。
導入手順に関しては、grpc-elixirのリポジトリに丁寧に書いてあるので、ここではその手順を軽く辿るのみとします。
0. プロジェクトへのインストール
mix.exsにパッケージを追加します
def deps do
[{:grpc, github: "tony612/grpc-elixir"}]
end
mix deps.get
1. protocol buffer elixirプラグインのインストール
gRPCでは、.proto拡張子が付くIDL(インターフェス定義言語)を定義します。protocツールを使って、各言語用のスキーマーや構造体を作成します。各言語用のprotocプラグインがあるのですが、なんと、elixirプラグインであるelixir-protobufもtony氏が作られたものです。
以下のコマンドを使ってprotocのプラグインをインストールします。(protocコマンドはOSのパッケージマネージャーでインストールしてください)
mix escript.install hex protobuf --force
インストール後は~/.mix/escripts
にパスを通す必要があります。
PATH=~/.mix/escripts:$PATH
2. protoファイルから、構造体ファイル(.pb.ex)を作成
protoc --elixir_out=plugins=grpc:./lib/ helloworld.proto
IDLのhellowold.protoからは、.pb.ex
という拡張子で次のようなRequestとReply用のモジュールが生成されます。
defmodule Helloworld.HelloRequest do
use Protobuf, syntax: :proto3
@type t :: %__MODULE__{
name: String.t
}
defstruct [:name]
field :name, 1, type: :string
end
defmodule Helloworld.HelloReply do
use Protobuf, syntax: :proto3
@type t :: %__MODULE__{
message: String.t
}
defstruct [:message]
field :message, 1, type: :string
end
defmodule Helloworld.Greeter.Service do
@moduledoc false
use GRPC.Service, name: "helloworld.Greeter"
rpc :SayHello, Helloworld.HelloRequest, Helloworld.HelloReply
end
defmodule Helloworld.Greeter.Stub do
@moduledoc false
use GRPC.Stub, service: Helloworld.Greeter.Service
end
3.クライアント側コード
クライアント側のコードを対話環境で実行すると以下の通りとなります。
iex> GRPC.Server.start(Helloworld.Greeter.Server, 50051)
iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051")
iex> request = Helloworld.HelloRequest.new(name: "grpc-elixir")
iex> {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(request)
4.サーバー側コード
また、サーバー側のコードは以下の通りです。
上記で定義したモジュールを使いながら記述していきます。
defmodule Helloworld.Greeter.Server do
use GRPC.Server, service: Helloworld.Greeter.Service
@spec say_hello(Helloworld.HelloRequest.t(), GRPC.Server.Stream.t()) ::
Helloworld.HelloReply.t()
def say_hello(request, _stream) do
Helloworld.HelloReply.new(message: "Hello #{request.name}")
end
end
以下のようにApplicationモジュールのSupervisorツリーにhelloworldサーバーを登録します
defmodule YourApp do
use Application
def start(_type, _args) do
import Supervisor.Spec
children = [
# ...
supervisor(GRPC.Server.Supervisor,
[{Helloworld.Greeter.Server, 50051}])
]
opts = [strategy: :one_for_one, name: YourApp]
Supervisor.start_link(children, opts)
end
end
mix.exsにアプリケーションモジュールを登録します
def application do
[
mod: {YourApp, []}, # これを登録
extra_applications: [:logger]
]
end
config.exsにアプリケーション設定するか、mixコマンドで待ち受けが可能です。
mix grpc.server
Docker-composeで双方向通信環境を作る
ここからは、docker環境を作り相互通信をするフェーズです。
2つ以上の言語を使った通信なので、環境まわりを楽にすべくDocker-composeを使います。といっても、ファイルを3種コピペすれば通常のDocker環境で実行できるので、気軽に読み進めて下さい。
ここでは2種類のコンテナを一気に立ち上げます
- Elixirコンテナ
elixir-node
- Golangコンテナ
go-node
Play with Dockerについて
無料で4時間Docker環境が使えるサービスです。Docker Hubに登録していれば誰でも使用できます。ホストVMのメモリも4GBでvCPU8コアなので、速度はかなり速いです(個人の感想です)。Docker Hubに登録すればすぐ使用できるので、ご登録をお勧めします。
詳しくはDocker 入門にはインストールなしで使える「Play with Docker」がいいと思うをご覧ください。
以下の説明はPlay with Dockerにて進めますが、ご自分のPCにDockerをインストールされている方も、ほぼ変わらない手順で動作します。(touch等必用ない分そちらが楽です)
コンテナ構築
Play with Dockerで行う場合は、内臓のEditorを使うためにtouchコマンドでまず空ファイルを3種作ります。(Vimも使えますが、コピペに難があります)
コンテナには、elixir-nodeとgo-nodeという名前を付けています。docker-composeだとホスト名として扱えるので生IPアドレスを扱わなくて済むので楽です。
まずは「ADD NEWINSTANCE」ボタンを押して下さい。ブラウザ内にLinuxコンソールが立ち上がります。そこで以下を入力します。
$ touch docker-compose.yml
$ touch Dockerfile-go
$ touch Dockerfile-elixir
Editorボタンを押すと、数秒後にフォルダ構成が出て来ます。
それぞれのファイルに以下をそれぞれコピペしてSaveボタンを押してください。
Dockerfile-elixir
公式のaplineコンテナを使用して、grpc-elixirのリポジトリをクローン。protobuf関連の準備をするコンテナです。
FROM elixir:1.7.4-alpine
RUN apk -U update && apk --update --no-cache add protobuf git vim && \
cd ~ && \
git clone https://github.com/tony612/grpc-elixir.git && \
mix local.hex --force && \
mix local.rebar --force && \
mix hex.info && \
mix escript.install hex protobuf --force
ENV PATH /root/.mix/escripts:$PATH
CMD ["ash"]
Dockerfile-go
gRPCの公式コンテナを引用して、Goのパッケージを準備を行います。
各種サンプルを事前コンパイルして、helloworldのサーバーを実行して待機する内容です。
FROM grpc/go:latest
RUN go get -u golang.org/x/net/http2 golang.org/x/net/context && \
cd /go/src/google.golang.org/grpc/examples/helloworld/greeter_server && \
go build main.go && \
cd /go/src/google.golang.org/grpc/examples/helloworld/greeter_client && \
go build main.go && \
cd /go/src/google.golang.org/grpc/examples/route_guide/server && \
go build server.go && \
cd /go/src/google.golang.org/grpc/examples/route_guide/client && \
go build client.go
CMD ["/go/src/google.golang.org/grpc/examples/helloworld/greeter_server/main"]
EXPOSE 50051 10000
docker-composer.yml
elixir-nodeとgo-nodeを定義します。2つのコンテナの由来や関連付けを行います。コメント化された一行は次回で使用します。
version: '3'
services:
elixir-node:
build:
context: .
dockerfile: Dockerfile-elixir
command: ash
tty: true
environment:
- MIX_ENV=dev
go-node:
build:
context: .
dockerfile: Dockerfile-go
# command: sh -c "cd /go/src/google.golang.org/grpc/examples/route_guide/ && server/server"
エディタを閉じコンソールに戻ります。
以下を起動すると、ビルドが始まりますのでしばらくお待ちください。
docker-compose.yml
を見てわかるように、elixir-node
とgo-node
という2つのコンテナを同時に起動します。
docker-compose up -d
HelloWorld gRPC サンプル
Elixirのコンテナにashを起動します。
HelloWorld : Elixir -> Golang
$ docker-compose exec elixir-node ash
以下elixir-nodeのコンテナ内での作業です。フォルダ移動と依存関係の取得、コンパイルを行います。
# cd ~/grpc-elixir/examples/helloworld
# mix deps.get && mix compile
まずはクライアントを実行してみます。
priv/client.exsにクライアント用のコードが用意されているのですが、サーバーがlocalhost固定ですので、ここは動きを理解するためにも、手動で実行します。
# iex -S mix
まずはGRPC.Stub.connect関数でコンテナgo-nodeのgrpcサーバーとの接続を確立します。
iex(1)> {:ok, channel} = GRPC.Stub.connect("go-node:50051")
{:ok,
%GRPC.Channel{
adapter: GRPC.Adapter.Gun,
adapter_payload: %{conn_pid: #PID<0.216.0>},
cred: nil,
host: "go-node",
port: 50051,
scheme: "http"
}}
無事接続できました。protoファイルで定義されているsay_hello関数を実行します。
iex(2)> {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir"))
{:ok, %Helloworld.HelloReply{message: "Hello grpc-elixir"}}
構造体にHello grpc-elixir
という文字列が返ってくることが確認できました。
順調に行き過ぎてるように見えるので、接続先を変えてみます。
iex(3)> {:ok, channel2} = GRPC.Stub.connect("localhost:50051")
** (MatchError) no match of right hand side value: {:error, "Error when opening connection: :timeout"}
きちんと、タイムアウトでエラーとなりましたね。
ping代わりのテストなので大したことをやってるわけではないのですが、priv/client.exsを見てわかるとおり、スタブに対してsay_hello関数を呼び出すだけでRPCが実行できる簡単な流れとなっているのがおわかり頂けると思います。
Ctrl+C を2回押してiexから抜けてください。
HelloWorld : Golang -> Elixir
こんどは、Elixir側を待ち受けにしてGolang側からAPIを叩いてみます。
以下でgrpcサーバーが待ち受けとなります。
# mix grpc.server
Ctrl+p, Ctrl+q でコンテナを抜けて、go-nodeのコンテナに入ります。
$ docker-compose exec go-node bash
helloworld/greeter_clientフォルダーに移動します
cd /go/src/google.golang.org/grpc/examples/helloworld/greeter_client
サンプルプログラムはLocalhostのサーバーに対して、リクエストを送るようになっているので宛先をelixir-nodeに変えます。このコンテナにはvimが入ってないので、sedでlocalhostを強制変換して再コンパイルします。
# sed -i -e s/localhost/elixir-node/ main.go
# go build main.go
実行ファイルを起動します。
# ./main
2018/12/10 05:11:17 Greeting: Hello world
無事にElixirとの接続が確認できました。
コンテナを抜け、コンテナを終了させましょう。
# exit
$ docker-compose down
まとめ
protcのプラグインのインストールと、Stubの作成を乗り越えてしまえば、簡単に多言語と通信できることが理解頂けたのではないかと思います。今回は、HelloWorldのみでしたが、次回は1接続でストリーミングを行うコードを含んだRoute Guildサンプルを試してみます。お楽しみに。
明日のfukuoka.ex Elixir/Phoenix Advent Calendar 2018 12日目の記事は, @tuchiro さんの「BehaviorとMix.Configで切り替え可能なStubを実装する」です。こちらもお楽しみに!