第12章 云计算:Google App Engine

我们的行业正经历一波发明浪潮,其背后的主要现象是云。但没人能知道什么是云,也不知道其确切含义。

——Steve Ballmer,2010年10月

本章内容:

简介;

云计算;

沙盒和App Engine SDK;

选择一个App Engine框架;

Python 2.7支持;

与Django比较;

将“Hello World”改成一个简单的博客;

添加Memchace服务;

静态文件;

添加用户服务;

远程API Shell;

问与答(Python实现);

使用XMPP发送即时消息;

处理图片;

任务队列(非定期任务);

使用Appstats进行分析;

URLfetch服务;

问与答(无Python实现);

厂商锁定;

资源。

12.1 简介

接下来要介绍的开发系统是Google App Engine。App Engine不提供类似Django的全栈框架(尽管可以在App Engine中运行Django,后面会介绍),这是一个开发平台,最初专注于Web应用(其有自己的微型框架,即webapp,或其替代品,即新的webapp2),但App Engine仍然可以构建通用的应用和服务。

这里的“通用”并不是意味着任何应用都可以创建并移植到App Engine中,而是表示需要用到HTTP的网络应用,包括但不限于Web应用。一个著名的非Web应用示例就是面向用户的移动客户端的后端服务。App Engine属于云计算的范畴,专注于为开发者提供一个平台,用于构建并托管应用或服务的后端。在实际了解平台的细节之前,首先介绍一下云计算的生态圈,这样可以更好地了解App Engine的适用范围。

12.2 云计算

基于 Django、Pyramid,或 TurboGears 的应用都由供应商或自己的电脑运行来提供服务,而Google App Egine应用由Google运行,作为云计算范畴下许多服务的一部分。这些服务的主要前提是公司或个人将计算架构分离出去,如实际硬件、应用开发和执行或者软件托管。如果使用云计算,则将应用的计算、托管、服务等任务委托给其他公司,不再需要自己完成。

这些服务只能通过因特网来完成,用户可能并不清楚对方的实际物理地址。服务包括以应用使用的原始硬件 [5]到应用的所有内容,以及其他所有可能的服务,如操作系统、数据库、文件和原始的磁盘存储、计算、通知、电子邮件、即时信息、虚拟机、缓存(多级,从Memcahed到CDN)等。这个行业中有许多佼佼者,同时厂商会继续提供新的服务。这类服务一般通过订阅或使用次数来付费。

公司通常出于成本考虑才部署云计算服务。但各公司的需求有很大不同,所以要分别调研来判断部署云服务是否明智。一个创业公司能负担起所有硬件吗(不是租用数据中心或托管设施的设备)?没关系,可以租用亚马逊的计算设备,或使用 Google 的存储设备。过去,小型创业公司的合伙人必须自掏腰包购买这样的设备,而现在他们可以专注于应用的业务问题。

大公司或“财富500强”公司的情况有所不同,他们有足够的资源,但发现无法完全利用到所有潜能。这些公司无须像亚马逊那样创建云业务(下一节会介绍),只须在内部创建一个私有云服务,或者构建一个混合云,用私有设备处理敏感数据,通过如Google或亚马逊这些公共云外包其他部分(如计算、应用、存储等)。

根据自身管理方式,部署云服务的公司通常必须要关心物理存储地址、安全性、服务级别协议(SLA)以及承诺。很明显,当外包应用、数据等内容时,公司希望能确保这些内容安全有保障,公司的管理团队(如果有)能够在任何时候去实地查看物理存储设备。当满足了这些需求后,下面就要决定需要哪个层次的云计算。

12.2.1 云计算服务的层次

云计算有三个层次。图12-1显示了每个层次,以及对应层次的代表产品。最低层的是IaaS (Infrastructure-as-a-Service),即提供计算机本身基本的计算能力(物理形式或虚拟形式)、存储(通常是磁盘)、计算。亚马逊Web服务(Amazon Web Services,AWS)提供了弹性计算云(Elastic Compute Cloud,EC2),以及简单存储系统(Simple Storage System,S3)服务,这两者就在IaaS层面。Google也提供了IaaS存储服务,称为Google Cloud Storage。

Google App Engine作为云计算的中间一层,称为Paas(Platform-as-a-Service)。这一层为用户的应用提供执行平台。最高一层是 Software-as-a-Service(SaaS)。在这一层,用户只须简单地访问应用,这些应用位于本地,但只能通过因特网访问。SaaS的例子包括基于Web的电子邮件服务,如Gmail、Yahoo! Mail和Hotmail。

AUTOGENERATED - 图1 图12-1 云计算的三个层次

在这三层当中,IaaS和SaaS是最常见的,而PaaS则没有像前者那样引起注意。不过情况正在改变,PaaS也许是这三者之间最强大的。通过PaaS,可以免费获得IaaS,但PaaS中含有许多非常服务,自行维护这些服务的开销非常大且很麻烦。这些功能位于IaaS层和上面的层次中,包括操作系统、数据库、软件授权、网络和负载平衡、服务器(Web 和其他)、软件补丁和升级、监控、警告、安全修复、系统管理等。使用云服务的主要好处是与自行维护相关设备相比,使用这一层的服务不会让设备空闲。因为购买计算机设备的数量是根据原先预计的网络流量计算的。如果花费大量资金购置的设备没有充分得到利用,在那里闲置就非常令人沮丧。

云计算的概念已经出现很久了,Sun Microsystems的John Gage在1984年创立了最初令人记忆深刻的术语——网络即是计算机。但云计算在 21 世纪初才商业化。具体来说,是在2006 年年初,亚马逊推出了 AWS。亚马逊闲置的功能促使他们推出这个服务。亚马逊必须购置足够多的计算资源,来满足购物季在线购物的流量和业务需要。

根据亚马逊的白皮书 [6],亚马逊声称:“在2005年,他们必须花费数百万美元来构建并管理大规模、可用且高效的IT设备,来支撑世界最大的零售平台的运作。”

但凭借所有的存储和计算能力,这些设备中的大部分在一年的其他时间里会做些什么呢?老实说,闲置在那里。所以为什么不将这些额外的CPU和存储能力租出去,提供一个服务呢?亚马逊的确这样做了。从那时开始,其他几家大型科技公司也加入了这种趋势:Google、Salesforce、Microsoft、RackSpace、Joyent、VMware,以及许多其他公司都加入到这个行列中。

当亚马逊的EC2和S3服务清晰地定义了云服务的层次,为需要托管应用的客户打开了一个新的市场,具体来说,就是能够编写自定义软件系统,来利用 Salesforce 公司的(顾客关系)数据。这让Salesforce创建了force.com,这是第一个专门做这个业务的平台。当然,并不是所有人想用Salesforce的专有语言编写应用。所以Google开发了另一个更通用的PaaS服务,称为App Engine,在2008年4月闪亮登场。

12.2.2 App Engine

为什么在一本介绍Python的书中介绍App Engine?App Engine是Python的核心部分,或者重要的第三方包吗?尽管都不是,但App Engine的出现对Python社区和市场产生了深远的影响。实际上,之前许多读者都要求添加介绍Google App Engine的章节(他们也同样要求在Python Web Development with Django书中也添加相关章节,这本书是由我和我尊敬的同事——Jeff Forcier和Paul Bissex共同撰写的)。

不同的 Web 框架之间有相同点和不同点,而 App Engine 与这些都有区别。因为 App Engine不仅仅是一个开发平台,还是一个带有应用的主机服务。后者是使用App Engine开发应用的主要原因。用户现在可以更方便地选择开发和部署一款应用,也可以像以前那样,自己搭建支持应用的硬件设备。在App Engine上部署应用,所有要做的额外工作只有设计、编码、测试应用。

使用App Engine,开发者无须处理ISP或自行托管,只须将应用上传到Google,Google会处理在线维护的逻辑。普通的Web开发者现在可以享用与Google相同的资源,在相同的数据中心运行,使用支撑这个互联网巨人相同的硬件。实际上,通过App Engine和其他云服务,Google实际上为自己使用的设备提供了一个公共API。这包括App Engine API如,数据存储(Megastore、Bigtable)、Blobstore、Image(Picasa)、Email(GMail)、Channel(GTalk)等。另外,现在开发者无须关心计算机、网络、操作系统、电源、冷却、负载平衡等问题。

这些都很棒,但Python在这当中扮演什么样的角色?

当App Engine于2008年最先上线时,唯一支持的语言运行时就是Python。Java在一年后支持,但Python处于特殊的地位,因为Python是App Engine第一个支持的运行时。目前Python开发者已经知道了Python是易用、鼓励协同开发、允许快速开发的语言,不需要使用者具有计算机科学的学位。这样能吸引许多不同专业背景的用户。Python的创建者自身就在App Engine的团队 [7]。因为App Engine是突破性的平台,且与Python社区连接紧密,所以很有必要向读者介绍App Engine。

App Engine整个系统由4个主要部分组成:语言运行时、硬件基础设施、基于Web的管理控制台、软件开发包(SDK)。SDK 为用户提供了相应的工具,即开发服务器及访问 App Engine的API。

语言运行时

关于语言运行时,很明显要将时间花在Python上面,但在编写本书时,App Engine已经支持Java、PHP和Go。同样,因为已经支持Java,所以开发者可以使用能在Java虚拟机(JVM)上运行的语言,如Ruby、JavaScript和Python,分别由JRuby、Quercus、Rhino、Jython运行,还有Scala和Groovy。通过Jython运行的Python是最奇妙的,有些人会困惑为什么会有用户在有Python可用的时候去使用Jython。首要原因是有用户希望用Python开发新项目,但有现成的Java包可以利用。不难理解,用户想利用已有的包,但不想花时间将这些库移植到Python中。

硬件基础设置

硬件基础设施对用户来说完全是一个黑盒。用户并不知道代码运行在什么硬件上。大致就是这个黑盒使用某种配置的 Linux,坐落在连接到全球网络的数据中心。读者可能听过Bigtable,这是App Engine用于存储数据的非关系数据库系统。对于大多数人来说,这就是所要知道的内容。记住,有了云计算,无须关心这些内容。云计算中非常复杂的工作、设备维护的细节,以及设备的可用性,都隐藏到幕后。

基于Web的管理和系统状态

本章剩余的部分将会介绍Google App Engine的Python应用编程接口(API)的不同特性。注意,在生产环境中,应用程序可能运行在不同版本的Python(或Java)解释器上。而且因为该应用与其他用户的应用程序共享资源,所以需要考虑安全问题。所有应用必须在沙盒中执行,即一个受限的环境中运行。是的,这样在某种程度上降低了控制力,增加了组件构建难度,降低了扩展性。

作为补偿,App Engine提供了基于Web的管理控制台,让用户可以深入了解应用,包括流量、数据、日志、账单、设置、使用状况、配额等。图12-2显示了一个应用的管理控制台的截图。

AUTOGENERATED - 图2 图12-2 Google App Engine应用的管理控制台(图片由Google提供)

同样还有系统级别的状态页面(见图12-3),用于监控App Engine中当前所有应用的运行情况。

注意,这里的“所有应用”就是字面意思。在2010年冬天,Google App Engine每天要处理超过100万个Web页面。当创建并部署一个应用时,就会添加一个新的管理页面。尽管这样感觉很激动人心,但再次提醒一下,因为App Engine对于所有开发者都是可用的,所以要学会如何使用沙盒。沙盒并不像听起来那么糟糕,因为App Engine为开发者提供了许多服务和API。

AUTOGENERATED - 图3 图12-3 Google App Engine应用的系统状态页面(图片由Google提供)

12.3 沙盒和App Engine SDK

开发者都不会希望别人的应用能访问他的应用的源代码或数据,其他应用也同样如此。在沙盒中有一些无法绕过的限制(如果某些行为现在认为是安全的,Google 会取消这些限制)。禁止的行为包括但不限于下面这些。

不能创建本地磁盘文件,但可以通过Files API创建发布的文件。

不能打开入站网络套接字连接。

不能派生新进程。

不能执行(操作)系统调用。

不能上传任何非Python源码。

由于这些限制,App Engine SDK提供了一些高阶的API,来弥补这些限制带来的功能损失。

另外,因为App Engine使用的Python版本(目前是2.7)仅仅是所有Python版本的一部分,所以无法使用Python所有的功能,特别是由C编译而来的特性。App Engine中有一些C编译的Python模块。Python 2.7版本支持更多的C库,如一些常用的外部库,如NumPy、lxml和PIL。实际上,2.5版本支持的C库组成了一个“白名单”,2.7版本支持更多的库,这个列表实际上是一个“黑名单”。

http://code.google.com/appengine/ kb/libraries.html中列出了Python 2.5中可用但Python 2.7中不可用的C库(对于Java类也有一个类似的列表)。如果想使用任何第三方Python库,只要这个库是纯Python,就可以打包到源码中(纯Python意味着没有可执行程序,没有.so或.dll文件等),同时不要使用不在白名单中的模块或包。

记住,可以上传的文件总数量是有限制的(当前是10000个),同时对所有上传文件的总大小也有限制(当前是1GB)。这里包括应用程序的文件,以及一些静态资源文件,如HTML、CSS、JavaScript等。单个文件的大小也有限制(当前是32MB)。若想了解当前大小的限制,可以访问https://cloud.google.com/appengine/docs/quotas,App Engine的团队在努力提高限制的上线。当然,有几种办法能绕过这些令人头疼的限制。

如果应用需要处理的媒体文件超过了单个文件大小的限制,可以将这个文件存储在App Engine Blobstore(见表12-1)中,这里可以存储任意大小的文件。也就是说,没有单个文件(blob)大小的限制。如果.py文件的数目超过了限制,需要将这些文件存储到Zip中并上传。不管打包了多少.py文件,只要是单个Zip文件即可。当然,这个Zip文件必须小于单个文件大小的限制,但至少无须担心文件数目的问题。关于使用Zip文件的更多信息,可以参考这篇文章(http://docs.djangoproject.com/en/dev/ref/settings,注意,这篇文章开头的提示)。

解决了文件限制问题,回头来看执行限制(不能使用套接字、本地文件、进程、系统调用)。不能使用这些功能,看起来无法构建一个非常有用的应用。不要沮丧,下面会有解决办法!

12.3.1 服务和API

为了能让用户完成工作,Google不断提供新的功能来解决这些核心限制。例如,什么情况下需要打开网络套接字呢?是否需要与其他服务器通信?在这种情况下,使用 URLfetch API。发送或接收电子邮件怎么样?那么可以使用Email API。同样,使用XMPP(eXtensible Messaging and Presence Protocal,或简单一点的Jabber)API可以发送或接收即时消息。各个操作都有对应的 API,如访问基于网络的辅助缓存(Memcache API),部署反向 AJAX 或browser push(Channel API),访问数据库(Datastore API)等。表12-1列出了本书编写时App Engine开发者所能使用的所有服务和API。

表12-1 Google App Engine的服务与API(有些为实验性的) AUTOGENERATED - 图4

”(续表) AUTOGENERATED - 图5

听起来很不错,说得够多了,现在开始动手!首先要做的就是选择一个用来构建应用的框架。

12.4 选择一个App Engine框架

如果编写不是面向用户的应用,也就是说,仅仅编写一个让其他应用调用的应用,那么选择框架就不那么重要。目前,有多个框架可供选择,如表12-2所示。

表12-2 用于Google App Engine的框架 AUTOGENERATED - 图6

App Engine的大多数初学者会直接使用webapp或webapp2来了解App Engine,因为 App Engine 自带这两个框架。这是不错的选择,因为尽管 webapp 很简单,但其提供了一些基本工具,能够构建有用的应用。但有一些熟练的Python Web开发者之前使用了很久的Django,想继续使用,由于App Engine中有限制环境,在默认情况下无法使用Django的所有特性。不过,App Engine与Django之间仍然有某种联系。

Django中的一些组件已经集成进App Engine,Google在App Engine的服务器上提供了某个版本的Django(尽管有些老),用户无须将完整的Django安装包与自己的应用一同上传。在编写本书时,App Engine提供了0.96、1.2和1.3版的Django,读者阅读本书时可能会包含新的版本。但Django中有几个关键部分没有引入到App Engine中,最重要的包括对象关系映射器(Object-Relational Mapper,ORM),ORM需要一个传统的SQL关系数据库基础。

这里使用“传统”是因为有若干计划尝试让Django支持非关系(NO SQL)数据库。但在编写本书时,还没有这样的项目集成到Django发行版中。也许在读者阅读时,Django或许已经同时支持关系和非关系数据库。除了对Django 1.3和1.4的提议,还有一个比较著名的就是 Django-non-rel 这个项目。这是 Django 的一个分支,其中含有针对 Google App Engine和MongoDb的适配器(还有其他正在开发的适配器)。同时还有将JOIN引入NoSQL适配器,但此项目仍在开发中。如果后面有与Django的非关系型开发者相关的信息,到时候会注明。

Tipyfy是专门针对App Engine开发的轻量级框架。可以认为其是webapp++或“webapp 2.0”,其中含有webapp中弃用的一些功能。Tipyfy的功能包括(但不限于)国际化、会话管理、其他形式的验证(Facebook、FriendFeed、Twitter等)、访问Adobe Flash(AMF协议访问,以及动画消息)、ACL(访问控制列表),以及额外的模板引擎(Jinja2、Mako、Genshi)。Tipyfy基于WSGI并关联到Werkzeug工具集,这个工具集是任何兼容WSGI应用的基础。关于Tipfy的更多信息可以访问这个链接中的站点和wiki页面http://tipfy.org。

web2py是Python中4个著名的全栈Web框架之一(另外3个是Django、TurboGears和Pyramid)。这是第二个兼容Google App Engine的框架。web2py侧重于让开发者创建基于数据库系统的快速、可扩展、安全且可移植的Web应用。其中数据库可以是关系型的,也可以是Google App Engine的非关系数据存储。web2py可以使用许多不同的数据库。其中有一个数据库抽象层(DAL)将ORM请求实时转成SQL形式,以此作为与数据库交互的接口。自然,对于App Engine应用程序,依然需要受到Datastore抽象出来的关系数据库的限制(即没有JOIN)。web2py还支持多种Web服务器,如Apache、ligHTTPS,或任何兼容WSGI的服务器。对于已经在使用web2py且想将应用移植到App Engine中的用户来说,使用web2py是很自然的事。

用户也可以选择其他框架来开发应用。任何兼容 WSGI的框架都可以。这里使用 App Engine 中最常用的 webapp,同时也鼓励读者使用 webapp2 来完成这里的示例,以此来提升自己。

介绍一点历史知识:一个富有热情的App Engine开发者不满足现有的框架,于是导致他开发了tipfy。接着他试图改进webapp、于是放弃了tipfy,构建了webapp2。webapp2开发得很好,于是Google将其集成到了2.7版本的运行时SDK中(11章开头的引言就是说的这件事)。

12.4.1 框架:webapp到Django

第11章介绍了Django,以及如何使用Django创建博客。这里默认使用webapp也创建博客。与Django示例相同,这里 将介绍如何使用App Engine构建相同的东西,使用App Engine开发环境运行。用户还可以创建Google Account或其他OpenID身份(或使用已有的),并设置应用,让其运行在实时App Engine生产环境中。本章将介绍如何完成这些内容。虽然在应用上线时不需要信用卡,但需要一部能接收短信或文本信息的手机。

总结一下,本章会把上一章使用Django完成的博客应用移植到App Engine(开发或生产环境)中。App Engine的概念和特性足够再写一本书了,所以这里不会全面介绍。现有的介绍完全可以让读者流畅地完成这个App Engine产品的各个方面。

下载并安装App Engine SDK

首先,需要获取对应平台的App Engine SDK。SDK有多个平台的版本,所以需要注意当前系统对应的版本。访问Google App Engine的主页(位于http://code.google.com/appengine),单击Downloads链接。在这里可以找到适合当前平台的版本。SDK还有针对Java开发者的版本,但这里只关注Python。

Linux或*BSD用户应该下载Zip文件,解压后,将google_appengine文件夹放到合适的地方(如/usr/local中),创建dev_appserver.py和appcfg.py命令的链接。除此之外,也可以直接将/usr/local/google_appengine 添加到系统路径中(对于这些用户,可以跳过本节剩下的部分及后面一节,直接阅读12.6.2节)。

Windows用户应该下载.msi文件。Mac用户应该下载.dmg文件。当找到合适的文件后,双击或启动来安装App Engine SDK。这个过程同时会安装Google App Engine Launcher。Launcher可以用来管理位于开发电脑上的App Engine应用,并能帮助将应用上传到Google,让其运行在生产环境里。

使用Launcher创建“Hello World”(仅限于Windows和Mac用户)

当启动了Launcher后,会看到如图12-4和图12-5所示的控制面板。

AUTOGENERATED - 图7 图12-4 Mac中的App Engine Launcher

AUTOGENERATED - 图8 图12-5 Windows中的App Engine Launcher

控制面板中会多个按钮,可以启动(或暂停)开发服务器(Run按钮),浏览日志(Logs按钮),浏览开发管理控制台(SDK Console按钮),编辑配置信息(Edit按钮),将应用上传至 App Engine 生产服务器(Deploy 按钮),或者跳转至当前应用的管理控制台(Dashboard按钮)。首先创建一个新应用,在开发过程中会用到Launcher中的这些按钮。

为了做到这一点,从菜单栏的下拉菜单中选择创建一个新应用。赋予一个唯一的名字,“helloworld”应该已经被占用了。还可以为应用设置其他选项,如创建新样板文件的文件夹路径,以及服务器的端口号。完成这些后,将会在Launcher的主面板中看到这个应用,这表示它已经可以运行了。在运行之前,先查看自动创建的三个文件:app.yaml、index.yaml和main.py。

App Engine的默认文件

app.yaml文件表示应用的配置信息。默认生成的文件如示例12-1所示。

示例12-1 默认的配置文件(app.yaml)

AUTOGENERATED - 图9

YAML(yet another markup language)文件由一系列的键值对和序列组成。关于YAML格式的更多信息,可以访问http://yaml.org和http://en.wikipedia.org/wiki/Yaml。

逐行解释

第1~4行

第一部分是纯配置,为App Engine应用(APP_ID)赋予一个名称,其后需要跟着一个版本号。对于开发来说,可以选择任何想要的名字,如“blog”。如果需要上传到App Engine的生产环境,则需要选择一个从来没有用过的名称。应用名称要注意以下几点,名称不能转移,不能回收。当选定名称后,该名称就会一直被占用,即使删除该应用也是如此,所以谨慎选择

版本号是一个唯一可以自行选择的字符串。其取决于读者如何设置版本号。可以使用传统的0.8、1.0、1.1、1.1.2、1.2等,也可以使用其他命名方式,如v1.6或1.3beta。虽然这只是一个字符串,但只能使用字母或数字,以及连字符。可以为应用创建最多10个版本(主版本号和副版本号表示不同的版本)。在此之后,除非删除其他版本,否则不能上传新版本。

在版本号下面是运行时类型。这里是Python和第1版的AP。还可以修改app.yaml来使用 Java 或 JRuby,以及其他 JVM 运行时。app.yaml 文件用于生成产 web.xml 和appengineweb.xml文件,也就是servlet需要的文件。

第6~8行

最后几行指定了处理程序。与Django URLconf文件类似,需要指定匹配客户端请求的正则表达式,并提供对应的处理程序。在Django中,这些“处理程序url脚本”键值对对应项目级别的URLconf文件,在此之后是应用层面的URLconf。在app.yaml中类似,脚本指令将请求发送给Python脚本,后者含有更精细的URL,并将其映射到处理程序类,Django应用中的URLconf也是这样将请求指向一个视图函数。

关于应用配置的更多内容,阅读官方文档(参见 http://code.google.com/appengine/docs/python/config/appconfightml)。

现在来看下index.yaml文件。

indexes:

AUTOGENERATED

This index.yaml is automatically updated whenever the dev_appserver

index.yaml文件用于为应用创建自定义索引。为了让App Engine更快地查询datastore,需要每个查询有对应的索引(简单的查询会自动创建索引,无须手动添加)。除非是非常复杂的索引,否则一般无须考虑索引问题。关于索引的更多内容,可以阅读官方文档, http://code.google.com/appengine/docs/python/config/indexconfig.html)。

最后一个由Launcher自动生成的文件是主应用文件(main.py),如示例12-2所示。

示例12-2 主应用文件(main.py)

AUTOGENERATED - 图10

逐行解释

第1~2行

前两行导入了webapp框架,以及其中的run_wsgi_app()工具函数。

第4~6行

在导入语句之后,会看到MainHandler类。这是本例中的核心功能。其中定义了get()函数,从名称就能看出,该函数用于处理HTTP GET请求。处理程序的实例会有request和response属性。在这个例子中,只将HTML/text写出,并通过response.out文件返回给用户。

第8~11行

接下来是 main()函数,用于生成并运行应用的实例。在实例化 webapp.WSGIApplication的调用中,会发现一些二元组,目前只有一个它指定了每个请求对应的处理程序。在这里,目前只须处理“/”这一个URL,这个请求将由刚刚介绍的MainHandler类处理。

第13~14行

最后,根据Python源码是作为模块导入,还是直接作为脚本执行,以此来决定执行方式。如果不熟悉这里的代码,建议回顾第3章和和本章前面的内容。

这些代码都很简单,即使有些是第一次见到。从这里开始,本书后面将持续修改应用,改进或添加新功能。

一些代码清理

在向添加应用新功能之前,先对 main.py 做一点修改,这些修改不影响代码执行,如示例12-3所示。

示例12-3 应用主程序的一些清理工作(main.py)

AUTOGENERATED - 图11

清理内容及原因

1.不希望在每次运行应用的时候实例化WSGIApplication。将其从main()函数中移入全局代码块中,只实例化该类一次,而不是每次请求都实例化。这样能带来一些性能提升,虽然不大,但无论是否是App Engine或其他框架,在每个Python应用中都能做这样类似的简单优化。唯一的小缺点是该应用现在使用了一个全局变量与一个局部变量。

2.因为只使用了webapp.util中的一个函数,所以可以简化导入语句,直接导入函数名称,加快调用run_wsgi_app()时的查找速度。调用util.run_wsgi_app()与调用run_wsgi_app()在一两次时没什么区别,但考虑到应用需要处理数百万条请求,这个改进带来的收益就很大了。

3.将“URL-处理程序”对分割到多行,有利于在后续添加新的处理程序,例如:

('/', MainHandler),

('/this', DoThis),

('/that', DoThat),

这就是目前所做的改动。如果非要给个称号的话,这就是偏向“Django风格”。

12.5 Python 2.7支持

Google App Engine最初支持的是Python 2.5(具体来说,是服务器上的Python 2.5.2)。Google最近发布了新的Python 2.7版运行时,在本书编写时对Python 2.7的支持依然是实验性的 [8]。所以这些代码都在Python 2.5中运行,可以使用Python 2.6或Python 2.7来开发。但对于新版本的GAE,需要注意其中的一些改动。后面还会指出一些代码上的差异,这样读者就可以自行修改代码以便在Python 2.7版中运行了。

12.5.1 一般差异

首先介绍一个重要的差异是Python 2.7的运行时支持并发性。通过App Engine的定价模式,用户根据应用中运行的实例数目(即流量)付费。由于Python 2.5的运行时不支持并发性,因此如果运行的实例无法应对流量限制,必须生成新实例。这会导致开销增加。使用并发性,应用可以异步响应,显著降低对实例数目的需求。

接下来,可以使用之前版本无法使用的C库。包括PIL、lxml、NumPy和simplejson(即json)。Python 2.7版还支持Jinja2模板系统,以及Django模板。若想了解Python 2.5和Python 2.7版运行时之间的所有区别,可以查看官方文档,参见http://code.google.com/appengine/docs/python/python27/newin27.html。

12.5.2 代码中的差异

代码中也有一些微小的差异,若想在Python 2.7版运行时中执行应用,必须对代码进行修改,因此有必要了解这些改动。app.yaml 文件中需要修改 runtime 字段。另外,可能还需要通过threadsafe指令打开并发性支持。另一个主要改动是转向了纯WSGI,现在不指定一个需要执行的脚本,而是指定一个应用程序对象。所有这些改动在示例12-4中用斜体表示。

示例12-4 Python 2.7示例配置文件(app.yaml)

AUTOGENERATED - 图12

Python 2.7版运行时提供了新的改进过的webapp框架,名为webapp2。因为使用WSGI,而不是CGI,所以可以移除之前底部多余的“main()”。示例12-5列出了所有改动,从中可以看到,修改过的代码更加短小且易读。

示例12-5 Python 2.7主程序文件示例(main.py)

1 import webapp2

注意,app.yaml文件指向了main.py中的应用程序对象main.application。关于Python 2.5和Python 2.7版本之间main.py的更多差异可以参考:http://code.google.com/appengine/docs/python/tools/ webapp/overview.html。

关于使用Python 2.7版运行时以及刚刚介绍的相关差异的更多信息,可以查看这篇文档,链接为http://code.google.com/appengine/docs/python/python27/using27.htm。

12.6 与Django比较

App Engine并不是作为由一个或多个应用组成的项目来构建一个Web站点。而是将所有内容组成一个应用。前面提到过,app.yaml文件基本上与Django中项目级别的urls.py类似,二者都将URL映射到对应的处理程序。其还含有settings.py中元素,因为这也是一个配置文件。

main.py文件类似Django中应用级别的urls.py加views.py的组合。在创建WSGI应用时,需要一个或多个处理程序,用于指示哪些类需要实例化来处理相应的请求。这个类的定义以及相应的 get()或 post()处理程序同样在这个文件中创建。这些处理程序类似Django中的视图函数。

通过第11章,读者能够使用开发服务器测试应用。App Engine有自己的开发服务器,后面会用到。

12.6.1 开始“Hello World”

有两种方法在开发服务器上启动一个应用。如果在Launcher中,选择应用所在的行,单击“Run”按钮。几秒钟后,会看到该图标转成绿色。此时可以单击Browse按钮,启动Web浏览器打开应用。

若想通过命令行启动应用,确保 dev_appserver.py 文件位于系统路径中,接着执行下面的命令。

$ dev_appserver.py DIR.

其中,DIR是应用所在的路径名(即app.yaml和main.py文件所在的目录)。是的,如果当前就位于这个路径下,可以直接使用下面的命令。

$ dev_appserver.py.

与Django有所不同,Django使用基于项目的的命令行工具(manage.py),GAE使用为所有App Engine应用安装的通用命令行工具。另外一个小区别是,Django开发服务器从端口8080 开始,GAE 使用 8000。这只是意味着 URL 必须改为 http://localhost:8080/或http://127.0.0.1:8080。如果使用Launcher,在创建新应用时,会自动授予一个唯一的端口号,可以直接使用这个端口号,也可以手动选择其他端口号。

12.6.2 手动创建“Hello World”(Zip文件用户)

如果不使用Launcher,则不需要前面输入的那些代码。因为index.yaml文件在此时不是必需的,只需一个框架性的app.yaml和main.py文件。读者可以手动输入,或者可以在本书的Web站点中从“Chapter 12”文件夹中下载到这些文件。当有了这两个文件后,通过前面介绍的命令就可以启动开发服务器(dev_appserver.py)。

*将应用实时上传至Google

现在还有点早,但如果读者愿意,可以跳过在开发服务器上运行应用的阶段。直接将其上传至 Google,在生产环境中运行。让全世界都能使用这个“Hello World”(除了一些不能使用Google服务的地方)。这完全是可选的,所以如果读者不感兴趣,直接跳到下一节来继续构建博客应用。

App Engine提供了免费的服务层,可以免费开发简单的低流量应用。需要一台支持SMS的手机,以及一个Google账号,信用卡并不是必需的,除非应用需要用到超过配额限制。访问http://appengine.google.com,登录并创建App Engine账号。

为了上传应用(以及相应的静态文件,如果有),要么可以使用Launcher(仅限Windows或Mac),要么可以使用命令行工具appcfg.py。需要在app.yaml文件所处的顶层目录中使用update命令。下面是一个在当前目录下执行appcfg.py文件的示例。注意,需要输入该应用开发者的验证信息(电子邮件地址和密码),如下所示。

$ appcfg.py update .

Application: APP_ID; version: 1.

Server: appengine.google.com.

Scanning files on local disk.

Initiating update.

Email: YOUR_EMAIL

Password for YOUR_EMAIL: *

Cloning 2 static files.

Cloning 3 application files.

Uploading 2 files and blobs.

Uploaded 2 files and blobs

Precompilation starting.

Precompilation completed.

Deploying new version.

Checking if new version is ready to serve.

Will check again in 1 seconds.

Checking if new version is ready to serve.

Will check again in 2 seconds.

Checking if new version is ready to serve.

Closing update: new version is ready to start serving.

Uploading index definitions.

上传应用至多需要大概一分钟的时间。前面这个例子只用了3秒。

等上传完成后,任何人都可以访问 http://12-X.appspot.com来查看“Hello World!”这个输出,多么激动人心!

核心提示:仔细选择应用名称

在上传应用的源码和静态文件之前,需要选择一个没有被占用的名称(通过app.yaml指定),应用的名称是永久的,不能重用或转移,即使禁用或删除应用也不行。

12.7 将“Hello World”改成一个简单的博客

既然成功地创建并运行简单的“Hello World”应用,就应该能够打开浏览器并访问对应的地址。从 Launcher 中,只须单击“Browese”按钮,如果不使用这个按钮,也可以在浏览器中访问http://localhost:8080,如图12-6所示。

AUTOGENERATED - 图14 图12-6 Google App Engine的Hello World

下一步是开始修改这个应用,增加新功能。这里将这个简单的“Hello World”转化成一个博客来重新实现前面的Django示例。这么做是为了让读者能够比较Django和App Engine中的weabapp框架。

12.7.1 快速发现改动:30秒内将纯文本转成HTML

首先,为了确认每次更新代码都能反映到开发服务器上的应用中。需要向输出行添加<H1></H1>标签。将文本改为其他任意文本,如“The Greatest Blog”,即“<h1>The Greatest Blog</h1>”。再次保存改动(或每次有改动后都保存),确认后,返回浏览器,刷新页面,接着确认改动,如图12-7所示。

AUTOGENERATED - 图15 图12-7“Hello World 2”的改动立即反映到刷新后的浏览器页面中

12.7.2 添加表单

现在来进行应用开发中重要的一步,即添加接受用户输入的功能。将插入一个带有字段的表单,让用户创建新的博文。字段有两个,分别是博文标题和正文。修改后的MainHandler.get()方法选择应该类似下面这样。

class MainHandler(webapp.RequestHandler):

def get(self):

self.response.out.write('''

<h1>The Greatest Blog</h1>

<form action="/post" method=post>

Title:

<br><input type=text name=title>

<br>Body:

<br><textarea name=body rows=3 cols=60></textarea>

<br><input type=submit value="Post">

</form>

<hr>

''')

整个方法构成了Web表单。的确,如果这是实际应用,所有HTML应该位于模板中。

图12-8显示了刷新后的页面,以及新的输入字段。

AUTOGENERATED - 图16 图12-8 向Blog应用添加表单字段

现在可以自行填写字段,如图12-9所示。

AUTOGENERATED - 图17 图12-9 填写Blog应用的表单字段

与前面的Django示例类似,这里并不能完全处理这些数据。用户填写完并提交表单后,控制器无法处理这些数据,所以如果试图提交,要么会触发一个错误,要么看到一个空白页面。这里需要添加一个POST处理程序来处理新博客文章,所以创建一个新的BlogEntry类和一个post()方法。

class BlogEntry(webapp.RequestHandler):

def post(self):

self.response.out.write('<b>%s</b><br><hr>%s' % (

self.request.get('title'),

self.request.get('body'))

)

注意,方法名称是post()(与get()相对)。这是因为表单提交的是POST请求。如果想支持GET,需要另一个名为get()的方法。现在定义好了类和方法,但如果在创建应用对象时没有指定(URL-类对),则应用无法用到这个处理程序。完整代码如下所示。

application = webapp.WSGIApplication([

('/', MainHandler),

('/post', BlogEntry),

], debug=True)

通过这些改动,现在可以填写表单字段并向应用提交。图12-10所示的结果页面与post()处理程序指定的完全相同,它显示BlogPost标题及其内容。

AUTOGENERATED - 图18 图12-10 表单提交结果

12.7.3 添加Datastore服务

看到输入很好,但由于应用没有保存任何数据,现在这是完全没用的博客。这里与Django中有所区别。在Django中,必须设置数据库,首先编写数据模型。App Engine更偏重应用本身,在拥有数据模型之前就创建应用。实际上,甚至不需要拥有一个数据库,可以只用缓存,将数据存在Blobstore中,或存在云端的其他地方。

App Engine的数据存储机制是Datastore。Google很明显想将其与数据库区分开来, Datastore在术语上显示起来与database稍微有所不同。Datastore是非关系数据库管理系统(RDBMS),其构建在Google的Bigtable[9]之上,提供分布式、可扩展、非关系型的持久数据存储。还使用Google的Megastore[10]技术来提供强持久和高可用性。

记住,在向App Engine生产环境中部署应用时才用到Datastore。在开发服务器上运行时,可以用二进制格式(默认情况下)存储数据,或在运行 dev_appserver.py 时通过—use_sqlite标志使用SQLite。

现在到了了解数据模型的时候。分析并比较Django与App Enging中的模型类,并注意这里非常相似。

Django

class BlogPost(models.Model):

title = models.CharField(max_length=150)

body = models.TextField()

timestamp = models.DateTimeField()

App Engine

class BlogPost(db.Model):

title = db.StringProperty()

body = db.TextProperty()

timestamp = db.DateTimeProperty(auto_now_add=True)

对于App Engine应用,需要将这个模型添加到已有的main.py文件中,没有等价的models.py 文件,除非自行显式地创建它。不要忘了通过下面的导入语句添加 Datastore服务。

from google.appengine.ext import db

如果读者是Django-nonrel用户,意味着更想让Django应用运行在App Engine上,可以直接使用Django定义的类,而不使用App Engine数据模型。

无论在开发时使用或选择什么类,现在都可以通过底层的持久存储机制获取数据持久存储能力。创建这个类仅仅是第一步。存储实际数据需要执行与之前在Django中相同的步骤:创建实例,填写用户数据,保存。对于这个应用,需要替换 post()方法中的代码。现在的方法很简单,直接将填写的内容显示出来,既没用,也不持久。

标题和正文都很简单,在创建实例后,将其从提交的表单数据中提取出来,作为属性赋值给对应的变量。时间戳是可选的,因为现在根据实例创建时间自动设置。当对象完成后,通过调用数据实例的put()方法将其保存至App Engine的Datastore中,接着将用户重定向至应用的主页面,类似前面Django版本中所做的那样。

下面是新的BlogEntry.post()方法,其中含有刚刚讨论的所有改动。

class BlogEntry(webapp.RequestHandler):

def post(self):

post = BlogPost()

post.title = self.request.get('title')

post.body = self.request.get('body')

post.put()

self.redirect('/')

注意,现在完全替换掉了之前只回显用户输入内容的 post()方法。在前面的例子中,没有将数据保存至持久存储中。通过这个大幅度改动,将博文的所有数据保存至Datastore中。同样,需要对GET处理程序进行相应的改动。

具体来说,应该显示之前的博文,因为现在已经能够持久存储用户数据。在这个简单的示例中,首先显示表单,接着转储任何已有的BlogPost对象。向MainHandler.get()方法做以下改动。

class MainHandler(webapp.RequestHandler):

def get(self):

self.response.out.write('''

<h1>The Greatest Blog</h1>

<form action="/post" method=post>

Title:

<br><input type=text name=title>

<br>Body:

<br><textarea name=body rows=3 cols=60></textarea>

<br><input type=submit value="Post">

</form>

<hr>

''')

posts = db.GqlQuery("SELECT * FROM BlogEntry")

posts = BlogPost.all()

for post in posts:

self.response.out.write('''<hr>

<strong>%s</strong><br>%s

<blockquote>%s</blockquote>''' % (

post.title, post.timestamp, post.body)

)

这些代码为客户端生成 HTML 表单。在此之后,添加从 Datastore 获取结果的代码,显示给用户。App Engine提供了两种查询数据的方式。

一种是以“对象”的方式,类似Django的查询机制,即请求BlogPost.all()(类似Django的BlogPost.objects.all())。App Engine也通过SQL提供了另一种更方便的方式,使用简化版的查询语言语法,即GQL。

因为无法处理所有的SQL(如没有JOIN),且使用SQL不符合Python风格,所以强烈建议使用本地对象的方式。但如果实在无法接受这种方式,能使用 BlogPost.all()上面注释掉的GQL语句执行等价的功能。最后,末尾的循环仅仅遍历每个实体,并显示每篇博文合适的数据。

通过这些改动,重新进入相同的博文,可以看到有所区别,如图12-11所示。

图12-12和图12-13显示现在可以连续添加博客项,确认能保存用户数据。

AUTOGENERATED - 图19 图12-11 表单提交结果(保存到Datastore中)

AUTOGENERATED - 图20 图12-12 为第二个BlogPost填写表单

AUTOGENERATED - 图21 图12-13 第二个BlogPost对象,保存后再显示

12.7.4 迭代改进

与前面的Django示例类似,现在按时间逆序排列博文,并只显示最近的10篇文章,以此来让博客更具实用性。下面就是需要对查询行所要做的改动(以及等价的GQL改动)。

post = db.GqlQuery("SELECT * FROM BlogEntry ORDER BY timestamp

DESC LIMIT 10")

posts = BlogPost.all().order('-timestamp').fetch(10)

对比Diango的查询以发现相似性

posts = BlogPost.objects.all().order_by('-timestamp')[:10]

所有其他内容保持不变。关于在Google App Engine中查询的更多内容,可以访问对应的文档页面,参见http://code.google.com/appengine/docs/python/datastore/creatinggettinganddeletingdatahtml。

12.7.5 开发/SDK控制台

Datastore查看器

与Django的admin应用相比,这个功能显得有点弱。App Engine带有一个开发控制台。可通过在Launcher中单击SDK Console按钮启动这个控制台。如果没有Launcher,需要手动输入特定的URL即http://localhost:8080/_ah/admin/datastore。该页面显示的是Datastore查看器,如图12-14所示。

Datastore 查看器中可以创建应用中定义的任何实体的实例。在这个例子中,只有BlogPost。还可以查看Datastore中对象的内容。图12-15显示了之前创建的两篇博文。

AUTOGENERATED - 图22 图12-14 App Engine中SDK Console里的Datastore查看器

AUTOGENERATED - 图23 图12-15 查看已有的BlogPost对象

交互式控制台

在前面看到了Django在开发过程中提供了一个Python shell。尽管App Engine没有完全相同的功能,还也可以进行相似的操作。单击 SDK Console 中导航链接左边的 Interactive Console链接,会转到一个页面左边有编码面板右边有结果显示区域的Web页面。在这里,可以输入任何Python命令并观察执行结果。图12-16显示了一个示例结果。

AUTOGENERATED - 图24 图12-16 在Interactive Console中执行代码

这里的代码非常简单,如下所示。

from main import BlogPost

print '#posts: ', BlogPost.all(keys_only=True).count()

posts = BlogPost.all()

for post in posts:

print post.title

这段代码非常简单简单。其中可能引起兴趣的是第一条 print 语句,显示本地 Datastore中当前BlogPost对象的数目。读者也许会想到使用BlogPost.all(),但这个函数返回的查询结果是Query对象,而不是序列,且BlogPost没有重写len(),所以无法对其使用len()。唯一的方式是使用count()方法,下面有进一步的介绍。

http://code.google.com/appengine/docs/python/datastore/

queryclass.html#Query_count

单击“Run Program”按钮就可以了。

另一个需要注意的是,通过交互式命令行指定的代码会直接访问本地的 Datastore。与Django博客示例类似,可以使用Python代码自动生成更多的实体,如下面的代码生成图12-17中的内容。

from datetime import datetime

from main import BlogPost

for i in xrange(10):

BlogPost(

title='post #%d' % i,

body='body of post #%d' % i,

timestamp=datetime.now()

).put()

print 'created post #%d' % i

图12-18演示了现在可以根据时间戳逆序排列,并查看原先的两个BlogPost对象,以及图12-17中生成的10个对象。

from main import BlogPost

print '#posts: ', BlogPost.all(

keys_only=True).count()

posts = BlogPost.all().order(

'-timestamp')

for post in posts:

print post.title

核心提示:计数

尽管使用Django和关系数据库计数很简单,但必须承认App Engine无法方便地计数,因为其使用的是大规模的分布式存储。没有任何表,没有 SQL,这意味着无法对 BlogPost执行类似SELECT COUNT(*)这样的SQL语句。许多开发者需要对应用创建一个业务处理计数器,或对许多业务创建一个“共享计数器”。更多信息,参考以下链接。

http://code.google.com/appengine/articles/sharding_counters.html

http://code.google.com/appengine/docs/python/ datastore/ queriesandindexes.html# Query_Cursors

http://googleappengine.blogspot.com/2010/08/multi-tenancy- support-high-performance_17.html

过去,App Engine的计数功能比现在更糟,所以忍忍吧。以前每次查询和计数的实体要小于1000个。通过1.3.1 版中额外的游标,这条限制移除了,所以现在无论是获取、遍历,或者使用游标,都没有上限。但这条限制仍然影响计数和偏移里,意味着为了统计实体数目,仍然需要游标来遍历数据集。在1.3.6版中,这个限制移除了。

现在在Query对象上调用count()要么返回实体的准确数目,要么超时。就像文档中对count()的说明,不应该用这个函数来对大量的实体进行计数:“最好在数目较小的情况下使用 count(),或者指定一个上限。count()没有最大上限。如果不指定上限,Datastore 会一直计数,直到完成或超时。”再次提醒,该函数可能和期望的不同,但仍然算是 App Engine在2010年年初之前的重大改进。

再次提醒,最好的开发实践是不要总是计数。如果需要计数,则维护一个计数器。仅仅需要改变使用App Engine Datastore的思维方式。失去了这个之前用过的功能,换来的是复制和扩展性。在之前,构建这两个功能的开销非常庞大。

另外一个提示是如果需要计数,只对键计数。换句话说,如果创建了查询对象,传递keyonly标记并设为True,这样就无须从Datastore中查询所有实体,如BlogPost.all(key only=True)。下面是与其相关一些有用的链接。

http://code.google.com/appengine/docs/python/datastore/queryclass.html#Query

http://code.google.com/appengine/docs/python/datastore/modelclass.html#Model_all

http://code.google.com/appengine/docs/python/datastore/queriesandindexes.html# Queries_on_Keys

最后,App Engine团队撰写了一系列的文章帮助用户了解Datastore。可以通过下面这个链接访问。

http://code.google.com/appengine/articles/datastore/overview.html

甚至可以回到Datastore查看器来了解每个实体更多的信息,如图12-19所示。

AUTOGENERATED - 图25 图12-17 使用Python创建更多实体

AUTOGENERATED - 图26 图12-18 同时显示新旧实体

AUTOGENERATED - 图27 图12-19 通过使用交互式命令行改变实体显示顺序

如果不想让这些假的 BlogPost 项污染了数据,可以用下面的代码移除它,执行结果如图12-20所示(在返回“Intercative Console”之后)。

from google.appengine.ext import db

from main import BlogPost

posts = BlogPost.all(keys_only=True

).order('-timestamp').fetch(10)

db.delete(posts)

print 'DELETED newest 10 posts'

如果粘贴、复制这段“数据转储”代码,接下来可以确认已经完成了删除工作。

这就是开发中的所有内容。此时,需要在实际应用和生产环境的Datastore中拥有相同的功能。这里可以使用两个相似的工具。

在生产环境中,可以使用远程API为应用添加一个shell(更多内容参考12.11节)。如果为Admin控制台启用了Datastore Amin,还可以将数据整体删除或复制到另一个App Engine应用中。

这就是对SDK控制台的简要介绍。当然,SDK控制台没有Admin Console那么多功能,但它仍然是一个有用的开发工具。后面会再次用到。这里先介绍应用需要用到的另一个服务:缓存。

AUTOGENERATED - 图28 图12-20 删除BlogPost

12.8 添加Memcache服务

App Engine的新用户会感觉数据库的访问非常慢。这仅仅是相对来说,但读者会认为与标准的关系型数据库相比,GAE的速度的确要慢。但要记住,这里有一个很重要的折中,为了换来在云端分布式、可扩展、多副本的存储,会遇到访问速度慢的问题。因为大家都知道,不可能不劳而获。提升速度的一种方式是通过缓存来让数据与应用“更近”,而不是直接访问Datastore。

高流量的页面很少受限于Web服务器能发送多少数据到客户端。瓶颈一般在于数据的生成,数据库可能无法快速应答,或服务器的CPU不停地为所有请求执行同样的代码。还在为多个请求获取或计算相同的数据负载上浪费资源。

通过将数据放在更高的层次,更接近请求,以此来减少数据库或用于生成返回结果的代码所要完成的任务。中间缓存是临时存储获取数据最好的地方。通过这种方式,对于相同的请求,客户端可以重复发送相同的数据,而无须重新获取数据或为不同的用户重新计算。如果发现应用为不同的查询重复获取相同的实体,这一点就对App Engine用户非常重要。

对象缓存的一般模式(在App Engine或其他框架中)如下:检查缓存是否含有所需的数据。如果有,直接返回。否则,获取并缓存这个数据。

如果使用伪代码编写上面的动作,类似下面这样,其中使用一些常量KEY存储缓存的数据。

data = cache.get(KEY)

if not data:

data = QUERY()

cache.set(KEY, data)

return data

不要惊讶,这基本上就是解决方案的Python代码。其中只少了KEY的值和一个数据库的QUERY,这引入了App Engine底层兼容Memcache的API。

from google.appengine.api import memcache

在应用程序的代码中,向MainHandler.get()方法中获取数据部分的两侧添加了几行代码,只有在没有缓存数据集的情况下才会从Datastore查询。

修改前:

posts = BlogPost.all().order('-timestamp').fetch(10)

for post in posts:

修改后:

posts = memcache.get(KEY) # check cache first

if not posts:

posts = BlogPost.all().order('-timestamp').fetch(10)

memcache.add(KEY, posts) # cache this object

for post in posts:

不要忘了为缓存设置键,即KEY = 'posts'。

通过这个 add()调用,可以有效地缓存对象,除非显式删除它(下面会看到),或为最近访问的数据腾出空间而被替换出去。为感兴趣的读者介绍一下,Memcache API使用了最近最少使用算法(Least Recently Used,LRU)。第三种方式是带有期限的缓存。例如,如果想缓存某个对象一分钟,则可以这样调用。

memcache.add(KEY, posts, 60)

最后一个难点是在新博客条目进来的时候验证缓存。为了做到这一点,在代码中通过BlgoEntry.post()向Datastore发送新条目时刷新缓存。

post.put()

memcache.delete(KEY)

self.redirect('/')

完成这样改动之后,可以在浏览器中尝试。但由于这个应用中使用的数据集较小,很难判断数据是来自缓存还是来自Datastore。最简单的方法是在SDK Console中使用Memcache查看器(见图12-21)。

为了看到处理过程,需要两个浏览器窗口,一个用于打开应用,另一个打开SDK Console中的Memcache查看器。要保证在应用中有一些Blogpost对象,接着刷新若干次应用的浏览器页面。然后刷新Memcache查看器页面,观察mamcache使用情况。这里已经完成了一遍,所以能在图12-22中看到使用结果。

应该会看到一个未命中缓存,但后续都会命中,意味着只有在第一次时访问了Datastore,为用户在第一次获取数据后提高了性能。关于App Engine的Memcache API的更多内容,阅读这个链接中的文档:http://code.google.com/appengine/docs/python/memcache。

AUTOGENERATED - 图29 图12-21 Memcache查看器目前是空的

AUTOGENERATED - 图30 图12-22 Memcache查看器显示了一些使用情况

第 11 章没有介绍缓存。Django 有不同层次的缓存服务,包括这里所做的对象缓存,还有QuerySet缓存,帮助将底层的对象缓存起来。关于不同类型的Django缓存的更多内容,可以查看Python Web Development with Django一书的第12章。

让服务器省去获取重复数据的时间,对象层次的缓存仅仅是其中一种方式。然而数据并不总是来自数据库。Web页面通常还包括许多静态文件。App Engine也为开发者在这种情况下提供了多种优化方式,如在适当的地方使用HTTP Cache-Control头来请求上游缓存。如果可以通过边缘或代理缓存,则可以直接向客户端返回数据,此时无须使用App Engine应用。

12.9 静态文件

除了含有动态数据之外,Web页面还有静态元素。这包括图片、CSS、文本(XML、JSON,或其他标记语言)、JavaScript文件。除了让开发者通过处理程序获取这些数据之外,还可以在app.yaml配置文件中指定一个静态文件目录,让App Engine直接返回这些数据。需要在app.yaml中的handlers部分添加一个专门的处理程序。它类似下面这样。

handlers:

  • url: /static

static_dir: static

  • url: .*

script: main.py

这里将静态处理程序放在第一个位置,这样对“/static”路径匹配的请求就会第一个处理。其他路径会由main.py中的处理程序处理。这意味着处理静态文件时不用执行应用代码。

实际上,为什么直接查找拥有的.js、.css,或任何静态文件呢?以 main.css 为例,在顶层目录(app.yaml和main.py文件所在的位置)创建一个名为static的文件夹,更新app.yaml,添加相应的内容。启动开发服务器,使用浏览器访问 http://localhost:8080/static/main.css。在生产环境中也会这样起作用。App Engine可以直接访问静态数据,无须使用应用程序的处理程序。

12.10 添加用户服务

在第11章中,对于Django博客,没有添加任何验证内容(用户、密码、账号等),但在TweetApprove应用中使用了Django自己的验证系统。类似地,在这里使用Google账号在博客中进行验证。确保只有作者能向页面添加新的博文。如果其他人能修改,就成了留言簿了。不应该对添加验证系统感到吃惊。假设读者需要创建一个企业级的博客,类似TechCrunch、Engadget等。博客需要支持多个作者,只有博客作者才能发布博文,而不是任何人都可以在某个人的博客中发布博文。

12.10.1 Google账号验证

创建App Engine应用之后,默认使用的验证是Google账号。但如果不向配置设置或实际代码中添加任何验证机制,实际上就没有任何验证机制。任何人都可以发布博客。现在在MainHandler.get()的起始处添加几行代码来增加验证功能,如下所示。

from google.appengine.api import users

…class MainHandler(webapp.RequestHandler):

def get(self):

user = users.get_current_user()

if user:

self.response.out.write('Hello %s' % user.nickname())

else:

self.response.out.write('Hello World! [<a href=%s>sign

in</a>]' % (

users.create_login_url(self.request.uri)))

self.response.out.write('<h1>The Greatest Blog</h1>')

if user:

self.response.out.write('''

<form action="/post" method=post>

Title:

<br><input type=text name=title>

<br>Body:

<br><textarea name=body rows=3 cols=60></textarea>

<br><input type=submit value="Post">

</form>

<hr>

''')

posts = memcache.get(KEY)

if not posts:

posts = BlogPost.all().order('-timestamp').fetch(10)

memcache.add(KEY, posts)

for post in posts:

self.response.out.write(

'<hr><strong>%s</strong><br>%s

<blockquote>%s</blockquote>' % (

post.title, post.timestamp, post.body

))

如果不想像上面这样通过特定的代码让用户登录,可以在 app.yaml 配置层面完成验证。只须添加login: required命令即可。任何人访问这个页面,在使用应用或看到内容之前,都需要先登录。下面是一个使用该命令的示例,所有没有使用Google账号登录的人都无法访问主处理程序:

  • url: .*

script: main.py

login: required

另一种方式是login: amdin,这种方式只有应用中登录管理员才能访问相应的处理程序,如关键用户、应用、数据访问或处理。非管理员用户会得到一个错误页面,告知只有管理员才可以处理。关于这些指令的更多信息,可以访问这个链接http://code.google.com/appengine/docs/python/ config/appconfig.html#Requiring_Login_or_Administrator_Status。

12.10.2 联合验证

如果不想创建自己的验证系统,或不想要求所有用户都有 Google 账号,可能会想通过OpenID进行联合登录。有了OpenID,可以允许用户使用其他应用的账号登录应用,包括但不限于Yahoo!、Flickr、WordPress、Blogger、LiveJournal、AOL、MyOpenID、MySpace,甚至Google。

如果使用联合登录,需要调整创建登录的链接,添加 federated_identity 参数,如users.create_login_url(federated_identity=URL),其中URL是任何OpenID提供商(gmail.com、yahoo.com、myspace.com、aol.com 等)。未来对联合验证的支持可能会集成进新的 Google Identity Toolkit(GIT)中。

关于GIT和OpenID的更多内容,参考下面的链接。

http://code.google.com/appengine/docs/python/users/ overview.html

http://code.google.com/appengine/articles/openid.html

http://openid.net

http://code.google.com/apis/identitytoolkit/

12.11 远程API shell

为了使用远程API shell,在app.yaml文件中需要在应用的处理程序上方添加下面的内容,如下所示。

  • url: /remote_api

script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py

login: admin

  • url: .*

script: main.py

如果在该文件中还有前面提到的静态文件部分,两者的顺序不影响远程API创建处理程序。重要的是,二者都必须在主处理程序前面。在前面的例子中,删除了静态文件的内容,同时还添加了显式的管理员登录,因为可以确定不想其他人访问生产环境中的Datastore。

此时需要用一个应用的数据模型的本地版本。在正确的目录下执行下面的命令(将 ID换成实时生产环境的应用),提供合适的验证信息。

$ remote_api_shell.py APP_ID

Email: YOUR_EMAIL

Password: *

App Engine remote_api shell

Python 2.5.1 (r251:54863, Feb 9 2009, 18:49:36)

[GCC 4.0.1 (Apple Inc.build 5465)]

The db, users, urlfetch, and memcache modules are imported.

APP_ID> import sys

APP_ID> sys.path.append('.')

APP_ID> from main import *

APP_ID> print Greeting.all(keys_only=True).count()

24

这个远程API shell只是为实时运行的应用提供一个Python交互式解释器。远程API还有其他用途,最著名的是从应用的Datastore中大量上传或下载数据。关于使用远程API的更多内容,可以了解官方文档,参见http://code.google.com/appengine/articles/remote_api.html。

Datastore Admin

Datastore Admin是最近添加的特性,用于向实时应用的管理控制台(不是SDK开发服务器的控制台)中添加组件。能够删除大量(或所有)特定类型的实体,以及向其他实时应用复制实体。唯一缺点是在复制时应用必须是可读模式。为了启用Datastore Admin,向app.yaml文件中添加下面的内容。

builtins:

  • datastore_admin: on

无须强记下这项内容,因为所要做的只是在Admin Console中单击Datastore Admin链接。如果还没有启用它,Admin Console会提示需要在app.yaml启用这个配置。

启用配置后,单击它会弹出一个或两个登录窗口,接着应该能看到如图12-23所示的内容。

若想了解启用Datastore Amin的app.yaml示例,以及允许另一个应用从当前应用复制所有实体的 appengine_config.py 文件,访问 http://code.google.com/p/ google-app-engine-samples/source/browse/#svn%2Ftrunk%2Fdatastore_admin链接中的样例代码库。

关于datastore admin及其特性的更多内容参考下面的链接:

http://code.google.com/appengine/docs/adminconsole/ datastoreadmin.html

http://googleappengine.blogspot.com/2010/10/new-app-enginesdk-138-includes-new.html

AUTOGENERATED - 图31 图12-23 App Engine Datasotre Admin界面

12.12 问与答(Python实现)

完整的 App Engine 平台的范围和特性可以写一整本书。这里的目标是让读者对 App Engine有个整体的认识,能够开始上手,仅此而已。在结束之前,再来个“问与答”环节,为读者提供一些代码实例,这些示例可以直接使用,无须集成进前面的博客应用中。当然,这些内容对本章的练习会有帮助。

12.12.1 发送电子邮件

在第11章的Twitter/Django应用中,介绍了如何使用Django的电子邮件服务。在App Engine中发送电子邮件也很简单。所要做的就是导入mail.send_mail()函数并使用。其使用方式很简单:mail.send_mail(FROM, TO, SUBJECT, BODY),各部分如下所示。

AUTOGENERATED - 图32

还可以向 send_mail()传递其他消息字段,相关内容可参考 http://code.google.com/appengine/docs/python/mail/emailmessagefields.html。

为了阻止发送未经请求的邮件,From: address有所限制。它必须是其中之一。

电子邮件地址是应用登记的管理者(开发者)。

当前登录的用户。

通过应用验证的电子邮件接收地址(xxx@APP_ID.appspotmail.com的形式)。

下面是一个代码示例,它含有导入语句和一个可能的send_mail()调用。

from google.appengine.api import mail

mail.send_mail(

user and user.email() or 'admin@APP_ID.appspotmail.com', # from

'corepython@yahoo.com', # to

'Erratum for Core Python 3rd edition!' # subject

"Hi, I found a typo recently.It's…", # body

)

mail API还提供了其他函数,如只向应用的管理者发送邮件、验证邮件地址等,还提供了EmailMessage类。还可以向出站邮件添加附件,但附件类型仅限于几种常见的格式,而且不是加密的。其中包括.doc、.pdf、.rss、.css、.xls、.ppt、.mp3/.mp4/.m4a、.gif、.jpg/.jpeg、.png、.tif/.tiff、.htm/.html、.txt等。最新支持的附件类型参见http://code.google.com/appengine/docs/python/mail/overview.html#Attachments。

最后,入站或出站消息都必须小于10MB(在本书编写时)。最新的大小限制可以访问下面的链接。

http://code.google.com/appengine/docs/quotas.html#Mail

http://code.google.com/appengine/docs/python/mail/overview.html#Quotas_and_Limits

关于发送电子邮件的更多信息参考以下链接。

http://code.google.com/appengine/docs/python/mail/overview.html#Sending_Mail_in_Python

http://code.google.com/appengine/docs/python/mail/overview.html#Sending_Mail

http://code.google.com/appengine/docs/python/mail/sendingmail.html

12.12.2 接收电子邮件

只有发送,没有接收吗?应用当然可以接收电子邮件。但比发送稍微复杂一点。

设置

为了编写处理入站电子邮件的代码,需要向app.yaml配置文件添加一些内容,最重要的是启用接收邮件的服务。默认情况下,入站电子邮件的代码是关闭的。为了启用,必须在app.yaml的“inbound_services:”部分启用它(如果没有就添加一个)。

同样,前面提到的正确的邮件发送地址就是这里应用正确的邮件接收地址。即xxx@APP_ID.appspotmail.com这样。既可以用一个处理程序处理所有可能的邮件地址,也可以用不同处理程序处理特定的邮件地址。也就是在app.yaml文件中创建一个或多个额外处理程序。为了了解如何创建处理程序,需要知道所有入站电子邮件是POST请求,URL的形式为/_ah/mail/EMAIL_ADDRESS。

下面是需要向app.yaml添加的相关内容。

inbound_services:

  • mail

handlers:

  • url: /_ah/mail/.+

script: handle_incoming_email.py

login: admin

前两行启用接收邮件。“inbound_services”:部分还用来启用接收XMPP消息(关于XMPP的更多内容参考12.13节)、预请求,更多服务可以阅读官方文档中关于应用配置和app.yaml的内容,参见http://code.google.com/appengine/docs/python/config/appconfig.html#Inbound_Services。

第二部分是“handlers”:部分,它含有入站电子邮件的处理程序。正则表达式/_ah/mail/.+匹配所有电子邮件地址,但也可以为不同的电子邮件地址创建单独的处理程序。

  • url: /_ah/mail/sales@.+

script: handle_sales_email.py

login: admin

  • url: /_ah/mail/support@.+

script: handle_support_email.py

login: admin

  • url: /_ah/mail/.+

script: handle_other_email.py

login: admin

可以使用login: admin指令来阻止恶意应用和用户访问电子邮件处理程序。当App Engine收到一条电子邮件消息时,其生成请求并通过POST发送到应用中,让应用作为“admin”调用处理程序。

处理入站电子邮件

可以使用默认方法处理电子邮件,这需要编写处理程序,与创建标准的Web处理程序类似,这需要创建mail.InboundEmailMessage的实例。

from google.appengine.api import mail

class EmailHandler(webapp.RequestHandler):

def post(self):

message = mail.InboundEmailMessage(self.request.body)

当然,在创建WSGIApplication时必须安装该处理器程序。

application = webapp.WSGIApplication([

('/_ah/email/+.', EmailHandler),

], debug=True)

另一种方式是使用预定义的辅助类,InboundMailHandler,它位于 google.appengine.ext.webapp.mail_handlers中。

from google.appengine.ext.webapp import mail_handlers

class EmailHandler(mail_handlers.InboundMailHandler):

def receive(self, msg):

这里没有从请求中提取电子邮件消息,而是自动处理它,所以所要做的仅仅是实现receive()方法,来调用这条消息。还获得一个快捷的类方法mapping(),它自动生成二元组,二元组将电子邮件指向处理程序。使用方法如下。

application = webapp.WSGIApplication([

EmailHandler.mapping(),

], debug=True)

有了消息之后,就可以检查邮件的正文,不论它是纯文本还是HTML(或两者都有)都可以处理,还可以访问消息的附件或其他部分,如发送者、主题等。关于接收电子邮件的更多内容可以访问以下链接。

http://code.google.com/appengine/docs/python/mail/overview.html#Receiving_Mail_in_Python

http://code.google.com/appengine/docs/python/mail/overview.html#Receiving_Mail

http://code.google.com/appengine/docs/python/mail/receivingmail.html

12.13 使用XMPP发送即时消息

与发送电子邮件类似,应用还可以通过App Engine的XMPP API发送即时消息。XMPP的全称是eXtensible Messaging and Presence Protocol,但其最初称为Jabber协议,名称来自Jabber开源社区命名,在20世纪90年代末创建。除了发送之外,通过App Enigne的XMPP API 还可以接收即时消息、检查是否有用户可以聊天或向一个用户发送聊天邀请。但除非用户手动接受一个聊天邀请,否则应用不能进行交流。

下面是向用户发送聊天邀请的伪代码,假设已经正确地为USER_JID 赋予了合法的 IM用户名(或Jabber ID)。

from google.appengine.api import xmpp

xmpp.send_invite(USER_JID)

self.response.out.write('invite sent')

..

下面是另一段代码,用于在用户接受聊天邀请后向用户发送 IM(MESSAGE 字符串),将用户的USER_JID替换为Jabber ID。

if xmpp.get_presence(USER_JID):

xmpp.send_message(USER_JID, MESSAGE)

self.response.out.write('IM sent')

第三个XMPP函数是get_presence(),当用户在线时返回Ture,离开、不在线、未接受应用邀请时为False。关于这个三个函数,以及其他XMPP API的更多内容,可以参考下面的链接。

http://code.google.com/appengine/docs/python/xmpp/overview.html

http://code.google.com/appengine/docs/python/xmpp/functions.html

接收即时消息

接收IM的设置与接收电子邮件相同,即在app.yaml文件的“inbound_services”:部分添加下面的内容。

inbound_services:

  • xmpp_message

同样与接收邮件的地方相同,来自系统的消息由App Engine通过POST发送到应用。使用的URL路径是/_ah/xmpp/message/chat。下面是在应用中接收聊天消息的示例。

class XMPPHandler(webapp.RequestHandler):

def post(self):

msg_obj = xmpp.Message(self.request.POST)

msg_obj.reply("Thanks for your msg: '%s'" % msg_obj.body)

当然,需要注册处理程序。

application = webapp.WSGIApplication([

('/_ah/xmpp/message/chat/', XMPPHandler),

], debug=True)

12.14 处理图片

App Engine有Images API,用来对图片进行简单的处理,如旋转、翻转、改变大小、裁剪。图片可以由用户通过POST发送到应用,或从Datastore或Blobstore中提取出来。

下面是一段HTML代码,用户可以用来上传图片文件。

<form action="/pic" method=post enctype="multipart/form-data">

Upload an image:

<input type=file name=pic>

<input type=submit>

</form>

下面的示例代码通过调用Image API的resize()函数为图片创建一个缩略图,并把它返回给浏览器。

from google.appengine.api import images

class Thumbnailer(webapp.RequestHandler):

def post(self):

thumb = images.resize(self.request.get('pic'), width=100)

self.response.headers['Content-Type'] = 'image/png'

self.response.out.write(thumb)

下面是对应的处理程序项。

application = webapp.WSGIApplication([

('/pic', Thumbnailer),

], debug=True)

关于 Images API 的更多内容可以访问 http://code.google.com/appengine/docs/python/images/usingimages.html

12.15 任务队列(非定期任务)

在App Engine中,任务用来处理额外的工作。这些工作可能需要作为应用的一部分来完成,但无需生成返回给用户的响应。这些辅助工作包括登录、创建或更新Datastore实体、发送通知等。

App Engine支持两种类型的任务。第一种称为Push队列,这是应用必须创建并快速和尽可能并发执行的任务。这些任务不允许有外部影响。第二种类型是Pull对象,这种任务有点灵活,同样由App Engine应用创建,但可以由App Engine或外部应用通过REST API使用或“租用”。后面的部分会详细介绍Push队列,然后简要介绍Pull队列。

12.15.1 创建任务

把任务可以由面向用户的请求的处理程序创建,也可以由其他任务创建。后者有一种情况是第一个任务处理的工作无法及时完成(如截止日期很近,执行时间较短),此时第一个任务创建的工作还没有完成。

把任务添加到任务队列中。队列有自己的名称,也可以有不同的执行速率、补充或突发速率、重试参数。用户能获得一个默认队列,但如果需要其他的队列则必须指定队列名称(后面会介绍更多内容)。向默认队列添加任务很简单,只须在导入taskqueue API后执行一个简单的调用。

from google.appengine.api import taskqueue

taskqueue.add()

所有队列请求都会通过POST发送到URL中,用处理程序处理。如果用户没有创建自定义URL,请求会根据队列的名称发送到默认的URL中,如/_ah/queue/QUEUE_NAME。所以对默认队列,它可能为/_ah/queue/default。这意味着创建WSGIApplication时应该提供一个处理程序设置:

def main():

run_wsgi_app(webapp.WSGIApplication([

('/_ah/queue/default', DoSomething),

]))

当然,需要添加处理实际任务的代码,例如,下面是刚刚定义的处理程序:

class DoSomething(webapp.RequestHandler):

def post(self):

do the task here

logging.info('completed task')

在末尾添加了一个简单的日志条目,用来确认任务真的执行了。很明显,并不是一定要记录,但这样可以确认是否完成了任务。实际上,如果函数中没有完成实际的任务代码,甚至可以将日志项作为占位符(当然,如果真想记录某些内容也可以,只须确保前面有import logging语句)。

12.15.2 配置app.yaml

关于配置,可以不修改app.yaml,直接使用用于所有URL的默认处理程序。

handlers:

  • url: .*

script: main.py

这个设置会将普通和匹配/_ah/queue/default 的应用URL都定向到main.py,这意味着任务队列的请求会发送到那里,这可能是期望的行为。但这种设置的问题是任何人都可以从外部访问/_ah/queue/default URL,即使它们不是作为任务创建的。

最好的方法是将这个URL仅限于任务请求,通过添加login: admin指令可以做到这一点,就像前面配置应用来接收电子邮件那样。现在必须将任务的URL从中分离出来,如下所示。

handlers:

  • url: /_ah/queue/default

script: main.py

login: admin

  • url: .*

script: main.py

12.15.3 其他任务创建和配置选项

前面介绍使用 taskqueue.add()这种创建任务的最简单方式。当然,还有其他选项,可以用来为不同任务队列(非默认队列)创建任务的选项,如指定时间后执行、向任务传递参数等。下面列出了其中一些选项,用户可以选择其中一个或多个参数。

1.taskqueue.add(url='/task')

2.taskqueue.add(countdown=300)

3.taskqueue.add(url='/send_email', params={'groupID': 1})

4.taskqueue.add(url='/send_email?groupID=1', method='GET')

5.taskqueue.add(queue_name='send-newsletter')

在第一个调用中,传递了特定的URL。这里首选自定义URL,而不是默认的URL。在第二个情形中,传递了一个countdown参数,用于延迟执行,只有在第二个参数倒计时完毕时才会执行该任务。第三个调用既传递自定义URL也传递任务处理程序参数。第四个示例与第三个相同,但用户需要GET请求,而不是默认的POST请求。最后一个示例是在定义自定义任务队列,而不是默认队列。

这仅仅是 taskqueue.add()支持的众多参数中的一小部分。关于其他参数,可以参考 http://code.google.com/appengine/docs/python/taskqueue/functions.html。

到目前为止,前面的例子使用的是默认队列。也可以创建其他队列,在编写本书时,免费应用可以有最多 10 个额外队列,付费应用可以有最多 100 个。为了做到这一点,需要在queue.yaml中进行配置,格式类似下面这样。

queue:

  • name: default

rate: 1/s

bucket_size: 10

  • name: send-newsletter

rate: 1/d

默认情况下是正常自行创建的,但如果想为其选择不同的参数,可以在queue.yaml中指定。就如同刚刚那样,将默认rate改成5/s,将bucket_size改为5(这个rate是任务执行的速率,而bucket_size控制队列多快能处理随后的任务)。send-newsletter队列用于每天一次的电子邮件简讯。关于队列所有配置参数的细节可以访问http://code.google.com/appengine/docs/python /config/queue.html。

关于任务最后一点要介绍的是还有另一种队列,这种队列让用户能更灵活地决定在什么时候创建、消耗、完成任务。本节将要介绍的这种类似的任务队列是Push队列,这意味着应用根据需求生成任务,将工作按需压入队列中。

前面提到过,App Engine有其他任务接口,可以在Pull队列中创建任务。App Engine和外部应用通过REST接口可以直接访问这个队列。这意味着工作可以原先来自App Engine应用,在其他地方执行或处理。因此,在处理的时间线上也更加灵活,关于Pull队列的更多内容可以访问http://code.google.com/appengine/docs/python/taskqueue /overviewpull.html中的文档。

12.15.4 将发送电子邮件作为任务

在前面的例子中,介绍了如何从应用中发送电子邮件。如果其他人创建一篇博文项时,只向应用管理员发送一条消息,则可以将发送这封电子邮件作为处理那条请求的一部分。但如果向数千用户发送电子邮件,就不是什么好主意了。

发送电子邮件可以用任务来完成。除了发送邮件之外,这个处理程序也会创建任务,传递参数(如电子邮件地址或用户组 ID,只有属于这一组的用户才能收到消息),在任务在自己的时间(不是用户的时间)内发送邮件后将响应返回给用户。

假设有个Web模板,让用户配置电子邮件消息和收件人组。当用户将表单提交到/submit URL时,由FromHandler类处理它,这一部分可能如下所示。

class FormHandler(webapp.RequestHandler):

def post(self): # should run at most 1/s

groupID = self.request.get('group')

taskqueue.add(params={'groupID': groupID})

FormHandler.post()方法调用 taskqueue.add(),后者用来向默认队列添加任务,传递用于接收订阅邮件的用户组ID。当App Engine执行任务时,它生成到/_ah/queue/default的POST请求,因此需要为任务定义另一个处理程序类。

由于这里使用默认队列,因此需要使用前面定义的app.yaml,其中含有额外的安全锁login:admin。现在主处理程序(main.py)可以为前面例子中的表单指定处理程序,并为/_ah/queue/default指定下面即将创建的SendNewsletter任务处理程序:

def main():

run_wsgi_app(webapp.WSGIApplication([

('/submit', FormHandler),

('/_ah/queue/default', SendNewsletter),

]))

现在来定义任务处理程序,即 SendNewsletter,它接收来自表单处理程序中带有用户组ID的入站请求。接着使用一个普通函数来发送简讯邮件。下面是一种创建SendNewsletter类的方法。

class SendNewsletter(webapp.RequestHandler):

def post(self): # should run at most 1/s

groupID = self.request.get('group')

send_group_email(groupID)

当然,假设已经创建了 send_group_email()函数来处理这个任务,接收用户组 ID,获取所有该组的电子邮件地址(可能从Datastore中提取出来),构建消息正文(方法包括从Datastore中获取、自动生成、从其他服务器获取,等),当然,调用实际的 mail.send_mail()。下面是其代码。

from datetime import date

from google.appengine.api import mail

def send_group_email(groupID):

group_emails = …# get addresses for groupID members

msg_body = …# get custom msg for groupID members

mail.send_mail('noreply@APP_ID.appspotmail.com', group_emails,

'%s Newsletter' % date.today().strftime("%B %Y"), msg_body)

为什么创建一个单独的 send_group_email()函数?不能直接将这些代码放到处理程序,避免额外的函数调用吗?这么想很正常,但代码重用是一个崇高的目标。封装成单独的函数可以在别的地方重用代码,包括在命令行工具中、特殊的管理员界面/函数中,甚至是其他应用中。如果将这些代码放到这里的处理程序中,则必须粘贴复制这些代码,最终这些代码也会分离到一个单独的函数中。所以还是现在就使用单独的函数吧。

创建任务来执行非面向用户的应用工作不是很难。App Engine的用户经常会用到任务,建议读者尝试一下。在使用任务之前,读者可以考虑使用deferred这个方便的库来简化工作。

12.15.5 deferred包

前一节介绍过,App Engine的任务队列是一种委托额外工作的好方式。这类工作一般不是面向用户的,开发者不希望这些工作拖延应用对用户的响应。然而尽管App Engine让开发者能灵活地自定义创建和执行任务,但即使只是运行简单的任务,也要做一些准备工作。此时就可以用到deferred。

deferred 包是一个方便的工具,用来隐藏许多设置和执行任务时的准备工作。这些工作包括:必须调整表单处理程序来创建任务,必须提取并提供合适的任务参数和执行规则,必须创建并配置独立的任务处理程序等。为什么不能将这些准备工作委托给一个任务呢?这就是deferred工具的作用。

只须使用一个函数 deferred.defer(),将使用这个函数创建一个延迟任务。该函数使用起来如同日志函数一样简单,如下所示。

from google.appengine.ext import deferred

deferred.defer(logging.info, "Called a deferred task")

这个函数只使用deferred库配置应用,没有其他内容。延迟的任务(默认情况下)在默认队列中运行,正如前面提到的,无须做任何特别的事来进行设置,除非想改变默认队列的默认特性。也无须在应用中为延迟任务指定处理程序,deferred 库都自己实现了。从前面短小的例子中可以看出,只须向deferred.defer()传递一个Python可调用对象,以及任何其他参数或关键字参数参数。

另外,还可以传递任务参数(前一节介绍过),但需要对其进行预处理,防止该参数与延迟的回调弄混。因此需要在任务参数前面加上下划线,防止将其作为执行程序的参数。例如,对于上面的调用,需要延迟5秒,可以使用下面这样的方式。

deferred.defer(logging.info,

"Called a delayed deferred task", _countdown=5)

可以轻松地将前面发送电子邮件的示例转成下面等价的形式。

class SendNewsletter(webapp.RequestHandler):

def post(self):

groupID = self.request.get('group')

deferred.defer(send_group_email, groupID)

延迟任务可以调用函数、方法和任何其他可调用的或有call定义的对象。根据代码中的文档,下面是可以用作延迟任务的可调用对象。

1.模块顶层定义的函数。

2.模块顶层定义的类。

a.实现call的类实例。

b.这些类的实例方法。

c.这些类的类方法。

3.内置函数。

4.内置方法。

但下面的这些是不允许的(同样在代码中的注释中说明了)。

嵌套函数或闭包

嵌套类本身或嵌套类的对象。

Lambda函数。

静态方法。

另外,可调用对象的所有参数都必须是“pickleable”,这意味着只有基本的Python对象(如常量、数字、字符串、序列和哈希类型)才可以。完整的列表可以参考 Python 官方文档,链接为http://docs.python.org/release/2.5.4/lib/node317.html(Python 2.5)或http://docs.python.org/library/pickle.html#what-can-be-pickled-and-unpickled (最新版)。

例子中还有一个限制是 sendgroupemail()需要在不同的模块中,然后导入主处理程序中。这样做的原因是此时“推迟”了任务,且任务是“序列化”的,它记录了代码属于main模块,但当deferred包在接收由任务创建的POST请求后,执行可调用对象时,deferred模块就是正在执行的内容(因此其中有__main属性,这意味着此时找不到可调用对象的代码)。如果将函数foo()推迟,会收到下面这样的错误。

Traceback (most recent call last):

File "/usr/local/google_appengine/google/appengine/ext/deferred/

deferred.py", line 258, in post

run(self.request.body)

File "/usr/local/google_appengine/google/appengine/ext/deferred/

deferred.py", line 122, in run

raise PermanentTaskFailure(e)

PermanentTaskFailure: 'module' object has no attribute 'foo'

但通过放在 main.py的外面(或其他含有主处理程序的Python 模块中),能避免这种混乱并让代码正确地导入和执行。如果对main还不太了解,可以阅读本书关于模块的章节。更多deferred的关于内容,可以查看http://code.google.com/appengine /articles/deferred.html中的这篇原始资料的。

12.16 使用Appstats进行分析

在App Engine中能够分析应用的行为优势是很重要的。可以使用Appstats进行分析,这是SDK中的一个工具,用来优化应用的性能。Appstats不是普通的“代码验证工具”,这款工具跟踪应用中不同的API调用,衡量通过远程过程调用(RPC)完成后端服务所耗费的时间,并提供一个基于Web的接口,用于观察应用的行为。

配置Appstats来记录事件非常简单。只须在应用的根目录中创建appengine_ config.py文件(如果有就直接打开),添加下面的函数。

def webapp_add_wsgi_middleware(app):

from google.appengine.ext.appstats import recording

app = recording.appstats_wsgi_middleware(app)

return app

这里还可以使用其他功能,文档中对这些功能做了介绍。当添加完这些代码后,Appstats会从根据应用的活动记录事件。记录器非常轻量,所以它对性能没有什么影响。

最后一步是设置管理接口,这样可以访问Appstats的记录。下面三种方式中的任何一种都可以完成该任务。

1.在app.yaml中添加标准的处理程序。

2.添加自定义Admin Console页面。

3.作为内置界面启用。

12.16.1 在app.yaml中添加标准处理程序

为了在app.yaml(自然在“handler:部分”)中添加标准处理器程序,使用下面的代码。

  • url: /stats.*

script: $PYTHON_LIB/google/appengine/ext/appstats/ui.py

12.16.2 添加自定义Admin Console页面

如果想将Appstats UI作为自定义Admin Console页面,可以在app.yaml中的admin-console:部分进行如下修改。

admin-console:

pages:

  • name: Appstats UI

url: /stats

12.16.3 作为内置界面启用界面

可以将作为内置界面启用Appstats UI,即像下面这样修改app.yaml中的“builtins”:部分。

builtins:

  • appstats: on

这样将UI配置到默认的/_ah/stats路径下。

可从下面的链接中了解Appstats提供的其他内容。

http://code.google.com/appengine/docs/python/tools/appstats.html

http://googleappengine.blogspot.com/2010/03/easy-performance-profiling-with.html

http://www.youtube.com/watch?v=bvp7CuBWVgA

12.17 URLfetch服务

当使用App Engine工作时需要考虑的一个限制是,不能创建网络套接字。这会让大多数应用变得毫无用处,但SDK提供了更高层次的功能作为代理。创建并使用套接字能够与网络上的其他应用交互。为此App Engine提供了URLfetch服务,通过这个服务,应用可以向其他在线服务器生成 HTTP 请求(GET、POST、HEAD、PUT、DELETE)。下面是一个简短的示例。

from google.appengine.api import urlfetch

res = urlfetch.fetch('http://google.com&#39;)

if res.status_code == 200:

self.response.out.write(

'First 100 bytes of google.com:<p>%s</p>' %

res.content[:100])

除了App Engine的urlfetch模块之外,还可以使用标准的urllib、urllib2库,以及httplib模块,这些模块自身修改过,使用App Engine的URLfetch服务进行交互(自然运行在Google的可扩展架构上)。

这里需要注意一些警告,如通过 HTTPS 和请求头与服务器通信,不能修改或设置。关于这些限制,以及如何使用URLfetch服务的细节,可以查看文档,链接是http://code.google.com/appengine/docs/python/urlfetch/overview.html。

最后,由于有些负载是高延迟的,因此同样可以用异步的URLfetch服务。还可以查询是否有请求完成或提供回调。关于异步 URLfetch 的更多内容可以访问 http://code.google.com/appengine/docs/python/urlfetch/asynchronousrequests.html。

12.18 问与答(无Python实现)

这里是另一个“问与答”环节,本节将介绍其他配置过的特性。这一节没有源代码。

12.18.1 Cron服务(计划任务作业)

cronjob是一个任务,原先用在POSIX系统上,用来按计划执行。App Engine为用户提供一种 cron 类型的服务。这种类似的服务实际上不使用Python 代码,只是期望相应的处理程序在合适的时间执行。

为了使用cron服务,需要创建corn.yaml文件,其中含有下面的内容。

cron:

  • url: /task/roll_logs

schedule: every day

  • url: /task/weekly_report

schedule: every friday 17:00

还需要正确地指 定“description:”和“timezone:”字段。调度格式非常灵活。更多App Engine中 cron 任务的关于内容可以参考其文档,链接为 http://code.google.com/appengine/docs/python/config/cron.html。

12.18.2 预热请求

预热请求用来降低延迟,当需要新的实例用来服务更多用户时,可以提高用户对应用的体验。假设用一个实例来完成应用的某个任务。但突然来了很多服务会导致堵塞。当运行的实例无法支持这个负载时,必须上线新的实例来服务所有请求。

如果没有预热请求,第一个用户访问应用时,会创建新的实例,与已有一个正在运行的实例相比,此时必须要等待稍微长一点。额外的延迟是因为必须等待载入新的实例后才能处理用户的请求。如果可以“预热”新的实例,即在遇到阻塞之前预载入到应用中,用户就不会感到延迟。这就是预热请求的工作。

与其他 App Engine 特性相似,预热请求默认情况下不开启。为了开启预热请求,向app.yaml的“inbound_services”:部分添加下面一行。

inbound_services:

  • warmup

另外,当新实例上线时,App Engine会向/_ah/warmup发起一个GET请求。如果为其创建一个处理程序,可以在应用中预载入任何数据。只须记住,如果应用没有遇到阻塞,则没有运行任何实例,第一个请求依然会触发载入请求(即使已经启用了预热)。

如果思考一下,原因很简单:预热请求并不是万能的,实际上,还会增加延迟,因为必须先完成载入请求。因为不想应用对第一个用户响应之前,除了载入请求之外,还在预热请求这方面花费时间。预热请求只在服务器已经处理应用的阻塞时才有用,此时App Engine可以预热新的实例。

配置这个特性时同样不需要任何Python代码。关于预热请求的更多内容参考下面的这些链接:

http://code.google.com/appengine/docs/adminconsole/instances.html#Warming_Requests

http://code.google.com/appengine/docs/python/config/appconfig.html#Inbound_Services

12.18.3 DoS保护

App Engine提供了防止应用受到系统的拒绝服务(Denail-of-Service,DoS)攻击的简单方式。这项功能需要创建dos.yaml文件,其中含有“blacklist”:部分,如下所示。

blacklist:

  • subnet: 89.212.115.11

description: block DoS offender

  • subnet: 72.14.194.1/15

description: block offending subnet

可以将独立的IP或IPv4和IPv6的子网加入黑名单。此时上传dos.yaml文件后,来自特定IP或子网的请求会过滤掉,无法到达应用代码。应用不会为这些黑名单中的IP或网络中发送的请求消耗资源。

关于Dos保护的官方文档参见http://code.google.com/appengine/docs/python/ config/dos.html。

12.19 厂商锁定

最后一个需要讨论的问题是厂商锁定。锁定通常指系统在内部就很难或者无法迁移数据/业务逻辑到类似或竞争的系统上。在App Engine简短的生命周期,一向认为其“强迫用户”使用Google的API访问App Engine,很难将应用移植到其他平台中。

Google强烈建议用户使用它们的API,来充分利用系统的优势,用户必须理解这是个取舍。看上去为了利用 Google 的可扩展架构(由这个公司独立管理),而必须使用 Google 的API 编写代码比较公平。再一次提醒,不能不劳而获,对吗?毕竟构建这样的扩展能力是非常困难且开销很大的工作。Google也在尽量尝试取消锁定,同时让用户仍然能使用App Engine的优点。

例如,尽管App Engine有webapp(或webapp2)框架,但依然能使用其他开源且兼容App Engine的框架。包括Django、web2py、Tipfy、Flask、Bottle。对于Datastore API,如果使用 Django-non-rel 系统和 djangoappengenine,可以完全忽略掉这个。这些库允许在 App Engine上直接运行纯Django应用,所以可以自由地在App Engine和任何支持Django的传统系统之间迁移。另外,还不仅限于Python,Java方面也同样如此。App Engien团队尝试了很多努力让其API尽量兼容Java Specificaiton Request(JST)标准。如果读者了解如何编写Java servlet,就很容易转到App Engine中。

最后,还有两个开源后端系统声称兼容 App Engine 客户端,分别是 AppScale 和TyphoonAE。后者以更传统的开源项目的方式维护,前者在加利福尼亚大学圣芭芭拉校区活跃地开发着。关于这两个项目的更多内容可以访问http://appscale.cs.ucsb.edu和http://code.google.com/p/typhoonae。如果想对应用有全方位的掌控,不想运行在Google的数据中心,可以用这两个系统之一来组建平台。

12.20 资源

App Engine 可以写一本书了(实际上已经有人写了),但是,这一章不能面面俱到。但如果读者想深入研究,下面的一些资源和特性可能有所帮助。

Blogstore,让用户处理对Datastore来说太大的数据对象(blob)(如媒体文件) http://code.google.com/appengine/docs/python/blobstore/overview.html

功能

http://www.slideshare.net/jasonacooper/strategies-for-maintaining-app-engine-availability-during-read-only-periods

http://code.google.com/appengine/docs/python/howto/maintenance.html

Channel:让应用直接向浏览器发送数据的服务,即Reverse Ajax、browser push、Comet。

http://googleappengine.blogspot.com/2010/12/happy-holidays-from-app-engine-team-140 .html

http://blog.myblive.com/2010/12/multiuser-chatroom-with-app-engine.html

http://code.google.com/p/channel-tac-toe/

http://arstechnica.com/web/news/2010/12/app-engine-gets-streaming-api-and-longer-back ground-tasks.ars

高复制Datastore。

http://googleappengine.blogspot.com/2011/01/announcing-high-replication-datastore.html

http://code.google.com/appengine/docs/python/datastore/hr/overview.html

Mapper:MapReduce的第一个步骤,让用户遍历用户持久数据。

http://googleappengine.blogspot.com/2010/07/introducing-mapper-api.html

http://code.google.com/p/appengine-mapreduce/

Matcher:高可扩展实时匹配架构:注册查询来匹配对象流。

http://www.onebigfluke.com/2010/10/magical-api-from-future-app-engines.html

http://groups.google.com/group/google-appengine/browse_thread/thread/5462e14c31f44bef

http://code.google.com/p/google-app-engine-samples/wiki/AppEngineMatcherService

命名空间:通过划分Google App Engine数据,创建多租户的应用程序。

http://googleappengine.blogspot.com/2010/08/multi-tenancy-support-high-performance_17.html

http://code.google.com/appengine/docs/python/multitenancy/overview.html

http://code.google.com/appengine/docs/python/multitenancy/multitenancy.html

OAuth:联合验证服务,无须交换凭证信息就可让第三方访问应用和数据。

http://code.google.com/appengine/docs/python/oauth/overview.html

http://oauth.net

流水线:管理多个长时间运行的任务/工作流,整理其结果(参见 Fantasm,第三方编写的另一个更简单的工作流管理器)。

http://code.google.com/p/appengine-pipeline/wiki/GettingStarted

http://code.google.com/p/appengine-pipeline/

http://news.ycombinator.com/item?id=2013133

http://googleappengine.blogspot.com/2011/03/implementing-workflows-on-app-engine.html

表12-3列出本章出现的许多开发框架的Web地址。

表12-3 与Google App Engine共同开发的框架 AUTOGENERATED - 图33

12.21 总结

从本章和第11章的这么多内容可知,Django和Google App Engine是今日Python社区中两个最强大和灵活的Web框架。而其他框架(TurboGears、Pyramid、web2py、web.py等),也很强大,这些框架都有很好的生态圈,增加Python用户编写Web应用的选择。更重要的是,所有Python Web框架背后都有专注的开发者和忠诚的追随者。

根据对任务的契合程度全能的程序员,甚至会过一段时间就换一个框架。社区能拥有这么多大而且著名的框架非常不错。尽管本章开头的引用有点口是心非,但背后依然有一些道理,如果每个人都需要自己编写Web框架,世界会变得很糟。

最后一点提示,本章的实例都不支持Python 3,因为这些框架目前都不支持Python 3。当支持Python 3时,会在本书的配套网站上放出相关资源,本书的新版(第4版)到时候也会涵盖Python 3版本。

12.22 练习

Google App Engine

12-1 背景。使用Google App Engine,Python需要做哪些事情?

12-2 背景。Google App Engine与其他开发环境有哪些区别?

12-3 配置。Django和App Engine的配置文件有哪些区别?

12-4 配置。介绍Django应用执行“URL到处理程序”映射的地方。同样介绍App Engine应用完成这个任务的地方。

12-5 配置。如何让Django应用无须更改就能运行在Google App Engine上?

12-6 配置。对于这个练习,访问 http://code.google.com/appengine,接着下载并安装针对读者系统的最新的Google App Engine SDK。

a)如果是 Windows或Max系统,使用Launcher应用创建名为“helloworld”的应用。在其他系统上,创建含有下面内容的文件。

i.第一个文件为:app.yaml

application: helloworld

version: 1

runtime: python

api_version: 1

handlers:

  • url: .*

script: main.py

ii.第二个文件为:main.py

from google.appengine.ext import webapp

from google.appengine.ext.webapp.util import run_wsgi_app

class MainHandler(webapp.RequestHandler):

def get(self):

self.response.out.write('Hello world!')

application = webapp.WSGIApplication([

('/', MainHandler),

], debug=True)

def main():

run_wsgi_app(application)

if name == 'main':

main()

b)通过Launcher或执行“dev_appserver.py DIR”来启用应用,其中DIR是app.yaml和main.py所在的目录。接着访问http://localhost:8080(或自己指定的端口号)来确认代码正常工作,浏览器中显示了“Hello world!”。将显示内容改为其他内容。

12-7 教程。完成位于 http://code.google.com/appengine/docs/python/gettingstarted 的“Getting Started”教程。注意,不要直接复制网页上面的代码。同时希望读者能对教程中的应用进行修改,添加一些其中没有的功能。

12-8 沟通。电子邮件是应用的关键特性。在上一章的练习中,添加了当创建新博客项时发送邮件的功能。在App Engine的博客应用中添加相同的功能。

12-9 图片。允许用户为每篇博客提交图片,并恰当美观地显示博文。

12-10 游标和分页。类似Django博客应用,显示前10篇博文还可以,但让用户能翻页到前面的博文就更好了。使用游标并向应用添加分页。

12-11 沟通。允许用户在应用中使用IM沟通。创建菜单命令来发布博客项,获取最新的博文,以及其他很“酷”的特性。

使用Django或App Engine开发

12-12 用户云数据管理系统。构建天气监控系统。允许系统中多个用户使用任何验证系统。每个用户应该有一组位置信息(邮编、机场编码、城市等)。用户应该位于一个位置网格中,其中含有当前的天气以及3~5天的天气预报。网上有多个在线天气API可以使用。

12-13 金融管理系统。创建股票/股本组合管理系统。处理内容包括普通股票、合股投资、交易所交易基金、美国存托凭证、证券交易指数或其他可通过包含的股票代号搜索的数据。如果读者不在美国,使用所在国的交易工具。

12-14 运动统计应用。假设你是一个狂热的全球保龄球运动参与者,则可以用一个应用程序来管理得分、计算平均值等,但是可以做得更多。预测得分、每天的平均得分等,允许用户输入分数和未击倒的数量。这样可以验证是否真的打了一场很好的游戏,或是否一晚上都很幸运。还包括一个复选框,可以选择表示是否认同一局游戏,并可以链接到特定游戏的视频剪辑。不在保龄球馆也能分析运动。创建一个网络服务器,在外面时也可以通过因特网或在手机上访问这些数据。

12-15 课程逻辑和社会管理系统。实现一个中学或大学课程管理系统。它可以支持用户登录,包含实时的聊天室,有离线带外通信(OoB)的论坛,还有专门提交作业和获取成绩的地方。同样对于老师来说,可以增加和批改作业,与学生一起参与聊天和论坛,给学生发布课程通知、静态文件,发送消息。选择 Django 或GoogleAppEngine来实现你的方案,或者有更好的选择,使用Django-non-rel创建一个Django应用程序,在Google App Engine或传统的主机环境下运行。

12-16 菜谱管理器。开发一个应用程序来管理一个虚拟的烹饪食谱集合。这有点不同于管理MP3或本地文件夹中收藏的其他音乐。这些食谱只存在网上,当用户输入菜谱URL时,应用程序应当允许用户将URL放在多个分类下(但是实际URL仅仅只保存了一次)。同时,当菜谱链接失效时,通过电子邮件、IM/XMPP、SMS(如果能找到合适的电子邮件到SMS网关,参见http://en.wikipedia.org/wiki/List_of_SMS_gateways)向用户发送通知。在列出食谱时创建一个微型爬虫,显示某个菜谱页面上的找到缩略图(如果有),像菜谱URL一样(如果是可用)。用户可以通过类别/菜系进行浏览。