📳

PHPでgRPCサーバーとクライアントを作ってみる

2024/01/25に公開

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を定義しました。(参考

pinger.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 --from=vendor /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 --from=roadrunner /usr/bin/rr /usr/local/bin/rr

ENTRYPOINT ["rr", "serve", "-c", ".rr.yaml"]

2023.X.Xはお好みのバージョンを入れてください。ここでは執筆時点で最新の2023.3.8を入れたとします。

RoadRunnerの設定用ファイルとして、.rr.yamlを作成して配置します。詳細な内容については公式ドキュメントを参考にしてください。
ここでは最低限の記述のみしておきます。(参考

.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.php
$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クラス)になっていて、$statusGoogle\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に書きます。

public/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プラグインか公式プラグインか

ここまでに言及していないもの

やってみるまでわからないところもあるので、新たな領域を開拓するつもりで気になるものはどんどん採用していこうと前向きに考えています。

この記事もいつかの誰かの参考になればと思います。

参考記事

NE株式会社の開発ブログ

Discussion