在GAE上使用Python 2.7的注意事项

随着 GAE SDK 1.5.5版的发布,开发者终于可以使用Python 2.7了。
不过今天我试用了一下,发现了一些需要注意的问题,于是记录在此。

  1. 它目前还是个实验性质的runtime,因此还没法本地测试,必须部署到云端。
  2. 它只支持HR datastore,不符合条件的需要迁移数据。
  3. 一些库的版本变了,在app.yaml的libraries部分可以配置。
  4. 增加了一些C库,可以加快性能。当不确定runtime版本时,可以使用这种方式来引入:
    try:
    	import json
    except ImportError:
    	import simplejson as json
    或者判断runtime版本:
    if os.environ['APPENGINE_RUNTIME'] == 'python27':
    	import json
    else:
    	import simplejson as json
  5. 它支持CGI和WSGI这2种handler。
    CGI其实也有2种方式:
    1. 解析os.environ,用print或sys.out.write输出响应(包括HTML头)。
    2. 生成一个WSGI应用,在main()函数里传递给google.appengine.ext.webapp.util.run_wsgi_app()来执行。
    使用CGI方式时,app.yaml的script写这个脚本的路径,例如main.py。
    这种方式会缓存脚本的main()函数,在处理后续的请求时,将直接执行main()函数。

    WSGI方式和CGI的第2种方式很像,只不过生成WSGI应用后,不需要自己运行。并且__name__ == '__main__'也是不成立的,main()函数也不会被缓存。
    而在app.yaml的script里写的是这个应用的路径,例如main.application(必须为模块的全局变量)。
    一些内置的handler可以用builtins来开启,另一些则需要修改,例如“$PYTHON_LIB/google/appengine/ext/admin”要改成“google.appengine.ext.admin.application”。这些应用名一般都是用application,具体情况可以查看SDK源码。

    实际上WSGI方式使用的是/base/python27_runtime/python27_lib/versions/1/google/appengine/runtime/wsgi.py这个脚本,可惜SDK中并没有包含它,只能用dir(google.appengine.runtime.wsgi)之类的方式来窥视一下。
  6. 它支持并发请求,这种情况下必须在app.yaml里设置threadsafe: true,并使用WSGI方式。
    这里的线程安全实际上就是指不要滥用全局变量(可以使用常量)。
    在非并发方式下,处理一个请求的过程中,全局变量只会被当前线程改变,因此很容易控制。而在并发方式下,全局变量随时可能被其他线程改变,因此就变得不可靠了。

    以Doodle的hook机制为例,之前我是这样做的:
    hook.py:
    request_arrive_time = 0
    db_count = 0
    db_time = 0
    db_start_time = 0
    
    def before_db(service, call, request, response):
    	global db_count, db_start_time
    	db_count += 1
    	db_start_time = time()
    
    def after_db(service, call, request, response):
    	global db_time
    	dt = time() - db_start_time
    	db_time += dt
    
    apiproxy_stub_map.apiproxy.GetPreCallHooks().Append('before_db', before_db, 'datastore_v3')
    apiproxy_stub_map.apiproxy.GetPostCallHooks().Push('after_db', after_db, 'datastore_v3')
    blog.py:
    def main():
    	hook.db_count = 0
    	hook.db_time = 0
    	hook.db_start_time = 0
    	hook.request_arrive_time = time()
    	util.run_wsgi_app(application)
    
    if __name__ == '__main__':
    	main()
    我在hook.py中使用了4个全局变量,如果在并发方式下,这种实现方式就可能会记录下错误的数据。
    另一个要注意的是,在WSGI方式下,main()函数并不会被运行。

    为了解决这2个问题,我进行了如下修改:
    class WsgiApplication(yui.WsgiApplication):
    	def __call__(self, environ, start_response):
    		local.request_arrive_time = time()
    		local.db_count = 0
    		local.db_time = 0
    		local.db_start_time = 0
    		return super(WsgiApplication, self).__call__(environ, start_response)
    
    
    local = threading.local()
    local.request_arrive_time = 0
    local.db_count = 0
    local.db_time = 0
    local.db_start_time = 0
    
    def before_db(service, call, request, response):
    	if hasattr(local, 'db_count'):
    		local.db_count += 1
    	else:
    		local.db_count = 1
    	local.db_start_time = time()
    
    def after_db(service, call, request, response):
    	if hasattr(local, 'db_start_time') and hasattr(local, 'db_time'):
    		dt = time() - local.db_start_time
    		local.db_time += dt
    先说threading.local(),它会生成一个线程安全的local对象。
    假设线程1将这个对象的db_count属性设为1,线程2将其设为2,之后在获取时,它们分别会获取到1和2,而不会被其他线程覆盖。
    如此一来,它就变成了线程安全的全局变量了。

    再说那个WsgiApplication,它是一个WSGI应用类。
    这个类的对象就是一个WSGI应用,在app.yaml中将其设为script后,处理新请求的入口就是它的__call__()方法。
    因此,我把需要在main()里做的事移到它的开始部分,即可完成初始化的功能。

    此外,我还能将其当成一个简单的函数来处理:
    def hook_app(app):
    	def wrap(environ, start_response):
    		local.request_arrive_time = time()
    		local.db_count = 0
    		local.db_time = 0
    		local.db_start_time = 0
    		return app(environ, start_response)
    	return wrap
    而在blog.py中还需要手动封装一下:
    application = hook.hook_app(application)

    最后,有些handler可能没有import hook(例如SDK自带的),但也访问了数据库,这种情况下before_db()和after_db()仍会执行,但local的4个属性都是没有设置的。
    因此,为了避免出错,还需要用hasattr()来检测这些属性是否存在。

你可能感兴趣的:(python,GAE,2.7)