サーバーレス失敗談 - テーブル設計編

Posted on
Serverless DynamoDB

こんにちは、エンジニアの肥前です。この記事は Serverless Advent Calendar 2018 の13日目です。 また、HiCustomer Tech Blog の最初の記事でもあります。今後もチームで継続して得られた知見を書き溜めていく場にしたいなと思います。

さて、いわゆる ”サーバーレスアーキテクチャ” を採用してスタートアップのプロダクト開発を開始しておおよそ一年が経過しました。もちろん恩恵に預かっている部分、省力化できた点は多々あるのですが、今改めて振り返るとアンチパターンを踏んでいたり、このまま放置すると負債化しそうな箇所が見えてきています。失敗した話というのはなかなか表に出しづらいものではあるのですが、少しでもこれから取り組む方の助けになればと思い記事に残してみようと思います。サーバーレス界隈の方々にはあるあるネタだと思うのでご笑納ください。

前提となる構成

事情により簡略化している部分があります

HiCustomer は SaaS として提供されるカスタマーサクセス管理プラットフォームで、各テナントから送信されてくるログデータに基づき顧客のスコアリングやその他の集計処理、またそれらをトリガーとした通知の送信などを行います。

現状は以下のような構成を採用しています:

各 SaaS プロダクトから送信するログデータは下記のように抽象化して保存されます:

テーブル設計における失敗

以下に実際に発生した問題と、それに対するアプローチを記していきます。

当初のテーブル設計方針

システムの中心となるデータは公開API経由で高頻度で書き換えられるため、CQRS の考え方を取り入れ更新系と参照系でテーブルを分離することにします。

  • 更新系: 全てのトランザクションを記録
  • 参照系: リソースの最新状態を保持

更新系から参照系への書き込みは Dynamo Streams を Lambda でポーリングして実現することにします。さらにアイテムの種別それぞれに対してもテーブルを分割し、下記のような構成をとりました。

当初の設計の問題

半年ほど上記の設計で運用したところ、下記のような問題が顕在化しはじめました。

  • 利用頻度の低いテーブルでスロットリングが頻繁に発生する
  • テーブル数が多くキャパシティユニットが最適化しづらい
  • DynamoDB Streams とそれを受ける Lambda も同様に数が増えるため管理が煩雑になる

問題の原因として不適切なテーブルの分割を行ったことが挙げられます。RDBMS に慣れていると当然のようにデータ型ごとにテーブルを分けたくなりますが、 DynamoDB はキーのハッシュ値によりワークロードを分散する機構を採用しているため負荷分散目的であればテーブルの分割は不要です。むしろオートスケーリングやオンデマンドモードを選択している場合、利用頻度の低いテーブルに急なアクセスがあるとコールドスタートに近い状態になり立ち上がりでスロットリングが発生する可能性が高まります。

HiCustomer の場合は event_summary の参照頻度が customer_summary の100分の1程度のため、テーブルをまとめて RCU を共有することでスロットリングを回避することができそうです。 この設計については AWS による DynamoDB のベストプラクティス の中でも

複数のテーブルを使用する特定の理由がない限り、優れた設計のアプリケーションで必要なテーブルは一つのみです

と述べられており、基本的には複数のテーブルを一つにまとめる方向で再設計を行った方が良さそうです。

どうすればよかったのか

基本的には原則にしたがってテーブルをまとめていくことにします。ただし、大容量の時系列データを扱う必要があり、かつパーティションキーが書き込み日時に依存する(ホットパーティションが発生する)ような場合はテーブルを分割するほうが効率がよいことを考慮します。大容量とは物理パーティションが分割される閾値を超えるようなものを指しており、2018年12月13日時点では1,000WCU、または容量が10GBを超えるようなケースです。これを超えると DynamoDB は自動的にパーティションを分割してスケールアウトしますが、一度分割されたパーティションが減ることはありません。テーブルに割り当てた CU はパーティションごとに均等に分割されるため、利用頻度が低いパーティションがテーブルに存在すると CU が希薄化されてコスト非効率になります。このような場合、下記のようなアプローチが有効です:

  • 日時などの期間ごとにテーブルを分割する
  • 古くなったデータを別のテーブルにアーカイブする

ここから HiCustomer のケースを検討します。更新系のテーブルのパーティションキーはイベント発生日の日付を表す文字列なので、データの書き込み対象は常に当日を表すパーティションであり、その他のパーティションにはほとんど書き込みがありません。このようなときは日毎にテーブルを分割して当日分のテーブルのみ過去のリクエスト数を元に必要と予測された WCU でプロビジョニングします。ほとんどアクセスの発生しない過去の日付のテーブルはオンデマンドモードを選択します。オートスケールやプロビジョニング CU を選択している場合は WCU/RCU を最低でも1に設定する必要があるので継続的に費用が発生しますが、オンデマンドであればリクエストごとに料金が適用されるためこのようなケースでも有用です。上記を踏まえて下記のような構成を取り入れるとより DynamoDB の性能を活かすことができます。

余談

今回は DynamoDB の設計に関する失敗談を紹介しましたが、事前に AWS が提供している “DynamoDB のベストプラクティス” (原文/日本語訳) に目を通しておくことで大方避けられるのではないかと思います。当時はなかったんですよ…。
紹介した設計変更については現在移行を進めています。幸いにも参照系と更新系のテーブルを分離していたので段階的な設計変更がしやすく、その点はよかったなと思います。

最後に、HiCustomer ではエンジニアを募集しています。この記事を読んで少しでも興味をお持ちいただけた方、ぜひオフィスに遊びにきてください。@hizeny まで、もしくは Wantedly 経由でご連絡をお待ちしています!