2009年2月23日

アーキテクチャ

APIとの通信効率をよくする実装例(1) curl_multi

  • このエントリーをはてなブックマークに追加
こんにちは。ライフスタイル事業部のアリタと申します。

Yahoo!のサービスも裏側ではWebAPIが多用されています。1つのページを構成するのに5、6個のAPIを使うこともざらですが、それではさすがにパフォーマンスが問題となってきます。その原因と対策例を紹介していきましょう。

■モデルケース


例として、3つのAPIから取得したデータをマッシュアップしたページがあるとします。(ここではサンプルコードを簡単にするため、APIの代わりに以下のRSSで説明としました)

20090223_1



Yahoo!トピックス エンターテインメントRSS
  http://dailynews.yahoo.co.jp/fc/entertainment/rss.xml
Yahoo!ミュージックマガジン最新情報RSS
  http://magazine.music.yahoo.co.jp/rss/ALL/rss.xml
Yahoo!検索トレンドRSS
  http://searchranking.yahoo.co.jp/rss/trend-rss.xml


■問題点


これらとの通信を最も手抜きで実装するなら、こんなコードになるでしょう。(PHPの場合。※1)
$rss_news  = file_get_contents("http://dailynews.yahoo.co.jp/fc/entertainment/rss.xml"); // エンタメトピックス
$rss_music = file_get_contents("http://magazine.music.yahoo.co.jp/rss/ALL/rss.xml");     // ミュージックマガジン最新情報
$rss_trend = file_get_contents("http://searchranking.yahoo.co.jp/rss/trend-rss.xml");    // 検索トレンド

1つ目の通信が終わるのを待って次、2つ目終わったら次、...と順に取りに行くので"直列通信"と呼んでいます。これでは、通信先のAPI数が増えるほど処理時間が深刻に。もし1つのAPIに1秒かかるとしたら、トータル3秒。その分お客さんは待たされる事となってしまいます。(※2)

そこで、せーの! で一斉に取りに行くのはどうでしょう。同時に通信するので"並列通信"や"マルチリクエスト"と呼んでいます。これなら待ち時間は減り、1秒で済みますね。(※3)

20090223_2



※1:裏話として、Yahoo!JAPAN内部では通信目的での「file_get_contents()」は使いません。後述のCURL関数を使っています。
※2:Yahoo!のAPIは1秒もかからないかと思いますが、チリも積もれば山となります。
※3:せーので問い合わせた結果、反応が速いAPIから順にバラバラと返ってくるので、実際は「待ち時間=最も反応の遅かったAPIの所要時間」です。

■並列通信の実装方法(curl_multi)


では、どうすればそのようなロジックを書けるのでしょう。実装例を紹介します。
幸いなことにPHP5から CURL関数群 にその為の道具「curl_multi_〜」が追加されており、これを使うことで実現できます。(※4)

※4:以前はYahoo!独自の並列通信用ライブラリを使っていました。しかしその難解さやメンテナンスコストの高さから、オープンソースの機能をそのまま使ったほうがお得ということでcurl_multiの採用が増えつつあります。

CURL関数の基本系はこのような形です。(通信先が1つのみの場合。ここではエラーハンドリングは省略しています)


$ch = curl_init("http://magazine.music.yahoo.co.jp/rss/ALL/rss.xml"); // Curl Handleを用意
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$rss = curl_exec($ch);  // 通信実行し、結果のXMLを取得
curl_close($ch);

これを複数束ねるように直すと、複数URLと同時通信できるようになります。こんな流れです。
 1. 先ほど出てきた「$ch」は、通信先URLごとに用意
 2. それらをマルチハンドルというもので束ねる
 3. 束ねたものに対し、実行を指示(=同時に通信が走る)


// 並列通信用マルチハンドルを用意
$mh = curl_multi_init();

// 通信先ごとにCurl Handleを作り、それを $mh にaddしていく
$ch_news = curl_init("http://dailynews.yahoo.co.jp/fc/entertainment/rss.xml");
curl_setopt($ch_news, CURLOPT_RETURNTRANSFER, TRUE);
curl_multi_add_handle($mh, $ch_news);

// 同様に
$ch_music = curl_init("http://magazine.music.yahoo.co.jp/rss/ALL/rss.xml");
curl_setopt($ch_music, CURLOPT_RETURNTRANSFER, TRUE);
curl_multi_add_handle($mh, $ch_music);

// 同様に
$ch_trend = curl_init("http://searchranking.yahoo.co.jp/rss/trend-rss.xml");
curl_setopt($ch_trend, CURLOPT_RETURNTRANSFER, TRUE);
curl_multi_add_handle($mh, $ch_trend);

// せーので複数の通信を同時実行。whileで全て返ってくるのを待ちます
do { curl_multi_exec($mh, $running); } while ( $running );

// 個々のXMLは、それぞれのCurl Handleを指定することで取得できる
$rss_news  = curl_multi_getcontent($ch_news);
$rss_music = curl_multi_getcontent($ch_music);
$rss_trend = curl_multi_getcontent($ch_trend);

// 後始末
curl_multi_remove_handle($mh, $ch_news);
curl_close($ch_news);

curl_multi_remove_handle($mh, $ch_music);
curl_close($ch_music);

curl_multi_remove_handle($mh, $ch_trend);
curl_close($ch_trend);

curl_multi_close($mh);

もう少し汎用的にまとめるなら、例えばこんな形はどうでしょう。(実際はニーズにあわせてクラス化したほうがよいと思います)
/**
 * 複数URLのコンテンツ、及び通信ステータスを一括取得する。
 * サンプル:
 *   $urls = array( "http://〜", "http://〜", "http://〜" );
 *   $results = getMultiContents($urls);
 *   print_r($results);
 */
function getMultiContents( $url_list ) {
    // マルチハンドルの用意
    $mh = curl_multi_init();

    // URLをキーとして、複数のCurlハンドルを入れて保持する配列
    $ch_list = array();

    // Curlハンドルの用意と、マルチハンドルへの登録
    foreach( $url_list as $url ) {
        $ch_list[$url] = curl_init($url);
        curl_setopt($ch_list[$url], CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch_list[$url], CURLOPT_TIMEOUT, 1);  // タイムアウト秒数を指定
        curl_multi_add_handle($mh, $ch_list[$url]);
    }

    // 一括で通信実行、全て終わるのを待つ
    $running = null;
    do { curl_multi_exec($mh, $running); } while ( $running );

    // 実行結果の取得
    foreach( $url_list as $url ) {
        // ステータスとコンテンツ内容の取得
        $results[$url] = curl_getinfo($ch_list[$url]);
        $results[$url]["content"] = curl_multi_getcontent($ch_list[$url]);

        // Curlハンドルの後始末
        curl_multi_remove_handle($mh, $ch_list[$url]);
        curl_close($ch_list[$url]);
    }

    // マルチハンドルの後始末
    curl_multi_close($mh);

    // 結果返却
    return $results;
}

次回は、並列通信などにより生まれる別の課題について取り上げてみようと思います。


(2009/2/24追記)
はてなブックマークでコメント頂いたのでアドバイスです。
> 接続失敗時の処理をどうするのだろう、ちょい調べる
上記サンプルコードの30行目に curl_getinfo() という関数が出てきますが、通信結果のHTTPレスポンスコード、 かかった時間など様々な値を得ることができます。お試しください。
(2009/4/10追記)
以前に指摘いただいていた、foreachまわりの冗長だったコードをシンプルに直しました。

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加