生涯未熟

生涯未熟

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

2020年振り返り

blog.unasuke.com

上記記事を読んで今年何やったっけなってのを振り返りたくなったので振り返る。中身のない振り返りをします。

利用した技術

  • Language
    • Go
      • 主に使っていた。部署としては第一候補の言語として挙がるくらいには根付いた。
    • Ruby
      • ここ最近使っている。久々に使っているがGoに慣れきった身としては型が無いのが辛く感じる。
    • Python
      • 機械学習周りで雰囲気で触っていた。未だにわからん。
    • JavaScript/TypeScript
  • Framework
    • Echo
    • Rails
    • React
      • 昔に触って以来久々に触れたがReact Hooksがめちゃ便利だった。昔より取っつきやすくなったかな?
  • Middleware/Infrastructure
    • Docker/Docker Compose
    • Kubernetes
      • 今年はk8sがわからんと回らん業務が殆どだったのでめちゃ習熟した
    • MySQL
    • Airflow
      • 機械学習の前処理で使用。情報があんまり無くて割とむずかった。
    • GCP
      • Kubernetes Engine
      • Cloud Run
      • Cloud Functions
      • Cloud Storage
      • Cloud KMS
      • Cloud Composer
      • AutoML Tables
        • ポチポチっとトレーニングデータ食わせると良い感じに精度の高いモデル生成してくれた。やべぇ
      • BigQuery
      • SQL
      • Spanner
        • 100ノードくらい動かしたりしたがとても良い分散DBだった。高い。
      • Memorystore for Redis
    • AWS
      • Elastic Beanstalk
      • Auto Scaling
        • プッシュ通知配信サーバから配信日時前後に自動でオートスケールするスクリプト組んだりした
      • EC2
  • CI/CD
    • GCP
      • Cloud Build
      • Container Registry
    • ArgoCD
    • Github Actions
    • CircleCI
  • 概念的なもの
    • WebSocket
    • REST API
    • GraphQL
      • 初期の頃に遊びで触っていたがついに本格的に触るタイミングがやってきた
    • gRPC
    • GitOps

感想

去年に増して色々触れた感がありありだった。必要に請われて機械学習周りの基盤作ったり、リリース間近の鉄火場に緊急登板されたり、放置気味だったタスクに首突っ込んでみたり、配置転換でまた毛並みの違った技術スタックに触れたり・・・まぁ便利おじさんな立ち回りをしてました。 来年も便利おじさんとしてより一層磨きをかけて精進します。

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に気を付ける

現場からは以上です。