在GAE上使用Python 2.7的注意事项
2011 10 16 02:04 AM 5743次查看
分类:Google App Engine 标签:Python, Google App Engine
不过今天我试用了一下,发现了一些需要注意的问题,于是记录在此。
- 它目前还是个实验性质的runtime,因此还没法本地测试,必须部署到云端。2011年11月7日更新:SDK 1.6.0已经支持本地测试。
- 它只支持HR datastore,不符合条件的需要迁移数据。
- 一些库的版本变了,在app.yaml的libraries部分可以配置。
- 增加了一些C库,可以加快性能。当不确定runtime版本时,可以使用这种方式来引入:
或者判断runtime版本:try: import json except ImportError: import simplejson as json
if os.environ.get('APPENGINE_RUNTIME', None) == 'python27': import json else: import simplejson as json
- 它支持CGI和WSGI这2种handler。
CGI其实也有2种方式:- 解析os.environ,用print或sys.out.write输出响应(包括HTML头)。
- 生成一个WSGI应用,在main()函数里传递给google.appengine.ext.webapp.util.run_wsgi_app()来执行。
这种方式会缓存脚本的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)之类的方式来窥视一下。 - 解析os.environ,用print或sys.out.write输出响应(包括HTML头)。
- 它支持并发请求,这种情况下必须在app.yaml里设置threadsafe: true,并使用WSGI方式。
这里的线程安全实际上就是指不要滥用全局变量(可以使用常量)。
在非并发方式下,处理一个请求的过程中,全局变量只会被当前线程改变,因此很容易控制。而在并发方式下,全局变量随时可能被其他线程改变,因此就变得不可靠了。(注:每个线程都拥有自己的os.environ,访问它是线程安全的。)
以Doodle的hook机制为例,之前我是这样做的:
hook.py:
blog.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')
我在hook.py中使用了4个全局变量,如果在并发方式下,这种实现方式就可能会记录下错误的数据。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()
另一个要注意的是,在WSGI方式下,main()函数并不会被运行。
为了解决这2个问题,我进行了如下修改:
先说threading.local(),它会生成一个线程安全的local对象。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
假设线程1将这个对象的db_count属性设为1,线程2将其设为2,之后在获取时,它们分别会获取到1和2,而不会被其他线程覆盖。
如此一来,它就变成了线程安全的全局变量了。
再说那个WsgiApplication,它是一个WSGI应用类。
这个类的对象就是一个WSGI应用,在app.yaml中将其设为script后,处理新请求的入口就是它的__call__()方法。
因此,我把需要在main()里做的事移到它的开始部分,即可完成初始化的功能。
此外,我还能将其当成一个简单的函数来处理:
而在blog.py中还需要手动封装一下: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
application = hook.hook_app(application)
最后,有些handler可能没有import hook(例如SDK自带的),但也访问了数据库,这种情况下before_db()和after_db()仍会执行,但local的4个属性都是没有设置的。
因此,为了避免出错,还需要用hasattr()来检测这些属性是否存在。
不过这种方式下,每个线程获取到的全局变量是不一样的。有些情况下需要在各个线程之间共享全局变量,这时候就需要用锁来实现了。
例如我在YUI中使用了__app_cache这个全局变量,而在clear_expired_server_cache()中没有加锁:
这样当2个线程同时调用这个函数时,有可能重复del同一个key,这样第二次就会抛出异常了。def clear_expired_server_cache(): global __app_cache for key, value in __app_cache.items(): expiry = value[1] if expiry and time() > expiry: del __app_cache[key]
为了解决这个问题,可以先创建一个RLock全局对象:
然后在clear_expired_server_cache()中加锁:from threading import RLock __lock = RLock()
这样在调用__lock.acquire()的时候,第一个线程可以获取锁,但第二个线程就会被阻塞,直到__lock.release(),这样也就保护了__app_cache变量不会同时被多个线程访问。def clear_expired_server_cache(): __lock.acquire() global __app_cache for key, value in __app_cache.items(): expiry = value[1] if expiry and time() > expiry: del __app_cache[key] __lock.release()
因为手动加锁和解锁太麻烦,而且如果遇到异常,还有可能忘了解锁。为了避免这些问题,可以将其改成with语句:
在进入with语句时会自动调用acquire()方法,而在退出时会执行release()方法,因此也能达到目的。from __future__ import with_statement def clear_expired_server_cache(): with __lock: global __app_cache for key, value in __app_cache.items(): expiry = value[1] if expiry and time() > expiry: del __app_cache[key]
最后,如果很多函数都需要加锁的话,还能写个通用的装饰器来修饰:def thread_safe(func): def safe_func(*args, **kw): with __lock: return func(*args, **kw) return safe_func @thread_safe def clear_expired_server_cache(): global __app_cache for key, value in __app_cache.items(): expiry = value[1] if expiry and time() > expiry: del __app_cache[key]
向下滚动可载入更多评论,或者点这里禁止自动加载。