PHPでSpannerを使ったときにハマったことを思い出す

こんにちは。カヤックボンドの駒田です。

この記事は 面白法人グループ Advent Calendar 2022 の2日目を予定していた記事でした。

今年ももう終わりですね。

さて、何について書こうか悩みましたが…
過去にPHP Laravel x Spanner の組み合わせで苦労したことを書いてみようと思います。

1.Spannerとは

Cloud Spanner は、フルマネージドのミッション クリティカルなリレーショナル データベース サービスです。グローバルなトランザクション整合性、高可用性のための自動の同期レプリケーション、2 つの SQL 言語(Google 標準 SQL(拡張機能を含む ANSI 2011)と PostgreSQL)が含まれています。

Cloud Spanner ドキュメント  |  Google Cloudより

Google Cloud の Spanner というフルマネジメントRDBのサービスですね。
最近は結構あちこちで採用されている印象です。

2.Spannerの特徴

NewSQLなどと呼ばれているものの一種で

なのが特徴です。

概ねフルマネージメントかつオートシャーディングを目当てに導入される印象ですね。

その分コストが高いのですが、シャーディングが必要になる規模ですと
ユーザーデータの格納先用途等で、候補に挙がってくるサービスじゃないかな、と思います。

ちなみに、昔よりもこまかーいユニットという単位で立てれるようになったので
最小構成で立てる場合はそこまで高くないようです。
1ノードだと、2週間たたずに無料枠クレジットが吹っ飛んでいたのでこれはありがたい!
https://cloud.google.com/blog/ja/products/databases/use-spanner-at-low-cost-with-granular-instance-sizing

3.PHP(Laravel)とSpanner 導入

そんなSpannerのためのクライアントは各種言語が用意されていますがGoで触る人がやはり多いようで
phpで扱おう!というケースはあまり無いんじゃないかな、と思います。

私が扱ったタイミングは少し前になってしまうのですが、
その当時もやはりphpで扱おうという人は同様にまだあまり多くは無く…。
どうしたもんかな、と思っていた中、コロプラさんがLaravelでSpannerを扱うためのOSSを提供しておりました。

github.com

有難くこちらを利用させて頂き、比較的スムーズに導入することができました。

その後、MySQLに存在する機能が使えずにMigration(afterが使えなかったりした)や
その他クエリ実行でコケたりと細かい苦労はありつつ、とはいえ比較的スムーズに導入、実装は進みました。
(今はエミュレータがありますし、機能の対応範囲も増え、このあたりの苦労は激減しています。)

で、わーいやったぜ。これでRDBは安心だーと思っていた、のですが…

4.導入して困った事

負荷試験をしたところ、Spannerの接続周りが明らかにボトルネックになってしまっていました。
各種Spanner導入記事にも「sessionの構築にはコストがかかる、session poolはしっかり管理せえよ」
と記載があることが多いですが、見事にそこにハマった形です。

では、いったい何が原因だったのか。
いくつかあるので順番に説明していきます。

4-1. flockによる排他制御

Laravel-Spannerでは、Spannerへの接続にCloudSpannerのセッションをプールしておくSessionPoolのキャッシュ、
そしてGCPのアクセストークンのキャッシュであるAuthCacheを利用しているようで
デフォルトですと、このキャッシュはファイルキャッシュとしての実装がされていました。

このプロジェクトにおけるデフォルトの排他制御はflockが使われており
これによるロック解除待ちが発生し、1つずつ順番にしかアクセスできないような状態になっていました。

そのためまずこの形式をセマフォへ変更してみたところ
全体的な性能の改善は見られませんでしたが、ロック周りの時間は短くなり、負荷の傾向に変化がみられました。

4-2. ファイルキャッシュを別のキャッシュ形式へ

負荷の傾向が変わった、ということはボトルネックの箇所が変わった、ということで
一旦ロックはこのままで、別のアプローチをとることにしました。

Laravel-SpannerのReadMeには下記の記載があり、そもそもキャッシュを何で持つか、を変更できるようになっていました。

By default, laravel-spanner uses Filesystem Cache Adapter as the caching pool. If you want to use your own caching pool, you can extend ServiceProvider and inject it into the constructor of Colopl\Spanner\Connection.

そのためファイルキャッシュから共有メモリ内へのキャッシュへと変更を行うことで改善を図りました。

これを行ったところある程度の性能改善がみられました…が、
セッションプールの異常な増加、一定の負荷を超えるとメモリ不足になる、といったような事象が発生し始めてしまいました。

4-3. 共有メモリキーの分離

共有メモリの格納はSysVCacheItemPoolを利用して実装していましたが パラメータにprojというものがありました。

ftok() - プロセス間通信 (IPC) キーの生成

ftokによる共有メモリキーの指定に使われる値であり、デフォルトだとAが指定されます。
そのため未指定の場合だと、SpannerのセッションもAuthCacheも常にAの指定になってしまっていました。
これに加え、AuthCacheについてはロック処理は入っていないことが分かったことで

おそらく
同一の領域が参照された場合に共有メモリに格納されたキャッシュデータが破壊される
→ 破壊された部分が使われないゴミキャッシュとして中途半端に残る
→ 新規で作られたセッションやAuthCacheが書き込まれ、
→ また破壊され…のループでメモリが溢れる。
というような状態になってしまっていたものと思われました。

この対応として、明示的にprojを分けて共有メモリに書き込むようにすることでボトルネックは解消し
おおむね予想していた性能がでる結果となりました。

もちろんこの後も細かい諸々だったり付随する対応はあったのですが、大きなハマりはこのあたりだったかなと思います。

5. 他、quotaについてなど

これは言語に依存する部分ではないですが、spannerについてその他での注意点といえばquota周りですよね。

https://cloud.google.com/spanner/quotas?hl=ja

何かと困るのはミューテーション制限!
バッチ処理とかでもあっさり引っかかる値なので、十分に注意が必要ですね。 ちなみに22年10月に倍に増えたらしいです、やったね。

上限
commit サイズ(インデックスを含む) 100 MB
セッションあたりの同時読み取り数 100
commit あたりのミューテーション(インデックスを含む) 20,00040,000に増えた!
データベースあたりのパーティション化 DML の同時実行ステートメント数 20,000

Cloud Spanner のトランザクションあたりの更新回数が 2 倍に増加 | Google Cloud 公式ブログ ※ミューテーションの推定なども詳しく書いてあるので是非。


というわけでPHP Laravel x spanner の組み合わせの話でした。
この組み合わせ、あまり情報も多く無いので、もし今後同じ組み合わせを試す人が居たときに参考になるかなあと思い記事にしてみました。
いやあ、いいサービスですよね。Spanner。

それでは皆様、良いお年を。