PHPでgRPCサーバーとクライアントを作ってみる
NE株でPHPを書いている谷口(@taniguhey)です。
新しいアプリケーションの開発を始める際の設計でマイクロサービス間の通信にgRPCが候補に挙がることがあると思います。
弊社では技術スタックのうちプログラミング言語はPHPが多くを占めていたので、実装はPHPのままマイクロサービスでgRPCを使えるかということを試してみる機会がありました。
特に、PHPにおいてはgRPCの公式のドキュメントが更新されておらず日本語の情報も少ないため、PHPでgRPCサーバーは立てられないように捉えられるので、サーバー側についても書き残していこうと思います。
なお、記載しているサンプルコードは重要な部分だけを切り抜いたものの可能性があるため、そのまま動作することを保証するわけではありません。
gRPCとは
gRPCに馴染みのない人に向けて簡単に説明しておきます。
gRPCは、RESTful APIなど同様にAPIの設計だと捉えて問題ありません。
こちらの記事gRPC と REST の比較 - アプリケーション設計の違い - AWSで違いについて詳しく説明されていますが、特徴をまとめると
- REST APIはリソース(名詞)とHTTP Method(動詞)の組み合わせでAPIを呼び出す
POST /orders <headers> (customer_id, item_id, item_quantity) -> order_id
- 一方gRPCは、サービス(関数)を呼び出すようにAPIを呼び出す
createNewOrder(customer_id, item_id, item_quantity) -> order_id
- REST APIはデータとしてJSONをそのまま送る
- 一方gRPCはprotocol bufferを利用してJSONを圧縮して送信する
- gRPCはprotocol bufferを利用しているのでクライアントとサーバーのコードを自動生成できる
protocol bufferは、OpenAPI Schemaのようなスキーマ言語の類だと思ってもらえればOKです。
このあたりを理解していただければこの記事中では問題ありません。
実際にREST APIではなくgRPCを採用する場合は、作っているアプリケーションの性質と照らしあわせメリットデメリットを比較する必要があります。
PHPで試してみる
言語をPHPでという前提で、クライアントとサーバーを作ってみます。
動かしてみることが第一目標なのでローカルのdocker上で動かすことを目標とします。
protobuf
.proto
まずは、どのようなAPIを作るかをprotocol bufferに基づいて.protoファイルに定義します。
.protoファイルの詳細な書き方は今回は解説しないので、公式サイトを確認してください。
今回は以下のような.protoを定義しました。(参考)
syntax = "proto3";
option php_namespace = "GRPC\\Pinger";
option php_metadata_namespace = "GRPC\\GPBMetadata";
package pinger;
service Pinger {
rpc ping (PingRequest) returns (PingResponse) {}
}
message PingRequest {
string name = 1;
}
message PingResponse {
int32 status_code = 1;
}
protoc
次に、この.protoをもとにソースコードを生成するための準備をします。
コンパイラ(protoc
)そのものと、言語に応じたプラグインを組み合わせて生成する手順になっています。
protoc
はprotobufのリポジトリからバイナリを入手できます。
Releases · protocolbuffers/protobuf
protoc-{version}-{host_os}-{cpu-arch}.zip
のような命名になっているのでここから適したものをダウンロードします。
次にPHP用のプラグインです。
こちらは、grpcのリポジトリにプラグインのビルド方法が載っているためそれに従って入手します。
grpc/src/php at master · grpc/grpc
私の環境ではbazel
を使う方法でうまくビルドすることができました。
コンパイラとプラグインが手に入ったので、PHPのソースコードを生成することができるのですが、サーバー側のコードを生成するためのプラグインには選択肢があります。
gRPCの公式プラグインで生成する以外に、PHPアプリケーションサーバー独自で提供しているプラグインが存在します。
今回のモチベーションとしては”試してみる”ことが大きいので、公式プラグインではなくRoadRunnerが提供するgRPCプラグインを使って生成してみることにします。
入手方法は、リポジトリからバイナリをダウンロードする方法と、composerで導入できるRoadRunnerのバイナリrr
にコマンドを打ってダウンロードする方法があります。
- コンパイラ(
protoc
) - クライアント用プラグイン(
grpc_php_plugin
) - サーバー用プラグイン(
protoc-gen-php-grpc
)
が揃ったので、PHPファイルを生成してみます。
grpcのリポジトリに載っているコマンドを元に、ファイルパスとプラグインを指定して実行します。
クライアント用
protoc \
--plugin=protoc-gen-client=./bin/grpc_php_plugin # 利用するプラグイン
--php_out=. \ \ # 生成されるファイルの出力先
--client_out=. \ # プラグインで生成されるファイルの出力先
-I=. \ # .protoが置いてあるディレクトリのパス
pinger.proto
サーバー用
protoc \
--plugin=protoc-gen-server=./bin/protoc-gen-php-grpc # 利用するプラグイン
--php_out=. \ \ # 生成されるファイルの出力先
--server_out=. \ # プラグインで生成されるファイルの出力先
-I=. \ # .protoが置いてあるディレクトリのパス
pinger.proto
-
\Grpc\BaseStub
を継承したClientクラス -
Spiral\RoadRunner\GRPC\ServiceInterface
を継承したサービスのインターフェース -
\Google\Protobuf\Internal\Message
を継承したRequestクラスとResponseクラス - Metadataのクラス
が生成されたと思います。
公式のプラグインを利用する場合は以下を参考にgenerate_server:
というオプションをつけると良いようです。
grpc/examples/php/greeter_proto_gen.sh at master · grpc/grpc
grpc/examples/php/greeter_server.php at master · grpc/grpc
環境構築
クライアント側
gRPC公式サイトのサポートしている言語一覧を見るとPHPがあります。このページは、リポジトリに含まれているチュートリアルのセットアップを前提に解説されているため、1から構築したい場合には向いていませんでした。
gRPC for PHP のインストール | Google Cloudの通り行うとスムーズだったので、こちらを参考にします。
ディレクトリはこのような構造としています。app/GRPC/
配下には後ほど生成したファイルをそのまま配置します。
.
├── app/
│ ├── composer.json
│ ├── conposer.lock
│ └── GRPC/
│ └── ...
└── docker/
└── php/
├── Dockerfile
└── php.ini
ベースとなるイメージはphp:8.2-fpm
として、dockerのイメージを作ります。
FROM composer:2.6 as vendor
COPY app/composer.* /app/
RUN composer install \
--no-dev \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--optimize-autoloader \
--prefer-dist
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
gnupg \
g++ \
git \
unzip \
zlib1g-dev \
libicu-dev \
openssl
RUN pecl install grpc protobuf
RUN docker-php-ext-enable grpc protobuf
RUN docker-php-ext-install sockets
COPY docker/php/php.ini /usr/local/etc/php/
COPY app/ /var/www/app
COPY /app/vendor /var/www/app/vendor
gRPCを試すのに重要なのは
RUN pecl install grpc protobuf
RUN docker-php-ext-enable grpc protobuf
の部分だけですので、他の部分は自分のアプリケーションに必要なカスタマイズをしてください。
伝えておきたい注意点としては、RUN pecl install grpc
にかなり時間がかかることに注意してください。約30分かかるので、時間があるときにビルドするようにしましょう。
執筆時点で、composerのgrpc/grpc
パッケージは1.57.0
がインストールされました。
サーバー側
RoadRunner公式の手順に従って導入します。ここはgRPCとはあまり関係ない部分です。
Installation - RoadRunner
クライアント側と同様にDockerfileを用意します。差分としては、以下の記述を追加します。
FROM ghcr.io/roadrunner-server/roadrunner:2023.X.X AS roadrunner
...
COPY /usr/bin/rr /usr/local/bin/rr
ENTRYPOINT ["rr", "serve", "-c", ".rr.yaml"]
2023.X.X
はお好みのバージョンを入れてください。ここでは執筆時点で最新の2023.3.8
を入れたとします。
RoadRunnerの設定用ファイルとして、.rr.yamlを作成して配置します。詳細な内容については公式ドキュメントを参考にしてください。
ここでは最低限の記述のみしておきます。(参考)
version: '3.8'
grpc:
listen: 'tcp://0.0.0.0:6001'
proto:
- GRPC/*/*.proto
server:
command: 'php public/grpc.php'
アプリケーション部分
自動生成されたクラスの名前空間は.protoに記載してある通りになるので、実際に出力したパスと付けたい名前空間と配置したいパスが異なる場合など、必要であればcomposerのautoloadに自動生成したクラスへのパスを設定しておきます。
"autoload": {
"psr-4": {
"Grpc\\": "GRPC/"
}
},
クライアント側
生成されたクライアントを使ってサーバー側にgRPCリクエストを送るところを書いてみます。
$client = new PingerClient('localhost:6001', ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
$request = new PingRequest(['name' => 'test']);
[$response, $status] = $client->ping($request)->wait();
echo $response->getStatusCode();
PingerClientは自動で生成したクラスを利用しています。'localhost:6001'
の部分は自分の環境に合わせて調整してください。
オプションにcredentials
がないと失敗してしまうので、動作確認ではcreateInsecure()
を設定しておきます。
この場合、戻り値はmixedなのですが、$response
は.protoで定義したサービスの戻り値(PingResponse
クラス)になっていて、$status
はGoogle\Rpc\Status
というクラスになっています。
サーバー側
サーバー側はRoadRunnerのプラグインを使うことにしたので公式通りに進めます。
composerでspiral/roadrunner-grpc
を入れておきます。
クライアント側からリクエストが来た場合に、対応するgRPCのサービスが何を行うかを書いていきます。
自動生成されたInterfaceを実装したクラスを作ります。
use Spiral\RoadRunner\GRPC;
final class Pinger implements PingerInterface
{
public function ping(GRPC\ContextInterface $ctx, PingRequest $in): PingResponse
{
echo 'PING:' . $in->getName();
return new PingResponse([
'status_code' => 200
]);
}
今回は、リクエスト内容を標準出力し200
をパラメータに含めレスポンスを返すとしました。
次に、RoadRunnerがリクエストを受け付けた際に、呼び出すPHPのエントリポイントの部分を書きます。
.rr.yamlに自由に指定できるので、今回だとpubluc/grpc.php
に書きます。
use Spiral\RoadRunner\GRPC\Server;
use Spiral\RoadRunner\Worker;
$server = new Server();
$server->registerService(PingerInterface::class, new Pinger());
$server->serve(Worker::create());
registerService
の第二引数に先ほど作ったInterfaceを実装したクラスを入れておきます。
実行
動作するコードの準備ができたので、あとはdocker-composeなどで同一network内に配置しておくなどしてコンテナを立ち上げてRoadRunnerを実行し、クライアント側からclient.phpを実行することでリクエストを送信できます。
まとめ
クライアントもサーバーも実装をPHPで行いながらgRPCを問題なく使えることがわかりました。
やり始めた当初、公式のチュートリアルではサーバー側の立て方が載っておらず、githubのリポジトリを見ても明記されていなかったため、PHPでサーバー側を作るのは無理だと思っていました。
調べ物を進めるうちに、RoadRunnerを使う方法や公式プラグインでも利用できることがわかったため、サーバー側の情報ももっと周知されれば良いなと感じました。
これから
PHPを貫けることはわかりましたが、運用上の問題、実際のワークロードで動かした場合のパフォーマンスやどの範囲のアプリで利用するかなどを考えなければいけません。
gRPC自体は、言語に依らないというメリットがあるため最終的にPHPは切り捨てるがgRPCは採用するみたいな可能性だってあります。今回の動作確認の時点でもすでに考慮しておく事柄がいくつか見つかっています。
-
pecl install grpc
の遅さ - RoadRunnerプラグインか公式プラグインか
ここまでに言及していないもの
-
grpcライブラリ内の警告
- 公式的にはサーバー側のライブラリは非推奨
-
Server Reflection
- PHPは対応していない(はず)
- e2eテストのしやすさに関わりそう?
やってみるまでわからないところもあるので、新たな領域を開拓するつもりで気になるものはどんどん採用していこうと前向きに考えています。
この記事もいつかの誰かの参考になればと思います。
参考記事
NE株式会社のエンジニアを中心に更新していくPublicationです。 NEでは、「コマースに熱狂を。」をパーパスに掲げ、ECやその周辺領域の事業に取り組んでいます。 Homepage: ne-inc.jp/
Discussion