我的毕业 project 是在 Google App Engine 上完成的。
当初选这个平台有很多原因。曾经也考虑过其他平台,比如 Amazon Web Service,但是后来考虑到开发速度,还是使用了 GAE,毕竟 AWS 还需要自己去搭建一个完整的 web 服务。而且 V2EX 和 Livid 也给了我很大信心。
而在 GAE 上,一切都准备好,需要做的只是写好代码,部署,job done。
当然,便捷就意味这有很多约束,需要根据 Google 制定的规则来玩。这也是写这篇的文章原因。还有一个原因是因为我明天要 viva,算是对这个平台技术细节的复习。
部署在 GAE 上的应用在国内是全部访问不了的,这确实带来了一些麻烦。但我现在已经基本不考虑这个因素了,翻墙应该成为一个公民的生存技能。不过为了方便我国内的朋友们,我最终还是使用 nginx 和 AWS 用反向代理解决这个问题。
我在 project 里使用了 GAE 提供的 以下服务:Images, Memcached, Task Queue, Blobstore,当然还有每一个 web app 都无法不用到的 GAE datastore。
Images 的 API 很简单,其实 Google 提供的这一系列服务,还包括其他的诸如 Mail, Users, Channel 都能很快上手。文档写得也很清楚。即便有疑惑,去相应的 Google 小组也很快能获得答案。回到正题,GAE 提供了一套工具能完成剪裁、旋转、缩放等操作。对于储存在 Blobstore(Binary Large Object)里的图片还能调用一个函数直接获得图片 URL 并同时进行剪裁和缩放。Project 里的所有头像因此都采用这种方式,用户上传,放在 Blobstore 里,app 读出来,根据相应的尺寸进行缩放。曾经图片服务考虑使用 Yupoo(又拍),但是考虑到他们数据中心在国内,和 GAE 通信很难通畅。最终作罢。
Blobstore 的 API 稍微复杂一些。Blobstore 最多支持用户上传 2G 的文件,所以叫 Binary Large Object。类似的课题其实已经在很多学校的 database 课程里提到过了。在 GAE 里,用户使用 POST 方式将文件直接传到 Blobstore 里(因此在生成上传页面的时候,需要先调用 createUploadUrl() 这个函数,生成一个动态的 Blobstore 地址),Blobstore 收到数据后,存储数据,重写收到的 HTTP request,将原来 request 里的文件变成对应的 BlobKey 然后传回对应的 handler。Handler 再做相应的处理。我用 Blobstore 主要是用来存储头像,如上段所说,Images service 和 Blobstore 有很好的整合。所以直接将 BlobKey 储存,下次调用即可。
Memcached 或者类似的中间缓存机制已经成为每一个 web app 的出家旅行必备了。我很惊讶的是,Twitter 最早的网页端没有使用 cache ,当然现在他们已经严重依赖 cache 了。新浪微博则使用了 与 Memcached 类似的 Redis 作为 cache 实现工具。使用 cache 的最大原因是,数据是存储在内存而不是硬盘中,所以读取速度要快很多。我对 GAE Memcached 的理解是,它是一个巨大的分布式 HashMap。这其实也能用来理解大部分 Key-Value 式的数据库。Livid 的 V2EX 已经证明了使用 Memcached 后访问速度的巨大提升,更重要的是,使用 cache 节省了很多 CPU 资源。所以到后来在写代码的时候,我发现,架构上的优化不仅是一种精神洁癖的表现,更直接的是,省钱。(如果超过免费额度,Google 会收费。其中 CPU 资源是最容易超的,而且为了使用 Blobstore,我的银行卡已经绑定了,所以未来的某一天是一定被扣钱的。)
如何使用和何时使用 cache 是一门大学问,这个问题包括 Twitter,新浪微博和豆瓣相信都在持续研究。所以也不就不在这讨论了。但是有一个小窍门是,由于 GAE 是分布式架构,如果有一个常量你的 app 会经常用到,可以放在 Memcached 中,然后用到的时候读出来。而不是将这个常量使用 static 储存。
最后一个是我的最爱,Task Queue。每次写到它,我都觉得内心幸福的像开花一样。实在是太爱 Google 提供这个服务了。为什么?因为它实在解决太多的问题。一一列举,1)在传统 Computer Science 领域,有一个问题是不管什么应用都会遇到,concurrency。而队列是一个解决这问题很好的方式,synchronised => asynchronised。也就是说,别抢,大家排队来做事。Task Queue 可以用来解决这个问题。2)有些时候,用户发起请求后,app 需要计算大量数据。如果不用 Task Queue,用户就一直在那傻等着等结果而且有些时候这些结果并没有实时的要求。使用 Task Queue 则可以解决这个需求。3)这一条和 1) 很类似,那就是使用 queue 缓解大量的并发请求。Twitter 的数据是,在奥巴马就任的那天,他们平均每秒收到成百上千的 Tweet,正是他们用 Scala 设计的 Message Queue 解决了这个问题。
Task Queue 的 API 并不是很复杂,简单一句就是将 task 传给一个 handler,剩下的就是 app 的事了。
说完这些其实还没到核心。GAE Datastore 才是重点。
与传统的 Relational Database 不同,datastore 是 Key-Value 型的 NoSQL,也就是一个大型的分布式 HashMap。很多思维方式都与传统的 RDMS 不同。
比如,在传统的 RDMS 中,每一个 table 都遵循一个 schema,而 datastore 不需要。每一个 column 里的值都必须 atomic,而在 datastore 里,可以允许 List 和 Set。在 RDMS 里,你可以使用 join 将两个表联系在一起,但是 datastore 不允许 join。(不过这个有办法 work around,使用 List)。在传统 RDMS 里,比如豆瓣、新浪微博都用的 MySQL,没有 explicit hierarchy,datastore 确有。
而这些都是在进行数据库设计时必须考虑到的,有些时候错误的理解会带来很大麻烦。
比如接下来讨论的问题,Transaction。
在 datastore 里,每一个 data object 称为 entity,每一个 entity 可以有若干 properties。比如,一个 member entity,可以有性别、家庭住址这些 property。之前说 datastore 可以存在 hierarchy,所以 member 也可以属于某一个 department entity。这个 department entity 也就和 这个 member entity 组成一个 entity group,department 是 parent。(如果 entities 不属于任何一个 parent,他们自己就组成了自己的 entity groups。)
Transaction 的存在也是为了解决 concurrency 的。而 GAE 的 transaction 原则是,每一个transaction 只能修改一个 entity group。在最开始的时候,没有充分理解 GAE 关于 entity group 的概念,所以很不解:当时我以为,所有 member entities 都属于一个 group,更新其中一个 member 会导致同时更新另外一个 member 的操作失败。当时我觉得这设计太傻逼和不合理,仔细一查,才发现,这是与 RDMS 的 table 概念混淆了。事实上,每一个 entity 都组成了自己的 entity group。如果 member 有 1000 个 entities,那就有 1000 个 data entities groups。所以当 app 更新 A 时不会导致 B 的数据不能更新。不过当 app 更新用户 A 的性别(比如他去泰国了)时,就不能再同时更新他的女朋友性别。所以这也提醒了所有开发者,如果某些 entities 很热门,经常更新,他们需要减小 entity group size,essentially, shard oft-written entities。
使用 Transaction 就必然带来了另外一个问题,Transaction isolation。这是一个蛮有意思的东西。
Isolation level 的定义是,when the changes made by one operation become visible to other concurrent operations。
一句话,Transaction 导致的数据变化什么时候能被其他并发请求读取或者操作。
在 datastore 里,每一个 transaction 都有两个 milestone,第一个 milestone 是改变 value(比如某一个 member 的身高),第二个 milestone 是改变 indices。如果有两个 request 几乎同时到达 app,一个是 transaction 更新数据,一个是读取数据。读取数据的 request 如果在两个 milestone 之前,则不会得到更新后的数据。如果在两个 milestone 之间,因为 index 仍然没有更新所以采用 predicate(「where」clause in SQL)来读取的话也得不到更新结果,而使用 getEntityByKey则能读到最新修改的数据。
这个概念比较复杂,Google 员工也专门写了一篇文章来解释这个问题。
刚才说到 sharding 的问题,其实这已经成为优化数据库性能的重要方式。FriendFeed 就使用了这种办法解决了 MySQL 在面对大数据量和 schemaless 遇到的困难。相信 Facebook 也才采用了这种方式。
说的有点远,我其实只是想说 sharding counter 的问题。Concurrency 确实是很讨厌的东西,用 trasanction 来保护也只是下策,如果遇到频繁更新的 entity,整个 app 就近乎没法工作了。比如某一个 counter 可能每秒更新 3 次。这时候就只能使用 sharding counter 了。原理很简单,多存储几个 counter,每次更新的时候随机选一个,最后统计的时候加起来就行了。
我一开始觉得这个概念很爽,便想采用。但是仔细一算,发现自己太自恋了,我们假设如果有一个 entity 能有每秒 3 次更新的频率。这是一个什么的访问量?假设 app 每天的活跃时间是 10 小时,这个 app 每天至少能获得 108,000 的访问量。
算完后,我就乖乖的去写 transaction 了。
其实我挺喜欢 schemaless 的数据库,至少维护数据库的时候太方便了。而且新的 feature 更新也很方便。
但是 datastore 有一个特性很讨厌,它其实有挺多讨厌的特性,不过都被我躲过去了,但是这个没法躲。那就是分页。
我一开始觉得分页很简单,无非就是 start = size – (page – 1) * PAGE_SIZE – 1,bla bla。但是 datastore 比较操蛋的是,一个 query 只能返回 1000 个结果(如果你不使用 primary key 进行 query)。一开始,我为了完美解决这个问题,想了很多方法。包括使用 datastore cursor,可以用,但是 it was pain in the ass。我动用了无数资源去完成这件事。后来 stackoverflow 上一位大侠敲醒了我,你 Google 的时候,会翻到第 50 页去看结果吗?
于是我又乖乖的去用笨方法了。
总体上说,Google App Engine 上开发很轻松,技术上没有太多需要担心的事情。文档很全面,而且 Google 非常重视这个项目,有很多 Googler 都参与到这个项目中,包括 Python(豆瓣使用的语言) 作者也有参与。今年 Google I/O 之后(我必须要吐槽一下,我他妈的因为签证没去成,可能是全世界唯一一个有票但是没去的人),又增加一堆新特性和新的语言,Go。
当然唯一需要担心的事是如何让国内人访问。不过这个问题也不是难事,找一个没被封的 VPS 做反向代理就行了。
这段时间在缓慢的读 Chris Anderson 的名著 Long Tail,一本其实是经济类型的书却说的我很激动。这个世界在慢慢的变成一个 niche 的世界,有头,更有长长的尾巴,而对于每一个 computer scientist/engineer 来说,我们的目标是让更多的人接触以前从来见不到的尾巴,让信息比以前更流畅更有组织的流到你的眼前。
谢谢 Amazon,谢谢 Google,还有许多其他,让这一切变得简单。
