Twisted で reactor のループをブロックせずにクライアントにファイルを転送するツールを書いてみた。
Twisted にはファイル転送のための FileSender があるが、reactor のループ中にファイルの読み込みを行うため、その間、他のリクエストを処理できずに応答性が低下してしまうケースがある。以下の NonblockingFileSender では ファイルの読み込みを別スレッドで行うため、ファイルIOを行いつつちょっとしたリクエストにも応答できるようになっている。また、読み込んだデータをクライアントに送出した後、データの送信を待たずに次に送信する分のデータを自動的に読み込むため、効率的にファイルの転送が行えるようになっている。
from zope.interface import implements from twisted.internet import threads, defer, reactor, interfaces, protocol from twisted.protocols import basic class NonblockingSender: implements(interfaces.IProducer) deferred = None _loading = False _waiting = False _buf = None def beginFileTransfer(self, consumer): self.consumer = consumer self.deferred = deferred = defer.Deferred() self.consumer.registerProducer(self, False) return deferred def _loadData(self): if self._loading: return if not self.deferred: return self._loading = True d = self.readbytes(self.CHUNK_SIZE) def dataarrived(chunk): self._loading = False if chunk is None: self.consumer.unregisterProducer() if self.deferred: self.deferred.callback(True) self.deferred = None else: assert self._buf is None self._buf = chunk if self._waiting: self.resumeProducing() def onerr(reason): self._loading = False if self.deferred: self.deferred.errback(reason) self.deferred = None self.consumer.unregisterProducer() d.addCallback(dataarrived) d.addErrback(onerr) return d def resumeProducing(self): if self._buf: self.consumer.write(self._buf) self._buf = None self._waiting = False else: self._waiting = True self._loadData() def pauseProducing(self): pass def stopProducing(self): if self.deferred: self.deferred.errback(Exception("Consumer asked us to stop producing")) self.deferred = None class NonblockingFileSender(NonblockingSender): CHUNK_SIZE = 1024*1024 # 1 MBytes def __init__(self, filename, chunksize=None): self._file = open(filename, "rb") if chunksize: self.CHUNK_SIZE = chunksize def readbytes(self, n): def readfile(): ret = self._file.read(n) if not ret: return None return ret return threads.deferToThread(readfile) def run(): class NBProtocol(basic.LineReceiver): def lineReceived(self, n): s = NonblockingFileSender(n, chunksize=1024*1024) d = s.beginFileTransfer(self.transport) d.addCallback(self._finished) d.addErrback(self._err) def _finished(self, result): self.transport.loseConnection() def _err(self, reason): print "Failed", reason self.transport.loseConnection() class NBFactory(protocol.ServerFactory): protocol = NBProtocol reactor.listenTCP(10000, NBFactory()) run()
というのを書いたので使ってみたが、あんまり効果を体感できなかった。一応、高速化はされるけど誤差に毛が生えた程度 :-( 。手元で用意できる環境ではこういったファイル転送のボトルネックはネットワーク部分であり、ここが飽和していてはサーバで何を工夫しても当然無駄である。ディスクの転送速度 << ネットワーク転送速度な環境であれば結構な効果があるとは思うが、その辺に転がっていた中古PCをちょっとサーバにしてあいてるHubに刺してあるような環境ではあんまり意味がない。
ネットワークの転送速度を無視できるように、localhostにクライアントとサーバを両方実行してテストすると、Twisted の FileSender に比べて転送速度・応答速度ともに大きく改善された。どのくらい改善されるかはファイルのサイズやファイル読み込みのブロックサイズ、並列実行されるリクエストの数によって大きく異なるが、おおむね1.3〜10倍程度にはなるようだ。
ということで、正直、それなりに本格的なネットワークアプリケーション以外ではわざわざNon-blockingなファイル転送とか考えず、 basic.FileSender を使っておくのが適切だと思う。ディスクもプラットフォームのOSもファイルシステムもどんどん賢くなっていて、単純なシーケンシャルリードぐらいではそれほど時間はかからなくなっているようだ。こんなチューニングをしている暇があったら、賢いストレージの購入予算をひねり出す算段でもしていた方が有益かも知れない。