1000万件オーバーのレコードのデータをカジュアルに扱うための心構え

自分が所属している会社のメンバーの教育用資料として、それなりの規模のデータを扱う時に前提として意識しておかなければいけないことをざっくりまとめたので、弊社特有の話は除外して公開用に整理してみました。

大規模データ処理、分散処理に慣れている人にとっては今更改めて言うことじゃないだろ、みたいな話ばかりだと思いますが、急激にデータスケールが増大してしまったりすると環境に開発者の意識が追い付かないこともあるかと思います。

そういったケースで参考にできるかもしれません。

弊社は基本的にAWSによって運用されているので、AWSを前提にした様なキーワードやサービス名が出てきます。後、句読点があったり無かったりしますが、ご容赦ください。

追記: 社内用の資料の編集なのでかなりハイコンテキストな内容だから誤解するかもしれませんが、これらはそもそもRDBの話ではありません。(関係無くは無いけど) 1000万オーバーの件数から数件取るとかそういう話ではなく数億件の中から1000万件を取得して数分以内に処理を終わらせるとかそういう処理を頻繁に(カジュアルに)実装しなければならない弊社の話でした。

1ノード, 1コアで処理してはいけない

  • 1件が200byteとしても1000万件を処理するなら2GB。これだけならメモリに乗り切らない程ではないが、オブジェクトにマッピングすると数GBを余裕で越えたりするし、1億件なら数十GBを越えるかもしれない。
  • 1件処理するのにかかる時間が1msecでも1000万件を処理すると1万秒 (3時間弱) かかる。
  • RubyはGILに引っかかるのでそもそも余り向いてない。可能ならマルチスレッドで処理を行い易いGoかJava辺りの利用を検討すること。(別にRustとかC++でも良いけど)
  • 上記以上の規模や負荷がかかる場合はノードごと分散することを検討する

通信回数を減らせ

  • 1度の通信が1msecで完了しても、1000万件を処理すると1万秒 (3時間弱) かかる。
  • 通信レイテンシやラウンドトリップタイムによってスループットの限界が決まる。通信回数が多いとワイヤースピードよりも遥かに低速で律速する。
  • redisキャッシュに接続してデータ取るのにもコストがかかる。

通信は常に失敗する可能性がある

  • 通信の信頼性はそんなに高くない。AWSの中でも度々不可解なネットワークの断絶が起きる。
  • 突然通信が終了すると、片側で終わったということすら検知できない場合があるため、状態遷移が中途半端な状態で処理が止まる可能性を考慮しなければならない

エラー対象を特定可能にしておく

  • エラーが起きた時に、どのデータに異常があったか判別できないと1000万オーバーのデータから大体の検討で探すことになり、非常に時間がかかる
  • 実装に不具合があった場合、特定パターンのデータが到達したら即座にシステムごと死ぬケースもある。
  • エラートラッキングサービスに必要なメタデータを付与してエラーを送る様にする
  • 一方で、ignoreして処理継続可能なエラーを都度トラッカーに送ると、一瞬で数十万のエラーが積み重なって課金額がえらいことになる危険があるので、注意すること。

リトライ可能ポイントを考慮しろ

  • リトライ機構は常に必要である
  • 1000万件処理している時の990万件目で処理が死んだ時、残りの10万件でリトライ可能にできるかを考慮する
  • 処理が複数ステップに跨る時は、ステップごとにコンポーネントを分けワークフローエンジンによってコントロールすること
    • 個別のステップを独立して実行可能にしてリトライ可能ポイントやテストデータ投入ポイントを挟む
  • 仕組みの単純化トレードオフになるケースもあるので、フルリトライが最善であるケースもある

羃等性の維持

  • リトライ時に状態の回復を要求すると自動リトライが困難になる
  • 処理の最初から流せば常に同じ状態になる様に設計することで、リトライに関わる処理が単純化できる
  • at least onceはexactly onceより圧倒的に処理負荷が軽い、羃等であるならat least onceで安全に処理できる
  • 羃等にすることが難しい処理に注意
    • カウントアップ等の今のデータからの差分を書き込む処理

分散処理ではロカリティ(データの所在)を意識する

  • 複数データのJOINが必要な場合、同一のキーであらかじめデータを同じ箇所に集めておいてから結合しないと、データの再配置が必要になる。再配置 = 大容量の通信処理であり高負荷。
  • 出力の全件Orderingは基本的に全てのデータを集約しないとソートできない

モニタリング必須

  • 関連するコンポーネントが増えるとボトルネックが分かり辛くなる。全ての箇所でモニタリング機構を整備すること。
  • コンポーネントに跨る様な処理の場合はdatadogに全体を俯瞰できる専用のdashboardを作る。
  • 最低限下記のリソースをモニタリングすること

    • CPU利用率とiowait
    • Disk I/O (iopsとスループット)
    • ストレージ消費
    • メモリ消費
    • ネットワークスループット
    • GC統計
    • 通信レイテンシ (中央値・最大値、90〜95パーセンタイル辺り)
  • 追加でモニタしておきたいもの

    • 無視できるエラーの回数、異常データの数や出現頻度
    • 処理遅延 (キューイングから処理されるまでの待ち時間)
    • DBへのqps
    • キャッシュヒット率 (キャッシュ機構がある場合)

余裕を持ったリソース確保 (staticかオートスケーリング かを問わない)

  • 唐突に大きな顧客からのアクセスが増えることがあるため、常に最低でも2倍ぐらいの負荷に耐えられる余地を残しておきたい
  • staticなノードの場合は、上記の様にモニタリングをしっかり整備し、早い段階でノード追加、スケールアップを行える様にアラートを整備する
  • それ以外のノードはオートスケーリングもしくは処理のキューイングに応じて自動的に必要なリソースを確保する機構が必須である
    • FargateやLambdaも活用できるし、リソース確保が簡単になる。それらの制約に対応出来る場合は検討の余地がある。
    • オートスケーリングの基本は増やす時は大胆に増やし、減らす時は少しづつ減らす

工程毎のログを吐け

  • バルク処理は長時間動作する上、複数ステップを伴うことがあるので、適宜ログを吐かないと、どこまで進んでいるか分からない。
    • 特定処理でスタックしてエラーにもならなかった場合に、進捗が確認できなくなる状態を避けたい
  • 一方で一件単位で詳細なログを吐くとログのデータサイズやログ出力の負荷が馬鹿にならないので、レコード個別のログについては必要性を考慮すること

汎用的なETL基盤を構築すること

  • importやexport等を個別の機能ごとに作るべきではない
  • 個別に作るとスケーラビリティの問題に個別で対処しなければならなくなる
  • 現状だとembulkをラップして実行できる基盤を準備し、必要があれば適宜プラグインを書くと概ね対応できる。

既存のコードを信用するな

  • 誰が書いたコードであっても1年以上前のコードがベースになっている箇所を信用してはならない
    • 実装を理解し現時点でも問題無いかどうかを確認すること
  • 1年あれば提供規模や顧客規模も変わるし、会社として注力している箇所も変わる。

    手癖で書くな

  • 量が変われば解決方法が変わる
  • 1年前と同じ方法では機能提供できないケースが多い
  • 特にWebシステム開発の手癖をバルク処理に持ち込んでは駄目。

クラウドサービスを使え

  • クラウドサービスの機能を理解し、不要な開発工程を減らすこと
  • ちょっとでも関係しそうなサービスを見かけたらとりあえずドキュメントを読む癖を付けておかないと、いざという時に思い付かなくなる。
  • 以下のサービスはデータ処理基盤において特に重要で、日常的に検討対象に入るので定期的に復習し新機能をキャッチアップすること
    • S3
    • SQS
    • SNS
    • Elasticsearch
    • ElastiCache
    • ECS/Fargate
    • StepFunctions
    • CloudWatchEvents

追記: 社内用の資料から対象を絞り込んだ時に消えてたもの

  • EMR
  • Athena