『じゃらん』『ホットペッパー
グルメ』を支えるクラウド・
データ基盤
目次
● 弊社事業の紹介
● Capture EveryThing (CET) の取り組み
● API による分析結果の提供
● リアルタイム処理システム
● Wrap Up
自己紹介
● 堀澤健太
○ Twitter : @horiken4
● 略歴
○ 2011 年 スマートフォン向けゲーム開発会社
■ 開発, マネジメント, 採用, R&D
○ 2016 年 株式会社 リクルート ライフスタイル
■ CET アプリケーションエンジニア リーダー
弊社事業の紹介
リクルートのビジネスモデル
<価値提供>
人生や生活の中で意
思決定においてその
人が必要とする情報
を提供
<価値提供>
ユーザとの出会いの
機会や最終的なマッ
チングを創出
リクルートライフスタイルのサービス概要
Capture 
EveryThing
の取り組み
Capture EveryThing
● サービス横断でリアルタイムにデータを収集
○ 約 300GB /日
● リアルタイムデータ分析に必要な処理を
一気通貫で実施
● 分析結果を WebAPI として各サービスへ提供
チーム体制
● エンジニア, データサイエンティスト, ビジネス系メンバで構
成
● チームを跨がないため高速なイテレーションが可能
● スキルのオーバーラップ
● 専門領域以外も担当
○ データサイエンティストがインフラ構築
○ エンジニアが分析
アーキテクチャ
● 黎明期は AWS を利用
● 現在は GCP と AWS のマルチクラウド構成
○ GCP の割合が大きい
● 同等の性能でコストを安く抑えることが可能
○ GCP を利用し始めたきっかけ
AWS
Log Aggregator
Visualize (short period)
Web Beacon
Real-time Processing
Our Service
Visualize (long period) API
Machine Learning
アーキテクチャ
Re:dash
Rundeck
Kibana
elastic
search
nginx
fluentd
API による
分析結果の提供
API
● 分析結果を API として各サービスに提供
○ 機械学習による予測の結果
○ レコメンデーション
● 基本的に Key に対する Value を返せば十分
○ リクエストのたびに予測はしない
API
US Region
Our Service
Asia Region
アーキテクチャ
Cloud Load
Balancing
API
Container Engine
Asia
API
Container Engine
US
Cloud
Bigtable
API Data Loader
Worker
Compute Engine
Real-time Processing
Pipeline
Cloud Dataflow
API Data Loader
● API で提供するデータを Bigtable へ反映
○ S3 へのファイル配置イベントを SQS へ流す
○ Worker が Receive して Bigtable へ書き込む
API Data Loader の利用ケース
● 他チームの API 利用者
● 各種バッチ
● 最大数千万行の Bigtable への Put オペレーション
API サーバ
● Scalatra ベース
● GET のみ
○ リクエストパスに Key を含める
○ Key で Bigtable を引き Value を JSON で返す
API サーバ
● 列ファミリの TTL は厳密ではない
○ Bigtable の GC のタイミングで実際に削除
○ Cell の Timestamp を参照し Expire を管理
Pod の終了時
● Pod が SIGTERM を受信
● API サーバが(デフォルト)で 30 秒後に終了
Graceful Shutdown
● Pod の readinessProve と
terminationGracePeriodSeconds を設定
● SIGTERM 受信後 readinessProveに対して 500 を返す
○ readinessProve が失敗する
● 新規リクエストはルーティングされなくなる
● terminationGracePeriodSeconds 秒後に SIGKILL
● Pod が終了
Graceful Shutdown
● CMD を bash -c の exec 形式にする
○ 環境変数を展開して引数に渡すことが可能
○ シグナルを子プロセスへ伝搬
GKE と GCE を組み合わせて利用
● GKE マルチゾーン クラスタ
● Instance Group のオートスケーラ
● Instance Group のオートヒーラ
Pod 数は固定
● HPA で Pod が増減するタイミングは必ずしも
CPU 使用率が増加するタイミングではない
● リソース不足でスケジューリングに失敗する
Pod が発生
● HPA は固定し必要な Node を用意
理想のスケールアウト
● 理想は Pod 数が増減したらノード数が増減
● リソース不足による Pod のスケジューリングが失敗
○ その時に Node Pool がスケール
GKE Cluster Autoscaler
● リソース不足によりスケジューリングを待たされている
Pod を監視
● そのような Pod が発生した場合 Node Pool を大きく
● Node のリソースが十分使用されてなければ小さく
オートヒーリング
● http ヘルスチェックと Instance Group の
Auto Healing を設定
● kube-proxy により適切な Pod へルーティング
○ 実際に Unhealthy なのは別 VM かもしれない
○ Pod が Unhealthy なだけかもしれない
Node Auto-Repair
● Node のヘルスステータスをチェック
● 連続してなにも報告しない or NotReady だと再作成
負荷試験
● 性能劣化, ボトルネックの確認
● 2つの GKE クラスタを作成
○ Loader と API
○ Preemptible VM を利用
● GKE クラスタ, k8s リソースの作成, 削除が軽快
○ CI にも組み込みやすい
Loader と API
● Locust で負荷試験シナリオを作成
● Job で Loader の Pod をデプロイ
○ Pod の再作成を防ぐ
■ locust … || exit 0
● Deployment で API の Pod をデプロイ
○ Service は type: LoadBalancer
複数の負荷試験
● CI では同時に複数の負荷試験をしたい
○ API の Service の externalIPs は固定しない
○ 動的に API の IP アドレスを変更したい
● 変更可能にするため sed を挟む
■ cat job.yml | sed -e
“s/__IP__/${IP}/g” | kubectl
create -f -
ボトルネックの調査
● VisualVM を利用
○ JMX のポートに接続する必要あり
○ Service を作成する必要なし
● kubectl port-forward <pod-name> <jmx-port>
○ VisualVM で localhost:<jmx-port> に接続
リアルタイム
処理システム
概要
● サービス側からのリアルタイムなログを集計
○ 約1万 msg/s
● リアルタイムに結果を API に反映
ユースケース
● ページ閲覧 UU 数集計
● 最新予約日時抽出
● リアルタイムユーザ属性推定
○ 実サービスへの導入は未実施
Real-time processing
Our Service
アーキテクチャ
Pipeline
Cloud Dataflow
Cloud
Pub/Sub
Temporary Table
Cloud Bigtable
API Table
Cloud Bigtable
Model
Cloud Storage
Log
Aggregator
ページ閲覧 UU 数の集計
● 30 秒ごとに 30 分間の UU 数をページごとに集計
● ウィンドウが重複するのでスライディングタイムウィンドウ
を利用
SlidingWindows の問題点
● SDK 標準のスライディングタイムウィンドウ実装
● 30 分未満のウィンドウを排出してしまう
30 sec
30 min
・・・
NonMergingWindowFn を
継承し assignWindows を
Override する
ウィンドウの開始時刻<パ
イプライン開始時刻なウィ
ンドウには要素をアサイン
しない
for (long start = lastStart;
start > timestamp.minus(size).getMillis();
start -= period.getMillis()) {
if (start <
firstWindowStartBoundary.getMillis()) {
break;
}
windows.add(
new IntervalWindow(new Instant(start),
size));
}
初期のウィンドウを破棄
Bigtable の Write 変換
● SDK 標準の CloudBigtableIO
● 内部では ParDo 変換で BufferedMutator を利用
○ 並列で Bigtable へ書込み
Bigtable の Write 変換
● 書込みスループットが大きすぎる
○ Bigtable クラスタの理論限界値 50MB/sec
○ パイプライン 25MB/sec
● 複数のパイプラインを同時実行できない
Combine 変換を利用
extractOutput はシングル
スレッド
シンクなので null を返す
public Void
extractOutput(List<Mutation> accum) {
BufferedMutator mutator
= getBufferedMutator()
mutator.mutate(accum);
mutator.flush();
return null;
}
Write 変換のスロットリング
最新予約日時の抽出
● 予約ログを Pull するたびに予約日時をパース, Put オペ
レーションを実行
● 集計する必要はないのでグローバルウィンドウを利用
最新予約日時の抽出
● 予約ログの到達順序はログの発生時刻順ではない
○ Subscription からの Pull 順序は保証されない
○ ログの遅延によって古い予約ログが最近到達するか
もしれない
予約時刻を Put オペレー
ションのタイムスタンプに設
定
タイムスタンプが新しい場
合にのみ Cell 更新
public void processElement(ProcessContext c) throws
Exception {
KV<String, DateTime> e = c.element();
Put put =
new Put(Bytes.toBytes(e.getKey()),
e.getValue().getMillis())
.addColumn(
BIGTABLE_COLUMN_FAMILY,
BIGTABLE_COLUMN_QUALIFIER,
Bytes.toBytes(""" +
e.getValue().toString(DateTimeFormat
.forPattern("yyyy-MM-dd'T'HH:mm:ssZZ")
.withZone(DateTimeZone.forID("Asia/Tokyo")))
+ """));
c.output(put);
}
古い時刻での更新を防ぐ
ユーザ属性推定
● 直近数件のページ閲覧ログと検索ログから推定
● アイテムの特徴量の平均値が推定値
○ 推定処理は ParDo で実行
○ 直近数件のログを保持するため Bigtable を利用
ユーザ属性推定
● 書き込みのバッファリングが効くように固定時間ウィンドウ
を採用
○ ウィンドウごとに BufferedMutator で書込み
● 特徴量は更新を容易にするため GCS に配置
○ 有限 PCollection
○ 推定の ParDo へ副入力
複数の検収環境
● 弊社サービスには複数の検収環境が存在
● 検収環境ごとに異なる Topic にログを Publish
● 複数の Subscription から Read 変換し
無限 PCollection を作成
Flatten 後の副入力あり ParDo 変換
● エラーが発生してパイプラインが実行されない
○ GetData failed: status: APPLICATION_ERROR(3):
Computation F64does not have state family S1 for
value read
変換の順番を入れ替える
● 副入力 ParDo と Flatten の順番を入れ替えて回避
○ 複合変換は PCollectionList を受け取り
PCollectionList を返す
○ Put オペレーションを作成する ParDo 直後で Flatten
パイプラインの自動スケーリング
● スケールする時にパイプラインが一時的に詰まる
○ Unacknowledged Messages が増加
● 自動スケーリングは無効化
パイプラインの監視
● Unacknowledged Messages の監視
● Bigtable への書込み回数を監視
○ 書き込み時にログを出力
○ Stackdriver logging からログベース指標を作成
● Alerting Policy で Slack へ通知
Wrap Up
Wrap Up
● CETの取り組み
● API
○ GKE, Kubernetes, Bigtable
● リアルタイム処理
○ Pub/Sub, Dataflow
Thank You.

『じゃらん』『ホットペッパーグルメ』を支えるクラウド・データ基盤