生涯未熟

生涯未熟

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

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はユーザー体験としては素晴らしかったです。高いけど。

Memorystore for redis雑感

雑に作って雑に使えるが乗っかってるCPUが1コアなので、雑にハッシュ型をポイポイ〜と投げるとすぐにCPUが100%に張り付く。 実際に7万qps相当のHSETを投げたが、残念ながらCPUが天井を越えて使い物にならなくなってしまった。 こういうCPU使いまっせっていう時は大人しくGCEかGKEで建てた方がいいですなぁという雑感。

Spannerで書き込みの速が出なかったお話

Spanner、皆さんどう使ってるでしょう?めちゃくちゃ雑に使っても大概のシステムの要件を満たせるくらいに捌いてくれる良い子なので、利用シーンは色々あるかとおもいます。値段はお高いですが・・・👀

さて、そんなSpannerで唯一"速" が出なかったパターンを発見したので記述しておく。

どういう状況か?

Spannerはベストプラクティスに従ったスキーマ設計をした場合(hot-spotを作らないetc...)、1ノードあたり読み取りで10000QPS・書き込みで2000QPSのパフォーマンスが出ます。ノードを増やしていくと20000QPS、30000QPSてな感じで性能は上がっていくのですが(あくまでもカタログスペック通りなら)、あるテーブルに対して負荷試験をしている時にどれだけノードを増やしても書き込みで5000~7000QPS以上の速が出ないという状況に陥りました。

調査

今回、負荷試験としてはGKE + Locust + Go(Echo)といった感じでGCPの例Pixivさんの例を参考に環境構築しました。で、上記のような状況になった際に「もしかしてLocustのWorker力が足りないか?」とか「いやいやGoで作ったアプリがおかしいのか?」とか「きっとSpannerのClientライブラリにあるSessionPoolの設定値がおかしいんだ!」とか色々頭を悩ませましたが、Spannerのメトリクスをモニタリングしていたら異常にLatencyが増えていることに気付き、「Spanner側の問題では・・・?」と疑いを持つこととなった。

この時に頭にあったのは「カタログスペックは1レコード1kbを想定しているので、もしや1レコードが重すぎた?」というのと「Unique Indexを使っているテーブルなのでこれが原因か?」という2つでした。問題の切り分けとして、以下のようなテーブルを作成し、試してみることに。

  1. UUIDを主キーとして持つ1カラムのテーブル
  2. ↑のテーブルに1カラムとUnique Indexを追加したテーブル
  3. 今回の状況の際に対象としたテーブルと同じカラム構造だがUnique Indexを持たないテーブル

これらのテーブルに対して、Spannerのノード数を5にしLocustから10000RPSで負荷をかけたところ

  1. 10000QPS
  2. 6000QPS
  3. 10000QPS

という結果になりました。

で、Unique Indexがなんでこんな悪さをしとるのじゃろ?と調べていましたが、sinmetalさんが発表されていたスライドにこんな一文が。

f:id:syossan:20200823191147p:plain 引用:Google Cloud Spanner Deep Dive - Google スライド

うわ〜〜〜〜これか〜〜〜〜。実はUnique Indexは特に分散させなくとも良いだろーと負荷をかける際にInsertしていたデータがhotspotを気にしていないものだったのが原因でした・・・

感想

うーん、Unique Indexを使う際はhotspotに気を付けねばならんですね。(Spannerの公式ドキュメントに書いといてよというお気持ちが少しある) ちなみに気になったので、②のパターンでノード数を上げてみるとどうなるのかやってみたのですが20ノードに上げてみたら逆に5000QPSに下がってしまいました。まぁhotspotなところにどれだけノード数増やしても無駄ですもんねぇ。

結論:Unique Indexのhotspotに気を付ける

現場からは以上です。

grpc-gatewayにおけるHTMLの出力

grpc-gatewayを使って組んでるシステムでJSON以外にHTMLを出力せねばならなかったので調査。

grpc-ecosystem.github.io

結論としては↑の記事を参考にすればOKだった。(これに全然辿り着けなくて苦労した・・・

filterとか挟み込む場合はproto.Messageの名前で判定してフィルタリング処理を実行すると思うが、↑の例だと google.api.HttpBody の名称で返ってくるのでなんらか考慮が必要になるかもとぼんやり思ったり。

Cloud Runにおける段階的ロールアウトのメモ

Cloud Runの段階的ロールアウトに関して知らなかったことがあったのでメモ。

段階的ロールアウトって?

ここを見よう。

cloud.google.com

要するにデプロイ後、最新リビジョンに追従するのではなくトラフィックの管理を行い、段階的にロールアウトしていこうというやつ。

知らなかったこと

「新しいリビジョンの編集とデプロイ」→「このリビジョンをすぐに利用する」のチェックを外すことで最新リビジョンへの追従を止めることが出来るが、その後に gcloud run deploy でデプロイを行うと段階的ロールアウト状態のままとなり、デプロイしたリビジョンは最新リビジョンとはならない。

ドキュメント見たところ gcloud run deploy のoptionsに「このリビジョンをすぐに利用する」に該当するoptionが無かったので、再度最新リビジョンへ追従したい場合には手動でチェックボックスをonにしてデプロイするしかない。(と今のところは認識している