More Related Content More from MicroAd, Inc.(Engineer) (20) Scala、DDD、Akkaで立ち向かう 〜広告配信システムに課せられた100msの制約〜3. 自己紹介
● 名前: 松宮 康二(まっつー)
● 最近触ってるもの
● 最近の趣味
○ ラーメン作り
● Twitter
○ @mattsu6666
3
4. もくじ
● 広告配信システムの宿命 (5分)
● なぜScala, DDD, Akkaなのか (14分)
● 性能面の具体的な工夫 (10分)
● 可読性を維持する方法 ←スライド作ってから35分の発表では収まらない事に気付いた
● まとめ (1分)
4
割とボリュームが多くなってしまったので
駆け足気味で進みますm(__)m
8. 増え続ける広告在庫
● ビジネスのスケールとともに扱う広告在庫は増大 ※広告在庫とは広告の表示可能回数のこと
● 1度のRTBで出来るだけ多くの広告を配信対象にしたい
● 多くの広告在庫を扱える = より精度の高い広告を配信可能 = 高い広告効果
○ また、たくさん表示可能なため単純に売上が伸びる
● 一方で扱う広告の増大は処理負荷の増大に直結
● 少なくても業務は成り立つけど、スケールしない
8
多くの広告在庫を扱えることは業務が成立する十分条件
SSP DSP
入札要求
応札
化粧品関連のページから
リクエスト
うーん・・・とりあえず健
康関連? DSP 化粧品が最適!(略)
入札要求
応札
DSP(略)
入札要求
応札
(タイムアウト)
化粧品
関連
健康
関連
旅行
関連
広告在庫
健康
関連
旅行
関連
広告在庫
旅行
関連
健康
関連
化粧品
関連
広告在庫
・・・
お、おおすぎる!
処理が間に合わねぇ
広告在庫が少ないと・・・ 広告在庫が多いと 広告在庫が多過ぎると
処理が間に合わず機会損失が発生するのは
もったいない!
こうならないようにエンジニアが頑張る
9. 増え続けるリクエスト
● ビジネスのスケールとともに受けるリクエストは増大
● 出来る限り多くのリクエストを受けたい
● 大量のリクエスト = 入札機会の増大 = 売上アップ
● 大量のリクエストを捌きたいなら・・・
○ サーバを増やす
○ サーバのスペックを上げる
● しかし・・
○ サーバは高い(1台辺り10~20万)
○ ラックに限りがある
○ 1台辺りの収益率が落ちるのは本末転倒
● よって、富豪的アプローチで万事解決とは行かない
● ハードに頼り切るのではなく、ソフトウェアも限界までチューニング
9
大量のリクエストが捌けることは業務が成立する十分条件
日毎のリクエスト数 ※軸の値は伏せてます
増加傾向にある
13. 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文を使わずにループ処理)
14. なぜScalaなのか
● 関数型言語の特徴を持っており、シンプルに記述できる
○ イミュータブル
○ 副作用を発生させづらい構文(例えばif文ではなくif式)
○ nullを発生させないデータ構造
● オブジェクト指向言語の特徴を持っており、DDDとの相性が良い
○ ポリモーフィズム(最も重要!) ※後述
○ カプセル化
○ 継承
● JVM言語の安心感
○ 十分な実績を持つJavaとの互換性
○ JVM上で動作するため、Javaに近い性能を期待できる
○ コンパイル言語なので、型安全による安心感
14
15. 関数型言語はなぜシンプルなのか
● 関数型プログラミングは純粋関数だけを使う
○ 純粋関数とは副作用がない関数
● 副作用とは関数内で副次的に発生するアクション
○ グローバル変数に再代入する
○ データベースを参照・更新する
○ 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を新規作成」
17. DDD
● DDD(ドメイン駆動設計)はソフトウェア設計手法のこと
○ 技術では無く、ドメイン知識(業務知識)を中心に考える
● 事業価値を最大化することが我々の究極の目標
● しかし実際には...
○ ドメインエキスパート(PDMなどドメインに詳しい人)は事業価値の向上に注力
○ エンジニアは業務上の問題を技術的に解決しようとする
○ エンジニアによって翻訳されたソフトウェアになってしまう
○ 両者のコミュニケーションの相違が起こってしまう
● DDDはドメインエキスパートとエンジニアが共通認識(ユビキタス言語)を
持ったソフトウェアを作る(コードがドメインモデルを反映)
17
(・ω・*) PDM
「広告」には「画像」とか「動画」とかの種類があるんだよ
なぁ。
「広告主」がどっちか選んで「 登録」するんだよねぇ
エンジニア (,,゚Д゚)
画像とか動画とかいってもフォーマット色々あるし、「 JPEG
広告」「PNG広告」「MP4広告」みたいな感じで実装すっか。
生成ロジックを統一したいし、「 ユーザ」が「広告ジェネレー
タ」を使って「生成」するようにしよっと。
下の会話では両者が違う言葉を使っている。
この状況が続くと、両者の認識のズレは拡大し、いつかプロ
グラムの修正が困難な状況に陥る可能性が高い
18. ドメインモデルをピュアにすべきな理由
● ドメインモデルは何にも依存させるべきではない(ピュアにすべき)
● さらに、ビジネスルールは技術的関心事に左右されるべきではない
● ビジネスルールとはドメインモデルの構成要素
○ "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
21. アクターモデル
● アクターはメッセージ駆動の計算実体(スレッドに似ている)
○ アクターは非同期に実行される
● アクター同士(数千・数百万)が協調して1つのアプリケーションを構成する
● アトミックやロックを使わず、安全に並行並列処理が実装できる
○ アクターはメールボックス(メッセージキューみたいな)を持っていて、1度に1つ処理
● アクターはfire-and-forget(撃ちっぱなし)の性質によって、非同期処理を実現している
● スケーラビリティのためにアクター同士は疎結合になっている
○ 空間/位置: 位置透過性とも。同じノード、異なるノード等、どこにアクターがいても良い
○ 時間: アクターはタスクの完了について何も保証しないし、期待もしない
○ インターフェース: アクター同士は何も共有しない。インタフェースが正しいかとか関係ない
21
Actor
Actor
Actor
メッセージ送信
メッセージ送信
メッセージ送信
アクターシステム
メールボックス
(メッセージキュー)
アクターは非同期で動作するのでア
クターの数だけ並行並列に動作可
能
ロックとかアトミックな操作は一切不
要
メッセージを受信し
て何か処理
22. スループットではなくレイテンシを高める
● アクターモデルによって、並行並列処理が安全に実装可能
● スレッドでは実現出来なかったスケールアウトも可能
● アクターによって処理を細かい単位で分離するので、局所的にスケール可能
● よって、Akkaを採用することにした
● しかし・・・良いことばかりではない
○ 高い学習コスト
○ アプリケーションの性質上、コアなロジックはアクターによるスケールアウトが難しい(通信コストが無視出来ない)
○ まだ発展途上のOSSのため、バージョン上がるとガッツリAPIが変わってる
● スループットは変わらないが、レイテンシは高めたい(アクターモデルの恩恵を享受)
○ 全体をそれなりに早くではなく、1リクエスト辺りの応答速度を高めたい
○ 大量のリクエストを満遍なくタイムアウトさせるより、少量のリクエストを確実に返せた方が良い
○ そのためにはアクターモデルを活用した並行並列処理が使いやすい
22
リクエスト1の処理
リクエスト2の処理
リクエスト1の処
理
リクエスト1の処
理
リクエスト2の処
理
リクエスト2の処
理
スレッド1
スレッド2
200ms
スレッド1
スレッド2
200ms
200msでどちらも2件のリクエストを
捌いた。
しかし、左は200msかかってしまっ
てタイムアウトになった
一方で右は100msで応答したので
入札できた
広告配信システムでは100msの制
約があるのでタイムアウト!
超理想の話なので実際にはこんな
綺麗に並列化は難しい
23. なぜScala, DDD, Akkaなのか
● Scala
○ オブジェクト指向、関数型のメリットを享受できる
○ JVM上で動作するので性能面での安心感
○ 強力な型システムによって規模の拡大が原因でメンテナビリティが低下しづらい等
● DDD
○ 複雑なドメインに立ち向かう手段
○ ビジネスルールに集中でき、事業価値を高めやすい
○ Scalaと相性が良い(オブジェクト指向との相性が良い)
● Akka
○ 並行並列処理、分散処理を安全に実装できる
○ 様々な便利なコンポーネントが使える
23
25. オンメモリ戦略
● アプリケーションのボトルネックの大部分はI/O
○ 通信コスト
○ データを検索するコスト
● 載せれるデータはローカルのメモリ上に持てるだけ持つ
○ いわゆるオンメモリ戦略
● オンメモリ化の条件
○ ある程度の更新間隔を許容(数分~数十分は古くても良い)
○ 件数が膨大過ぎない(メモリに載る容量か)
○ 揮発しても良いデータ(どこかで永続化されてる必要がある)
● 以上の条件を満たせばメモリに載せる
● 単純な戦略だが最も効果的
● ただし、メモリには限界があるので無敵ではない
25
アプリ DB
キャッシュ
定期的にDBのデータをキャッシュ
アプリ DB
毎回DBに問い合わせるので遅い
常に最新のデータを取得できるが、
本当にその必要がある?
速度を犠牲にしてまで最新データにこだわるべき
か?
最新のデータは使えないが、ローカルのメモリに
キャッシュするので高速。
ただし、データの容量等の様々な制限はある
引用: https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html
メインメモリの参照は100ns
SSDのランダムリードは16μs ≒ 16000ns
単純計算で160倍の速度差
さらに、ネットワークのレイテンシを考えればオン
メモリ化が如何に効果的かわかる
26. 整合性の担保
● オンメモリ戦略を使った場合の考慮点として整合性がある
● いくら古いデータを許容できるといっても、過去の異なる時点のデータを参照するのは不味い
● そこで、IOモナドを利用してデータの取得と更新タイミングを分離(更新だけ遅延させる)
○ IOモナドって何・・? => 関数合成出来て、遅延評価が可能な性質が重要。厳密な定義は知りません。
● DBへの問い合わせを先にやり、全てのデータが揃った時点で、一括でオンメモリのデータを差し替える
● これによってほぼ整合性を担保できる(厳密にはDBの問い合わせタイミングが若干ズレるがそこは許容)
● 万一DBの問い合わせが一部失敗した場合は、全てロールバックする
● ただし、一時的に使用するメモリが本来保持するデータ量の2倍になる
26
アプリ
DB A
新キャッシュ
DB B
DB C
キャッシュ
アプリ
新キャッシュ
キャッシュ各種DBからデータを取得していく
が、全てのデータが揃うまで、既
存のキャッシュは更新しない
全てのデータが揃ったタイミングで既存のキャッ
シュを新キャッシュで置き換える
(参照が切り替わるだけ)
またキャッシュ参照時にはリエントラントロックを
かける
27. リアルタイムなデータはインメモリデータベース
● オンメモリ戦略を適用できないデータはどうするか?
○ リアルタイム性が求められるデータ
○ ローカルのメモリには乗り切らないデータ
● リモートホストのインメモリデータベースにキャッシュする
○ 通信は発生するがデータは高速で読める
○ マイクロアドでは主に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型
キーを単純にしたことで検索性能が改善
28. 本質でない処理を切り離す
● 広告配信システムの本領は入札要求に対して入札すること
○ しかし、それ以外にも色々やっている(ログの書き出し, DBの更新, 他サービスとの連携等)
● 例えば、入札は出来る限り高速に応答したいが、ログの書き出しは多少遅くても良い
● 本質でない処理にCPUのリソースを割くのは勿体ない。そこでアクターモデルを活用
○ fire-and-forgetの性質によって応答を待つ必要はない
○ アクターは位置透過性なのでログの書き出しはリモートホストで処理してもいい
● ただし、メッセージが到達する信頼性は「at-most-once」
○ つまり、メッセージは喪失する可能性があるので重要なメッセージは送れない
○ at-least-onceを保証する方法も一応可能。ただアクターモデルの旨味が消える
28
RTB処理はログ記録の応答を一切待たないの
でログ記録の影響を受けない
ログ記録がリモートホストに存在する場合、ログ
は喪失する可能性がある
ログの重要度によって、対応方法を変える必要
がある
←ログが消滅!!!
重要なログだったらヤバイ・・・
31. レイヤー構造に基づいたクラス設計
● レイヤーの構成方法は三者三様
○ レイヤードアーキテクチャ, クリーンアーキテクチャ, オニオンアーキテクチャ, ヘキサゴナルアーキテクチャ, 3層 + ドメインモ
デル等
○ 色々あるし、独自にカスタマイズしても問題無い
● 一番重要なのはドメインモデルをピュアにすること
● そして、レイヤーの依存関係をないがしろにしないこと
○ ScalaならsbtのdependsOnを利用するとコンパイラレベルで強制可能
● レイヤー構成を定めると修正箇所を局所化できる
● また、設計の指針になるため、人によって実装方法がブレ辛い
31
プレゼンテーション層
データソース層
アプリケーション層 ドメイン層
HTTPやRPCなどの入力の
インタフェースを担当
ユースケースを担当
(ビジネスロジック)
DB接続周り担当
ビジネスルール担当
(ドメインモデル)
ドメイン層は誰にも依存していない。
つまり、ドメイン層以外が修正されてもドメイン
層には何も影響がない。
何も影響がない状況を保ち続けるのが超重
要!!!
33. まとめ
● 広告配信システムの成立条件のお話をした
○ 100msの制約を守ることは業務が成立する必要条件
○ 機能追加は業務を継続する必要条件
○ 多くの広告在庫を扱えることは業務が成立する十分条件
○ 大量のリクエストが捌けることは業務が成立する十分条件
● 以上の成立条件を満たすためにScala, DDD, Akkaによるアプローチを紹介した
○ Scalaは「関数型言語」、「オブジェクト指向言語」、「JVM言語」の視点から
○ DDDは「複雑なドメインに立ち向かう手段」として
○ Akkaは「並行並列処理を安全に実装」するため
● そして「性能面の具体的な工夫」として、マイクロアドで実践している内容を挙げた
○ オンメモリ戦略
○ 整合性の担保
○ リアルタイムなデータはインメモリデータベース
○ 本質でない処理を切り離す
● 加速性を維持する方法ではDDDを実践していくコツを紹介した ※今回はしてない
33