我有一个面向事件的服务器,它已经使用select.epoll()。
现在应该解决一个新要求:应该获取URL(异步)。
到目前为止,我总是使用请求库,我总是使用它同步,从不异步。
如何将请求库(或其他urllib)与linux epoll结合使用?
请求库文档有关于此的说明,但是只提到了异步框架(不是select.epoll()):http://docs.python-requests.org/en/master/user/advanced/#blocking-or-non-blocking
我没有和select.epoll()结婚。它一直在努力。如果可行,我可以使用不同的解决方案。
背景:更大的问题是“我应该使用select.epoll()还是python所拥有的众多异步框架之一?”。但StackOverflow的问题不能过于宽泛。这就是为什么这个问题集中在“通过select.epoll()检索多个URL”的原因。如果你有更大问题的提示,请发表评论。
如果您感到好奇,我在业余时间开发的小项目需要这个问题:https://github.com/guettli/ipo(IPO是一个基于PostgreSQL的开源异步作业队列。)
答案 0 :(得分:3)
不幸的是,除非考虑到这种集成而构建了这样的库,否则你无法做到。 epoll ,以及选择 / 投票 / kqueue ,其他 I / O多路复用系统调用和整个程序架构需要围绕它构建。
简而言之,典型的程序结构归结为以下
之后,这是外部代码处理这些描述符的工作,即计算出可用的数据量,调用一些回调等。
如果库使用常规阻塞套接字,并行化它的唯一方法是使用线程 / 进程 关于这个问题,这是一个很好的article,这个例子使用的是C,这很好,因为它更容易理解引擎盖下实际发生的事情
让我们看一下建议的内容here
如果您担心使用阻塞IO,那么有很多 那些将Requests与Python之一结合起来的项目 异步框架。一些很好的例子是 请求 - 线程,问候和请求 - 期货)。
requests-threads - 使用线程
grequests - 与gevent集成(这是一个不同的故事,见下文)
requests-futures - 实际上也是线程/进程
它们都与真正的异步性没有任何关系
请注意, epoll 是特定于Linux的野兽,它不起作用,即OS X上有一种不同的机制叫做 kqueue 。由于您似乎正在编写通用作业队列,因此它似乎不是一个好的解决方案。
现在回到python。您有以下选择:
threads / processes / concurrent.futures - 由于您的应用是典型的C10K服务器
,因此您的目标不太可能epoll / kqueue - 你必须自己做所有事情。在获取HTTP URL的情况下,您不仅需要处理http / ssl,还需要处理异步DNS解析。另外,请考虑使用提供一些基本基础架构的asyncore []
扭曲/龙卷风 - 基于回调的框架已经为你做了所有低级别的东西
gevent - 如果你要重用现有的阻塞库(urllib,请求等)并同时使用python 2.x和python 3.x,这可能是你想要的。但这个解决方案是设计上的黑客攻击。对于你的大小的应用程序,它可能没问题,但我不会将它用于任何更大的应该坚如磐石并且在生产中运行
<强> ASYNCIO 强>
此模块提供编写单线程的基础结构 使用协同程序的并发代码,通过套接字多路复用I / O访问 和其他资源,运行网络客户端和服务器等 相关原语
它拥有您可能需要的一切。 还有一些库与流行的RDBM和http一起工作 https://github.com/aio-libs
但它缺乏对python 2.x的支持。有ports asyncio to python 2.x但不确定它们的稳定性
所以如果我能牺牲python 2.x我会亲自去asyncio&amp;相关图书馆
如果你真的需要python 2.x,请根据所需的稳定性和假定的峰值负载使用上述方法之一
答案 1 :(得分:2)
在进行高性能开发时,我们总是根据自己的情况选择武器。所以它仍然太宽泛而无法回答。
但是你更大的问题是更简单的问题。只有IO绑定程序才适合异步。
epoll和异步的目的是什么?避免CPU等待IO并且什么都不做.CPU等待IO块,IO块因为没有数据读取或没有写入空间。
引入缓冲区以减少系统调用。当您在流上调用read时,实际上是从缓冲区中读取的。(概念,不太准确)
选择或epoll是非阻塞繁忙的轮询(通过中断底层的epoll实现)。就像下面的内容一样
while true {
for i in stream[]{
if i has data
read until unavailable
}
}
这是愚蠢的,所以有选择和epoll。
每次从缓冲区读取时,都有数据等着你,它是高速IO,然后epoll / select是你最好的选择。当缓冲区总是空的时候,它是一个缓慢的流,IO绑定,异步非常适合这个情况。
我不太清楚异步,对我而言,这只是内部的软中断和大量的回调。
答案 2 :(得分:1)
上面的要点是正确的,从技术上讲,您无法通过针对多路复用I / O的阻塞调用来执行此操作,例如select()
,epoll()
和BSD / iOS,Windows变体。这些调用允许超时规范,因此您可以在短时间间隔内重复轮询,然后将工作传递给主线程的异步处理程序。在这种情况下,读取是在主线程上完成的,多次读取可以表示它们已准备好,主线程主要用于该任务。
如果您的问题规模小到中等,那么任何事情都不会超过epoll()...read()
甚至select()...read()
。如果您的问题(读取通道的数量)偏小。因此,我鼓励您考虑一下 - 从主线程中获取尽可能多的工作,这些工作可以用于请求。
如果您正在寻找异步解决方案,那么您最好的选择之一就是grequests
库,这两个库都是为了便于使用和提高性能。要了解,请运行以下客户端 - 服务器对。请注意,龙卷风的使用在这里是无关紧要的,只在服务器端,而您关心的是客户端。
试试这个 - 性能差异是白天和黑夜。
您的解决方案由下面的client.py类表示;它使用grequests
异步发出get()
个请求。
<强> server.py 强>
from tornado import (httpserver, options,
ioloop, web, gen)
import time
import ujson as json
from collections import defaultdict
class Check(web.RequestHandler):
@gen.coroutine
def get(self):
try:
data = int(self.get_argument('data'))
except ValueError:
raise web.HTTPError(400, reason='Invalid value for data')
delay = 100
start = time.time()
print('Processed: {!r}'.format(data))
yield gen.Task(ioloop.IOLoop.instance().add_timeout, start + delay / 1000.)
self.write('.')
end = time.time()
self.finish()
if __name__ == '__main__':
port = 4545
application = web.Application([
(r'/get', Check)
])
http_server = httpserver.HTTPServer(application)
http_server.listen(port)
print('Listening on port: {}'.format(port))
ioloop.IOLoop.instance().start()
<强> client.py 强>
import grequests
from tornado.httpclient import HTTPClient
import time
def call_serial(num, httpclient):
url = 'http://127.0.0.1:4545/get?data={}'.format(num)
response = httpclient.fetch(url)
print('Added: {!r}'.format(num))
def call_async(mapper):
futures = (grequests.get(url) for url,_ in mapper)
responses = grequests.map(futures)
for response, (url,num) in zip(responses, mapper):
print('Added: {!r}'.format(num))
def check(num):
if num % 2 == 0:
return False
return True
def serial_calls(httpclient, up_to):
for num in range(up_to):
if check(num):
call_serial(num, httpclient)
def async_calls(httpclient, up_to):
mapper = []
for num in range(up_to):
if check(num):
url = 'http://127.0.0.1:4545/get?data={}'.format(num)
mapper.append((url,num))
call_async(mapper)
if __name__ == '__main__':
httpclient = HTTPClient()
print('SERIAL CALLS')
serial_calls(httpclient, 100)
print('ASYNC CALLS')
async_calls(httpclient, 100)
httpclient.close()
这是一个真正的异步解决方案,或者尽可能接近CPython / python。没有使用过的广告。