gRPCのスタブコードを生成するための Dockerfile を作る

以前 PHP 向けに gRPC のスタブコードを生成するための方法についての記事を書きました 。 その中でコードを生成するツールの protoc およびそのプラグインである grpc_php_plugin を Docker を用いて用意する方法を説明しましたが、そのときは最終的に必要になる Dockerfile を貼り付けるだけで終わりにしていました。 上記の記事ではどうやって protocgrpc_php_plugin を用意しているかの説明までしてしまうと記事全体の流れが悪くなってしまうのでそのときは仕方なかったと思いますが、これらのツールをバージョンアップしようとしたときにどうやればいいか思い出せず困ってしまったので今回はその方法についてメモしようと思います。

protoc

前回の記事を見てもらえば分かると思いますが、protoc は alpine のパッケージ管理システム apk で管理されているので apk add するだけでいいです。

grpc_php_plugin

これは apk からは多分落とせないはずなのでソースからビルドしました。 大まかな流れは gRPC の公式ドキュメント に書いてあるのでそれの通りにやっていけばいいだけです。 ただし Docker Image を alpine から作るとビルドに必要なツールがないので apk から落としてくる必要があります。

そうなると何のパッケージを追加すればいいんだよ、となるのですがこればっかりは動かしてみてトライ&エラーしたほうが早いと思います。 $ docker run -it alpine /bin/sh で alpine のコンテナを起動して 公式ドキュメント のビルド手順を動かし、エラーが起きたら必要なものをインストールしていく、といった流れで alpine のベースイメージに何を追加すれば良いかが分かります。 例えば最初に git clone をするので git が必要だ、とか cmake するので cmake が必要だ、とかそんな感じです。 あとは実際にビルドをするときや protoc を実行するときに共有ライブラリが必要になってくるのでそれも適宜インストールしていく感じです。

最終的に

gRPC v1.52.0 では以下のような Dockerfile になりました。

FROM alpine:3.17

RUN apk add --no-cache --allow-untrusted \
    gcc \
    g++ \
    protoc \
    && apk add --virtual=.build-deps --no-cache --allow-untrusted \
    git \
    make \
    cmake \
    && git clone --depth 1 --recurse-submodules --shallow-submodules -j4 -b v1.52.0 https://github.com/grpc/grpc \
    && mkdir -p grpc/cmake/build \
    && cd grpc/cmake/build \
    && cmake ../.. \
    && make grpc_php_plugin \
    && mv grpc_php_plugin /usr/local/bin \
    && apk del .build-deps \
    && apk del *-dev \
    && cd ../../.. \
    && rm -rf grpc \
    && mkdir -p /project

WORKDIR /project

git の author を最初の commit から修正する

1つ前の記事 を書いたときにサンプルの git リポジトリを作ったんですが、そのときに author の設定を間違えてしまっていて修正したので、修正方法をメモしておきます。

TL;DR

  1. git config --local user.name "[ユーザー名]"git config --local user.email "[メールアドレス]"
  2. git rebase -i --root して全 commit を edit する
  3. git commit --amend --reset-author
  4. git push -f

git rebase -i の時点で手動になるのでもっといい方法がありそう。 今回は 3 commit で終わったので手動で妥協しました。

author を修正する

git の author の情報(name, email)は git config --local user.name "[ユーザー名]" および git config --local user.email "[メールアドレス]" で修正できます。 --localリポジトリごとに、 --global でデフォルト設定を修正できます。

直前の commit の author を修正する

直前の commit の author は git commit --amend --reset-author で修正できます。 詳しくは以下のブログを参考にしてください。

yuzu441.hateblo.jp

過去の commit の author を修正する

git commit --amend では直前の commit しか修正できません。 そこで、 git rebase -i [commit hash] を用いて過去の commit を修正します。 こちらも詳しくは以下のブログを参考にしてください。

tech-1natsu.hatenablog.com

First commit まで rebase する

git rebase -i [commit hash] だと指定した commit hash の次の commit からしか操作できず、最初の commit を修正できません。 最初の commit を修正するには git rebase -i --root と指定すればいいです。 こちらも詳しくは以下のブログを参考にしてください。

ken-c-lo.hatenadiary.org

リモートリポジトリに反映する

修正した内容はリモートリポジトリにも反映させておきましょう。 git push -f と force push する必要があります。

PHP から gRPC を叩くときの大まかな流れ、注意ポイント

PHP で gRPC クライアントを実装する機会があったので、大まかな流れや実装時に引っかかったことをまとめておこうと思います。

本記事で紹介する内容のサンプルを↓に貼っておきます。 gRPC サーバーの用意などができてないので、だいぶ雑なサンプルですが……

github.com

環境を準備する

PHP で gRPC クライアントを実装するためには以下のライブラリおよび拡張が必要です。

  • ライブラリ
    • grpc/grpc
  • 拡張
    • grpc
    • protobuf
      • PHP 実装のライブラリでも可だが、 C 実装の拡張の方が実行速度が速いため拡張を推奨

また、開発の過程で protobuf 周りのデバッグをしたい場合があると思います。 そのときのために dev 依存に google/protobuf (PHP 実装の protobuf ライブラリ)を入れておくといいでしょう。

php.ini (や php.ini で読み込んでいる他の設定ファイル)で protobuf 拡張を読み込ませないようにすれば PHP 実装の protobuf ライブラリを使用するため、デバッガを用いたデバッグが可能となります。 Docker 環境を用いており docker-php-ext-enable で protobuf 拡張を有効化している場合、 /usr/local/etc/php/conf.d/docker-php-ext-protobuf.ini に protobuf 拡張を有効化する設定が書いてあります。

Docker で開発環境を準備する

上述したように、 PHP で gRPC クライアントを実装するためには C 拡張が必要です。 そのため、ローカル環境が「汚れて」しまう・複数人での開発時に環境を揃えることが難しいなどの問題が発生します。 これらの問題は Docker を用いて PHP の開発環境を整えることで解決できます。

例えば以下の Dockerfile でテスト用の PHP CLI 環境を整えることができます。 これを参考に PHP のバージョンや実行環境、ディストリビューションなどを修正していってください。

FROM php:7.2-cli-alpine

ENV COMPOSER_ALLOW_SUPERUSER 1

RUN apk add --no-cache --allow-untrusted \
    libstdc++ \
    && apk add --no-cache --virtual=.build-deps --allow-untrusted \
    gcc \
    g++ \
    make \
    autoconf \
    zlib-dev \
    linux-headers \
    && pecl install -o -f \
    xdebug \
    protobuf \
    grpc \
    && docker-php-ext-enable \
    xdebug \
    protobuf \
    grpc \
    && apk del .build-deps \
    && apk del *-dev \
    && rm -rf /tmp/pear \
    && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer \
    && mkdir -p /project/

WORKDIR /project

.proto ファイルを準備・コンパイルする

別記事 にまとめてあるので、そちらを参照してください。 .proto をコンパイルするツールである protoc および PHP 用の protoc プラグインの導入方法や、 .proto が依存するサードパーティの .proto を管理する方法を書いています。

生成したファイルを用いてクライアントを実装する

protoc を用いて生成したファイルを用いて gRPC クライアントを実装します。 細かい注意点などは後に記述しますが、大まかな流れは実際にソースコードを読むほうが早いと思うのでとりあえずソースコードを貼ります。

<?php

namespace GrpcClient\Sample;

use DateTime;
use Google\Protobuf\Timestamp;
use Sample_proto\Sample\SampleRequest;
use Sample_proto\Sample\SampleServiceClient;
use const Grpc\STATUS_OK;

function callSampleRpc(string $id)
{
    $client = new SampleServiceClient('localhost:50051', ['credentials' => \Grpc\ChannelCredentials::createInsecure()]);

    $req = new SampleRequest();
    $req->setId($id);

    $pbTime = new Timestamp;
    $pbTime->fromDateTime(new DateTime());

    [$res, $status] = $client->SampleRpc($req)->wait();

    if ($status->code !== STATUS_OK) {
        throw new APIException($status);
    }

    // Any 型を 扱う前に Any でラップした型のオブジェクトを生成するか対応するメタデータクラスの initOnce() を呼ぶ必要がある
    // https://github.com/protocolbuffers/protobuf/issues/7509
    \GPBMetadata\Proto\Sample::initOnce();
    $unpack = $res->getExtension()->unpack();
}

PHP namespace の取り扱い

生成されるスタブコードの namespace は .proto ファイルの package から決定されます。 .proto ファイルのコンパイルの記事にもある通り、生成されたコードを autoload するためには PSR-4 の設定か namespace の変更のいずれかが必要です。

スタブコードを autoload するためには、 composer.json の autoload -> psr-4 を編集し、 namespace とプロジェクトのディレクトリの対応を記述してください。 namespace をプロジェクトのディレクトリ構造と一致させたい(PSR-0 的な運用)場合など、スタブコードの namespace を変更したい場合は .proto ファイルの option で php_namespacephp_metadata_namespace を指定してください。

同等の内容が .proto ファイルのコンパイル周りの記事 にも書いてあります。

gRPC の認証について

protoc によって生成された gRPC クライアントのコンストラクタの第2引数で credential を指定する必要があります。 試しに動かしてみる場合は上のソースコードのように認証なし(Insecure)で動かせます。 詳細は 公式ドキュメント を参照してください。

Protocol Buffers の Any 型を使う

Protocol Buffers の Any 型は、任意の型の Protobuf メッセージをラップして扱えるようにしてくれる型です。 OSS などの公開 API に Protobuf を使用しているときに、ユーザ定義のメッセージ型を扱えるようにするときなどに用いられます。

PHP で Any 型を用いる場合、 Google\Protobuf\Any::pack()Google\Protobuf\Any::unpack() を呼び出すことになります。 pack()unpack() を呼び出す前に、 Any でラップするクラスのコンストラクタか自動生成されるメタデータクラスの initOnce() というメソッドを呼び出す必要があります。

ちなみに、Protobuf Message を表現するクラスのコンストラクタの中で initOnce() を呼び出しているため、いずれかを呼び出せば大丈夫です。 そのため initOnce() を必ず呼び出す必要があるわけではなく、 Any を操作するときにエラーが出たとき initOnce() を呼び出すよう修正すれば十分です。

https://github.com/protocolbuffers/protobuf/issues/7509

gRPC のステータスコード

gRPC のステータスコードこのように 定義されています。 これをもとに if ($status-code !== 0) のように記述すればステータスコードのハンドリングができます。 ただ、可能であればマジックナンバーではなく定数を用いてコードの意図をより明確にしたいところです。 幸いなことに gRPC ライブラリに \Grpc\STATUS_* 定数としてこれらのステータスコードが定義されているので、ステータスのハンドリングはこれらを使えばよいでしょう。

時刻の取り扱い

Protobuf Message で時刻表現に google.protobuf.Timestamp を用いている場合、PHP 標準の Datetime との相互変換が必要になると思います。 この相互変換は Google\Protobuf\Timestamp::toDatetime()Google\Protobuf\Timestamp::fromDatetime() で実現できます。 これらのメソッドが基底クラスに用意されており個人的に見つけるのに時間がかかったため、メモとして残しておきます。

Docker で PHP 用の gRPC スタブを生成する

gRPC のスタブコードを生成するには protoc と protoc プラグインgrpc_php_plugin が必要です。 protocmac でも容易に導入できるのですが、 grpc_php_plugin は自前でビルドする必要がある(=ビルド環境を整える必要がある)ため、ローカル環境を汚さないよう Docker を用いてビルドおよびスタブの生成を行いました。 この記事はその備忘録です。

生成するスタブの例として、Open Match の gRPC クライアントを生成します。

Docker で protoc および grpc_php_plugin を準備する

今回は alpine をベースにします。 結論から言うと、以下の Dockerfile で作れます。

FROM alpine:3.12

RUN apk add --no-cache --allow-untrusted \
    libtool \
    libstdc++ \
    linux-headers \
    zlib-dev \
    protoc \
    && apk add --virtual=.build-deps --no-cache --allow-untrusted \
    gcc \
    g++ \
    git \
    make \
    automake \
    autoconf \
    && git clone --recursive -b v1.34.1 https://github.com/grpc/grpc \
    && cd grpc \
    && make grpc_php_plugin \
    && mv bins/opt/grpc_php_plugin /usr/local/bin \
    && apk del .build-deps \
    && apk del *-dev \
    && cd .. \
    && rm -rf grpc \
    && mkdir -p /project

WORKDIR /project

実行するときに便利なので docker-compose.yml も用意しておきます。

version: "3"
services:
  protoc:
    build:
      context: .
    volumes:
      - .:/project

ちなみに

grpc_php_plugin は v1.35 以降でビルドの方法が変わっているようです。 可能であればビルドの方法を調べて書き換えるほうがいいかも。

.proto の依存を解決する

スタブを生成する環境は整ったので、実際に .proto ファイルからスタブを生成しましょう。

そのためにはまず、 .proto ファイルを取得する必要があります。 Open Match の API を定義する .proto ファイルは GitHub から入手することができます。 また、Open Match の frontend サービスの API を定義する frontend.protoAPI のリクエスト・レスポンスを表現する message.protoサードパーティ.proto に依存しています。 スタブを生成するためにはこれらを取得・管理する必要がありますが、バージョンなどの違いを考慮するとなかなか面倒な作業です。

.proto の管理は protodep というツールを導入するといい感じにできるので、今回はこれを導入します。

protodep は Go で書かれたツールなので、 go install でインストールできます。

$ go install github.com/stormcat24/protodep@0.1.3

インストールできたら、プロジェクトルートに protodep.toml を用意しましょう。

proto_outdir = "./third_party"

[[dependencies]]
  target = "github.com/googleapis/googleapis/google/api"
  revision = "aba342359b6743353195ca53f944fe71e6fb6cd4"
  path = "google/api"

[[dependencies]]
  target = "github.com/googleapis/googleapis/google/rpc"
  revision = "aba342359b6743353195ca53f944fe71e6fb6cd4"
  path = "google/rpc"

[[dependencies]]
  target = "github.com/protocolbuffers/protobuf/src/google/protobuf"
  revision = "v3.13.0"
  path = "google/protobuf"

[[dependencies]]
  target = "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options"
  revision = "v1.14.3"
  path = "protoc-gen-swagger/options"

[[dependencies]]
  target = "github.com/googleforgames/open-match/api"
  revision = "v1.0.0"
  path = "open-match/api"

protodep を用いて依存を解決します。

$ mkdir third_party
$ protodep up --use-https

これで third_party ディレクトリに依存をダウンロードすることができました。

補足

mac に protoc をインストールした場合、 protodep.toml に定義した依存のうち、3つめの github.com/protocolbuffers/protobuf 配下の .proto ファイルは /usr/local/include に入ってきていたのですが、 alpine では入ってきませんでした。 そのため、今回はこれも protodep で管理しています。

スタブコードを生成する

用意したビルド環境と定義ファイルを用いて、スタブを生成しましょう。 以下のコマンドで src/Openmatch ディレクトリにスタブを生成できます。

$ docker-compose run -w /project/third_party/open-match protoc protoc \
    -I .:.. \
    --plugin=protoc-gen-grpc=/usr/local/bin/grpc_php_plugin \
    --php_out=../../src/Openmatch \
    --grpc_out=../../src/Openmatch \
    api/messages.proto api/frontend.proto

このとき、生成されたコードの namespace は .proto ファイルのパッケージ名から生成されます。 生成されたコードを autoload するためには PSR-4 の設定か namespace の変更のいずれかが必要です。

PSR-4 の設定は composer.json に記述できます。 composer.json の autoload -> psr-4 に namespace とディレクトリの対応を記述してください。

{
    ...
    "autoload": {
        "psr-4": {
            "Project\\Namespace" : "src",
            "GPBMetadata\\Api\\": "src/ThirdParty/GPBMetadata/Api",
            "GPBMetadata\\ProtocGenSwagger\\": "src/ThirdParty/GPBMetadata/ProtocGenSwagger",
            "Openmatch\\": "src/ThirdParty/Openmatch"
        }
    }
}

namespace を変更する場合は、生成対象の .proto ファイル(今回では messages.protofrontend.proto )に option を追加しましょう。 追加する位置はもともと存在した option の直下あたりでいいのではないかと思います。

// 先頭のコメントは省略

syntax = "proto3";
package openmatch;
option go_package = "open-match.dev/open-match/pkg/pb";
option csharp_namespace = "OpenMatch";
option php_namespace = "Path\\To\\Openmatch\\Extension"; // ←ココ
option php_metadata_namespace = "Path\\To\\Openmatch\\GPBMetadata"; // ←ココも

// 後略

HonKit のデフォルトテーマの箇条書きのスタイルを微調整した

HonKit とは Markdown からドキュメントページや書籍を作成するツールです。 GitBook という Deprecated になった OSS をフォークして作られたものみたいです。

honkit.netlify.app

Markdown を書くだけでドキュメントページを作れるので便利なんですが、箇条書きのスタイルが個人的に気になっていました。

上の図だと listing 1-1 の上下にスペースがあってやたら孤立していることや、 listing 2 と listing 2-2 より listing 2-2 と listing 3 の方が近接していて listing 2-2 が listing 3 と関連しているように見えることが気になるポイントでした。

幸いなことに HonKit は色々とカスタマイズできるので、箇条書きのスタイルをちょっといじってみました。

zenn.dev

やり方としてはだいたい上に貼ったリンクの通りなんですが、まず book.jsonstyles/website.css を用意します。 用意したら book.jsonstyles/website.css を読み込ませるような設定を書いて、 styles/website.css で書きたい CSS を書くだけです。

book.json

{
    "styles": {
        "website": "styles/website.css"
    }
}

styles/website.css

.markdown-section > ul > li {
    margin-bottom: 0.85em;
}

.markdown-section ul > li > p {
    margin-bottom: 0;
}

結果は以下の通り。

やったね。

ちなみに

この問題、この記事を書いた時点での最新バージョンである v3.6.19 では直っていて、僕が使っていたのは v3.4.1 でした。 説明にイマイチやる気がないのはこのせいです。

Agones Controller がダウンした状態で GameServer を落としたらどうなるか確認する

世の中には Kubernetes 上でゲームの状態を管理するステートフルなサーバ(以下 GameServer)をいい感じに管理する Agones という OSS が存在します。 GameServer を管理するために Agones は Controller というコンポーネントを必要とするのですが、今回はこの Controller がダウンした状態で GameServer を削除してみたらどうなるかというのを検証してみました。

Agones とは何か、雑に説明する

Agones とは UbisoftGoogle が共同で開発している、 Kubernetes 上で GameServer を管理するためのライブラリです。 オンラインでのマルチプレイを実現する方式として、ユーザの機器同士を直接 P2P で繋げる方式とサーバ(GameServer)を経由する方式が考えられます。

Agones はこのうち後者の方式で使用される GameServer のためのライブラリです。 このような GameServer は生存期間が比較的短い(ゲームの1プレイと対応することが多いため、数分〜数時間)ためにコンテナや Kubernetes との相性がいいと考えられます。 そのため、 GameServer の管理に Kubernetes を使いたいという欲求が発生しますが、そのまま Kubernetes を用いると問題があります。 たとえば、 Kubernetes は GameServer がマルチプレイを実行中か判別することができないので、マルチプレイ中の GameServer(が動く Pod)を唐突に落とす可能性があります。 この場合、プレイ中のゲームセッションが唐突に終了してしまうので大変困ったことになっていまいます。 Agones はマルチプレイ中の GameServer が落とされるのを防いでくれるなどの機能を持っています。

Agones 自体については↓の記事などが詳しく解説してくれているので、気になったら読んで頂くといいと思います。

medium.com

本題: Agones の Controller が落ちたときってどうなるの?

GameServer などを管理するため、 Agones は Controller というコンポーネントKubernetes 上に立てます。 Controller のおかげで GameServer が管理されているのですが、実際に Agones を使用するとなると Controller が落ちたときに GameServer はどんな挙動をするのだろうというのが気になってきます。 気になってきたので Controller を落とした状態で GameServer を操作しようとしたらどうなるかというのを検証していました。 その過程で GameServer を削除しようとしてみたときに面白い挙動が見えたので、この記事ではそれを解説しようと思います。

さっそくですが、検証方法と結果は GitHub に上げてあります。 やり方とかはこっちに書いてあるので、具体的な操作やソースコードが気になる人は見てください。 なんなら結果も書いてあります。

github.com

検証内容

どのような検証をしたかというと、

  • 検証環境は minikube で作成
  • 削除する方法は以下の2通りが考えられたので2つを検証
    • kubectl delete gs などの Delete API を呼ぶことで直接削除する
    • kubectl edit gs などで GameServer の STATE を Shutdown にすることで Controller に削除してもらう
  • GameServer の操作は kubectl と Go によるプログラムの2通りを試す
    • 削除する方法2通り × 操作方法2通り = 4通りの操作を検証

といった感じです。

検証結果

検証結果はこんな感じになりました。

シェル経由で直接削除した場合

Controller が動いているときは当然 GameServer は削除されるんですが、削除される GameServer の STATE は直前まで Ready のままでした。 消される GameServer に割り当てが発生したりしないか不安です。

Controller がダウンしているときは kubectl のプロンプトが返ってきません。 しょうがないので Controller を立ち上げ直すと削除されてプロンプトが返ってきました。

プログラム経由で直接削除した場合

Controller が動いているときは当然 GameServer は削除されて、シェル経由の場合と同様 STATE は Ready のままでした。

Controller がダウンしているときはシェル経由の場合と異なりプログラムがブロックされることはなく、何も起きずにプログラムが終了しました。 その後 Controller を立ち上げ直すとそのうち削除されました。

シェル経由で STATE を Shutdown にした場合

Controller が動いているときは GameServer が削除されます。 削除される前に STATE が Shutdown になるので(STATE を書き換えているのでそれはそう)、割り当てが発生しなさそうな雰囲気があります。

Controller がダウンしているときは STATE が Shutdown になるだけで、 GameServer が削除されたり下の Pod が落ちたりはしません。 Controller を立ち上げ直すとそのうち削除されます。

プログラム経由で STATE を Shutdown にした場合

シェル経由と同じ挙動でした。

感想・考察

どうにせよ Controller がダウンしてたら GameServer が落ちないのは変わらないんですが、削除する方法によって STATE が更新される・されないといった違いが生まれました。 STATE が更新された方が事故が起きにくそうで良さそうな気がします。 本当に事故が起きるか起きないかは検証したほうが良さそうな気がします。 そのうちやるかも。

また、シェル経由で削除するかプログラム経由で削除するかでも微妙な違いが生まれたり生まれなかったりしました。 ここらへんは作りの違いって感じがするのでふ〜んって感じですね。

まとめ

Agones の Controller を落としたときに GameServer を削除しようとしたらどうなるかを検証しました。 直接削除するか STATE を書き換えるかの2択があるんですが、後者のほうが治安が良さそうな気がします。 本当に治安がいいかは要検証ですが。

エアコンのフィルターを掃除した(えらい)

3月に引っ越して以来やり方を調べるのを含めて面倒で半年以上放置していたんだけど、流石に良くない気がしたので掃除した。 いかにも年末にやりそうなことだけど、年が明けるまで気が向かなかったので、実際に手を動かしたのも年が明けてからになった。 年末の大掃除という習慣は、この手の普段面倒だったりやり方が分からなかったりで放置しがちな場所に目を向けるという意味で、いい習慣なんだと思う。 年末にはやらなかったけど。

やり方はテキトーにググった。 SEO 対策のしっかりしていそうなサイトがたくさん引っかかってげんなりしてしまったけど、少なくとも家事においては正しく情報弱者なのでしょうがない。

結局やったこととしては、

  • フィルターを外してフィルターの裏側からシャワーをかけてホコリを落とす
  • フィルターの周りのホコリを乾いた布で拭き取る

くらい。 思ったより簡単だった。 写真とかがあるとブログっぽくなるんだろうけど、面倒なので写真は撮ってない。