gae使用小提示

转眼间接触Google App Engine已将近2年了,经常看到有人问重复性的问题,于是在此总结一下。
由于Google App Engine一直处在不停的变化之中,本文中阐述的仅仅是当前的现状,未来如何尤未可知,但我会尽量保持更新本文内容。
此外,本文只是我自己的观点,有不同看法的欢迎留言提出。

入门


·         什么是**Google App Engine**
Google App Engine
(以下简称GAE)是Google提供的云计算平台,属于PaaS
目前GoogleGAE提供了Python2.52.7)、JavaGo3种运行环境,以及datastoreurlfetch等各种服务,以帮助开发者构建一个WEB应用。
此外,Google还提供了一些免费配额,你可以不花分文来用它搭建消耗不是很大的应用。
·         GAE**可以做什么,不能做什么?
正如上文所述,GAE可以用来构建WEB应用,举个典型的例子来说就是做网站。
它是GoogleHTTP为基础搭建的平台,因此大部分的服务都是基于HTTP的,其余服务(如XMPP和收邮件等)也是由GAE转换成HTTP请求来处理的,所以你不能以HTTP以外的方式来使用GAE(例如socket)。
此外,GAE还存在不能改写本地文件、进行系统调用和使用C库等安全限制,因此与之相关的库和函数都被禁用了。
·         如何绑定自己的域名到GAE上?
可以查看官方文档的描述,简述如下:


1.   将你的域名注册为Google Apps域名。
2.   登录GAE控制面板,进入想绑定的应用,在Application Settings页点“Add Domain…”按钮,将你的Google Apps域名填上(注意不是被绑定的子域名),继续点“Add Domain…”按钮。
3.   你会被跳转到Google Apps,在这里将子域名填上,然后CNAMEghs.google.com即可完成绑定。
限制:


4.   ghs.google.com经常被GFW,大陆无法正常访问。需要使用反向代理来绕过GFW的限制,有钱人可以自己买VPS做反向代理,没钱的可以用CloudFlare
5.   不能绑定裸域(即ooxx.com这种形式的域名),而只能绑定子域(即www.ooxx.com这种形式的域名)。你可以将裸域重定向到子域,也可以用反向代理。
6.   自定义域名暂时不支持HTTPS
·         如何与GAE support team联系?
填写Billing Support Request表单即可,详见《和GAE support team联系的方式》
·         遇到急需解决的bug怎么办?
去提交一个Production issue
·         如何开始学习使用GAE
直接看官方的使用入门文档:PythonJavaGo,然后再看Google所提供的服务使用方式即可。
·         尽量阅读英文文档。
你读完入门文档后,就要以英文文档为主了,因为中文文档更新缓慢(有的已几年没更新了),很多信息都过时了。
·         尽量使用Python 2.7
Python
GAE自诞生时就支持的语言,一直以来都更被GAE团队重视,新功能优先支持Python,使用起来更为简单,开发更为快捷。而Python 2.7相比2.5多了一些功能,提升了性能和并发,应当优先采用。
Java
作为GAE较晚支持的语言,直到近半年才慢慢跟上Python的更新速度,但例子和文档仍经常晚于Python,配套工具也少于Python(例如上传下载数据仍只能使用PythonBulkLoader),这也和Java开发更为繁琐有关。此外,Java还存在启动时间过长的问题,并且比Python占用更多的内存。
对于WEB应用最为重要的数据库操作,虽然GAE/Java也提供了JDOJPA2种标准方式,但实际上与标准实现不同,直接套用可能会遇到陷阱,并且存在性能问题,且无法像Python一样使用动态类型和动态属性。
因此,如果没有特殊需求(例如依赖一些无法替代的Java库,或者对运算性能要求非常苛刻,或者很难找到足够多合适的Python程序员),那么我强烈推荐使用Python
下文也会以Python为主来阐述。
GAE
Go的支持还属于实验性质,很多服务是不可用的,但它的开销和性能比PythonJava都有优势。如果你是Go语言的学习者,这也是个不错的试验田,其他人还是算了。

提示


·         关于应用和版本。
应用由appid来唯一标识,目前要求只能为字母、数字和连字符,不以连字符开头和结尾,长度介于630之间,不包含“google”,并且不与任何Google账号用户名相同(除非你用这个账号来创建)。
一个应用可以有10个版本,其中只有1个可以设为默认版本,这个版本可以直接以appid.appspot.com或被绑定的域名来访问,其他版本只能以version.latest.appid.appspot.com来访问。
版本号也是个字符串,可以包含字母、数字和连字符。
一个应用的各个版本可以使用不同语言(即可以同时用PythonJavaGo),它们之间共享一个datastorememcache,但task queuecron是不共享的,也不能调用其他版本的接口(除非使用urlfetch)。
·         避免部署时导致暂时无法访问。
部署应用的时候,GAE会停止该版本的所有instance,如果此时有人访问,可能会遇到500错误。
如果想避免出现这种情况,可以部署到另一个版本,等待部署完毕后,再将新部署的版本设为默认版本。
GAE
团队正在解决这个问题,未来应该就不需要自行处理了。
·         慎用wsgiref.handlers.CGIHandler(只针对Python 2.5**)。
它在初始化请求时没有重设环境变量,确保你使用的是webapp.util.runwsgiapp(application)
·         使用**logging**
Python
提供了一个很好的logging库,你可以在控制面板里看到由它记录的信息,方便跟踪和调试,并且它是免费的。
Java还需要做些配置。
·         选择好用的**IDE**
对于Python开发者,我推荐PyCharm,不过它只能试用30天,不是免费的。没钱的话可以试试Pydev,不过相对于前者实在逊色太多了。
对于Java开发者,你可以试试Google Plugin for EclipseIntelliJ IDEA。(后者虽然有免费版本,但收费的Ultimate版才能更好地支持GAE开发。)

设计


·         以数据库设计优先。
Web
应用基本上都是围绕数据库的,因此它是设计的核心。
Datastore是非关系型数据库,如果你的想法不能用它高效地实现,那么就考虑换个折中的办法,或者放弃这个想法,或者放弃使用GAEGAE并不是万能的)。
·         性能是第二重要的需求。
不管用户有没有特别说明,他们都是非常看重响应时间的。
如果你的应用几秒钟才能响应一次用户请求,即使功能做得再好,界面做得再美,他们也顶多停留数分钟,了解完他们需要的信息,然后遗憾地关掉窗口。
而如果你的应用就和打开本地文件一样快,不管你的应用本身是多么无趣,用户也会愿意去发掘一些有趣之处。
功能可以慢慢完善,但如果一开始就给人很慢的印象,你是很难挽回这些用户的。就好像我用Chrome浏览器,尽管刚推出时bug不断,只有最基本的浏览网页的功能,但我却对它的速度和简洁的体验而上瘾了;而反观Firefox,它拥有我需要的所有功能,可就是太慢了;于是在我淘汰IE浏览器后,Firefox仍然处于冷宫。我想和我有相同想法的用户应该不少,毕竟Chrome相对于Firefox也就那么屈指可数的几个长处。
一个比较通用的指标是,一般的页面都应该在1秒内响应(并最好载入用户可视的主要部分),而响应用户提交的表单应在5秒内。
·         最简化需求。
不要野心勃勃地想去实现各种各样的功能,而不顾实际的需求。你能想出一个点子,不代表这个点子是用户的需求。
即便是用户的需求,也要考虑是否有实现的必要,特别是与现有实现相冲突,或可能影响性能时。举例来说,你在做一个网页游戏,你的设计重心应该是游戏本身的操作体验上。如果用户反馈说想搜索聊天记录,可你甚至根本就没保存聊天记录;这种情况下,你当然可以花大力气去为他实现这个功能,但是这对游戏体验有什么帮助?
所以在设计时要去掉任何可有可无的功能,确认剩下的是用户最急需的功能,并优先完善这些最基本的功能。
你可以自己想想,Twitter拥有这么大的用户群,它接收到的用户反馈想必是不计其数的,可为什么它仍然只有那么简单的功能,甚至连发图都不行?(好吧,现在行了)
·         针对一类用户设计。
假设你的应用可以同时满足单用户博客、多用户博客和微博,那么我敢保证这个应用要么效率不高,要么功能不足。
不同的规模需要不同的需求,在单用户博客中可能不需要过多考虑并发,简单的实现可以做到最快的速度;但微博中不得不考虑扩展性和并发性的问题,上千人和百万人的解决方案是不一样的,不能不考虑用户规模就去设计。
因此,不要试图去满足所有人,而要针对一类用户去设计,哪怕因此发布多个版本。
·         将对数据库的操作和模型定义放在一起。
作为一个习惯,大家一般都会将所有的模型定义都写在model.py
而模型在定义时是可以写自定义方法的,尽可能把操作都用实例方法、类方法和静态方法来实现。对于可能会用deferred库调用的方法,也可以作为model.py中的函数。
这样的好处是在实现功能时,凡是涉及数据库的操作,都能在model.py里找到,这样无疑很利于重用。而如果找不到,就要重新考虑设计是否正确,是否需要为此添加方法了。

性能


·         尽可能少调用**RPC**
对于绝大多数的应用来说,RPC调用占据了80%以上的响应时间,因此减少它们将会明显加快响应速度。
其中datastore调用是最频繁而又昂贵的,大部分的超时都是由数据库引起的,因此能少使用就少使用。对于获取不太重要的内容,可以设置一个超时时间,超过后就忽略。
Memcache
的响应时间基本上只是网络延迟(毫秒级),使用get_multi()get()几乎是同样快的,因此可以用get_multi()来取代多次调用get()
还有一个容易被忽略的RPCUsers服务,create_login_url()create_logout_url()函数都会产生RPC调用(毫秒级)。因此可以生成一个固定的登录链接,在这个链接对应的页面里再调用这2个函数,然后将用户重定向到登录页面,返回时则可以取referer头字段。
·         使用异步**RPC**
部分RPC服务,如datastorememcacheurlfetch已提供异步API,尽可能使用它们来并行执行。不过异步导致逻辑更为复杂,设计时需要更多考虑。
·         正确使用缓存。
目前GAE上读取数据的速度是:内存 > 文件 > memcache > datastore > urlfetch,相邻2者之间的速度基本上是数量级的差距。
由此可见,对于基本无需改动的配置,放在文件里是最好的;如果要在线更改配置,则可使用memcache + datastore的方式实现。
此外,内存也是一个很重要的缓存,GAE称之为App Caching。它的意思是一个instance响应完一个请求后,main函数所处环境的全局变量(包括import的模块)仍然会保存在内存中;在接到下一个请求时,这些全局变量是可以直接重用的。
因此,对于无需更改的配置、URL映射、编译过的模板和正则表达式,是可以直接放在App Caching里的。但是如果是会经常变化的数据,必须注意App Caching是不跨instance的,只能更改当前instance的缓存,而无法让所有instances同步更改。唯一能解决的方式是部署一次,但这会导致停掉所有instances
使用memcache时需要注意粒度,尽可能同时保存相关数据,使用get_multi()来一次性获取多个值,并注意不要缓存没有必要缓存的数据。
此外,GAE上还存在一个反向代理,它可以节约请求数和大量流量,但是存在一些bug。如果要用于动态页面,请注意不要设置过长的缓存时间,也不要输出对需要区分用户的页面(例如有的页面只能给已登录用户或管理员查看)。最有效且基本没有弊端的用途是缓存不会更改的图像、重定向响应和Not Found页面。
·         **task queue**分离写操作。
用户进行GET请求时并不关心你后台的数据是否立即更新,而为了计数去更改一个实体可能是很耗时的。如果用task queue去更改实体,响应逻辑继续执行,就能节省这段时间。
·         采用**AJAX**
如果页面中有比较耗时而又不太重要的部分,把它们分离出去,用AJAX请求来载入这些数据。
使用jQueryAJAX库可以很轻易地完成这些任务。
但要注意为未开启JavaScript的用户和搜索引擎提供一个链接,确保他们能获得这部分数据。
·         选择轻量级框架。
提到Python Web框架,大多数人第一反应就是Django,但DjangoGAE上存在性能问题
如果你没钱开启always on的话,还是慎用它比较好。
仔细想想你会发现,实际上Django提供给你的好处(说实话我只觉得表单和admin有用),并不需要花太大力气就能实现;而且由于是你自己实现的,你不会受到任何限制,并且很难掉入陷阱。
·         正确使用**Appstats**
Appstats
是使用memcache来保存响应记录的,一次响应通常会多占用20msCPU时间,会对响应时间稍微造成一些影响。
如果不在乎这20ms,启用它当然没问题。但如果你的QPS特别大,那就不得不考虑钱的问题了。
我一般只在性能出现问题,需要进行调查时才启用,毕竟平时我也不会去看这些数据。
·         使用**Python 2.7取代2.5**
Python 2.7
可以使用C库和多线程,还能并发处理动态请求,不过要注意保证线程安全

数据库


·         新应用建议使用**High Replication Datastore,不要使用Master/Slave Datastore**
后者将被摈弃,选择前者可以避免未来迁移的麻烦。
·         采用较短的应用**ID、类型名和属性名,尽量少用ReferencePropertyUserProperty**等较大的属性。
原因可参见《记录一下GAE上各种属性所占的空间大小》
·         尽可能使用**Model,而不是ExpandoPolyModel**
ExpandoModel的反序列化更花时间
,而PolyModel会多生成一个class属性(也就意味着更多索引和复合索引)。
·         **ReferencePropertykey时,可以用ReferenceProperty.get_value_for_datastore**方法来避免访问数据库。
如果你使用ReferenceProperty,可能经常会遇到取多个实体的ReferenceProperty的情况。如果直接获取,每个实体都会造成一次数据库。可以get_value_for_datastore方法来避免自动解引用,然后一次db.get()来获取所有引用的实体。
·         不要以关系数据库的观点来使用datastore**
很多人会犯这个错误,把datastore当成关系数据库来用,用ReferenceProperty来联系各个模型之间的关系。
实际上由于不能进行join,采用这种建模方式并不能避免一次数据库访问。
而且ReferenceProperty是个很大的属性,我更推荐直接保存key nameid,然后自己维护实体关系。
·         使用实体组来对实体关系建模。
实体组也维护了一种实体关系,并且这种关系不像ReferenceProperty一样可以更改。
子实体或它的key可以很容易地获取父实体的key,而无需访问数据库。父实体可以通过祖先查询来获取子实体,而且这种查询性能很快,并可用于事务中。
一个实体组的所有实体都可以在一个事务中直接进行更改,而无需使用分布式事务。
注意实体组的改写不能太频繁,必须保证每秒修改不超过5次(这是极限情况,一般不要超过1次),否则会经常冲突。将实体组粒度保持为一个用户可以很好地满足这一限制(简单来说,即以用户为根实体)。
·         使用ListProperty对实体关系建模。
ListProperty
可以实现一对多关系,只要在一个实体aListProperty里保存关联实体(b1b2bn)的key即可。
a获取b,直接将aListProperty传给db.get()即可;由b获取a,只需在查询时指定这个ListProperty等于b即可。
限制就是一个ListProperty最多只能包含5000个元素,一个实体最多有5000个被索引的属性(ListProperty中每个元素都算作一个被索引的属性),并且要小心索引爆炸。
·         使用分布式事务。
Datastore
的事务要求只能对一个实体组进行改写,采用分布式事务可以突破这个限制。
·         使用key来减少一个属性。
每个实体都有一个不重复的key,而需要保持唯一性的属性可以用它来取代,其中数字id可以用db.Key.from_path()来生成key
使用key的好处除了保持唯一,还有性能上的因素:db.get()操作比query快很多(数量级的差异),而且可以批量get实体。
此外,减少了一个属性,还能减少2条索引,这也节省了不少空间。
缺点就是不能更改,排序和不等操作可能不适合,并且倒序查询需要创建一条索引。
·         索引越少越好。
很多人会奇怪,为什么自己的实体才几M或几十M,但是配额里却显示用了几百M甚至上G的空间。
实际上,datastore在默认情况下会把所有属性都进行索引,并对涉及多个属性且含不等或排序的查询使用复合索引,这些索引比属性本身所占的空间要多几倍。
并且复合索引是不能重用的,2个查询的filterancestorsort必须精确匹配才能共用一条复合索引,否则需要分别构建一条。
如果有些属性是无需查询的,请把它设为indexed=False
·         使用merge join来取代复合索引。
Merge join
是指对多个属性进行查询时,如果只用了相等和祖先查询,而没有使用不等或排序,就无需创建复合索引,而能直接将各个属性的查询集进行join。这会减少数据库空间占用,并加快写入操作。
因此请尽量考虑无需不等和排序的查询。有些时候用户对是否排序并不敏感,而且你也可在内存中排序。
但要注意结果集不能太大,否则也会影响响应时间。将结果集较小的filter放在前面,会加快响应时间(因此像是否有效等基本上为True的属性,就尽量放到后面去吧;但如果反过来查为False的,则放到前面更好)。
·         实体其实是个字典对象**
做维护等比较耗时的数据库操作,或需要很高的动态性时,可以采用实体来取代模型对象。
·         尽量不要使用**GQL**
QueryGqlQuery的创建快很多
,而且构造起来更灵活(至少你可以把Query对象传给一个函数,在这个函数里增加filtersort等),并且不存在GQL注入的问题。
只有在需要保持与SQL的兼容性(例如你的项目可能会移植到非GAE平台),或者想用JavaScript直接执行数据库查询时才使用(后者慎用,存在安全问题)。
·         自动生成的**id**不保证连续和递增,也不一定唯一。
Datastore
只保证自动生成的实体key是唯一的,id只要不冲突,可以随机选择生成任何一个id
key唯一指的是keypath唯一,这个path是包含appidnamespace、父实体的key、该实体的类型名和id(或key name)的。对于没有父实体的实体(根实体)来说,同一个类型是不会存在2id相同的实体的;但是如果有父实体,那么同一个类型可以存在多个id相同的实体,只要它们的父实体或类型名不同。
·         更改模型。
当需要更改一个模型的属性时,可以使用map-reduce来批处理;也可以将模型的父类改成Expando,利用动态属性来处理,完成后再换回Model
·         添加复合索引时,不要同时部署代码。
添加复合索引是需要很长一段时间的(基于你要索引的实体数),直到索引状态为“Serving”时,你才能使用它。因此,为了避免程序出错,应该先更新索引,等生效后再部署代码。或者部署到另一个版本,等生效后再设置成默认版本。
如果你的索引长期停留在“Building”状态,可以联系GAE support team,叫他们帮忙恢复;或者提交一个Production issue
·         分页。
由于datastore提供的fetch方式并不高效,当偏移量很大时,需要获取很多无用的实体,因此一般是不推荐的。
通常的做法是对__key属性或time属性进行排序,以此来作为分隔点。缺点是占用了一个不等于操作,并因此无法使用merge join,而且无法用页数来定位。
目前datastore已支持游标,无需占用不等于操作,缺点同样是是无法用页数来计算游标,且对用户输出游标显得不太美观。

安全


·         不要信任任何由用户生成的数据。
这些数据包括用户提交的表单和HTTP headers(如URLuser agentcookie等)。
切记处理它们时要考虑不完整和错误的情况(例如从字典中取一个值要注意key可能不存在的问题),对外输出时要进行正确的编码(例如URL要进行百分号编码,XMLHTML要进行实体编码,JSON要进行JSON编码)。
处理用户提交的HTML时,切记审核它们:移除不支持或冲突的标签和属性,关闭不完整的标签等,此外还得特别注意CSS样式和JavaScript代码。
·         审核用户权限。
不要让用户访问不该获取的资源,不要让任何人都能执行任意关于数据库的代码(特别是使用JavaScript时)。
特别注意在使用缓存时,如果资源是因人而异的,不要让它保存在代理服务器的共享缓存中(也就是确保Cache-Controlprivate,或包含Vary字段)。
·         捕捉所有异常。
保证出错时会输出对一个用户友好的页面,而不是异常栈的出错情况。
同时用logging.error()来记录异常栈,以跟踪排除出错原因。