Google App Engine Tips

标签:Google App Engine

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

入门
  • 什么是Google App Engine?
    Google App Engine(以下简称GAE)是Google提供的云计算平台,属于PaaS
    目前Google为GAE提供了Python(2.5或2.7)、Java和Go这3种运行环境,以及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
    2. 不能绑定裸域(即ooxx.com这种形式的域名),而只能绑定子域(即www.ooxx.com这种形式的域名)。你可以将裸域重定向到子域,也可以用反向代理。
    3. 自定义域名暂时不支持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(例如上传下载数据仍只能使用Python的BulkLoader),这也和Java开发更为繁琐有关。此外,Java还存在启动时间过长的问题,并且比Python占用更多的内存。
    对于WEB应用最为重要的数据库操作,虽然GAE/Java也提供了JDO和JPA这2种标准方式,但实际上与标准实现不同,直接套用可能会遇到陷阱,并且存在性能问题,且无法像Python一样使用动态类型和动态属性。
    因此,如果没有特殊需求(例如依赖一些无法替代的Java库,或者对运算性能要求非常苛刻,或者很难找到足够多合适的Python程序员),那么我强烈推荐使用Python。
    下文也会以Python为主来阐述。
    GAE对Go的支持还属于实验性质,很多服务是不可用的,但它的开销和性能比Python和Java都有优势。如果你是Go语言的学习者,这也是个不错的试验田,其他人还是算了。

提示
  • 关于应用和版本。
    应用由appid来唯一标识,目前要求只能为字母、数字和连字符,不以连字符开头和结尾,长度介于6到30之间,不包含"google",并且不与任何Google账号用户名相同(除非你用这个账号来创建)。
    一个应用可以有10个版本,其中只有1个可以设为默认版本,这个版本可以直接以appid.appspot.com或被绑定的域名来访问,其他版本只能以version.latest.appid.appspot.com来访问。
    版本号也是个字符串,可以包含字母、数字和连字符。
    一个应用的各个版本可以使用不同语言(即可以同时用Python、Java和Go),它们之间共享一个datastore和memcache,但task queue和cron是不共享的,也不能调用其他版本的接口(除非使用urlfetch)。
  • 避免部署时导致暂时无法访问。
    部署应用的时候,GAE会停止该版本的所有instance,如果此时有人访问,可能会遇到500错误。
    如果想避免出现这种情况,可以部署到另一个版本,等待部署完毕后,再将新部署的版本设为默认版本。
    GAE团队正在解决这个问题,未来应该就不需要自行处理了。
  • 慎用wsgiref.handlers.CGIHandler(只针对Python 2.5)。
    它在初始化请求时没有重设环境变量,确保你使用的是webapp.util.run_wsgi_app(application)。
  • 使用logging。
    Python提供了一个很好的logging库,你可以在控制面板里看到由它记录的信息,方便跟踪和调试,并且它是免费的。
    而Java还需要做些配置。
  • 选择好用的IDE。
    对于Python开发者,我推荐PyCharm,不过它只能试用30天,不是免费的。没钱的话可以试试Pydev,不过相对于前者实在逊色太多了。
    对于Java开发者,你可以试试Google Plugin for EclipseIntelliJ 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头字段。
  • 使用异步RPC。
    部分RPC服务,如datastore、memcache和urlfetch已提供异步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请求来载入这些数据。
    使用jQuery等AJAX库可以很轻易地完成这些任务。
    但要注意为未开启JavaScript的用户和搜索引擎提供一个链接,确保他们能获得这部分数据。
  • 选择轻量级框架。
    提到Python Web框架,大多数人第一反应就是Django,但Django在GAE上存在性能问题
    如果你没钱开启always on的话,还是慎用它比较好。
    仔细想想你会发现,实际上Django提供给你的好处(说实话我只觉得表单和admin有用),并不需要花太大力气就能实现;而且由于是你自己实现的,你不会受到任何限制,并且很难掉入陷阱。
  • 正确使用Appstats。
    Appstats是使用memcache来保存响应记录的,一次响应通常会多占用20ms的CPU时间,会对响应时间稍微造成一些影响。
    如果不在乎这20ms,启用它当然没问题。但如果你的QPS特别大,那就不得不考虑钱的问题了。
    我一般只在性能出现问题,需要进行调查时才启用,毕竟平时我也不会去看这些数据。
  • 使用Python 2.7取代2.5。
    Python 2.7可以使用C库和多线程,还能并发处理动态请求,不过要注意保证线程安全

数据库
  • 新应用建议使用High Replication Datastore,不要使用Master/Slave Datastore。
    后者将被摈弃,选择前者可以避免未来迁移的麻烦。
  • 采用较短的应用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()来记录异常栈,以跟踪排除出错原因。

暂时就写这么多吧,有补充以后再加上。

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

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

    想说点什么呢?