Pythonでアクセスカウンターを作る
なぜかカウントが1に戻ってしまったり、キリ番報告が3人もいるとかいうミステリアスなカウンターを作らないためには、排他制御のおさらいが必要だった。
わりとマトモなカウンターになったと思う。
ものすごく分かりやすかった排他制御の解説。
CGIやDBのロックと同時実行制御: CANO-Lab
http://jn.swee.to/cano/lock/index.shtml
#!/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]