アクセス解析ツール:多重同時リクエストに対しLOCK TABLESで対応

September 8, 2010 – 5:46 pm

アクセス解析ツール」を改良したのでメモしておく。改良点は、我がブログサイトに同一のホストから多重「同時」的にリクエストがあった場合、これを単一のリクエストとして「アクセス解析ツール」で取り扱えるようにしたところ。改良にあたって、MySQLのLOCK TABLESを使用した。

アクセス解析ツールで見出されていた問題: 昨年1月に「アクセス解析ツール」を作成して以来、このブログへのアクセスをDB上に記録してきた。おおむね順調に、この「解析ツール」が動作してきたといえる。

ただ、「ツール」作成時点では予想しなかった単一のホストからの二重同時的なリクエストといったアクセス形態が存在し、このかたちでブログが閲覧されると、「アクセス解析ツール」では、これを独立した複数のアクセスとして取り扱かってしまうという問題があった。

こうした問題にかかわる具体的な動作を下図にしめす:

この図から、9月6日19:20:45にinet-proxy311.toshiba.co.jpにより「HDDをOS込みで別PCに移設した」というWebページが閲覧されているが、「同時刻」に、同一内容のリクエストが2重に記録されていることがわかる。

また、このinet.proxy311.toshiba.co.jpからの一連のアクセスをみると(下図)、同一の並びで4つのWebページ夫々が、「同時刻」にアクセスされていることがわかる。

この例では、inet.proxy311.toshiba.co.jpによりサイトs.luna.tv経由で我がサイト上のWebページ「HDDをOS込みで別PCに移設した」にアクセスし、我がサイト上から同一Webページを再度呼び出しているような振る舞いをしている。

こうしたアクセス(HTTPリクエスト)の形態は、何らかの機械的な方法で我がサイトのWebページを閲覧したものと考えられるが、それはそれとしても、「アクセス解析ツール」上で、このアクセスをふたつの独立したアクセスとして取り扱うのは、ツールの機能からいって相応しいものではない。

LOCK TABLESによる対応: 上記したような形態のリクエストを受けた場合の対応措置として、「解析ツール」で使用しているDBMSであるMySQLのLOCK TABLES機能を活用し、対処することにした。

MySQLのLOCK TABLESにおいては、あるスレッドで特定のテーブルを(WRITE)ロックすると、ロックが有効なあいだ、他のスレッドによるDBへの書き込み(INSERTとかUPDATE)が規制される(LOCK TABLESの詳細な動作はMySQLのマニュアル参照のこと)。また、その間、他のスレッドからのLOCKの要請は、このスレッドのLOCKが解除されるまでWait状態になる(、と私は理解した)。

このLOCK TABLESを利用することで、「アクセス解析ツール」においてアクセス内容を書き込む関数部の最初にロックを発行し、「同時的」なアクセスを記録させない仕組みとし、書き込みにあたっては、その時点で最後に書き込みが行われたデータが同一ホストから同一時刻にアクセスされたものでないことをチェックすることにした。

これを行うため、「解析ツール」のphpプログラムのうちAccessDao.phpに含まれるinsertAccess()を以下のように修正した。なお、AccessDao.phpの修正前のソースの全体はPHPによる自前のWebアクセス解析ツールの作成」にある。

修正済みinsertAccess()ソース:

        public function insertAccess ( $access ) {
            is_null($this->mysqli) and $this->connect();
        // page_visited to page_id in table mypage;
// 修正(挿入)箇所(1)-- 始まり
        $sql_lock = "LOCK TABLES access WRITE";
        $result_lock = $this->mysqli->query($sqli_lock); 

        $sql_last_id = "SELECT count(*) AS last_id FROM access";
        $result      = $this->mysqli->query($sql_last_id);
        $row = $result->fetch_array(MYSQLI_ASSOC);
        $last_id = $row["last_id"];
        $sql_data        = "SELECT time, visitor FROM access WHERE id='" . $last_id . "'";
        $result_access   = $this->mysqli->query($sql_data);

        if ( $result_access->num_rows > 0 ) {
           $row = $result_access->fetch_array(MYSQLI_ASSOC);
           $time_access = $row["time"];
           $visitor_access = $row["visitor"];

           $time_in = $access->getTime();
           $visitor_in = $access->getVisitor();

           // Compare data between DB and InData
           $comp_visitor    = strcmp( $visitor_in,    $visitor_access );
           $time_diff       = strtotime( $time_in ) - strtotime( $time_access );

        }
// 修正(挿入)箇所(1) -- 終わり
        if ( $comp_visitor != 0 || $time_diff >= 1 ) {
            $sql = "SELECT id FROM mypage WHERE page_visited='" . $access->getPageVisited() . "'";
            $result = $this->mysqli->query($sql);
            if ( $result->num_rows == 0 ) {
               if ( strpos( $access->getPageVisited(), '.html' ) ) {
                    if (have_posts()) : while (have_posts()) : the_post();
                    $new_title = get_the_title();
                    endwhile; endif;
               }
               else {
                    $new_title = $access->getPageVisited();
               }
               $sql = "INSERT INTO mypage values('',
                       '" . $access->getPageVisited() . "', '" . $new_title . "')";
               if (!$this->mysqli->query($sql)) {
                       print "Failed to register(1010) " . $this->mysqli->error . "\n";
               }
               else {
                       $page_id    = $this->mysqli->insert_id;
                       $page_title = '$new_title';
               }
            }
            else {
                  $row = $result->fetch_array(MYSQLI_ASSOC);
                  $page_id = $row["id"];
                  $sql = "SELECT title FROM mypage WHERE id =' " . $page_id . "'";
                  if(!$this->mysqli->query($sql)) {
                       print "Failed to register(1011) " . $this->mysqli->error . "\n";
                  }
                  else {
                      $page_title = $row["title"];
                  }
            }

        // Set access table for new data
            $sql = "INSERT INTO access values('',
                           '" . $access->getTime() . "',
                           '" . $access->getVisitor() . "',
                           '" . $access->getIpAddress() . "',
                           '" . $access->getUniqueness() . "',
                           '" . $access->getReffUri()   . "',
                           '" . $access->getRefferer() . "',
                           '" . $access->getSearchEngine() . "',
                           '" . $access->getKeyWords() . "',
                           '" . $page_id . "',
                           '" . $access->getUserAgent() . "')";

            if(!$this->mysqli->query($sql) ) {
                 print "FAILED TO SET DATA(1012) " . $this->mysqli->error . "\n";
            }
// 修正(挿入)箇所(2) 始まり
        }
        $sql_lock = "UNLOCK TABLES";
        $result_lock = $this->mysqli->query($sql_lock);
// 修正(挿入)箇所(2) 終わり
        }

ちょっと考察: 数日間、上記で「アクセス解析」を行ったところ、以前に見られたような二重アクセスといった問題はうまく避けられている。なんとか意図したとおりに動作しているようだ。

ここで示した方法、「完璧」な対処法とはいえないが、我がサイトのように1日あたりのページビューが高々1000~2000程度の場合には実効的に問題なく機能すると考えられる。アクセス数が相当のレベルに達する場合でも、チェック部を拡張することにより、対応可能と考えられる。

「二重アクセス」というキーでGoogleの検索をしてみると、ほぼ同種の問題があるようだ。そうした問題への対処にあたっても、この方法で簡易的に対応できるのではないかと考えられる。


Post a Comment