Cloud Next '18 Tokyo の発表で追記したい API設計と Cloud Spanner 周りの話

はじめに

こんにちは、プラットフォーム事業本部の恩田です。先月 Google Cloud Japan の須藤さま、弊社須山と一緒に Google Cloud Next '18 にてお話させていただきました。表題は『DMM課金基盤 APIにおけるGAEとCloud Spannerによる NoOpsの実現』 となります。

ポイントは Full-Managedなサービスの活用 でした。皆さんは Full-Managed で楽をしたいですか? 僕は楽をしたいです。ここでは「発表内容の振り返り」と、時間やテーマの都合上「発表しきれなかったお話」をしたいと思います。

発表内容の振り返り

当日は以下の3点をお話しました。その中から当記事ではメインとなる「2. 課金基盤の課題解決」について簡単にご紹介します。

  1. App Engineと Cloud Spannerの紹介
  2. 課金基盤の課題解決
  3. API基盤・DB基盤の概要とプラクティス

1. 課金基盤の課題

DMMの課金基盤ですが、次のような課題を抱えています。なかなか苦しい状況です。

  • まだオンプレミスの部分も多く、構築・運用の対象スコープが広い
  • 支えているサービスが40+(50+という声も) のために障害やメンテナンスによる停止時の機会損失が大きい
  • 新規サービスが増えるなかでロードの特性も予測しづらい

2. マネージド・サービスによる解決

このあたりを Google先生の マネージド・サービスで解決しました。
採用したのは App EngineCloud Spanner で、ザックリ次の様に解決しています。

# 課題 解決 App Engine 解決 Cloud Spanner
1 オンプレなので構築・運用対象が多い Managedサービスの活用 Managedサービスの活用
2 障害・メンテ停止時の機会損失が大きい 全てのゾーンでの冗長化とオンラインデプロイ 全てのゾーンでの冗長化とオンラインスキーマ更新
3 ロードの予測が困難 自動で水平スケール 手動で水平スケール

GCPのマネージド・サービスは 必要な機能が揃っており使いやすく構成されているため楽でした。
未経験の方は是非体験してみてください。

発表しきれなかったお話

次に発表では割愛したお話をしたいと思います。発表自体が取り留めのないコンテンツを詰め合わせたような内容でしたが、それでも説明しきれなかった部分は残っています。

1. 何故GCPを使ったか

まずは時間の関係で詳しく話せなかったこのお話からです。

DMMというと従来はオンプレミス、最近はAWSも使っている会社…というイメージかもしれません。 実際そのとおりですが一応マルチクラウドを掲げており、GCPが適したシステムにはGCPを使うこともできます。

ちなみに 今回GCPを選択した理由ですが、簡単に箇条書きにしてみました。

  • GCPの得意なマネージド・サービスを使える

    • GCPは App Engine, BigQuery, Cloud Spanner などなど マネージド・サービスの質が高い
    • GCPのマネージド・サービスは周辺サービスとの統合がされている (監視・通知・ログなど)
    • 今回の案件は、上記マネージド・サービスの想定ユースケースに収まる
  • マネージド・サービスならインテグレーションが不要

    • CloudとはいえCode化して構築するには結構な時間が掛かるが、それが不要
    • Best Practice / Anti Pattern の学習コストが不要
    • 構築していないのだから保守・運用する必要もない

「余計な苦労をしたくないからマネージド, マネージドが進んでいるからGCP」 …という感じです。

2. App Engine内の API設計はどんな感じか

次に、テーマから逸れるので説明しなかったお話です。App Engineのロゴを載せてありますが その中身のAPIのお話ですね。 API設計はベースに Kotlin / SpringBoot を用いて構築しています。

ー 2-1. Kotlin の利用

言語は Kotlinの採用により Javaよりモデル作成が簡素化できました。たとえば次のような機能が地味に便利です。

  • getter/setter を暗黙的に生成
  • toString(), hashCode() を暗黙的に生成

言語の表現力も高いため、手続き的な表現を省略し直感的に表現することが可能です。また Javaの経験があるエンジニアのキャッチアップも早めでした。

ー 2-2. Layered Architecture の採用

ベースの設計は 3層レイヤで作成しています。古典的な設計手法ですが、内部のビジネスロジックも重視するAPIなのでこの構成です。レイヤを省略したほうが開発が早そうですが、API やスキーマに変更が発生してもビジネスロジックを守れるという価値を大事にしました。
(参考: Software Architecture Patterns by Mark Richards: Layered Architecture)

ー 2-3. Minimal Cake Patternの適用

次にDIに関してです。DIが得意なSpringBootを採用しているものの、実際は Springに依存せず Minimal Cake Patternで実現しています。SpringBootの起動速度では不足してきた場合に備えて容易に切り替え可能にしてあります。

とはいえ、現状の負荷程度ではSpringBootの立ち上がり速度は問題なさそうです。
(数倍の負荷を掛けた状態でTrafficを100%切り分けた際、立ち上がりが追いつかずリクエストの取りこぼしが発生したケースはありましたが、Trafficを徐々に切り分けることで回避可能でした)

ー 2-4. Arrow ライブラリの利用

各サービスの内部では関数型を利用して処理を簡素化しています。Kotlinも関数型は扱えますが強力とは言えないため、Arrow を導入して関数の表現力を高めています。多用するとコードが見づらいと感じる人もでるため利用は限定的にしてあります。便利なんですけどね。

3.Cloud Spanner の計測結果

Cloud Spanner の検証で幾つか検証したのですが、折角なので計測値を公開しようと思います。
なお Cloud Spannerの実装がいつ更新されるかはわからないため、現時点での参考程度にお願いします。

ー 3-1. セカンダリインデックスによる性能変化

セカンダリインデックスを利用した際にどの程度スループットが変化するのかを計測しましたので、公開します。
環境は東京リージョン、3ノード。対象となる表は文字列の属性を10列保持しており、これに対して10万件の挿入に掛かった時間を下記の条件で5回ずつ計測しています。

  • Indexがないもの
  • 1列で成り立つIndex単体を付与したもの (1, 2, 3, 6, 9個)
  • 2列で成り立つ複合Indexを付与したもの (1, 2, 3個)
  • 3列で成り立つ複合Indexを付与したもの (1, 2, 3個)
Index なし 単体*1 単体*2 単体*3 単体*6 単体*9 複合2列*1 複合2列*2 複合2列*3 複合3列*1 複合3列*2 複合3列*3
1回目 133691409214846198812327825632178441643118661151171881119633
2回目 145181763215197207762317823986185592079922318163401799517365
3回目 144861828517790218092217424231177451710420378197101771719479
4回目 141881497616732184462014826637163221867919468159201628317958
5回目 136851372120332200502141625591164561835522600169931736720906
平均 140491574116979201922203825215173851827320685168161763419068
比率 100112120143156179123130147119125135

平均値ベースで比較すると多少のムラがあるものの、一定の率でスループットが低下していることがわかります。

ー 3-2. 初期アクセス時の性能

次に初期アクセス時の遅延に関してです。
環境は同じく東京リージョン、3ノード。計測内容は次のサイクルを5回繰り返すものです。Queryは SELECT 1を発行しています。

  • 1秒間隔で5回アクセス
  • その後1分間アクセスをしない

これを5回繰り返したものを 1セットとし、5セット分の平均を求めたものが次の表となります。

- 1秒目 2秒目 3秒目 4秒目 5秒目 (間隔)
1巡目 (平均) 2739.618.819.616.415.0(1分間)
2巡目 (平均) 139.614.217.416.416.0(1分間)
3巡目 (平均) 139.814.017.014.014.6(1分間)
4巡目 (平均) 13817.617.614.419.2(1分間)
5巡目 (平均) 159.214.222.013.615.8(1分間)

改めて計測しても初回が秒単位で長め、インターバル後(n-1)には 100ms超えとなることがわかります。初回の性能劣化はセッションの確立が絡んでそうですが、インターバル後の性能劣化の原因はわかりません。また 以前計測した際はインターバルを長くするとレイテンシも長くなる傾向がありました。

以上の特性がありますので、試験的にアクセスした際、レイテンシが秒単位でも驚かないでください。

4. Cloud Spanner を支えるアーキテクチャ

ここでは Cloud Spannerの凄さを支えるアーキテクチャを簡単に説明したいと思います。スライドでは簡単にしか触れていない部分です。最初に Regional構成, Node数 3を指定した際のイメージを載せておきます。

ー なぜ水平スケールできるのか

Cloud Spannerは水平スケールが可能です。Cloud Spannerは計算処理を司る Node の数を増やすことでこれを実現しています。Node数を増やすと次第にデータの同期が難しくなり水平スケールに支障が出そうですが、実は1つのNodeが担当するデータの範囲と総量は決まっているため、Node数に応じた水平スケールが可能となっています。

この仕組みを実現するため データは適当な範囲で分割されます。この分割されたデータを Split と呼びます。Splitの正体は 特定データベース,特定テーブルにおける主キー順にソートされた連続するレコード範囲です。各 Splitには 1つの担当 Nodeが割り当てられます。逆に 1つの Nodeは多数の Splitを担当しますが その総量は2TB 以下と決まっています。このルールによって一定の性能が担保されます。

それでも性能を上げたい際は、Node数を増やすと Cloud Spanner側で Splitの再配置が行われ 1 Nodeあたりの仕事量が減ります。Cloud Spannerのスケールの秘密をゆるく表現するとこんな感じです。

  • 大きな仕事(Data)は小さく分ける(Split)
  • 担当者(Node)ごとに仕事(Split)を割り振るが、その際無理のない仕事量(2TB)に留める
  • 仕事が増えたら 担当者(Node)を増やし、仕事も再割り振り(Split)する

ー なぜ高い可用性をもつのか

Cloud Spannerは (Region間 / Zone間) にデータを複製することで高可用性を担保しています。どんな形式かは構成 (Multi-Region or Regional) に依存します。たとえば Regionalの東京リージョンであれば 3つのZone に同じデータが複製されます。同時にそのデータを処理する Nodeも複製されます。つまり Node数に3を指定していると3つのZoneに 合計9つのNodeが立つことになります。これによって、データと計算リソースの可用性を担保しています。

これは逆に言えば更新処理は (Region間 / Zone間) に複製しなければならないことを意味します。この複製フローは Paxosと呼ばれるアルゴリズムによって選出されたリーダーが責任もって指揮し充分な書き込みが成功したことを保証します。 下の図は Split1に対して更新が入った際、Paxosリーダに Zone-Bの Split1が選出され、対応する Zone-Bの Node1が更新の反映を指揮しているイメージです。

書き込まれたデータは Splitの保存先である Colossusと呼ばれる分散ファイルシステムに永続化されます。このGFSの後継であるファイルシステムは高いレベルで冗長化されており、保存されたデータは強い耐障害性をもちます。まとめるとこんな感じです。

  • Nodeと Dataは Region/Zone単位で冗長化されている
  • Dataは更に Colossusファイルシステムで分散管理されている

ー なぜ分散構成なのに高速なのか

Cloud Spannerは RegionやZoneをまたぐ分散DBですが、通常のDBと遜色ない性能を担保しています。この理由の一つが TrueTime API の活用です。

TrueTime APIは GPSや原子時計を活用したAPIで、これを使うと物理的に離れた場所でも正確な時刻(より正確には保証可能な時刻の範囲)を得られます。正確な時刻がわかって嬉しいのは、各環境のコミット履歴の時刻が信用できることです。コミット履歴上の時刻が信用できるので、時刻ベースの Snapshotが利用可能になります。

たとえば読み取り専用トランザクションでデータを読み取る際、最新の Snapshot上からデータを取得できます。Snapshotは追記されど更新されないので読み取りのロックが不要です。ロックが不要であれば調整用の通信も不要で高速に動きます(正確にはローカルのコミット履歴が最新かどうかを検証するために Paxosリーダーに最終更新時刻を確認するための通信が必要ですが通信コストは軽微なものです)。まとめると次のとおりです。

  • TrueTime APIで正確な時刻を得られる
  • コミット時刻ごとの Snapshotを使えば通信を削減できる

ー アーキテクチャのまとめ

こうして見ていくと、Cloud Spannerは Cloudのチカラでこれらの価値を生み出していることがわかります。

  • Cloudの調達力で Nodeを増やすことによる拡張性
  • Region/Zone構成や分散ファイルシステムでの永続化による可用性
  • GPS、原子時計を活用した TrueTime APIを用いた通信削減による高速化

Cloud Spannerは Cloudのチカラを上手に活用したデータベースと言えるのではないでしょうか。

5. お金のお話

Cloud Spannerは Full-Managedだけど高価とお話しましたが、個人的に注意すべきは App Engineのほうと考えています。理由は簡単で、Cloud Spanner は手動でノード数を調整することでスケールさせますが、App Engineは全自動でぐいぐいスケールしてしまうからです。
(負荷試験の際は英語版の appengine-web.xml Reference にある < max-instances> の設定が必須かもしれません)

実際、一番お金がかかったのはボトルネックの残るアプリを載せた App Engineに負荷を掛けて回してしまった時です。数百インスタンスが立ち上がってしまい、気づいた時には残念な出費が発生し、後に反省会を開催するに至りました。弊社ではそろそろ評価の季節ですが、何も考えないようにしようと思います。。。

発表を終えて

発表までは気を抜けなかったので終わってだいぶホッとしています。当面はスライドのない世界を楽しみたいです。 また何かネタが出来たらどこかでお話したいですが、しばらくはゆっくり開発・運用を進めたいと思っています。余裕ができたら今回のボツネタでも使ってお話でもしようかな。

ちなみに写真は発表後ではなく発表前のものです。発表前はガチガチに緊張するのかと思いましたが、思ったほどは緊張しなかったです。不思議ですが麻痺してただけですかね。

そういえば、発表で出るぞ出るぞと言っていた DMLサポート出ましたね。
Develop and deploy apps more easily with Cloud Spanner and Cloud Bigtable updates | Google Cloud Blog

では、またどこかで見かけたらよろしくおねがいします。