Logrepo: データに基づく意思決定を可能なものへ―

(本文中*印 訳者補足)

イベントログの重要性

データに基づいた意思決定は、Indeed のカルチャーの中でも核となる部分です。私たちがアプリケーションやサービスの変更をテストする際には ―それがユーザーインターフェースであっても、バックエンドのアルゴリズムであっても― なされた変更がどのように求職者に影響を与えるかを測定する必要があります。たとえば、私たちがランキングのアルゴリズムの変更をテストする場合、検索結果の CTR を測定基準として用います。CTRを計算するには、どの求人情報が検索結果に表示され、その中でもどれがクリックされたか、ということを知る必要があるのです。

そこで、私たちはLogrepo(Log repository の省略)と呼ばれる追跡システムを構しました。このLogrepo は、Ineed のサイトで発生する興味深いと思われるイベントを全て追跡できる分散型のイベントログシステムです。Logrepo を用いることにより、世界中にあるIndeed のデータセンターからイベントをひとつの場所に集めて分析することが可能になりました。

まず、Logrepo がセントラルレポジトリにイベントを収集します。すると、目的別に構築された分析ツールが集められたイベントを処理し、実際の使用データに基づいた意思決定をするために必要な情報を私たちにもたらします。強力な分析ツールの例として、私たちが構築したRamses というシステムがあります。Ramses は、Logrepo から引っぱってきた時系列データ上に構築された複数のフィールドと多次元にまたがるインデックスをクエリーし、その結果を素早く視覚化してくれるものです。このツールによって、私たちは求職者がどのようにIndeed のウェブサイトを利用しているかという情報を取得し、さまざまな種類のデータセグメントでの指標を比較することができます。

図1内のRamses のグラフは、(*Indeedサイトへの)検索結果のクリック数を新規対既存の訪問者で比較するために米国外で行われた、A/B テストの結果を示しています。この図で示されていることは、Ramses で出来ることのほんの一部でしかありません。イベントログデータを収集するための信頼できる一貫したアプローチなしに、このRamses というツールは存在しえないのです。

example Ramses graph

図1:動的なイベントデータクエリーツールであるRamsesによるグラフの例

Ramses は強力なツールのひとつですが、Ramses だけがLogrepo のデータを使っているシステムということでは全くありません。私たちは、Logrepo のイベントデータを処理できる、数々のビジネス指標のダッシュボードを構築しました。それにより、与えられたデータの整理する際に自由に次元を選んで分析することができるのです。また私たちは、マップに落とし込んだLogrepo のデータを処理して、機会学習モデルを分析・構築します。この機械学習モデルによって検索結果のランキングやおすすめの求人などの重要な機能を動かすことができます。

ログの種類

私たちは特定のフィールドの組み合わせを持った500種類以上のイベントのログをとります。ログイベントには、ユーザーアクションからシステムのパフォーマンスの指標まであります。図2は、Logrepo で取得したイベントデータの一例です。各アプリケーションは選択したどのデータのログをとる事もできますが、殆どのイベントをログするときに使われる共通フィールドの組み合わせも存在します。

イベントの種類 内容 ログデータの例
all events
(全イベント)
ほぼ全てのログイベントに共通するデータ UID、タイプ、ユーザー追跡クッキー、
有効なA/Bテストグループ、
ユーザーエージェント、リファラなど
Jobsearch
(求人検索)
全ての検索結果ページが
ログされている
検索ワード(キーワード、勤務地)、国、結果件数、ページ数、ページ上の求人件数、異なるバックエンドサービスの利用時間、など他60項目以上
orgClk ユーザーが検索結果で
求人をクリックした場合
ログされている
ソースになる求人検索イベントのUID、ジョブID
resContactEmail 個人のIndeedレジュメを通して、雇用主が求職者に連絡を取った場合にログされている ソースになるレジュメ検索イベントのUID、
雇用主のID、EメールメッセージのID、
課金される金額、レジュメの対象国、
メッセージが送信またはブロックされたか
(後者の場合理由も)、など30項目以上
図2:ログイベントデータの例

ゴール

Logrepo をデザインする際に、Indeed ではいくつかのゴールを掲げました。私たちが重点的に取り組んだのは、意思決定のプロセスを支える分析ツールに対して、どのようにLogrepo のシステムが最高のサポートを提供することができるか、という点です。

また、ログイベントの紛失や重複を防止する必要があります。仮に軽度の紛失や重複があったとしても、ログデータは私たちにとってやはり有益なものです。しかしながら整合性の精度が低下すると、システムが本来正しく答えることのできるような質問の種類が限られてしまい、私たちの意思決定に影響が出てしまいます。このような制限が出てしまう事態を避けるために、ログイベントを正確に一度だけ配信してくれる仕組みが必要となります。

問題をさらに複雑にするのは、Logrepo はどんな時でも1つのLogrepo のサーバーに障害に耐え、整合性を損なわずかつきちんと利用できる状態である必要があるということです。
また、Logrepo は特定のイベントタイプに応じて、Ramses のような分析ツールが使用できるよう構文解析可能なフォーマットでデータの配信ができなければいけません。どのようなプログラム言語を用いてもそうしたツールを使えるように、Logrepo はとてもシンプルである必要があるのです。

なぜFlumeを使用しないか?

Indeed 独自のログ収集システムの構築は、今日利用可能なオープンソースオプションの数を考えると不必要に思われるかも知れません。そう言ったオープンソースオプションのなかでも、Flume がおそらく一番人気があるでしょう。Indeed では2007年より、Flume やその他のオプションに先駆けてLogrepo を本番環境で稼働しています。しかしながら、今日でもFlume は、Logrepo のような送信の保証や、整合性を持ち合わせていません。以下では、私たちのトランスポートの構造を説明する際に、この2つシステムの違いについて話したいと思います。

Flume にはいくつかの特筆すべき利点があります。その強みはモジュラー構造にあり、新しいソースやコレクタおよびシンク(HDFS 用シンクなど)を簡単に追加できます。一方、Logrepo のデータをHDFS に複製することは、システム本来の目標ではありませんでした。またHDFS 複製中に整合性を保持することについては、私たちがいまだ完全に解決できていない問題です。

もう一つのFlume の強みは、ログを新着順に格納するという点です。したがって、ある一定の時点までログのストリームを処理すると、その時点までのストリーム内のすべてのイベントが確実に処理されます。それに対してLogrepo で は、全てのアクセスはタイムスタンプ順になされます。Logrepo 使用時にはシステムの非同期的な性質ゆえに、ある期間内の全ログイベントがシステムに到着したことを確認するには一定時間待機しなければなりません。上記に述べたように、リアルタイムでの処理という利点よりも私たちは整合性を優先しています。当社の一番の目的はモニタリングではなく分析のため、数分の待ち時間は許容できるものなのです

システムのアーキテクチャ

Logrepo を理解するために、システムの5つの部分を考える必要があります。

  1. エントリー形式:イベントログエントリがどのように構成されるか
  2. アーカイブの構造:ディスク上でログエントリーがどのようにファイルとして整理されるか
  3. ビルダー:エントリーを収集し、アーカイブ構造に変換する処理
  4. トランスポート:サーバーとデータセンター間でログエントリーを移動する処理
  5. リーダー デーモン:イベントログエントリーデータを配信する処理

transport diagram

図3:イベントログデータのトランスポートおよび格納方法

エントリー形式

簡単に多目的の分析ツールを構築するために 、私たちは標準化されたエントリー形式を求めていました。そこで、よく知られたURL クエリー文字列形式を使用して、キーと値とをペアとして各エントリーをエンコードする方法を選択しました。この形式は人間にも比較的解読可能であり、エスケープを適切に扱えて、ツールサポートもきちんとしています。

全てのログエントリーには、「UID」と「タイプ」という2つの共通フィールドがあります。UIDフィールドはタイムスタンプを含む独自の識別子です。また、タイプフィールドは、エントリー内に含まれるデータを提供する特定のアクションとソースアプリケーションを指します。

uid=14us0t7sm064g2uc&type=orgClk&v=0&tk=14us0soeo064g2m3&jobId=8005d47e09b124f4&onclick=1&url=http%3A%2F%2Faq.indeed.com%2Frc%2Fclk&href=http%3A%2F%2Faq.indeed.com%2FPlant-Manager-jobs&agent=...

図4:ログファイルエントリーの一部

アーカイブ構造

ディスク上にデータを格納するために使用される構造は、効率的に重複データの排除や格納ができて、ログエントリーが読めるようになっていなければなりません。ログエントリーは、全てのディスクI/O が読み書きともに、シーケンシャルに格納されています。この構造は、Indeed のLogrepo にとってシーケンシャルアクセス(Indeed でメインに使われている)に対するスケール上の大きな利点をもたらします。 一方で、B+ ツリーなどで実装される一般的な多目的データストアには、単一エントリーへのランダムアクセス(Indeed ではメインで使われてはいませんが)のパフォーマンスがあまり良くないという欠点があるため、Indeed で採用されているデータ格納の構造のほうが勝っている私たちは考えいます。

セントラルレポジトリでは、ログエントリーはUID 順で(したがってタイムスタンプ順でも)振り分けられ、テキストファイルに1行に1件ずつ格納されます。各ファイルが保存される経路は、データセンター、イベントタイプ、およびログエントリーのタイムスタンプによって決定されます。経路を決定するのに使用されるタイムスタンプは、45 bit のmillisecond のタイムスタンプの上部25 bit です。これにより、各ファイルに残るタイムスタンプに20 bit を残すので、各ファイルは17分間ほどのタイムスパンで対応しています。

各プレフィックスに一致する複数のファイルが存在する可能性がありますが、エントリーがソートされているので読み込まれると簡単に統合できます。

logrepo archival structure diagram

図5:パスのコンポーネントのあるログファイルパスの例

dc1/orgClk/15mt/0.log4181.seg.gz:
     uid=15mt000000k1j548&type=orgClk&...
     uid=15mt000010k2j72n&type=orgClk&...
     uid=15mt000050k3j7bd&type=orgClk&...
     uid=15mt000060k2j6lu&type=orgClk&...
     uid=15mt0000b0k430co&type=orgClk&...
     uid=15mt0000d0k2i1ed&type=orgClk&...

図6:UIDとタイプを表示したログエントリーの一部例

ビルダー

ビルダーは生のログエントリーデータをアーカイブ構造に変換します。ビルダーは新しいかも知れないログエントリーを取り出します。そしてそれらが新しいか判断し、その場合はログエントリーを追加します。ログは時間でソートされた順にアクセスされるので、ビルダーはアウトプットもソートする必要があります。パフォーマンスを維持するために、ビルダーによって行われる全てのディスク IO がシーケンシャルであることが重要です。

Logrepo のビルダーは受信したログエントリーをバッファし、ファイルに書き込みます。ビルダーは初めにタイムスタンプの初めの25bit に基づいてログエントリーを適切なバケットに振り分けます。エントリーを振り分け、バケットの1つがバッファサイズの制限値に達するか、もしくは書き出し時間の間隔が最大待機時間を超えると、ファイルにデータを書き出します。もしビルダーがバケットの既存のファイルを検出すると、エントリーを比較して重複したものを飛ばし、バッファおよびファイルで統合します。バケット内にファイルの数が多すぎる場合、将来の統合でかかる負荷を減らすため、1つの大きなファイルに統合される可能性があります。

バッファリングはログエントリーがリーダーで利用可能になるまで、遅れを生じさせます。スループットを制限するような多数のコピーと統合を避けるために、書き出す前に毎回相当量のデータをバッファしています。現在では、上記のアーカイブ構造で決定されるタイムスパンに対応するため、17分間隔で書き出しています。つまり、17分の間隔で全てのログが到着した事をきちんと確かめるために、私たちは34分間待機していると言う事です。

分析用のデータが利用可能になるまで、1、2時間の遅れを待つのは構いません。

リアルタイムのモニタリングには、レポジトリのアーカイブをバイパスする異なるストリームから同じログエントリーを使用しています。その場合は整合性を引き替えに、レイテンシを小さくしています。

トランスポート

トランスポートの役割は、ログメッセージをアプリケーションサーバーから各データセンター内のLogrepo サーバーを介して移動し、最終的にはセントラルログレポ ジトリーに移動することです。

各データセンターは2つのLogrepo サーバーを含み、その2つどちらもが

フォールトトレランスを高めるために、それぞれのデータセンター内で全てのログエントリーを受信します。私たちは、アプリケーションサーバーから両方のLogrepo サーバーにログエントリーを送信するために、syslog-ng を使用しています。全ログエントリーの3番目のコピーは、追加のフォールトトレランスのために、アプリケーションサーバーのローカルドライブに格納されます。Logrepo サーバーの1つは、プライマリとして指定され、Logrepo ビルダーを実行します。Logrepo ビルダーを通して、生のログがsyslog-ng からアーカイブ形式に変換されます。プライマリはそれからrsyncを使用して、アーカイブ内のファイルをセントラルログレポジトリーにプッシュします。一定の遅れの後で、プライマリは生のログをバックアップのLogrepo サーバーからコピーし、アーカイブ内で検出されないログエントリーを追加することで、欠損しているエントリーを確認します。UID フィールドの一意性によって、このプロセスが重複を含まないことが保障されます。

2つのサーバーへのログメッセージを重複して送信することにより、プライマリサーバーからセカンダリサーバーへのフェイルオーバーは常時可能になります。常に複製を作り、それから重複排除を行うことで、有用性や整合性を失うことなく、総合的なマシンの障害にも耐えうることができるのです。

対照的に、Flume は配信承認を通じて整合性の確保をします。メッセージが下りに送信(承認)されると、上りサーバーも応答します。もし下りサーバーに障害が発生した場合、メッセージはサーバーが復旧するまで無期限にバファリングしてしまいます。障害が復旧不可の場合、障害時に応答を受けていないメッセージが下りにコミットされているかどうかという点で、整合性の問題が発生してくるのです。Flume がスペアコレクタにフェイルオーバーする場合、スペアコレクタはどのメッセージがコミットされたのかという情報を持たないため、同様の問題が発生します。どちらにせよ、上りのFlume サーバーはメッセージを再送信するかどうかを決定しなければなりません。再送信する場合にはメッセージが複製される可能性があり、再送信しない場合には失われる可能性があります。

リーダー・デーモン

私たちは、Logrepo に格納されたイベントに、セントラルLogrepo サーバー

またはその複製上で実行されるリーダー・デーモンを通してアクセスします。サーバーは、タイプごと、および時間の範囲ごとでのログエントリーのクエリーに対応しています。ここに、Indeed で使用しているアーカイブ構造のメリットを見出すことができます。

クエリーの結果を出す場合、サーバーはそのタイプのすべてのファイル、および開始のタイムスタンプの25 ビットのプリフィックスを検出し、クライアントにログエントリーを返します。開始時間よりも小さいタイムスタンプを持つイベントを全てスキップし、ストリームしながらファイルからのイベントを統合します。このプロセスは、終了時のタイムスタンプがバケットに含まれるまで、その後に続く時間に基づいた各バケットに対して繰り返されます。終了時のタイムスタンプを超えたログエントリーを見つけると、終了したことを認識し、ストリームを終了します。

リーダーは一定の時間の範囲から単一のタイプのイベントを取得するために、遅延が少なくなるように最適化されます。私たちは、異なるタイプのインターリーブしたイベントに対しては最適化をしませんでした。また、典型的な分析リクエストは何百万、何十億のイベントを含んでいるので、UID ごとに単一のイベントを探せるようには最適化する必要もありませんでした。このシンプルな取得方法への最適化は、ログエントリーデータのストリームをアドホック分析のために作られたデータ構造へ処理する分析ツールを開発する上で、私たちにとってとても有益でした。

リーダー・デーモンのインターフェースは非常にシンプルです。イベントログデータを取得するために、TCPソケット上のデーモンに接続し、開始時間、終了時間のペア(エポックミリ秒)とイベントタイプからなる1行の文字列を送信します。合致したデータはすぐストリームで返されます。生のログデータをインタラクティブに見てみると、私たちは単純にUnix コマンドnc (netcat) をしばしば以下のように使います。

~$ date --date "2012-04-01 05:24:00" +%s
1333275840
~$ date --date "2012-04-01 05:25:00" +%s
1333275900
~$ echo "1333275840000 1333275900000 orgClk" | nc logrepo 9999 | head -2
uid=16pmmtjgh14632ij&type=orgClk&v=0&tk=16pmmsulc146325g&jobId=11eaf231341a048f&onclick=1&url=http%3A%2F%2Fwww.indeed.co.uk%2Frc%2Fclk&href=...
uid=16pmmtjh2183j353&type=orgClk&v=0&tk=16pmmntjr183j1ti&jobId=029b8ec3ddeea5ae&onclick=1&url=http%3A%2F%2Fwww.indeed.com%2Frc%2Fclk&href=...

図7 Logrepoリーダー・デーモンのクエリーに対するnetcatの使用

リーダー・デーモンのこのシンプルなインターフェースは、どんなプログラミング言語でもLogrepo データを処理するのを簡単にします。Python実装の核新となる部分は、13 行のコードなのです。

データを利用した意思決定

Indeed では1日あたり、10から20億のLogrepo イベントをキャプチャしています。私たちはそれらのイベントをRamses(今後のブログ記事で掘り下げていく予定です)のような様々な強力な分析ツールに配信します。私たちは、これらツールを使用して、どのように私たちのサービスを改善し、求職者にも雇用者にもより良いサービスを提供するかを判断しているのです。

過去5年間、Logrepo はIndeed に重要な価値をもたらしました。Logrepo は、Indeed がデータに基づいた意思決定を行う際の礎です。Indeed の製品は高い信頼性と柔軟性を備えたスケーラブルな構造のおかげで、その性能を飛躍的に高めました。向こう5年この状態を保つために、私たちは、信頼性・柔軟性・スケーラビリティを更に向上させる必要があります。

このような問題解決に挑みたい!と思った方は、Indeed の採用案内を是非ご覧ください。