次の方法で共有



April 2011

Volume 26 Number 04

F# における MapReduce - F#、MapReduce、および Windows Azure によるログ ファイルの解析

Noah Gift | April 2011

長年 Python プログラマとしてすごしてきましたが、F# 言語のアーキテクトである Don Syme 氏のインタビューに興味を持ちました。Don はインタビューの中で、「構文的には異なりますが、[F#] を厳密に型指定される Python のようなものと考える人もいます」と述べていたため、これは詳しく調べてみる価値があると感じました。

F# は奇抜で刺激的な新しいプログラミング言語ですが、今のところ、多くの開発者にとってはまだベールに包まれた状態です。F# を使用すれば、ここ数年 Ruby や Python のプログラマが得てきた高い生産性というメリットを同じように得ることができます。F# は Ruby や Python と同様、構文を最小限に抑え、表記を簡潔にした高水準言語です。F# を他の言語とまったく異なるものにしているのは、こうした実利的な特徴を、高度な型推論システムや関数型プログラミング環境における多くの優れたアイデアと組み合わせているためです。また、F# のプログラマがあまりいないのもこのためです。

ただし、現在使用可能な興味深い新しいテクノロジは、この生産性の高い新しいプログラミング言語だけではありません。

Windows Azure などのクラウド プラットフォームが幅広く利用可能になったことで、1 人の開発者からエンタープライズクラスの企業まで、分散ストレージ リソースや分散コンピューティング リソースを使用できるようになりました。また、クラウド ストレージと共に、水平方向にスケーラブルな MapReduce アルゴリズムのように有益なツールも登場しています。MapReduce アルゴリズムを使用すると、巨大化する可能性のあるデータ セットの分析や並べ替えを迅速に行えるコードを簡単に作成できます。

このようなツールを利用すれば、数行のコードを記述してクラウドに配置するだけで、何ギガバイトものデータを操作する作業が午後の勤務時間内に終わります。驚くべきことです。

今回は、F#、Windows Azure、および MapReduce のすばらしい機能の一部を紹介したいと思います。これらすべての考え方を 1 つにまとめ、F# と MapReduce アルゴリズムを使用して Windows Azure のログ ファイルを解析する方法を説明します。最初に、MapReduce によるプログラミングを簡素化するプロトタイプ作成技法をいくつか紹介し、次にその結果をクラウドで使用します。

F# の概要

.NET プログラマが F# を使って作業する際に利用できる新しいスタイルの 1 つが、対話型ワークフローです。対話型ワークフローは、Perl、Python、および Ruby の多くのプログラマにとっては使い慣れたワークフローです。このプログラミング スタイルでは、多くの場合、Python シェル自体や、readline 補完を提供する IPython などのツールのような対話型コーディング環境を使用します。この環境では、開発者は、モジュールからクラスをインポートし、そのクラスのインスタンスを作成してから、Tab 補完を使用してオブジェクトのメソッドとデータを検出できます。

.NET 開発者にとって使いやすい対話型開発の一般的な方法は、Visual Studio でコードを記述し、F# Interactive ウィンドウにスニペットを送信して実行する方法です。現在テキストを選択している状態のスニペットは、Alt キーを押しながら Enter キーを押して送信します。1 行のスニペットは、Alt キー、Shift キー、および 7 キーを同時に押して送信します。図 1 は、この手法の実行中の例を示しています。

F# Interactive ウィンドウの使用

図 1 F# Interactive ウィンドウの使用

この方法は、F# プログラムを簡便にデバッグするのに役立ちます。IntelliSense と Tab 補完は、どちらも開発中の F# スクリプトで使用できます。

次に紹介する方法でも Visual Studio でコードを記述しますが、この場合、コードの記述後、コードのセクションを Visual Studio からコピーしてスタンドアロンの F# Interactive コンソールに直接貼り付けます (図 2 参照)。この手法を使用する場合、貼り付けたコードの後に忘れずにセミコロンを 2 つ追加する必要があります。そうすることで、コードと対話でき、追加のメリットとして Tab 補完も使用できるようになります。対話性の高い形式でのプログラミングに慣れると、意識しないでもこの手法を使用するようになります。

F# Interactive コンソール

図 2 F# Interactive コンソール

Windows PowerShell から直接コードを実行する、つまり、fsi.exe (F# Interactive コンソール実行可能ファイル) 自体にスクリプトを渡しても、F# を使った対話型開発を実行できます。この方法には、スクリプトのプロトタイプをすばやく作成し、標準出力に結果を出力できるというメリットがあります。また、Notepad++ などの簡易テキスト エディタを使用して、対話的にコードを編集できます。図 3 は、MapReduce スクリプトを Windows PowerShell で実行したときの、スクリプト出力の例を示しています。この例は、今回の記事全体で使用します。

図 3 Windows PowerShell での F# スクリプトの実行

PS C:\Users\Administrator\Desktop> & 'C:\Program Files (x86)\FSharp-2.0.0.0\bin\fsi.exe' mapreduce.fsscript
192.168.1.1, 11
192.168.1.2, 9
192.168.1.3, 8
192.168.1.4, 7
192.168.1.5, 6
192.168.1.6, 5
192.168.1.7, 5

このようにさまざまなコード記述方法のすべてが、複雑なアルゴリズム、ネットワーク プログラミング、およびクラウドを使用する際に役立ちます。簡便なプロトタイプを作成し、コマンド ラインから実行して、期待する結果が得られるかどうかを確認できます。その後、Visual Studio に戻って、より大きなプロジェクトのビルドに着手できます。

ここまでの情報を念頭に置いて、実際のコードを記述してみることにしましょう。

MapReduce スタイルのログ解析

前述の対話型プログラミングのメリットに加えて、F# コードは簡潔かつ高機能でもあります。図 4 の例は 50 行にも満たないコードしかありませんが、MapReduce アルゴリズムの重要な部分をすべて網羅しており、一連のログ ファイルに記録された IP アドレスを数の多いほうから 10 個計算しています。

図 4 ログ ファイルを解析する MapReduce アルゴリズム

open System.IO
open System.Collections.Generic

// Map Phase
let inputFile = @"web.log"
let mapLogFileIpAddr logFile =
  let fileReader logFile = 
    seq { use fileReader = new StreamReader(File.OpenRead(logFile))
      while not fileReader.EndOfStream do
        yield fileReader.ReadLine() }    

  // Takes lines and extracts IP Address Out, 
  // filter invalid lines out first
  let cutIp = 
    let line = fileReader inputFile 
    line
    |> Seq.filter (fun line -> not (line.StartsWith("#")))
    |> Seq.map (fun line -> line.Split [|' '|])
    |> Seq.map (fun line -> line.[8],1)
    |> Seq.toArray
  cutIp

// Reduce Phase
let ipMatches = mapLogFileIpAddr inputFile
let reduceFileIpAddr = 
  Array.fold
    (fun (acc : Map<string, int>) ((ipAddr, num) : string * int) ->
      if Map.containsKey ipAddr acc then
        let ipFreq = acc.[ipAddr]
        Map.add ipAddr (ipFreq + num) acc
      else
        Map.add ipAddr 1 acc)
    Map.empty
    ipMatches

// Display Top 10 Ip Addresses
let topIpAddressOutput reduceOutput = 
  let sortedResults = 
    reduceFileIpAddr
    |> Map.toSeq
    |> Seq.sortBy (fun (ip, ipFreq) -> -ipFreq) 
    |> Seq.take 10
  sortedResults
  |> Seq.iter(fun (ip, ipFreq) ->
    printfn "%s, %d" ip ipFreq);;

reduceFileIpAddr |> topIpAddressOutput

これはスタンドアロン バージョンで、map フェーズ、reduce フェーズ、display フェーズという 3 つのフェーズに分かれています。このスタンドアロン バージョンは、後からネットワーク バージョンにします。

フェーズ 1 は、map フェーズです。mapLogFileIpAddr 関数は、ログ ファイルをパラメーターとして受け取ります。この関数の内部では、fileReader という別の関数を定義しています。fileReader 関数は、関数型プログラミング技法を使用して、ログ ファイルから 1 行のテキストを遅延生成します (C# や Python などの言語にもこの機能があります)。次に、cutIp 関数は入力した各行を解析し、コメント行は破棄して、IP アドレスと整数 1 を返します。

この処理がなぜ "遅延" 生成であるかを確認するには、map コード ブロック全体を選択し、次のコード行と共に F# Interactive ウィンドウで実行します。

let ipMatches = mapLogFileIpAddr inputFile

実行後、次のように出力されます。

val ipMatches : seq<string * int>

この時点ではまだ何も処理が完了しておらず、ログ ファイルは読み取られていません。式の評価が行われただけです。式を評価した後、実際に実行が必要になるまで遅延します。このとき、式を評価するためだけにデータがメモリに読み込まれることはありません。これはデータ処理の優れた手法で、ギガバイトまたはテラバイト レベルの巨大なログ ファイルを解析するときは特に役立ちます。

違いを比較し、コードを詳しく評価するのであれば、単純に 1 行追加した以下の cutIp 関数を参照してください (注: 行 |> Seq.toArray は、関数の構造上は完全に省略できます。今回の例でのこの行の目的は、関数の処理を意図的に高速にすることです。この行を省略すると、mapLogFileIpAddr 関数のように処理が遅延されるようになります)。

let cutIp = 
  let line = fileReader inputFile 
  line
  |> Seq.filter (fun line -> not (line.StartsWith("#")))
  |> Seq.map (fun line -> line.Split [|' '|])
  |> Seq.map (fun line -> line.[8],1)
  |> Seq.toArray
cutIp

変更したコードを F# Interactive ウィンドウに送り直します。このとき、ログ ファイルに数ギガバイトの大量のデータが含まれている場合は、コンピューターがファイル全体を読み取り、キーと値のマッピングをメモリ内で生成するのにかなりの時間がかかるため、ここで休憩を取ることをお勧めします。

次のデータ パイプラインのセクションでは、マップ結果の出力を受け取り、結果を匿名関数にシーケンシャルに渡します。この匿名関数では、シーケンス内での IP アドレスの出現回数をカウントします。この処理では、再帰手法を使用して、結果を連続的に Map データ構造体に追加します。このプログラミング スタイルは、関数型プログラミングに慣れていない開発者にとっては理解しづらい可能性があるため、何が行われているかを正確に把握するには、匿名関数内部に print ステートメントを埋め込みます。

命令型プログラミングのスタイルで同じことを実現する場合は、各 IP アドレスをキーとして保持する変更可能な辞書を更新し、IP アドレスのシーケンスをループ処理して、各値のカウントを更新します。

最後のフェーズは MapReduce アルゴリズムとは無関係ですが、プロトタイプを作成する段階でスクリプトを調べるのに役立ちます。map フェーズの結果は、Map データ構造体から Seq データ構造体にパイプライン処理します。結果が並べ替えられ、数の多い順に 10 個の結果が出力されます。このデータのパイプライン処理のスタイルでは、for ループを使わずに、ある操作の結果を次の操作にシームレスに受け渡すことができます。

MapReduce と Windows Azure

50 行にも満たないコードで概念実証スクリプトを完成できたので、このスクリプトを運用環境相当の環境に移行します。例として、ここではデスクトップから Windows Azure に移行します。

下準備として、Windows Azure F# の例 (code.msdn.microsoft.com/fsharpazure、英語) を参照し、Windows Azure テンプレートをインストールしておくと役に立つことがあります。特に興味深いのは、F# ワーカー ロールが Blob ストレージ エンドポイントと Queue ストレージ エンドポイントの両方を使用する Web クローラーの例です。これは、Windows Azure と F# の併用をさらに詳しく調べる際に、確認するのに便利なプロジェクトになります。

ここでは、マルチノード MapReduce ファームのセットアップについては詳しく説明しません。代わりに、より高度な内容について説明します。詳細については、Josh Twist が執筆した MSDN マガジンの記事「Windows Azure での複数ノードの同期」(msdn.microsoft.com/ja-jp/magazine/gg309174.aspx) を参照してください。

Windows Azure に MapReduce ファームをセットアップする方法はいくつかあります。図 5 は、Map ワーカーと Reduce ワーカーとに均等に分けられる、F# ワーカー ロールを使用した例を示しています。スクリプトに戻りましょう。このセットアップは、map 関数をコピーして Map ワーカーに貼り付け、reduce 関数をコピーして Reduce ワーカーに貼り付けるだけの作業です。

Windows Azure の MapReduce ファーム

図 5 Windows Azure の MapReduce ファーム

分散アルゴリズムと実装例の詳細については、Jeff Dean 氏と Sanjay Ghemawat 氏による MapReduce のプレゼンテーション (labs.google.com/papers/mapreduce-osdi04-slides/、英語) を参照してください。ただし、図 5 の例では、複数のログ ファイルが F# ワーカー ロールによって並列に使用されることを示しています。F# ワーカー ロールは、値 1 を含む IP アドレスのキーで構成される出力データを、Windows Azure AppFabric Service Bus 経由、またはディスクに書き込むことによって Reduce ワーカーに返します。

次に、Reduce ワーカーがこの中間データを読み取り、キーと値のペアを合計したカウントを計算して Blob ストレージに書き込みます。各 Reduce ワーカーが独自の合計レポートを作成します。この各レポートを、Master ワーカーが並べ替えて表示する前に組み合わせる必要があります。

ワーカー ロールの作成と発行

プロトタイプが完成し、高度なアーキテクチャを計画したので、次の手順として Visual Studio 2010 で必要なプロジェクトを作成し、Windows Azure に発行します。

F# ワーカー ロールの作成は意外と複雑なので、必要な手順について見ていきましょう。まず、前述の Windows Azure F# テンプレートをダウンロードする必要があります。次に、Windows Azure 用の Visual C# プロジェクトを作成する必要があります。今回は AzureFSharpProject というプロジェクトを作成しました。

次に、F# ワーカー ロールを作成するオプションを指定します (図 6 参照)。

F# ワーカー ロールの作成

図 6 F# ワーカー ロールの作成

この時点で、Map ワーカー ロールに map 関数を配置したり、Reduce ワーカー ロールに reduce 関数を配置したりできます。その後、データを高速処理するニーズの程度に応じて、追加の Map ワーカーまたは Reduce ワーカーに対応する ワーカー ロールを作成します。正規のドキュメントについては、Google MapReduce に関する資料 (labs.google.com/papers/mapreduce.html、英語) を参照してください。この資料では、MapReduce アーキテクチャ、注意点、および使用例について詳しく説明しています。

Windows Azure に発行する準備ができたら、プロジェクトを右クリックし、[Publish] (発行) をクリックして、[Create Service Package Only] (サービス パッケージのみを作成する) をクリックします (図 7 参照)。

Windows Azure への発行

図 7 Windows Azure への発行

最後に、新しい Windows Azure 管理ポータルにサインインし、ワーカー ロールを作成するインターフェイスを使用します (図 8 参照)。

新しいワーカー ロールの構成

図 8 新しいワーカー ロールの構成

ここで、適切だと思われる方法で複数のノードを接続し、MapReduce を使用してクラウドのログを解析できます。もちろん、単純なログ ファイル以外のデータ ソースにもこれと同じ手法を簡単に当てはめることができます。この F# MapReduce アルゴリズムの概要 (および MapReduce アルゴリズムをコーディングする際に説明した対話型の手法) は、ほぼすべての解析、マッピング、および縮小ジョブに使用できます。

次のステップ

F# は高機能言語です。F# を使用すると、簡単な調査を次から次へと行い、それらの調査からさらに複雑なソリューションを構築することで問題を解決できます。今回は、F# を使用して MapReduce アルゴリズムをごく小さな単位に分割しました。それにより、ほんの 50 行の F# コードで Windows Azure ベースのログ分析機能を実現する例を示すことがきました。

Windows Azure での MapReduce の実装については、このテーマに関する他の 2 つの興味深い記事を参照することをお勧めします。1 つ目は、ワーカー ロールと MapReduce について説明している MSDN の記事「Windows Azure 向けにスケーラブルなマルチテナント アプリケーションを作成する」(msdn.microsoft.com/library/ff966483、英語) です。もう 1 つは、Juan G. Diaz 氏が投稿した「Comparison of the use of Amazons EC2 and Windows Azure, cloud computing and implementation of Map-Reduce」(Amazons EC2 と Windows Azure、クラウド コンピューティングと MapReduce の実装の比較、bit.ly/hBQFSt、英語) というブログ記事で、これも一読する価値があります。

まだ F# に注目していなかった方も、この記事がきっかけでお試しいただければさいわいです。また、F# に関心を持つきっかけとなった Don Syme 氏のインタビューをすべてお聞きになるには、simple-talk ブログ (bit.ly/eI74iO、英語) にアクセスしてください。

Noah Gift は、AT&T Interactive でエンジニアリングの副責任者を務めています。彼はカルポリ サンルイオビスポ大学で栄養学士号を取得し、カリフォルニア州立大学ロサンゼルス校でコンピューター情報システムの修士号を取得しました。また、ビジネス分析、経済学、および企業家精神学を専門とするカリフォルニア大学デービス校の MBA 候補生でもあります。

この記事のレビューに協力してくれた技術スタッフの Michael BakkemoDon Syme、および Paras Wadehra に心より感謝いたします。