Python:通过select.epoll()检索多个URL

时间:2018-01-02 18:50:44

标签: python asynchronous python-requests urllib epoll

我有一个面向事件的服务器,它已经使用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的开源异步作业队列。)

3 个答案:

答案 0 :(得分:3)

如何将请求库(或其他urllib)与linux epoll结合使用?

不幸的是,除非考虑到这种集成而构建了这样的库,否则你无法做到。 epoll ,以及选择 / 投票 / kqueue ,其他 I / O多路复用系统调用和整个程序架构需要围绕它构建。

简而言之,典型的程序结构归结为以下

  • 一个人需要拥有一堆文件描述符(在你的情况下是非阻塞模式的套接字)
  • 系统调用(在 epoll 的情况下 man epoll_wait )阻塞,直到在一个或多个描述符上发生指定事件
  • 返回可用于I / O的描述符的信息

之后,这是外部代码处理这些描述符的工作,即计算出可用的数据量,调用一些回调等。

如果库使用常规阻塞套接字,并行化它的唯一方法是使用线程 / 进程 关于这个问题,这是一个很好的article,这个例子使用的是C,这很好,因为它更容易理解引擎盖下实际发生的事情

异步框架&请求库

让我们看一下建议的内容here

  

如果您担心使用阻塞IO,那么有很多   那些将Requests与Python之一结合起来的项目   异步框架。一些很好的例子是   请求 - 线程,问候和请求 - 期货)。

requests-threads - 使用线程

grequests - 与gevent集成(这是一个不同的故事,见下文)

requests-futures - 实际上也是线程/进程

它们都与真正的异步性没有任何关系

我应该使用select.epoll()还是python所拥有的许多异步框架之一

请注意, 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。没有使用过的广告。