さんまがおいしい季節だねー(´・ω・`)

PHP による Amazon PAAPI の毎秒ルール制限の実装とキャッシュの構築例

Amazon,PHP — タグ: , , , , , — さくら @ 2009/11/26 2:39

ヤシマの黒豹ロデムたんから、Amazon の Product Advertising API (PAAPI) を使って簡単な PHP ウェブアプリを作成中とのことでご相談を受けました。PAAPI では API 経由のアクセスを毎秒1回に制限しないといけないのですが、その辺のことに関しての質問です。

この毎秒1回のアクセスに制限しないといけないというのは、毎秒ルールとか1秒ルールとか言われてます。多くのウェブ API は呼び出し間隔を制限するように規程を設けており、PAAPI の場合は、Amazon.co.jp Product Advertising API ライセンス契約の「4. 利用条件」の以下の箇所に記載されています。

(p) お客様自身も、また、お客様が Product Advertising API にリクエスト送信を行うアプリケーションを作成し公表した場合は、エンドユーザーによりインストールされたかかるアプリケーションの各コピーは、毎秒1 コールを超えないものとし、また、当方の事前の書面による同意なしに、Product Advertising API 宛にまたはこれより、サイズが40Kを超えるファイルを送信しないものとします。

解決方法をいくつか検討しましたが、今回は flock による API アクセスの直列化と出力キャッシュを併用する方法をご説明したいと思います。これ以外の方法だと OS とかの知識が無いとちと説明が厳しいかなーという感じでしたので。

今回はこの方針で実装した簡単なライブラリを用意してみました。これを組み込んで頂くのはそんなに難しくないと思いますが、ライブラリが動くかお手元の環境でチェックしていただかないといけませんので、ちと記事の方は長めになってます。

あとこの記事では PAAPI の説明はまったくしてませんのであしからずw

flock によるアクセス直列化

flock とはファイルをロックする PHP の関数です。ファイルがロックされていると、並走しているスクリプトが同じファイルをロックしようとした場合に、ロックを保持しているスクリプトがロックを開放するまで待たされます。これを利用してロックファイルを介す全ての処理がサーバ上で直列化できます。

flock を使ったアクセス直列化の手順は具体的には以下のとおりです。

  1. ロックファイルを開く。(ロックファイルの中には API 呼び出し時刻を記録する。)
  2. 書き込み用に排他モード (LOCK_EX) でファイルを flock する。既に他のプロセスがファイルをロックしている場合、そのプロセスがロックを開放するまで待たされるので、ここでアクセスが直列化される。
  3. ファイルから最終 API 呼び出し時刻を読み込む。前回の API 呼び出しから1秒経過していなれば適当な間隔 sleep する。
  4. API を呼び出す/API 呼び出し時刻を記録/ロックを開放。

アクセス直列化手順自体はこれで問題無いのですが、flock のマニュアルには以下のような警告が記載されています。

警告

flock() は NFS 及び他の多くのネットワークファイルシステムでは動作しません。 詳細についてはオペレーティングシステムのドキュメントを確認ください。

いくつかのオーペレーティングシステムでflock() はプロセスレベルで実装されています。ISAPIのようなマルチスレッド 型のサーバーAPIを使用している場合、同じサーバーインスタンスの並 列スレッドで実行されている他のPHPスクリプトに対してファイルを保 護する際に flock()を使用することはできません!

flock()はFATのような 旧式のファイルシステムではサポートされていないため、 そのような環境の場合は常にFALSEを返すことになります。 (これは特にWindows98ユーザーにとって常に真です)

NFS とは Unix 系のネットワークファイルシステムのことで、ISAPI とは IIS 用の PHP サーバモジュールのことです。要はサーバ環境によって動かないことがあるということです。

flock が使えるかの確認方法

ご使用の環境で flock が使用できるかは、テストスクリプトを使って確認するのが手っ取り早いと思います。*NIX 上で CGI で PHP が動いている場合はだいたい利用できますが、NFS を使用してると使えない場合もあります。

  1. まず以下のスクリプトをコピペして flocktest.php という名前で保存してください。(実際はファイル名は何でも良いのですが説明の都合上アレです。)
    <?php
    
    $path = "locktest.txt";
    $wait = 4;
    
    $fh = fopen($path, "w");
    if (!$fh) {
        trigger_error("Failed to open {$path}", E_USER_ERROR);
    }
    
    if (!flock($fh, LOCK_EX)) {
        trigger_error("Failed to lock {$path}", E_USER_ERROR);
    }
    
    echo 'Start ' . date('r') . '';
    sleep($wait);
    echo 'End ' . date('r') . '';
    
    fclose($fh);
    
    ?>
    
  2. 保存したらサーバにアップロードしてください。アップロード先のディレクトリでは、locktest.txt という名前のファイルをウェブサーバから書き込める状態にしておく必要があります。あらかじめファイルを作成しておきパーミッションを書き込めるようにしておくか、ディレクトリ自体のパーミッションを書き込めるようにしておいてください。
  3. スクリプトをサーバに設置したら、ブラウザから flocktest.php へ同時に2つのアクセスを発生させてください。タブブラウザ等を使用しているなら、空白のタブを2つ開いてどちらもアドレスバーに flocktest.php の URL を入力してから、ほぼ同時に(続けて)URLの読み込みを開始すればおkです。

    以下は2つ同時にアクセスした際に表示された結果です

    Start Wed, 25 Nov 2009 03:25:34 +0900
    End Wed, 25 Nov 2009 03:25:38 +0900
    
    Start Wed, 25 Nov 2009 03:25:38 +0900
    End Wed, 25 Nov 2009 03:25:42 +0900
    

    ほぼ同時に両方ともアクセスしたにも関わらず、上のスクリプトの終了後に下のスクリプトが開始されてる形になってます。flock により直列化されているとこのような結果になりますが、flock や直列化に失敗している場合は何も表示されなかったり実行時間が重なったりします。(上の例ですと、両方とも3:25:34〜3:25:42みたいな形になります。)

  4. テストが終われば、テストに使用したファイル等はすべて削除してください。

このテストで直列化に失敗している場合、flock では API アクセスの直列化はできません。

ちなみに最近の *NIX 系のサーバですとマルチスレッドサーバでも flock は有効に動作するものが多いです。NFS をファイルロック可能な状態で動かしているかは環境次第ですが、その辺にある共有レンタルサーバで NFS 使ってるところは少ないと思いますので、あまり気にしなくても良いてかもしれません。いずれにしてもサーバごとに確認してもらった方が速いです。

以上の手順で flock が有効に動作するか確認できたら、API アクセス直列化するコードを実装します。

flock による API アクセス直列化の実装

実装方法を細かく説明すると長くなりそうですので、API アクセスを直列化するための簡単なライブラリを用意しています。前述のとおり flock が使えない環境だとちゃんと動きませんのでご注意ください。それと PHP5 以上が必要です。PHP4 で動かす場合は、microtime の呼び出し方を変えてもらえば動くと思います。

checkpoint.zip

ZIP ファイルには、checkpoint.php と Cache_Lite_Checkpoint.php というファイルが含まれています。ダウンロードしたら、ZIP ファイルに含まれてるスクリプトをすべて適当な(include 可能な)ディレクトリに保存してください。

API アクセス直列化に使用するのは checkpoint.php です。Cache_Lite_Checkpoint.php は API アクセスを直列化しつつリクエストをファイルにキャッシュするためのスクリプトですので後で説明します。

で、checkpoint.php の使い方ですが、以下のようなコードがあるとすると、

<?php

// PAAPI の呼び出し

// API データの出力

?>

…まあ PAAPI の呼び出し方法は色々あると思いますのでコメントだけにしてますがw、以下のような感じに直してください。

<?php

require_once 'checkpoint.php';

if (!checkpoint('/home/xxx/run/paapi', 1)) or die("checkpoint failed");

// PAAPI の呼び出し

// API データの出力

?>

PAAPI 等直列化対象の処理の直前で checkpoint 関数を呼び出してください。これで直列化されます。

checkpoint 関数の引数は、直列化に使用するロックファイルのパスと、直列化する際の実行間隔です。上の例では /home/xxx/run/paapi をロックファイルとして PAAPI 呼び出しを1秒に一回に制限します。ロックファイルの場所はどこでも良いです…ネットから見えない場所の方が良いですが。ロックファイルの中身は関数が適当に更新していきますが、ディレクトリは作りませんのであらかじめ作成しておいてください。上の例ですと /home/xxx/run が存在してないとエラーになります。

細かい注意点等はソースにコメントしてますのでそちらをご覧ください。checkpoint 関数だけだとあまりスケールしないと思いますので、だいたい読んだら(実際にコーディングせずに)次に進んでキャッシュと併用したものを実装した方が幸せになれます。

PHP 出力のキャッシュ

API 呼び出しの直列化だけでも API 規約は満たせますが、ウェブアプリの場合あんまり API 呼び出ししちゃうと転送量とかに影響して 503 を頻発する可能性もありますので、出力キャッシュもセットで実装してしまうのが良いと思います。

出力キャッシュでは、スクリプトからの出力をファイル等に保存しておき、キャッシュが有効な間は処理を行わずキャッシュファイルの中身をそのまま出力に使用します。キャッシュの有効期間はプログラムで制御します。

PAAPI の場合、ライブラリによっては入力キャッシュも簡単に行えますが、今回の内容は使用ライブラリについてはあまり限定したくないのと、入力キャッシュを行うとキャッシュファイルが結構な量になることが多いので、とりあえずやるなら出力キャッシュから検討した方が良いと思います。その辺について知りたい方は他の記事をあたってください。

先ほど checkpoint 関数だけだとあまりスケールしないと書きましたが、キャッシュせずに毎回 API を呼び出すと直列化のために毎回他のスクリプトの終了を待つことになります。1秒に1回の実行に制限した場合、同時に100アクセス発生すると最低100秒かかってしまうことになります。これだと転送量に関わらずサーバ管理者からスクリプトを止められることがあると思いますので、その辺を回避するためにもキャッシュしといた方が良いと思います。

あと、こちらの方が checkpoint 関数だけの場合と比べて厳密に毎秒1コールに従ってます。それもあって checkpoint のみの利用はあんまお勧めしません。

キャッシュにまつわる PAAPI の規約について

で、キャッシュの実装の説明に入る前に、PAAPI の場合データのキャッシュの保持期間にも制限がありまして、またまた Amazon.co.jp Product Advertising API ライセンス契約からですが、

(n) お客様は、画像で構成される商品関連コンテンツを格納またはキャッシュしてはいけませんが、画像で構成される商品関連コンテンツへのリンクを、24時間まで格納することができます。お客様は、画像で構成されていないコンテンツを、データキャッシュの目的で、24時間まで格納することができますが、それをした場合は、その後直ちに Product Advertising API にリクエスト送信を行うか、または新しいデータフィードを取り込み、お客様のアプリケーション上の商品関連コンテンツを刷新することにより、商品関連コンテンツを直ちに刷新し、再表示しなければなりません。別途当方より通知されない限り、お客様は、個別の Amazon Standard Identification Number (以下、「ASIN」といいます)を、本ライセンス契約の終了まで、期間の制限なく格納することができます。前述に拘わらず、お客様のアプリケーションがクライアントアプリケーションを含む場合、かかるクライアントアプリケーションは、商品関連コンテンツを格納またはキャッシュしてはいけません。当方の要求があれば、お客様は、当方の要求から3営業日以内に、お客様が本ライセンス契約を遵守しているかを確認できるよう、かかるクライアントアプリケーションのコピーを当方に提供するものとします。
(o) お客様が商品関連コンテンツをデータフィードから取得する場合、または、お客様が Product Advertising APIにリクエスト送信をする、もしくはお客様のアプリケーション上の商品関連コンテンツを刷新する頻度が1時間に1回以下の場合は、お客様のアプリケーション上の価格情報または発送可能時期についての情報に隣接して時刻/日付のスタンプを含むものとします。ただし、お客様が、アプリケーション上に表示された価格情報および発送可能時期についての情報を同日中にリクエストおよび刷新した場合は、スタンプの日付部分を省略することができます。許容されるメッセージの例は以下のとおりです。

  • Amazon.co.jp 価格: JPY 3,200 (01/07/2008 14:11 JST 時点 -詳細はこちら-)
  • Amazon.co.jp 価格: JPY3,200 (14:11 JST時点 -詳しくはこちら-)

また、お客様は、下記の免責事項を、価格情報または発送可能時期についての情報のそばに記載するか、またはハイパーリンク、ポップアップ、スクリプト・ポップアップその他類似の方法で提供しなくてはなりません。「価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。商品の販売においては、購入の時点で [Amazon.co.jp または Javari.jp の適用ある方] に表示されている価格および発送可能時期の情報が適用されます。」上記の例において、「詳細はこちら」および「詳しくはこちら」などとすれば、エンドユーザーが免責事項を読むよう仕向けることができます。

てけとーにまとめると、画像データはキャッシュダメ、その他は1時間以内ならキャッシュ可、または1時間以上キャッシュする場合はデータ取得時刻等を表示しないといけない、ということのようです。とりあえず最長1時間でキャッシュするのが良さげかと思います。

キャッシュの実装

出力キャッシュを実装するには、PEAR の Cache_Lite を使うのが便利だと思います。pear の easy_install が使用可能な場合は pear install でインストールしてください。使用できない場合は以下の手順でインストールできます。

  1. http://pear.php.net/package/Cache_Lite/download から最新版をダウンロード
  2. tgz アーカイブを展開すると(この記事執筆時点では最新版が1.7.8なので)Cache_Lite-1.7.8 というディレクトリができますので、ディレクトリ名を Cache_Lite-1.7.8 から Cache に変更
  3. Cache/Lite.php と Cache/Lite/*.php をサーバの適当な(include可能な)ディレクトリにアップロード

Cache/Lite をそのまま使用しても良いのですが、先ほどの ZIP ファイルに、Cache/Lite と checkpoint.php のラッパークラスを用意してますのでそちらをご利用頂いた方が簡単です。Cache_Lite_Checkpoint.php に入ってる Cache_Lite_Checkpoint がラッパークラスです。ご利用の際は、Cache_Lite_Checkpoint.php と checkpoint.php も include 可能なディレクトリに置いてください。

上の例を Cache_Lite_Checkpoint を使ってキャッシュするように書き換えるとこうなります。

<?php

require_once 'Cache_Lite_Checkpoint.php';

$cache = new Cache_Lite_Checkpoint(array(
    'cacheDir' => 'tmp/',
    'lifeTime' => 3600,
    'checkpointLockfile' => '/home/xxx/run/paapi',
    'checkpointInterval' => 1
));

$id = $_SERVER['PHP_SELF'];
if (isset($_SERVER['QUERY_STRING']))
    $id .= '?' . $_SERVER['QUERY_STRING'];

if (!$cache->start($id)) {

    // PAAPI の呼び出し

    // API データの出力

    $cache->end();
}

?>

Cache_Lite_Checkpoint のコンストラクタには実行時オプションを配列で渡してください。

checkpointLockfile と checkpointInterval はロックファイルのパスと直列化の間隔で checkpoint の引数と対応しています。

cacheDir がキャッシュファイルを置くディレクトリ、lifeTime がキャッシュファイルの生存時間(秒)です。上の例ですと ‘tmp/’ ディレクトリにキャッシュファイルを作成し、3600 秒(1時間)ごとに更新します。cacheDir の末尾のスラッシュは省略しないでください。cacheDir は自動で作成されませんのであらかじめ作成しておいてください。また、このディレクトリはネットから見えなくて良いので、外から見えない場所に置くか、以下の内容の .htaccess を置いて外部からアクセスできないようにしてください。

Order Allow,Deny
Deny from all

start() の $id 引数はキャッシュの ID です。出力ごとにユニークになる文字列なら ID は何でも良いです。上の例では $id を PHP_SELF と QUERY_STRING から初期化していますが、ブラウザに入力する内容次第で不要なキャッシュファイルを作ってしまうのであまり良いアイデアではありません。元の処理でページングとかしていて簡単にユニークな ID を振れる場合はそれを利用してください。他に $group 引数でもキャッシュファイルを制御できます。詳しくは Cache_Lite のマニュアルをご覧ください。

以上で毎秒コールを制限しつつ出力を1時間ごとにキャッシュするようになりました。把握してる限りでは PAAPI の規約的にも問題ないはずです。

ま、必要なら適当に改造してください。(正直言ってこの程度のソースでライセンスうんぬん言う方が面倒なのですが)一応 MIT ライセンスにしてますのでご自由にどうぞ。

最後に

今回の記事は書き始めてから公開するまで結構時間かかっちゃっいました。OS というかシステムコールプログラミングをご存知無い方に詳細を説明するのは難しそうでしたので、説明を省くために文章やら例やら添付ライブラリをあーでもないこーでもないといじくりだしたら終らねーって感じですw

最初に比べると現在のものはだいぶ使いやすくなってると思いますが、PAAPI 呼び出しが長時間に渡ったため管理者がスクリプトを強制終了するために kill した場合などにうまく適合できてないかもしれません。PAAPI の場合そこまでクリティカルに毎秒ルールをチェックしてる訳では無いみたいなので問題なさそうですが、それなりの規模のアプリを作る場合 PHP だけっていうのはしんどいと思います。(最初 cron ベースの説明にしようかと思ってたのですが、シェルの説明から始めるのもアレだなーということで今の形になりますたw)

ちなみに PAAPI の規約に関してだけ話しますと、ポックンが調べた限りでは Cache_Lite を使って適当な間隔キャッシュするだけでもアプリの種類によっては規約違反にならないようです。ただその辺の見極めはちゃんと規約を読んで貰わないとなんとも言えなかったりしたので、一応固めに規約を守るサンプルを用意しています。

この辺は A2Sdevelopper の Working With the “One-Second” Rule が詳しいと思いますのででそちらをご覧ください。ちなみにこの記事を超訳するとこんな感じ↓

           .∧
        ;へ  | |
       (_人ヽ_/ ノ
_,,.-‐-,,._    / 。。!
|.    |   ( ,,,,Y,,,)
|.    |==/ (,,゚Д゚)n== <まぁ、もちつけ、漏前ら
|.    |  i(ノ   ノ
|_,,.. -..,,_| C   /
`”’‐-‐”’´ ゙:、 丿丿
       U”U

おまけ

編集に悪意を感じるチェックポイント

3件のコメント »

  1. [...] PHP による Amazon PAAPI の毎秒ルール制限の実装とキャッシュの構築例 | さくらたんどっとびーず 今回はこの方針で実装した簡単なライブラリを用意してみました。これを組み込んで頂くの [...]

  2. [...] PHP による Amazon PAAPI の毎秒ルール制限の実装とキャッシュの構築例 | さくらたんどっとびーず 今回はこの方針で実装した簡単なライブラリを用意してみました。これを組み込んで頂くの [...]

  3. GlRsVKKqhCirVTUhlJ 7460

    コメント by lGhCzQXetkqxGym — 2014 年 10 月 5 日 @ 17:46

この投稿へのコメントの RSS フィード。 TrackBack URI

コメントする

Copyright © 2017 さくらたんどっとびーず | powered by WordPress with Barecity