Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scala、DDD、Akkaで立ち向かう 〜広告配信システムに課せられた100msの制約〜

mattsu
October 05, 2019

Scala、DDD、Akkaで立ち向かう 〜広告配信システムに課せられた100msの制約〜

https://camphor.connpass.com/event/146724/ こちらのイベントで発表した資料です

mattsu

October 05, 2019
Tweet

More Decks by mattsu

Other Decks in Technology

Transcript

  1. もくじ • 広告配信システムの宿命 (5分) • なぜScala, DDD, Akkaなのか (14分) •

    性能面の具体的な工夫 (10分) • 可読性を維持する方法 ←スライド作ってから35分の発表では収まらない事に気付いた • まとめ (1分) 4 割とボリュームが多くなってしまったので 駆け足気味で進みますm(__)m
  2. 100msの制約 • SSPがDSPへ入札要求をしてから100ms以内に返却することが必須条件 • 100msを超えた場合はタイムアウトで入札が拒否される • レイテンシを含む時間なので現実的には遅くても30ms程度で処理する • 入札できない =

    落札されない = 売上を生まない = 倒産 6 100msの制約を守ることは業務が成立する必要条件 SSP DSP A DSP B DSP C WEBページ 広告枠 入札要求 100ms以内に応答しないとタイムアウト 応札(50ms) 広告をリクエスト 広告をレスポンス 応札(100ms) 応札(150ms) 入札要求 入札要求 50msで応答してい るので入札成功 100msで応答して いるので入札成功 150msで応答して いるので入札失敗
  3. 1つのエンドポイントに全ての機能が詰まっている • 一般的なWebアプリ等と異なり、エンドポイントはSSP毎に1つだけ ◦ 設計次第だがマイクロアドではSSP毎にエンドポイントを分けている ◦ 1つのAPIに対して機能追加し続けるイメージ • ビジネスのスケールとともに機能は増え続ける •

    機能が増えても100msの制約は不変 • しかし機能追加は避けては通れない 7 機能追加は業務を継続する必要条件 SSP A DSP 入札要求 POST /rtb/a SSP B 入札要求 POST /rtb/b SSP C 入札要求 POST /rtb/c 位置情報を使う機能 ユーザの性別とか判定する機能 ユーザと似たユーザを検索する機能 行動履歴を使う機能 ブラックリストユーザ判定機能 ヽ(・ω・ヽ*) PDM オフライン購買データを使う機能 新機能追加して! RTBの内部処理 ・・・ 機能追加をすればする程、RTBの内部処理は 複雑かつ高負荷になっていく。
  4. 増え続ける広告在庫 • ビジネスのスケールとともに扱う広告在庫は増大 ※広告在庫とは広告の表示可能回数のこと • 1度のRTBで出来るだけ多くの広告を配信対象にしたい • 多くの広告在庫を扱える = より精度の高い広告を配信可能

    = 高い広告効果 ◦ また、たくさん表示可能なため単純に売上が伸びる • 一方で扱う広告の増大は処理負荷の増大に直結 • 少なくても業務は成り立つけど、スケールしない 8 多くの広告在庫を扱えることは業務が成立する十分条件 SSP DSP 入札要求 応札 化粧品関連のページから リクエスト うーん・・・とりあえず健 康関連? DSP 化粧品が最適! (略) 入札要求 応札 DSP (略) 入札要求 応札 (タイムアウト) 化粧品 関連 健康 関連 旅行 関連 広告在庫 健康 関連 旅行 関連 広告在庫 旅行 関連 健康 関連 化粧品 関連 広告在庫 ・・・ お、おおすぎる! 処理が間に合わねぇ 広告在庫が少ないと・・・ 広告在庫が多いと 広告在庫が多過ぎると 処理が間に合わず機会損失が発生するのは もったいない! こうならないようにエンジニアが頑張る
  5. 増え続けるリクエスト • ビジネスのスケールとともに受けるリクエストは増大 • 出来る限り多くのリクエストを受けたい • 大量のリクエスト = 入札機会の増大 =

    売上アップ • 大量のリクエストを捌きたいなら・・・ ◦ サーバを増やす ◦ サーバのスペックを上げる • しかし・・ ◦ サーバは高い(1台辺り10~20万) ◦ ラックに限りがある ◦ 1台辺りの収益率が落ちるのは本末転倒 • よって、富豪的アプローチで万事解決とは行かない • ハードに頼り切るのではなく、ソフトウェアも限界までチューニング 9 大量のリクエストが捌けることは業務が成立する十分条件 日毎のリクエスト数 ※軸の値は伏せてます 増加傾向にある
  6. 業務課題に対するアプローチとキーワード • プログラミング言語にScalaを採用 ◦ 関数型プログラミング ◦ オブジェクト指向プログラミング ◦ JVM言語 •

    ソフトウェアの設計手法にドメイン駆動設計(DDD)を採用 ◦ 3層 + ドメインアーキテクチャ ◦ ドメインモデル ◦ ユビキタス言語 • 主要なフレームワーク・ツールキットにAkkaを採用 ◦ 並行並列処理 ◦ リアクティブシステム ◦ アクターモデル 11 次のスライドから詳細な説明をしていきます
  7. Scala • JVM言語の1つ • 静的型付け言語 ◦ 型推論・パターンマッチング • 小規模なスクリプトから大規模システムの構築まで幅広く利用可能 ◦

    Scalaはscalable languageに由来する • すべてのJava製ライブラリと相互運用可能 • オブジェクト指向と関数型プログラミングの概念を持つ • 充実したコレクションAPI ◦ ループ処理ではなく述語関数によって表現 13 object Main extends App { println("Hello world") } Seq(1,2,3,4,5) .map(_ * 2) .filter(_ > 5) > Hello world > Seq[Int] = List(6, 8, 10) Hello worldの例 述語関数を使った例(for文を使わずにループ処理)
  8. なぜScalaなのか • 関数型言語の特徴を持っており、シンプルに記述できる ◦ イミュータブル ◦ 副作用を発生させづらい構文(例えばif文ではなくif式) ◦ nullを発生させないデータ構造 •

    オブジェクト指向言語の特徴を持っており、DDDとの相性が良い ◦ ポリモーフィズム(最も重要!) ※後述 ◦ カプセル化 ◦ 継承 • JVM言語の安心感 ◦ 十分な実績を持つJavaとの互換性 ◦ JVM上で動作するため、Javaに近い性能を期待できる ◦ コンパイル言語なので、型安全による安心感 14
  9. 関数型言語はなぜシンプルなのか • 関数型プログラミングは純粋関数だけを使う ◦ 純粋関数とは副作用がない関数 • 副作用とは関数内で副次的に発生するアクション ◦ グローバル変数に再代入する ◦

    データベースを参照・更新する ◦ I/O ◦ 例外を投げる等 • 純粋関数は入力に対して必ず同じ出力がある • システムの状態に依存しないので... ◦ 頭の中で状態を追いながら読まなく良い ◦ テストコードが書きやすい 15 class Auth { public void auth(Token token) { token.set(getAuth()) } } 副作用があるケース(コードはJava) class Auth { def auth(token: Token): Token = { token.copy(getAuth()) } } 副作用がないケース 返り値(結果型)がvoidかTokenの違いがある 副作用があるケースは「 tokenを更新」 副作用がないケースは「 tokenを新規作成」
  10. ポリモーフィズムが重要な理由 • ポリモーフィズム(多態性)はインタフェースに実装できる性質 ◦ オブジェクト指向以前もポリモーフィズムは可能だったが、言語レベルで安全に実装できるようになった (だからこそ重要) • インタフェースによってプログラムをより抽象的・汎用的に記述できる ◦ 例えばUNIXのIOデバイスドライバはopen,

    close, read, write, seekの標準機能を提供する ◦ もしインタフェースが統一されていなかったら、デバイス依存のコードになり用途が限られる • インタフェース同士が依存し合うことで高いモジュール性・疎結合性を得る • ポリモーフィズムによって、ソースコードの依存関係を絶対的に制御(※後述) 16 モジュール同士が抽象に依存 具象クラスを自由に付け替えられる => 疎結合 UNIXはデータの入出力をファイルとして扱う => デバイス非依存
  11. DDD • DDD(ドメイン駆動設計)はソフトウェア設計手法のこと ◦ 技術では無く、ドメイン知識(業務知識)を中心に考える • 事業価値を最大化することが我々の究極の目標 • しかし実際には... ◦

    ドメインエキスパート(PDMなどドメインに詳しい人)は事業価値の向上に注力 ◦ エンジニアは業務上の問題を技術的に解決しようとする ◦ エンジニアによって翻訳されたソフトウェアになってしまう ◦ 両者のコミュニケーションの相違が起こってしまう • DDDはドメインエキスパートとエンジニアが共通認識(ユビキタス言語)を 持ったソフトウェアを作る(コードがドメインモデルを反映) 17 (・ω・*) PDM 「広告」には「画像」とか「動画」とかの種類があるんだよ なぁ。 「広告主」がどっちか選んで「 登録」するんだよねぇ エンジニア (,,゚Д゚) 画像とか動画とかいってもフォーマット色々あるし、「 JPEG 広告」「PNG広告」「MP4広告」みたいな感じで実装すっか。 生成ロジックを統一したいし、「 ユーザ」が「広告ジェネレー タ」を使って「生成」するようにしよっと。 下の会話では両者が違う言葉を使っている。 この状況が続くと、両者の認識のズレは拡大し、いつかプロ グラムの修正が困難な状況に陥る可能性が高い
  12. ドメインモデルをピュアにすべきな理由 • ドメインモデルは何にも依存させるべきではない(ピュアにすべき) • さらに、ビジネスルールは技術的関心事に左右されるべきではない • ビジネスルールとはドメインモデルの構成要素 ◦ "Business rules

    describe the operations, definitions and constraints that apply to an organization. Business rules can apply to people, processes, corporate behavior and computing systems in an organization, and are put in place to help the organization achieve its goals." ◦ つまり、ビジネス活動の中で意思決定を行うための規則 ◦ 個人的には、コンピュータが消滅しても失われないルールのことだと思ってる ◦ 僕らが一番守りたいのはビジネスルール 18 ※広告の例だと 分かりづらかっ たので お店を例にしま した ECサイト ~インターネット時代~ 商品を買う例 お店 ~近代~ お店 ~昔~ • 合計の求め方 • 小計の求め方 • 消費税の求め方 などは「会計システム」「 POSシステ ム」「そろばん」(技術的関心事)に左 右されるべきじゃない! 引用: https://en.wikipedia.org/wiki/Business_rule
  13. 巨大なモノリシックなアプリのメンテナビリティ • 広告配信システムは非機能要件的にどうしてもモノリシックな一枚岩に • 一枚岩故にドメインはめちゃくちゃ複雑 • 一枚岩なのに色んなシステムと接続しまくる • 何も考えずに作ったらメンテナビリティは容易に悪化 •

    DDDを取り入れ多層アーキテクチャによってドメインモデルを保護 • レイヤーの依存関係を厳格にし、影響範囲の局所化と明確化 19 プレゼンテーション層 データソース層 アプリケーション層 ドメイン層 A B AはBに依存している HTTPやRPCなどの入力の インタフェースを担当 ユースケースを担当 (ビジネスロジック) DB接続周り担当 ビジネスルール担当 (ドメインモデル)
  14. Akka • Scala製の並行並列処理に特化したツールキット(フレームワークとライブラリを合わせたやつ) • 主にLightbend社によって開発・メンテされているOSS • 並行並列、分散システムを簡単に記述するためにアクターモデルを提供 ◦ アクターモデル自体は1973年に登場 ◦

    クラウドコンピューティングの進歩とムーアの法則の終焉などでスケールアウトが主流になってきたことで 再び注目を浴びてきた • アクター以外にAkka Streams, Akka Clustring, Akka HTTP等様々なコンポーネントを提供している • Akkaはリアクティブ宣言に基づいて設計されている ◦ 「リソースを効率よく利用し、アプリケーションをを自動的にスケールできるようにすること」 (Akka実践バイブルより引用) 20 引用: https://www.reactivemanifesto.org/ja
  15. アクターモデル • アクターはメッセージ駆動の計算実体(スレッドに似ている) ◦ アクターは非同期に実行される • アクター同士(数千・数百万)が協調して1つのアプリケーションを構成する • アトミックやロックを使わず、安全に並行並列処理が実装できる ◦

    アクターはメールボックス(メッセージキューみたいな)を持っていて、1度に1つ処理 • アクターはfire-and-forget(撃ちっぱなし)の性質によって、非同期処理を実現している • スケーラビリティのためにアクター同士は疎結合になっている ◦ 空間/位置: 位置透過性とも。同じノード、異なるノード等、どこにアクターがいても良い ◦ 時間: アクターはタスクの完了について何も保証しないし、期待もしない ◦ インターフェース: アクター同士は何も共有しない。インタフェースが正しいかとか関係ない 21 Actor Actor Actor メッセージ送信 メッセージ送信 メッセージ送信 アクターシステム メールボックス (メッセージキュー) アクターは非同期で動作するのでア クターの数だけ並行並列に動作可 能 ロックとかアトミックな操作は一切不 要 メッセージを受信し て何か処理
  16. スループットではなくレイテンシを高める • アクターモデルによって、並行並列処理が安全に実装可能 • スレッドでは実現出来なかったスケールアウトも可能 • アクターによって処理を細かい単位で分離するので、局所的にスケール可能 • よって、Akkaを採用することにした •

    しかし・・・良いことばかりではない ◦ 高い学習コスト ◦ アプリケーションの性質上、コアなロジックはアクターによるスケールアウトが難しい(通信コストが無視出来ない) ◦ まだ発展途上のOSSのため、バージョン上がるとガッツリAPIが変わってる • スループットは変わらないが、レイテンシは高めたい(アクターモデルの恩恵を享受) ◦ 全体をそれなりに早くではなく、1リクエスト辺りの応答速度を高めたい ◦ 大量のリクエストを満遍なくタイムアウトさせるより、少量のリクエストを確実に返せた方が良い ◦ そのためにはアクターモデルを活用した並行並列処理が使いやすい 22 リクエスト1の処理 リクエスト2の処理 リクエスト1の処 理 リクエスト1の処 理 リクエスト2の処 理 リクエスト2の処 理 スレッド1 スレッド2 200ms スレッド1 スレッド2 200ms 200msでどちらも2件のリクエストを 捌いた。 しかし、左は200msかかってしまっ てタイムアウトになった 一方で右は100msで応答したので 入札できた 広告配信システムでは 100msの制 約があるのでタイムアウト! 超理想の話なので実際にはこんな 綺麗に並列化は難しい
  17. なぜScala, DDD, Akkaなのか • Scala ◦ オブジェクト指向、関数型のメリットを享受できる ◦ JVM上で動作するので性能面での安心感 ◦

    強力な型システムによって規模の拡大が原因でメンテナビリティが低下しづらい等 • DDD ◦ 複雑なドメインに立ち向かう手段 ◦ ビジネスルールに集中でき、事業価値を高めやすい ◦ Scalaと相性が良い(オブジェクト指向との相性が良い) • Akka ◦ 並行並列処理、分散処理を安全に実装できる ◦ 様々な便利なコンポーネントが使える 23
  18. オンメモリ戦略 • アプリケーションのボトルネックの大部分はI/O ◦ 通信コスト ◦ データを検索するコスト • 載せれるデータはローカルのメモリ上に持てるだけ持つ ◦

    いわゆるオンメモリ戦略 • オンメモリ化の条件 ◦ ある程度の更新間隔を許容(数分~数十分は古くても良い) ◦ 件数が膨大過ぎない(メモリに載る容量か) ◦ 揮発しても良いデータ(どこかで永続化されてる必要がある) • 以上の条件を満たせばメモリに載せる • 単純な戦略だが最も効果的 • ただし、メモリには限界があるので無敵ではない 25 アプリ DB キャッシュ 定期的にDBのデータをキャッシュ アプリ DB 毎回DBに問い合わせるので遅い 常に最新のデータを取得できるが、 本当にその必要がある? 速度を犠牲にしてまで最新データにこだわるべき か? 最新のデータは使えないが、ローカルのメモリに キャッシュするので高速。 ただし、データの容量等の様々な制限はある 引用: https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html メインメモリの参照は100ns SSDのランダムリードは16μs ≒ 16000ns 単純計算で160倍の速度差 さらに、ネットワークのレイテンシを考えればオン メモリ化が如何に効果的かわかる
  19. 整合性の担保 • オンメモリ戦略を使った場合の考慮点として整合性がある • いくら古いデータを許容できるといっても、過去の異なる時点のデータを参照するのは不味い • そこで、IOモナドを利用してデータの取得と更新タイミングを分離(更新だけ遅延させる) ◦ IOモナドって何・・? =>

    関数合成出来て、遅延評価が可能な性質が重要。厳密な定義は知りません。 • DBへの問い合わせを先にやり、全てのデータが揃った時点で、一括でオンメモリのデータを差し替える • これによってほぼ整合性を担保できる(厳密にはDBの問い合わせタイミングが若干ズレるがそこは許容) • 万一DBの問い合わせが一部失敗した場合は、全てロールバックする • ただし、一時的に使用するメモリが本来保持するデータ量の2倍になる 26 アプリ DB A 新キャッシュ DB B DB C キャッシュ アプリ 新キャッシュ キャッシュ 各種DBからデータを取得していく が、全てのデータが揃うまで、既 存のキャッシュは更新しない 全てのデータが揃ったタイミングで既存のキャッ シュを新キャッシュで置き換える (参照が切り替わるだけ ) またキャッシュ参照時には リエントラントロックを かける
  20. リアルタイムなデータはインメモリデータベース • オンメモリ戦略を適用できないデータはどうするか? ◦ リアルタイム性が求められるデータ ◦ ローカルのメモリには乗り切らないデータ • リモートホストのインメモリデータベースにキャッシュする ◦

    通信は発生するがデータは高速で読める ◦ マイクロアドでは主にRedisを利用 (NoSQLの一つ、KVSとも) • Redisを活用することで、リアルタイムにデータを高速に読み込めるがKVSの特徴を理解した設計が必要 • 効率良くデータを取得するために、アプリケーションに特化したスキーマで構成する • 例えばユーザに行動履歴が紐づく場合は単なるString型(Key:Value形式)よりもHash型の方が効率が良い ◦ レコードに複数のデータを含む場合は、MessagePack等で事前に圧縮しておく 27 user1_action1 user1_action2 user1_action3 user2_action1 user2_action2 http://aaa.com http://bbb.com http://ccc.com http://aaa.com http://bbb.com key value user1 user2 http://aaa.com http://bbb.com http://ccc.com http://aaa.com http://bbb.com key value action1 action2 action3 field action1 action2 String型 Hash型 キーを単純にしたことで検索性能が改善
  21. 本質でない処理を切り離す • 広告配信システムの本領は入札要求に対して入札すること ◦ しかし、それ以外にも色々やっている(ログの書き出し, DBの更新, 他サービスとの連携等) • 例えば、入札は出来る限り高速に応答したいが、ログの書き出しは多少遅くても良い •

    本質でない処理にCPUのリソースを割くのは勿体ない。そこでアクターモデルを活用 ◦ fire-and-forgetの性質によって応答を待つ必要はない ◦ アクターは位置透過性なのでログの書き出しはリモートホストで処理してもいい • ただし、メッセージが到達する信頼性は「at-most-once」 ◦ つまり、メッセージは喪失する可能性があるので重要なメッセージは送れない ◦ at-least-onceを保証する方法も一応可能。ただアクターモデルの旨味が消える 28 RTB処理はログ記録の応答を一切待たないの でログ記録の影響を受けない ログ記録がリモートホストに存在する場合、ログ は喪失する可能性がある ログの重要度によって、対応方法を変える必要 がある ←ログが消滅!!! 重要なログだったらヤバイ・・・
  22. 統一したコードフォーマット • チーム開発ならコードフォーマットの統一は絶対した方がいい • みんながバラバラのフォーマッタを使っていると、無駄なコード修正が入って本質を見落とす可能性 • ScalaならScalafmtが良さげ ◦ 他にもscalariformとかIntellijの設定を共有する方法もある ◦

    まぁお好みでって感じだが、体感Scalafmtが一番使われてそう 30 object ScalafmtTest extends { val aaa = "MicroAd" def bbb(): Int = { val a = 1 val b = 2 a + b } } Scalafmt前 object ScalafmtTest extends { val aaa = "MicroAd" def bbb(): Int = { val a = 1 val b = 2 a + b } } Scalafmt 後
  23. レイヤー構造に基づいたクラス設計 • レイヤーの構成方法は三者三様 ◦ レイヤードアーキテクチャ, クリーンアーキテクチャ, オニオンアーキテクチャ, ヘキサゴナルアーキテクチャ, 3層 +

    ドメインモ デル等 ◦ 色々あるし、独自にカスタマイズしても問題無い • 一番重要なのはドメインモデルをピュアにすること • そして、レイヤーの依存関係をないがしろにしないこと ◦ ScalaならsbtのdependsOnを利用するとコンパイラレベルで強制可能 • レイヤー構成を定めると修正箇所を局所化できる • また、設計の指針になるため、人によって実装方法がブレ辛い 31 プレゼンテーション層 データソース層 アプリケーション層 ドメイン層 HTTPやRPCなどの入力の インタフェースを担当 ユースケースを担当 (ビジネスロジック) DB接続周り担当 ビジネスルール担当 (ドメインモデル) ドメイン層は誰にも依存していない。 つまり、ドメイン層以外が修正されてもドメイン 層には何も影響がない。 何も影響がない状況を保ち続けるのが超重 要!!!
  24. インタフェースに依存させる • ポリモーフィズムを活用してクラス同士の抽象度を高く保つ • 抽象的であればある程、変更に柔軟なコードになる • 「依存関係逆転の原則」を利用すると依存関係は思いのまま 32 インフラ層 ドメイン層

    Repository Repository Impl Domain Service Repository Impl ドメイン層からインフラ層は 参照してはいけない インタフェースをドメイン層に置けば 依存関係を保ったまま参照できる
  25. まとめ • 広告配信システムの成立条件のお話をした ◦ 100msの制約を守ることは業務が成立する必要条件 ◦ 機能追加は業務を継続する必要条件 ◦ 多くの広告在庫を扱えることは業務が成立する十分条件 ◦

    大量のリクエストが捌けることは業務が成立する十分条件 • 以上の成立条件を満たすためにScala, DDD, Akkaによるアプローチを紹介した ◦ Scalaは「関数型言語」、「オブジェクト指向言語」、「JVM言語」の視点から ◦ DDDは「複雑なドメインに立ち向かう手段」として ◦ Akkaは「並行並列処理を安全に実装」するため • そして「性能面の具体的な工夫」として、マイクロアドで実践している内容を挙げた ◦ オンメモリ戦略 ◦ 整合性の担保 ◦ リアルタイムなデータはインメモリデータベース ◦ 本質でない処理を切り離す • 加速性を維持する方法ではDDDを実践していくコツを紹介した ※今回はしてない 33
  26. 参考文献 • Scalaスケーラブルプログラミング第3版 • Scala関数型デザイン&プログラミング―Scalazコントリビューターによる関 数型徹底ガイド • Clean Architecture 達人に学ぶソフトウェアの構造と設計 •

    実践ドメイン駆動設計 • Akka実践バイブル アクターモデルによる並行・分散システムの実現 • 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向 の実践技法 35