关于自建适用于GAE上的web框架的构想

标签:Google App Engine

由于GAE上的web框架性能普遍不太理想,之前曾为此写过一篇《选择Google App Engine框架:慎用Django》,也去研究了一下WSGI。虽然很想自建一个web框架,但由于时间有限,只好暂时搁置,在此记录一些想法。

首先要明确的是,我的重点是提升性能,所以这个框架必须是轻巧快速的,特别是第一次加载的速度。
在JavaEE的世界,Spring算是很轻量级的框架了,但它在GAE上的表现可以用糟糕至极来形容,GAE论坛上经常有人抱怨启动时间超过20秒甚至超时。
在Python的世界,Django也已经算是轻量级的框架了,但一个完整的网站,启动时间也一般超过5秒,这仍然让我无法忍受。
于是转向更轻量级的web.py,发现它仍稍慢于webapp。

实际上CGI、WSGI和webapp这3种方式的启动时间都差不多:96ms,这就说明了有0.1秒是肯定无法避免的。
据文档描述,GAE采用的Python环境是CGI方式,但是可以重用,也就是类似FastCGI,而Python进程的启动和构造环境变量的时间大概就是这96ms了。
在此以外的时间,就是加载框架的代码所消耗的了。即便是web.py,这个时间也达到了60ms左右。
不过我用cProfile测试了一下wsgiref.simple_server.make_server()处理一次请求的速度,测试5次取平均值的结果:启动server约需要1.5秒,直接WSGI共需1.602秒,web.py是1.668秒,webapp是1.704秒,Tornado是1.769秒。
由于webapp实际上和直接采用WSGI的性能相当,因此这个结果表明webapp肯定是被Google优化过,测试时发现是被预加载了。而自己写的框架肯定不会被预加载,所以要超越webapp的性能的确得花费一番工夫。
我另外还做了个测试,含4个页面,5个模板,使用pyTenjin作为模板系统,不含任何数据库和memcache操作。分别使用3种方式实现:webapp,一个几分钟内编写出来的极度简化的WSGI框架(连url mapping都不支持正则表达式),同样也是几分钟写出来的CGI框架(直接print);不过后2者实现了延迟加载。
结果第一次访问首页,webapp是135 cpu ms,后2者都是116 cpu ms;在响应时间方面,CGI要强于WSGI和webapp;在后续的页面访问中,webapp占据一定优势,使用延迟加载的WSGI和CGI要稍逊一筹;但访问已缓存的页面,各种框架都差不多;使用Firebug测试加载速度,第一次访问首页都是450ms左右,后续页面稳定在350ms左右,各方式相差不到10ms,但能看出CGI最佳,WSGI次之,webapp最后;使用Load Impact进行测试,性能都差不多,10客户端约330ms,50客户端约250ms。
看来自建框架还是可以达到不错的性能的,只是不能太臃肿了。

其次,框架应该只有必要的功能,不要多余的功能。
这实际上也是为了提升性能,不过更重要的是给人一种纯净的感觉。
像Django,拥有很多强大的功能,虽然可以避免用户的重复劳动,但需要用户的学习,而且大部分功能是用不到的。
再如很多框架是MVC模式的,实际上对GAE来说并没什么用。首先说模型,GAE的模型不是关系型的,我也不喜欢自动生成的代码,因此Django和web2py的ORM对我来说根本没用,加载它们反而拖慢了速度。其次是视图,几乎每个框架实现了一个自己的模板,而使用者却有自己的选择,像我就偏重于性能和可以直接写Python代码这2点,而无视其他复杂的功能。
因此,我觉得GAE上的框架主要是实现一个控制器的功能,将请求转交给处理函数,然后将生成的数据传给用户设定的模板处理函数,或直接输出即可。因此重点就是url mapping了。

再次,url mapping有多种形式,这里也需要取舍。
像webapp和Tornado这种只需要定义处理类,就会自动根据方法类型来调用,这样很容易理解。但需要预加载所有的处理函数,对于大型网站会很影响性能。不过Facebook也用Tornado,毕竟它不用考虑冷启动的问题,只要保证后续访问很快即可。
像Django这种就过于强大,无需预加载所有的处理函数,可以传递参数等。辅助的功能暂且不说,至少对性能提升是有帮助的,而且便于重用。只是必须指定处理函数,而不能自动根据方法类型来调用。
而web.py相当于前2者的结合,感觉比较合我口味。
至于Pylons这种rails风格的,我并不欣赏这种死板的url格式;而且区分什么controller和action之类的只会增大学习负担,一个看一眼就会用的才是王道。

最后就是细节了。
使用webapp会觉得request和response对象非常麻烦,而区分它们在很多时候并没必要。例如输出需要写self.response.out.write()那么长一串,而用self.write()代替将会快捷很多;再如request的GET和POST字典,实际上response对象不可能有这2个对象,所以直接给self也是合理的。
此外,处理函数还需要做一些hook,方便用户在处理响应前和处理后各作一些工作。对于一部分url,应该可以在url mapping时指定一个共同的包装器,避免重复劳动;甚至还可以弄全局的包装器。
在缓存方面,还需要提供一些包装器来。经我测试,app cache很有用,而memcache占用的时间比较长(甚至超过生成时间),因此页面一般不适合使用后者做缓存。
另外,Tornado的输出Etag功能很不错,无需生成响应数据也可以节省很多时间。但是我认为这不应该默认开启,而必须由用户设定,在url mapping时或处理函数里指定是否使用。其实最简单的方式就是self.enableEtag = True,然后输出时判断一下即可。

大致就那么多了,以后还有想法再补上,先去睡觉=。=

12条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?