目录

Google App Engine Tips

由于Google App Engine一直处在不停的变化之中,本文中阐述的仅仅是当前的现状,未来如何尤未可知,但我会尽量保持更新本文内容。此外,本文只是我自己的观点,有不同看法的欢迎留言提出。

GAE入门

什么是Google App Engine?

Google App Engine(以下简称GAE)是Google提供的云计算平台,属于PaaS。目前Google为GAE提供了Python和Java这2种运行环境,以及datastore、urlfetch等各种服务,以帮助开发者构建一个WEB应用。此外,Google还提供了一些免费配额,你可以不花分文来用它搭建流量不是很大的应用。

GAE可以做什么,不能做什么?

正如上文所述,GAE可以用来构建WEB应用,举个典型的例子来说就是做网站。它是Google以HTTP为基础搭建的平台,因此大部分的服务都是基于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,在这里将子域名填上,然后CNAME到ghs.google.com即可完成绑定。

限制:

  1. ghs.google.com经常被GFW,大陆无法正常访问。需要使用反向代理来绕过GFW的限制,有钱人可以自己买VPS做反向代理,没钱的可以用CloudFlare、YOU8G或Chinasb。
  2. 不能绑定裸域(即ooxx.com这种形式的域名),而只能绑定子域(即www.ooxx.com这种形式的域名)。你可以将裸域重定向到子域,也可以用反向代理。
  3. 自定义域名暂时不支持HTTPS。

如何与GAE support team联系?

填写Billing Support Request表单即可,详见GAE support team联系的方式

遇到急需解决的bug怎么办?

去提交一个Production issue

如何开始学习使用GAE?

直接看官方的使用入门文档:Python、Java,然后再看Google所提供的服务使用方式即可。

尽量阅读英文文档。

你读完入门文档后,就要以英文文档为主了,因为中文文档更新缓慢(已经1年多没更新了),很多信息都过时了。

尽量使用Python。

Python是GAE自诞生时开始就支持的语言,一直以来都更被GAE团队重视,使用起来更为简单,开发更为快捷。

Java作为GAE较晚支持的语言,直到近半年才慢慢跟上Python的更新速度,但例子和文档仍经常晚于Python,配套工具也少于Python(例如上传下载数据仍只能使用Python的BulkLoader),这也和Java开发更为繁琐有关。此外,Java还存在启动时间过长的问题,并且比Python占用更多的内存。

对于WEB应用最为重要的数据库操作,虽然GAE/Java也提供了JDO和JPA这2种标准方式,但实际上与标准实现不同,直接套用可能会遇到陷阱,并且存在性能问题,且无法像Python一样使用动态类型和动态属性。

因此,如果没有特殊需求(例如依赖一些无法替代的Java库,或者对运算性能要求非常苛刻,或者很难找到足够多合适的Python程序员),那么我强烈推荐使用Python。

下文也会以Python为主来阐述。

提示

关于应用和版本。

应用由appid来唯一标识,目前要求只能为字母、数字和连字符,不以连字符开头和结尾,长度介于6到30之间,不包含“google”,并且不与任何Google账号用户名相同(除非你用这个账号来创建)。

一个应用可以有多个版本,其中只有1个可以设为默认版本,这个版本可以直接以appid.appspot.com或被绑定的域名来访问,其他版本只能以verion.latest.appid.appspot.com来访问。

版本号也是个字符串,可以包含字母、数字和连字符。

一个应用的各个版本可以使用不同语言(即可以同时用Python和Java),它们之间共享一个datastore和memcache,但task queue和cron是不共享的,也不能调用其他版本的接口(除非使用urlfetch)。

避免部署时导致暂时无法访问。

部署应用的时候,GAE会停止该版本的所有instance,如果此时有人访问,可能会遇到500错误。

如果想避免出现这种情况,可以部署到另一个版本,等待部署完毕后,再将新部署的版本设为默认版本。

GAE团队正在解决这个问题,未来应该就不需要自行处理了。

慎用wsgiref.handlers.CGIHandler。

它在初始化请求时没有重设环境变量,确保你使用的是webapp.util.run_wsgi_app(application)。

使用logging。

Python提供了一个很好的logging库,你可以在控制面板里看到由它记录的信息,方便跟踪和调试,并且它是免费的。

而Java还需要做些配置。

选择好用的IDE。

对于Python开发者,我推荐PyCharm,不过它只能试用30天,不是免费的。没钱的话可以试试Pydev,不过相对于前者实在逊色太多了。

对于Java开发者,你可以试试Google Plugin for Eclipse或IntelliJ IDEA。(后者虽然有免费版本,但收费的Ultimate版才能更好地支持GAE开发。)

设计

以数据库设计优先。

Web应用基本上都是围绕数据库的,因此它是设计的核心。

而Datastore是非关系型数据库,如果你的想法不能用它高效地实现,那么就考虑换个折中的办法,或者放弃这个想法,或者放弃使用GAE(GAE并不是万能的)。

性能是第二重要的需求。

不管用户有没有特别说明,他们都是非常看重响应时间的。

如果你的应用几秒钟才能响应一次用户请求,即使功能做得再好,界面做得再美,他们也顶多停留数分钟,了解完他们需要的信息,然后遗憾地关掉窗口。

而如果你的应用就和打开本地文件一样快,不管你的应用本身是多么无趣,用户也会愿意去发掘一些有趣之处。

功能可以慢慢完善,但如果一开始就给人很慢的印象,你是很难挽回这些用户的。就好像我用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()。

还有一个容易被忽略的RPC是Users服务,create_login_url()和create_logout_url()函数都会产生RPC调用(毫秒级)。因此可以生成一个固定的登录链接,在这个链接对应的页面里再调用这2个函数,然后将用户重定向到登录页面,返回时则可以取referer头字段。

正确使用缓存。

目前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页面。

使用异步Urlfetch。

同时请求多个链接时,异步会比同步节约很多时间,也能减少超时的概率。尽管逻辑会更为复杂,但值得为此付出。

用task queue分离写操作。

用户进行GET请求时并不关心你后台的数据是否立即更新,而为了计数去更改一个实体可能是很耗时的。如果用task queue去更改实体,响应逻辑继续执行,就能节省这段时间。

采用AJAX。

如果页面中有比较耗时而又不太重要的部分,把它们分离出去,用AJAX请求来载入这些数据。

使用jQuery等AJAX库可以很轻易地完成这些任务。

但要注意为未开启JavaScript的用户和搜索引擎提供一个链接,确保他们能获得这部分数据。

选择轻量级框架。

提到Python Web框架,大多数人第一反应就是Django,但Django在GAE上存在性能问题。

如果你没钱开启always on的话,还是慎用它比较好。

仔细想想你会发现,实际上Django提供给你的好处(说实话我只觉得表单和admin有用),并不需要花太大力气就能实现;而且由于是你自己实现的,你不会受到任何限制,并且很难掉入陷阱。

正确使用Appstats。

Appstats是使用memcache来保存响应记录的,一次响应通常会多占用20ms的CPU时间,会对响应时间稍微造成一些影响。

如果不在乎这20ms,启用它当然没问题。但如果你的QPS特别大,那就不得不考虑钱的问题了。

我一般只在性能出现问题,需要进行调查时才启用,毕竟平时我也不会去看这些数据。

数据库

采用较短的应用ID、类型名和属性名,尽量少用ReferenceProperty和UserProperty等较大的属性。

原因可参见《记录一下GAE上各种属性所占的空间大小》。

尽可能使用Model,而不是Expando和PolyModel。

Expando比Model的反序列化更花时间,而PolyModel会多生成一个class属性(也就意味着更多索引和复合索引)。

取ReferenceProperty的key时,可以用ReferenceProperty.get_value_for_datastore方法来避免访问数据库。

如果你使用ReferenceProperty,可能经常会遇到取多个实体的ReferenceProperty的情况。如果直接获取,每个实体都会造成一次数据库。可以用get_value_for_datastore方法来避免自动解引用,然后一次db.get()来获取所有引用的实体。

不要以关系数据库的观点来使用datastore。

很多人会犯这个错误,把datastore当成关系数据库来用,用ReferenceProperty来联系各个模型之间的关系。

实际上由于不能进行join,采用这种建模方式并不能避免一次数据库访问。

而且ReferenceProperty是个很大的属性,我更推荐直接保存key name或id,然后自己维护实体关系。

使用实体组来对实体关系建模。

实体组也维护了一种实体关系,并且这种关系不像ReferenceProperty一样可以更改。

子实体或它的key可以很容易地获取父实体的key,而无需访问数据库。父实体可以通过祖先查询来获取子实体,而且这种查询性能很快,并可用于事务中。

一个实体组的所有实体都可以在一个事务中直接进行更改,而无需使用分布式事务。

注意实体组的改写不能太频繁,必须保证每秒修改不超过5次(这是极限情况,一般不要超过1次),否则会经常冲突。将实体组粒度保持为一个用户可以很好地满足这一限制(简单来说,即以用户为根实体)。

使用ListProperty对实体关系建模。

ListProperty可以实现一对多关系,只要在一个实体a的ListProperty里保存关联实体(b1、b2、…、bn)的key即可。

由a获取b,直接将a的ListProperty传给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个查询的filter、ancestor和sort必须精确匹配才能共用一条复合索引,否则需要分别构建一条。

如果有些属性是无需查询的,请把它设为indexed=False。

使用merge join来取代复合索引。

Merge join是指对多个属性进行查询时,如果只用了相等和祖先查询,而没有使用不等或排序,就无需创建复合索引,而能直接将各个属性的查询集进行join。这会减少数据库空间占用,并加快写入操作。

因此请尽量考虑无需不等和排序的查询。有些时候用户对是否排序并不敏感,而且你也可在内存中排序。

但要注意结果集不能太大,否则也会影响响应时间。将结果集较小的filter放在前面,会加快响应时间(因此像是否有效等基本上为True的属性,就尽量放到后面去吧;但如果反过来查为False的,则放到前面更好)。

实体其实是个字典对象。

做维护等比较耗时的数据库操作,或需要很高的动态性时,可以采用实体来取代模型对象。

尽量不要使用GQL。

Query比GqlQuery的创建快很多,而且构造起来更灵活(至少你可以把Query对象传给一个函数,在这个函数里增加filter和sort等),并且不存在GQL注入的问题。

只有在需要保持与SQL的兼容性(例如你的项目可能会移植到非GAE平台),或者想用JavaScript直接执行数据库查询时才使用(后者慎用,存在安全问题)。

自动生成的id不保证连续和递增,也不一定唯一。

Datastore只保证自动生成的实体key是唯一的,id只要不冲突,可以随机选择生成任何一个id。

而 key唯一指的是key的path唯一,这个path是包含appid、namespace、父实体的key、该实体的类型名和id(或key name)的。对于没有父实体的实体(根实体)来说,同一个类型是不会存在2个id相同的实体的;但是如果有父实体,那么同一个类型可以存在多个id相同的实体,只要它们的父实体或类型名不同。

更改模型。

当需要更改一个模型的属性时,可以使用map-reduce来批处理;也可以将模型的父类改成Expando,利用动态属性来处理,完成后再换回Model。

添加复合索引时,不要同时部署代码。

添加复合索引是需要很长一段时间的(基于你要索引的实体数),直到索引状态为“Serving”时,你才能使用它。因此,为了避免程序出错,应该先更新索引,等生效后再部署代码。或者部署到另一个版本,等生效后再设置成默认版本。

如果你的索引长期停留在“Building”状态,可以联系GAE support team,叫他们帮忙恢复;或者提交一个Production issue。

分页。

由于datastore提供的fetch方式并不高效,当偏移量很大时,需要获取很多无用的实体,因此一般是不推荐的。

通常的做法是对key属性或time属性进行排序,以此来作为分隔点。缺点是占用了一个不等于操作,并因此无法使用merge join,而且无法用页数来定位。

目前datastore已支持游标,无需占用不等于操作,缺点同样是是无法用页数来计算游标,且对用户输出游标显得不太美观。

安全

不要信任任何由用户生成的数据。

这些数据包括用户提交的表单和HTTP headers(如URL、user agent、cookie等)。

切记处理它们时要考虑不完整和错误的情况(例如从字典中取一个值要注意key可能不存在的问题),对外输出时要进行正确的编码(例如URL要进行百分号编码,XML、HTML要进行实体编码,JSON要进行JSON编码)。

处理用户提交的HTML时,切记审核它们:移除不支持或冲突的标签和属性,关闭不完整的标签等,此外还得特别注意CSS样式和JavaScript代码。

审核用户权限。

不要让用户访问不该获取的资源,不要让任何人都能执行任意关于数据库的代码(特别是使用JavaScript时)。

特别注意在使用缓存时,如果资源是因人而异的,不要让它保存在代理服务器的共享缓存中(也就是确保Cache-Control为private,或包含Vary字段)。

捕捉所有异常。

保证出错时会输出对一个用户友好的页面,而不是异常栈的出错情况。

同时用logging.error()来记录异常栈,以跟踪排除出错原因。

参考