12月に入ってから久々にRubyを書いています。Rubyはあれやこれや色々揃っていて良いですな。
さて、掲題のことをやりたいなとこんなん書きました。
スッキリ書ける。いやーいいですな。
上記記事を読んで今年何やったっけなってのを振り返りたくなったので振り返る。中身のない振り返りをします。
去年に増して色々触れた感がありありだった。必要に請われて機械学習周りの基盤作ったり、リリース間近の鉄火場に緊急登板されたり、放置気味だったタスクに首突っ込んでみたり、配置転換でまた毛並みの違った技術スタックに触れたり・・・まぁ便利おじさんな立ち回りをしてました。 来年も便利おじさんとしてより一層磨きをかけて精進します。
会社で大きめの負荷を捌く案件があり、DBにSpannerを使ってみようかという機運がチーム内で高まり、本番運用まで使ってみました。 その中で色々と知見が得られたのでTipsを細々と書いていく。
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が下がる挙動になります。
SpannerはNodeの増減が1~2sで済むのですが、単純にNodeを増やしても性能はすぐには上がりません。hotspotの話でも挙げたsplitの生成がされない限り各Nodeが効率良く分散してデータ参照出来ないのが原因なのですが、サービスインの前などアクセスが集中することが分かっている時に悠長にsplitを待っていることが出来ない場面が多いとは思います。そのため、事前にピーク時負荷と同じ負荷をかけてsplitを生成するwarmupが必要になります。
splitが生成されるきっかけとしては2種類あります。
一番楽なのが単純にデータを増やすだけのsize-based splitだと思います。どのくらいのデータを入れたらsplitが生成されるのか?などを考察している以下の記事を読むとより理解が深まるかと。
個人的にこの辺りの情報がGoogle側から提供されていないので、何かしら詳しく書いた記事を出してくれないかなぁと常々思っております。(皆、肌感でやっている感じ)
あとは私がwarmup周りで勘違いしていたところなのですが、負荷をかけるテーブルは運用で用いる実際のテーブルではなく負荷用のテーブルに対して負荷をかける、ということです。負荷用のテーブルは実際のテーブルのPKに合わせたものにしてあげることで、適切なsplitが生成されます。 リリース前にデータベースの準備を行う に書いてあったのですが、見落としていて実際のテーブルに負荷をかけていたというミスをしてしまっていたので皆様はしないよう気を付けてください。
また、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を触っていたのですが、その中でこちらのリポジトリはめちゃくちゃ役に立ちました。
特にGoのSpannerクライアントのバージョン差分の変更点解説をしているissueは穴が空くほど読ませて頂きました。
また、以下の2つのOSSがとても便利でした。
これらが無いとちょっとSpanner運用は辛かったかなというくらいには便利でした。
あとwarmup後のテーブルからデータを削除する時に使ったコードをペタリ。
今回120Nodeでwrite 35万qpsまで確認出来たのですが、結構カタログスペック以上に性能が出るような感じがしました。ただ、限界が来るとエラーが頻発するので想定としてはカタログスペックを元にした方が無難かなという。 いやー、札束で叩くだけで素直に性能出てくれるSpannerはユーザー体験としては素晴らしかったです。高いけど。
雑に作って雑に使えるが乗っかってるCPUが1コアなので、雑にハッシュ型をポイポイ〜と投げるとすぐにCPUが100%に張り付く。 実際に7万qps相当のHSETを投げたが、残念ながらCPUが天井を越えて使い物にならなくなってしまった。 こういうCPU使いまっせっていう時は大人しくGCEかGKEで建てた方がいいですなぁという雑感。
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つでした。問題の切り分けとして、以下のようなテーブルを作成し、試してみることに。
これらのテーブルに対して、Spannerのノード数を5にしLocustから10000RPSで負荷をかけたところ
という結果になりました。
で、Unique Indexがなんでこんな悪さをしとるのじゃろ?と調べていましたが、sinmetalさんが発表されていたスライドにこんな一文が。
引用:Google Cloud Spanner Deep Dive - Google スライド
うわ〜〜〜〜これか〜〜〜〜。実はUnique Indexは特に分散させなくとも良いだろーと負荷をかける際にInsertしていたデータがhotspotを気にしていないものだったのが原因でした・・・
うーん、Unique Indexを使う際はhotspotに気を付けねばならんですね。(Spannerの公式ドキュメントに書いといてよというお気持ちが少しある) ちなみに気になったので、②のパターンでノード数を上げてみるとどうなるのかやってみたのですが20ノードに上げてみたら逆に5000QPSに下がってしまいました。まぁhotspotなところにどれだけノード数増やしても無駄ですもんねぇ。
現場からは以上です。