生涯未熟

生涯未熟

プログラミングをちょこちょこと。

Spannerを本番環境で使ってみたので感想戦

会社で大きめの負荷を捌く案件があり、DBにSpannerを使ってみようかという機運がチーム内で高まり、本番運用まで使ってみました。 その中で色々と知見が得られたのでTipsを細々と書いていく。

hotspotの話

SpannerはNewSQLのカテゴライズに含まれ、RDB + NoSQLの特性を持ちます。特性を持つということはハマりどころも同じく持つということで、今から話をするhotspotはNoSQLでよく見られるハマりどころです。hotspot自体については以下のGoogle公式ドキュメントを読んで頂けると理解出来ると思います。

ホットスポットを防ぐ主キーの選択方法

今回の案件で私自身もhotspotに何回かハマったわけですが、結構ハマりやすいのが「hotspotになっていてもある程度性能は出る」というところです。SpannerはPKを元に各NodeがどこからどこまでPKの範囲を参照するか?というsplitを持つことで複数Nodeが効率良くデータ参照することが出来、write/readがスムーズになります。そして、hotspotはその振り分けの元となるPKの値に偏りが生じることで特定のNodeにアクセスが集中してしまうことで性能が上がらないという事象ですが、裏を返して言うと1Node分の性能は出るわけです(カタログスペックではwrite 2000qps/read 10000qps)。そのため、開発環境等で1Nodeで動かしているとhotspotになっていることに気付かないということがあります。負荷検証を行う際はできればある程度のNode数で行うようにしましょう。

あと、PKにはUUIDを用いるのが王道ですがどうしても連番を使いたくなる場面が出てきます。勿論、連番を愚直に使ってしまうとhotspotになるため避けるべきなのですが回避策として「連番の値をビット順逆転する」という方法があります。

連続した値をビット順逆転する

Goだと以下のように変換するイメージになります。

また、この記事で指摘されているように各ID生成器の中で上位ビットにタイムスタンプを利用しているものはPKに使わないようにしましょう。

他にはSecondary Indexにもhotspotは存在するので注意しましょうとか細かいTipsは様々な方が記事に書いているので割愛。

一つhotspotになっているかどうかの判断材料として、30分~1時間負荷をかけてもcpu usageが上がらずlatencyが下がらない場合はほぼhotspotになっています。正常にsplitの生成が行われた場合にはcpu usageが上がり、latencyが下がる挙動になります。

warmupの話

SpannerはNodeの増減が1~2sで済むのですが、単純にNodeを増やしても性能はすぐには上がりません。hotspotの話でも挙げたsplitの生成がされない限り各Nodeが効率良く分散してデータ参照出来ないのが原因なのですが、サービスインの前などアクセスが集中することが分かっている時に悠長にsplitを待っていることが出来ない場面が多いとは思います。そのため、事前にピーク時負荷と同じ負荷をかけてsplitを生成するwarmupが必要になります。

リリース前にデータベースの準備を行う

splitが生成されるきっかけとしては2種類あります。

  • load-based split(write/readによる負荷上昇によって発生)
  • size-based split(データ量が増えることによって発生)

一番楽なのが単純にデータを増やすだけのsize-based splitだと思います。どのくらいのデータを入れたらsplitが生成されるのか?などを考察している以下の記事を読むとより理解が深まるかと。

wild-data-chase.com

個人的にこの辺りの情報がGoogle側から提供されていないので、何かしら詳しく書いた記事を出してくれないかなぁと常々思っております。(皆、肌感でやっている感じ)

あとは私がwarmup周りで勘違いしていたところなのですが、負荷をかけるテーブルは運用で用いる実際のテーブルではなく負荷用のテーブルに対して負荷をかける、ということです。負荷用のテーブルは実際のテーブルのPKに合わせたものにしてあげることで、適切なsplitが生成されます。 リリース前にデータベースの準備を行う に書いてあったのですが、見落としていて実際のテーブルに負荷をかけていたというミスをしてしまっていたので皆様はしないよう気を付けてください。

f:id:syossan:20201017155949p:plain

また、warmupしたテーブルを削除するとsplitが削除されると公式ドキュメントには書いてありますが、これはすぐに削除されるわけではなく数時間は残り続けるという挙動になっています。使いどころが特に無い情報ではありますが、間違ってテーブルを削除してしまっても慌てずまた同一PKのテーブルを作成して負荷をかけるとsplitは保持され続けるので安心してください。

splitの削除周りでついでに話をすると、splitは各Nodeが持っているものなので勿論ですがNode数を下げるとその分splitは削除されます。Node数をwarmupさせた上で10→1→10と変更して試したことがあるのですが、warmup前と同じ性能しか出ずNode数を下げた場合にはsplitが即時削除されるのだなということを知りました。 あと、splitはPKを元にしているのでPK以外のスキーマを変更してもsplitには影響しませんが、PKを変更した場合には再度warmupをする必要がありますのでご注意を。

その他色々

GoからSpannerを触っていたのですが、その中でこちらのリポジトリはめちゃくちゃ役に立ちました。

github.com

特にGoのSpannerクライアントのバージョン差分の変更点解説をしているissueは穴が空くほど読ませて頂きました。

github.com

また、以下の2つのOSSがとても便利でした。

github.com

qiita.com

これらが無いとちょっとSpanner運用は辛かったかなというくらいには便利でした。

あとwarmup後のテーブルからデータを削除する時に使ったコードをペタリ。

今回120Nodeでwrite 35万qpsまで確認出来たのですが、結構カタログスペック以上に性能が出るような感じがしました。ただ、限界が来るとエラーが頻発するので想定としてはカタログスペックを元にした方が無難かなという。 いやー、札束で叩くだけで素直に性能出てくれるSpannerはユーザー体験としては素晴らしかったです。高いけど。