Gevent: 优点,缺点,以及不优美的地方

jopen 10年前

原始出处:                    In the Milky way                

我不想用很多时间去描述Gevent是什么,我想它官网上的一句总结足矣:

“Gevent是一种基于协程的Python网络库,它用到Greenlet提供的,封装了libevent事件循环的高层同步API。”

接下来我将阐述在Mixpanel中一个内部项目使用Gevent的经验。 为了这篇文章我还动手写了几个性能小测试。(Whipped up这里的意思让我迷惑哎- -)

优点

首先Gevent最明显的特征就是它惊人的性能,尤其是当与传统线程解决方案对比的时候。

在这一点上,当负载超过一定程度的时候,异步I/O的性能会大大的优于基于独立线程的同步I/O这几乎是常识了。

同时Gevent提供了看上去非常像传统的基于线程模型编程的接口,但是在隐藏在下面做的是异步I/O。

更妙的是,它使得这一切透明。(此处意思是你可以不用关心其如何实现,Gevent会自动帮你转换)

你可以继续使用这些普通的Python模块,比如用urllib2去处理HTTP请求,它会用Gevent替换那些普通的阻塞的Socket操作。

当然也有一些需要注意的问题,我稍后会阐述。

接下来,见证性能奇迹的时候到了。

从上图看出

忽略其他因素,Gevent性能是线程方案的4倍左右(在这个测试中对比的是Paste,译者注:这是Python另一个基于线程的网络库)

在线程方案中,错误数随着并发连接数的增长线性上升(这些错误都是超时,我完全可以增加超时限制,但是从用户的角度来看漫长的等待和失败其实是一个妈生的)。

Gevent则是直到10,000个并发连接的时候都没有任何错误,或者说能处理至少5,000并发连接。

在这2种情况下,每秒实际完成的请求数都非常的稳定,至少直到Gevent在10,000个并发连接测试崩溃之前是如此。这一点让我感到非常的惊讶。我原本猜想的是RPS(每秒完成请求数)会随着并发的增多而下降。

线程模型在10,000并发连接测试中完全失败。我完全可以让它正常的工作(比如用一些资源优化技巧应该能做到),不过我纯粹是出于玩蛋来做这个测试的,所以没有在这上面花太多功夫。

如果这类东西能勾引起你的兴趣,我们正在招聘,你懂的。(没错,我就在内容里面混点广告宣传下。)

测试方法

这是我用来测试的Python代码:

#!/usr/bin/env python 
 
import sys 
 
def serve_page(env, start_response): 
    paragraph = '''
        Lorem ipsum dolor sit amet,
        consectetur adipisicing elit,
        sed do eiusmod tempor incididunt ut labore et
        dolore magna aliqua. Ut enim adminim veniam,
        quis nostrud exercitation ullamco laboris nisi ut aliquip
        ex ea commodo consequat.
        Duis aute irure dolor in reprehenderit in
        voluptate velit esse cillum dolore eu fugiat nulla pariatur.
        Excepteur sint occaecat cupidatat non proident,
        sunt in culpa qui officia deserunt mollit anim id est laborum.
    ''' 
    page = '''
        \<html\>
            \<head\>
                \<title\>Static Page\</title\>
            \</head\>
            \<body\>
                \
<h1\>Static Content\</h1\>
                %s
            \</body\>
        \</html\>
    ''' % (paragraph * 10,) 
 
    start_response('200 OK', [('Content-Type', 'text/html')]) 
    return [page] 
 
if __name__ == '__main__': 
    def usage(): 
        print 'usage:', sys.argv[0], 'gevent|threaded CONCURRENCY' 
        sys.exit(1) 
 
    if len(sys.argv) != 3 
        or sys.argv[1] not in ['gevent', 'threaded']: 
        usage() 
 
    try: 
        concurrency = int(sys.argv[2]) 
    except ValueError: 
        usage() 
 
    if sys.argv[1] == 'gevent': 
        from gevent import wsgi 
        wsgi.WSGIServer( 
            ('127.0.0.1', 10001), 
            serve_page, 
            log=None, 
            spawn=concurrency 
        ).serve_forever() 
    else: 
        from paste import httpserver 
        httpserver.serve( 
            serve_page, 
            host='127.0.0.1', 
            port='10001', 
            use_threadpool=True, 
            threadpool_workers=concurrency 
        )  

在客户端,我在下面这些参数下使用Apache Bench:

  • -c NUM: 这里的NUM是并发连接数。在每一个测试中这个数与服务器命令行中使用的那个数是匹配的。(译者注:这里指脚本运行时需要提供的第二个参数)

  • -n 100000: 所有的测试都需要完成100,000个请求。在上面的图中,错误率并没有统计,而是实际100,000请求中失败的请求数。

  • -r: 如果请求失败,自动重试

所有的测试包括服务端和客户端都是运行在一个低配置并且只有512MB内存的VPS上。

我最初以为我需要用一些方法来限制线程方案到一个CPU上,但事实证明就算这VPS号称是“四核”,你也就只能让一个核心到100%。就是这么蛋疼。

负载测试中所做的Linux优化

  • 当Linux处理超过500连接每秒的时候我遇到了一大堆的问题。
    基本上这些问题都是因为所有连接都是从一个IP到另一个相同的IP(127.0.0.1 <-> 127.0.0.1)。
    换句话说,你可能在生产环境中不会遇到这些问题,但几乎可以肯定的是这些问题一定会出现在测试环境中(除非你在后端跑一个单向代理)。

  • 增加客户端端口范围

    echo -e ’1024\t65535′ | sudo tee /proc/sys/net/ipv4/ip_local_port_range

    这一步将会使得客户端的连接有更多的可用的端口。没有这个的话你会很快的用尽所有端口(然后连接就处于TIME_WAIT状态)。

  • 启用TIME_WAIT复用

    echo 1 | sudo tee /proc/sys/net/ipv4/tcp_tw_recycle

    这也会优化停留在TIME_WAIT的连接,当然这种优化至少需要每秒含有同样IP对连接超过一定数量的时候才会起作用。同时另一个叫做tcp_tw_reuse的参数也能起到同样的作用,但我不需要用到它。

  • 关闭同步标签

    echo 1 | sudo tee /proc/sys/net/ipv4/tcp_syncookies

    当你看到”possible SYN flooding on port 10001. Sending cookies.”这种信息的时候,你可能需要关闭同步标签(tcp_syncookies)。在你生产环境的服务器上不要做这样的事情,这样做会导致连 接重置,只是测试的话还是没问题的。

  • 如果用到了连接追踪,关闭iptables

    你将会很快的填满你netfiler表。当然咯,你可以尝试增加/proc/sys/net/netfilter/nf_conntrack_max中的数值,但是我想最简单的还是在测试的时候关闭防火墙更好吧。

  • 提高文件描述符限制

    至少在Ubuntu上,默认每一个普通用户的文件描述符限制数是4096。所以咯,如果你想测试超过4000并发连接的时候,你需要调高这个数值。最简单 的方法就是你测试之前在/etc/security/limits.conf中增加一行类似于”* hard nofile 16384″的东西,然后运行ulimit -n 16384这条shell命令。

缺点

当然所有的事情不会这么好对吧?没错。事实上,如果有更完整的文档的话,很多我在用Gevent的问题会被解决得更好。(译者对于这类句子毫无抵抗力,凑合着看吧╮(╯_╰)╭)

文档

简单的说,这货一般般。我大概读了比文档更多的Gevent源码(这样很有用!)。事实上最好的文档就是源码目录下的那些示例代码。如果你有问题,认真的瞄瞄看它们先。同时我也花了很多时间用Google去搜索邮件列表的存单。

兼容性

这里我特别想提到eventlet。回想起来,这是有一定道理的,它会导致一些匪夷所思的故障。我们用了一些eventlet在MongoDB客户端(译者注:一种高性能文档型数据库)代码上。当我使用Gevent的时候,它根本不能在服务器上运行。

呃,使用顺序错误

在你导入Gevent或者说至少在你调用Monkey.path_all()之前启动监听进程。我不知道为什么,但这是我从邮件列表中学到的,另一点则是 Gevent修改了Python内部Socket的实现。当你启动一个监听进程,所有已经打开的文件描述符会被关闭,因此在子进程中,Socket会以未 修改过的形式重新创建出来,当然啦,这就会运行异常。Gevent需要处理这类的异常,或者说至少提供一个兼容的守护进程函数。

Monkey Pathing,抽风咩?

这么说吧,当你执行monkey.path_all()的时候,很多操作会被打上补丁修改掉。我不是很好这口,但是这样使得普通Python模块能够很好 的继续运行下去。奇怪的是,这丫的不是所有的东西都打上了这种补丁。我瞄了很久想去找出为毛的Signals模块不能运行,直到我发现是 Gevent.signal的问题。如果你想给函数打补丁,为毛的不全部打上咩?

这问题同样适用于Gevent.queue与标准的Python queue模块。总之,当你需要用到Gevent特定的API去替换标准模块/类/函数的时候,它需要更清晰(就像简单的list一样)。

不优美的地方

Gevent不能支持多进程。这是比其他问题更加蛋疼的部署问题, 这意味着如果你要完全用到多核,你需要在多个端口上运行多个监听进程。然后捏,你可能需要运行类似于Nginx的东西去在这些服务监听进程中分发请求(如果你服务需要处理HTTP请求的话)。

说真的,多进程能力的缺乏意味着为了使用中可用你又要在服务器上多加上一层东西。(译者注:真蛋疼的句子,不就是多一层Nginx或者HA么)

在使用Gevent客户端负载测试中,这真是一个大问题。我最终是实现了一个使用共享内存的多进程负载的客户端去统计以及打印状态。这需花费更多的工作量在上面。(如果有人需要做同样的事情,联系我,我会给你这个客户端脚本程序)

最后

如果你已经看到这里,你会发现我用了2个章节去阐述Gevent的缺点。不要被这些东西蒙蔽了你的眼睛。我相信Gevent对于Python网络编程来说是一种很伟大的解决方案。诚然它有各种各样的问题,但是大多数问题也仅仅是文档的缺失罢了,撑死了多用点时间嘛。

我们内部正在使用Gevent。事实上我们的服务非常的高效,以至于在我们所用的那种规格的VPS上,很容易在用光计算资源(包括CPU和内存)之前用光带宽资源。

# 这是一篇翻译的文章,原文见http://code.mixpanel.com/gevent-the-good-the-bad-the-ugly/

# 原文中由于有大量中英对照,考虑到适合阅读性,去掉了原文部分。

# 作者:In the Milky way