Pythonでアクセスカウンターを作る

なぜかカウントが1に戻ってしまったり、キリ番報告が3人もいるとかいうミステリアスなカウンターを作らないためには、排他制御のおさらいが必要だった。

わりとマトモなカウンターになったと思う。

ものすごく分かりやすかった排他制御の解説。
CGIやDBのロックと同時実行制御: CANO-Lab
http://jn.swee.to/cano/lock/index.shtml

access_counter.cgi

#!/usr/local/bin/python
#coding: utf-8

import fcntl, os, sys, time

COUNTER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "counter.txt")

def counter():
  lock_ex = False
  fd_open = False
  try:
    fd = os.open(COUNTER, os.O_CREAT|os.O_RDWR, 0755)
    fd_open = True
    for i in range(100):
      try:
        fcntl.flock(fd, fcntl.LOCK_EX|fcntl.LOCK_NB)
        lock_ex = True
      except IOError:
        # IOError: ロックを取得できなかった
        time.sleep(0.1)
        continue
      else:
        break
    if not lock_ex:
      sys.stderr.write("access_counter.cgi: File lock timeout.")
      return "File lock timeout."
    eof = os.lseek(fd, 0, os.SEEK_END)
    os.lseek(fd, 0, os.SEEK_SET)
    count = os.read(fd, eof)
    try:
      count = str(int(count) + 1)
    except ValueError:
      count = "1"
    os.lseek(fd, 0, os.SEEK_SET)
    os.ftruncate(fd, len(count))
    os.write(fd, count)
    os.fsync(fd)
  except AttributeError:
    # AttributeError: OSの制約で利用できない関数や定数があった
    return "Your environment not supported."
  finally:
    if lock_ex:
      fcntl.flock(fd, fcntl.LOCK_UN)
    if fd_open:
      os.close(fd)
  return count

if __name__ == "__main__":
  sys.stdout.write("Content-Type: text/html\n\n%s\n" % counter())

test_main.py
完全に同時とはいかないけど、コンテキストスイッチの影響を確かめるには、これでいいと思う。

#!/usr/local/bin/python
#coding: utf-8

import os, time
from subprocess import *

execute = os.path.join(os.path.dirname(os.path.abspath(__file__)), "access_counter.py")
total_start = time.time()
timeout = 0
for i in range(10000):
  rap_start = time.time()
  childs = [Popen(execute, stdin=PIPE, stdout=PIPE, stderr=PIPE) for ii in range(150)]
  popen_done = time.time()
  for p in childs:
    p.wait()
    (out, err) = p.communicate()
    if err:
      print err
      timeout = timeout + 1
  rap_done = time.time()
  print "%d rap done, childs born=%f, rap time=%f" % (i, popen_done - rap_start, rap_done - rap_start)
total_done = time.time()
print "total time %f" % (total_done - total_start)
print "timeout = %d" % timeout

test_sub.py

#!/usr/local/bin/python
#coding: utf-8

import os, time
from subprocess import *

execute = os.path.join(os.path.dirname(os.path.abspath(__file__)), "access_counter.py")
total_start = time.time()
timeout = 0
for i in xrange(500000):
  rap_start = time.time()
  p = Popen(execute, stdin=PIPE, stdout=PIPE, stderr=PIPE)
  p.wait()
  (out, err) = p.communicate()
  if err:
    print err
    timeout = timeout + 1
  rap_done = time.time()
  print "%d rap, time=%f" % (i, rap_done - rap_start)
total_done = time.time()
print "total time %f" % (total_done - total_start)
print "timeout = %d" % timeout

テスト方法

端末Aで次を実行
$ python test_main.py > main.log

その後まもなく、端末Bで次を実行
$ python test_sub.py > sub.log

mainで150万回、subで50万回、合計200万回カウンターを回す。

main.log、sub.logに記録されたtimeoutの数と、counter.txtの数を合わせて200万になればOK。

テスト結果

counter.txt
  1999703
main.log
  timeout = 295
sub.log
  timeout = 2

1999703 + 295 + 2 = 2000000

main.log, sub.logともにタイムアウトしたと思える実行時間が記録されていた。

>>> import pprint
>>> lines = open("main.log").readlines()
>>> raptimes = []
>>> for l in lines:
>>>   if not l.startswith("access_counter.cgi"):
>>>     raptimes.append(float(l.split(",")[2].strip().split("=")[1]))
>>> raptimes.sort()
>>> pprint(raptimes[-10:])
[6.8269399999999996,
 6.8799440000000001,
 7.1228569999999998,
 7.3894970000000004,
 7.6184669999999999,
 8.3026210000000003,
 9.5717949999999998,
 10.554797000000001,
 10.622182,
 10.824996000000001]
    
>>> times = [] >>> for l in lines: >>> if not l.startswith("access_counter.cgi"): >>> times.append(float(l.strip().split("=")[1])) >>> times.sort() >>> pprint(times[-10:]) [5.1106069999999999, 5.1604809999999999, 5.2510870000000001, 5.4495550000000001, 5.643491, 6.1125689999999997, 7.9487379999999996, 9.470364, 10.060624000000001, 10.087431]