2006年1月26日
Ruby, Pythonで並列に逆引きを行う
ウェブサーバのログを解析するときなど、IPアドレスからホスト名を引く処理 (逆引き) を大量に行いたいときがあります。DNS への逆引きの問い合わせには場合によっては数秒待たされることがあるため、大量の IP アドレスをひとつづつ順に処理していくとかなり時間がかかります。
本記事では Ruby または Python でマルチスレッドで並列に逆引きを行う方法を紹介します。
Ruby の場合
Ruby で逆引きを行うには socket ライブラリを使う方法と、Resolv ライブラリを使う方法があります。マルチスレッドで並列に逆引きを行うには Resolv ライブラリを使う必要があります。
socket ライブラリを使った場合、 Socket.gethostbyaddr か Socket.getaddrinfo を使って逆引きを行います。これらは同名の C の関数を内部的に呼びます。逆引きには数秒間かかることがありますが、Ruby のスレッドはユーザレベルスレッドであるため、C の関数を呼んでいる間は他のスレッドはまったく動きません。このため、マルチスレッドで並列に逆引きしようとしても実際には逐次的に行われてしまいます。
一方、 resolv ライブラリでは gethostbyaddr などに相当する部分が Ruby で書かれているため、Cの関数呼び出しで長時間ブロックされることなしに、並列に逆引きを行えます。
次のプログラムは Socket.gethostbyaddr を使った場合と Resolv.getname を切り替えられるようになっています。
require 'thread' require 'thwait' require 'socket' require 'resolv' def lookup_with_gethostbyaddr(ip_address) addr = ip_address.split('.').map {|x| x.to_i}.pack('C4') return Socket.gethostbyaddr(addr).first rescue SocketError return ip_address end def lookup_with_resolv(ip_address) return Resolv.getname(ip_address) rescue Resolv::ResolvError return ip_address end def usage puts "mt-resolver.rb METHOD NUM-THREADS FILE" exit end def main usage unless ARGV.length == 3 lookup_method = ARGV[0] num_threads = ARGV[1].to_i file_name = ARGV[2] queue = Queue.new File.readlines(file_name).map {|line| ip_address = line.chomp queue.push(ip_address) } mutex = Mutex.new threads = [] num_threads.times { queue.push(nil) thread = Thread.new { while ip_address = queue.pop host_name = send(lookup_method, ip_address) mutex.synchronize { puts host_name } end } thread.abort_on_exception = true threads.push(thread) } ThreadsWait::all_waits(*threads) end main
DNSのキャッシュが影響するため、速度を比較するのは難しいのですが、無作為にサンプリングした 100 の IP アドレスのリストを 2つ作り (重複するアドレスなし)、それぞれ lookup_with_gethostbyaddr を使った場合と lookup_with_resolv を使った場合で時間を計測してみました。
% time ruby mt-resolver.rb lookup_with_gethostbyaddr 100 list1 > /dev/null 670.04s total : 0.05s user 0.01s system 0% cpu % time ruby mt-resolver.rb lookup_with_resolv 100 list2 > /dev/null 130.20s total : 0.60s user 0.06s system 0% cpu
Socket.gethostbyaddr では 670秒かかりましたが、 Resolv.getname では 130 秒で終わりました。list1, list2 では重複しない IP アドレスを使っているため、DNSのキャッシュの影響はあまりないと思います。
Python の場合
Python のスレッドは OS のネイティブスレッド (pthreads) であるため、socket.gethostbyaddr を呼んでもブロックすることはありません。
import threading import Queue import socket import sys import fileinput def lookup(ip_address): try: return socket.gethostbyaddr(ip_address)[0] except socket.herror: return ip_address def resolver(queue, lock): while True: ip_address = queue.get() if ip_address == None: break host_name = lookup(ip_address) lock.acquire() try: print host_name finally: lock.release() def main(): queue = Queue.Queue() num_threads = int(sys.argv[1]) for line in fileinput.input(sys.argv[2]): ip_address = line.strip() queue.put(ip_address) lock = threading.Lock() for i in xrange(num_threads): queue.put(None) thread = threading.Thread(target = resolver, args = (queue, lock)) thread.start() main()
ふたたび無作為にサンプリングした 100 の IP アドレスのリストを 2つ作り (重複するアドレスなし)、それぞれスレッドの数が 1の場合と 100 の場合で時間を計測してみました。
% time python mt-resolver.py 1 list3 584.74s total : 0.02s user 0.03s system 0% cpu % time python mt-resolver.py 100 list4 20.07s total : 0.10s user 0.09s system 0% cpu
スレッドの数が 1の場合は Ruby で gethostbyaddr を使ったときとそれほど変わらず 584 秒かかりましたが、スレッドを 100 にすると、20秒で終わりました。こちらも list3, list4 で重複しない IP アドレスを使っています。
まとめ
Ruby または Python を使ってマルチスレッドで並列に逆引きを行う方法を紹介しました。元々は Ruby でも resolv ライブラリを使えば Python と同程度の速さで並列に逆引きできると予想していたのですが、やってみると、Python 版の方が速いという結果になりました。今回のようなケースではネイティブスレッドが有効に働くようです。