网站可扩展性入门

本文是一组关于网站可扩展性(Scalability)系列文章的合体的译文。

这段时间经常有人咨询关于网站高伸缩性的问题,我的回答比较长而且可能会有其他人感兴趣。所以我把它分四次分享到我的博客上面。

Part 1 - 克隆

Load balancer是支撑可扩展性网站的关键因素,服务器组隐藏在load balancer后面,然后load balancer会均衡地将用户的请求分配到你的网站的application服务器上。也就是说,例如,史蒂夫要访问你的网站,他的第一次访问可能是由你的网站1号服务器响应的,第二次是9号服务器,而第三次是2号服务器…

而且,无论是哪个服务器提供响应,史蒂夫都会得到相同的结果。这就会引出网站可扩展性的第一个黄金法则:每一个服务器都包含完全相同的code base,并且不要在本地磁盘或内存中存储用户相关的数据,例如sessions,profile照片等

Sessions需要存储在一个集中的数据存储设备上,而你的网站服务器可以访问这些数据存储。它们可以是一个外部的数据库,或者是一个外部的persistent cache,例如Redis。persistent cache的性能会高于外部数据库。这里说的‘外部’,是指数据不要直接存储在你的应用服务器上,而是要存储在你的应用服务器的‘数据中心’里面或者周边。

但是如何部署呢?如何保障你的code change会发送到所有的服务器呢?这是个棘手的问题,但幸运的是已经被伟大的工具‘Capistrano’解决了。你需要花些时间来学习如何使用‘Capistrano’,但是相信我,最终你会发现所有的这些付出是值得的。

在将你的sessions’外包‘出去并且将相同的代码部署到所有的服务器上之后,现在你可以从其中任意的一个服务器上创建一个’image‘文件(AWS中称为AMI - Amazon Machine Image)作为未来新家服务器的一个‘超级科隆’(super-clone),以后你可以‘克隆’这个AMI来为新加的服务器创建‘系统’。也就是说,每当新加一个新的服务器的时候,只需要基于你的最新的服务器代码在上面做一次‘初始化部署’(initial deployment)就万事大吉了。

Part 2 数据库

在参照Part 1的做法后,你的网站现在变得‘可横向扩展’(horizontally scale)了,能够同时服务大量的用户了。但是很快就会发现你的应用变得越来越慢直到宕机。原因就是:数据库。你在用MySQL对吗?

现在,我们需要更激进一些,不仅仅是像part 1中提到的那样加几台新的服务器了。关于数据库优化,我们现在有两条路可走:

途径 1

我们继续坚持使用MySQL并且努力让这个怪兽继续工作。雇一个DBA(数据库管理员 database administrator),然后要他去做master-slave replication:从slave读数据,而写入数据到master,并且不停的升级master服务器:加内存,加内存,加内存…

几个月后,你的DBA会不时的从嘴中冒出一些新名词,诸如‘sharding’,‘denormalization’或者’SQL tuning‘等等。要命的是,现在每次试图实施一个挽救你的数据库的新举措都会变得愈发艰难,需要比以往更多的时间和金钱。

事实上,如果在数据库很小的时候,你选择途径2,情况就会好多了。

途径 2

这个途径要求我们从一开始就‘非规范化’(denormalize),在做数据库query的时候不要使用’join‘。你可以继续使用MySQL,但是像使用NoSQL数据库那样去使用它,或者你可以直接切换到更好并且更容易扩展的NoSQL数据库,诸如MongoDB,CouchDB等。‘Joins’就需要拿到你的应用代码中去做了。越早去做这些,以后的工作中就会变得越轻松。

但是即便你做了这些,不久后,还是会发现数据库又开始变慢了。这个时候,我们需要引入cache了。

Part 3 cache

做完Part 2后,我们有了一个可扩展的数据库,终于不再受困于数据存储了,世界又开始变得美好。但是很快你的用户会发现当需要从数据库大量的获取数据时,请求响应又变的慢了起来。解决方法是提供缓存。

在这里,缓存是指内存缓存(in-memory caches),例如Memcached或者Redis,千万别去做基于文件的缓存,因为它会使得你的服务器克隆以及自动扩展变得痛苦。

让我们回到内存缓存,一个缓存是指一个简单的key-value存储并且其位于application和数据存储之间。当application想要读取数据的时候,首先它会尝试去从缓存中去找,只有在缓存中找不到的情况下才会去主数据源中查找。为什么要这样呢?因为缓存读取速度更快。

关于数据缓存,有两种模式:称之为旧模式和新模式。

1 - Cached Database Queries

这仍然是应用最多的缓存模式。当你查询数据库的时候,将得到的结果缓存起来。你的查询的hashed version会做为这个缓存的key。下一次做同样的查询的时候,系统会首先查找缓存,找不到时再去查找数据库。

不过这个模式有几个问题:最主要的问题是‘失效’。当你缓存了一个复杂的查询结果后,你很难去删除掉这个缓存。另外一个问题是:当某条数据变化后,你需要删除和这条数据相关的所有缓存。

2 - Cached Objects

这是我强烈推荐的缓存模式。像代码中的类/实例一样来看待你的data,让你的代码从数据库中提取并且组装数据,然后将这个实例或者组装的数据缓存起来。我知道,这听起来太理论化了,但是只是把它假想成你的实际的代码那样。例如,你有一个类叫做‘Product’,它有一个属性叫做’data‘,它是一个包含product价格,文本描述,图片以及用户review的数组。这个属性’data’会经过几次数据库查询并且通过几个方法从而来填充上数值,这使得它很难被缓存,因为好多东西都互相依赖。现在,让我们试试这个方法:当你的class完成product数据的组装后,直接将‘data’数组缓存下来,或者更好一些,将整个实例缓存下来。这样,当这个product的某些属性改变时,你可以很容易的将其缓存清理掉,并且使你的代码执行更加快速。

更棒的是,这使得异步处理变得可能。可以想象成很多的服务器在为你组装住距。应用只需要从最新的cache object中获取数据,而几乎不比在和数据库直接打交道。

一些可以应用object cache的场景:

  • 用户sessions (永远都不要用数据库)
  • 已经完全加载好的blog文章
  • 活动流(ctivity streams)
  • 用户的朋友关系

想必你已经看出来了,我是一个缓存的忠实粉丝。这很容易理解,缓存很容易实现而且效果显著。通常来说,在Redis和Memcached之间,我更青睐前者,这是因为我非常喜欢Redis的一些特性,例如persistence,以及其built-in的数据结果list,set等。Redis配上一个非常灵巧的key’ing,你甚至可以不再需要数据库。但是如果你只是想做缓存的话,Memcached梗好用一些。

Happy caching!

Part 4 Asynchronism

让我们从以下的这个场景来开始第四部分:想象一下你去面包店去买面包,但是你想要的面包还没好,店主希望你两小时之后再过来看看。很恼火,不是吗?

为了避免这种‘稍后再来’的局面,异步模式需要被引入进来。这不仅对你买面包有帮助,同样对你的web服务业大有裨益。

一般来说有两种异步机制可以选用:

Async #1

让我们继续刚才的场景:第一种处理方式是面包店在前一晚上将所有的面包都烤好,然后早上销售。对顾客来说,根本就不会再有等待。对wed服务来说:所有耗时的工作提前做完,然后在请求到达时再呈现出来。

这个模式被很频繁的用在了将动态内容转换成静态内容上了。当一个网站的网页(可能时通过一个大的framework生成出来的)有变化时,会被提前服务器上生成好。像这种计算工作一般会定时去做,例如用脚本每小时去做一次。这种‘预先计算’(pre-computing)能够极大的提高网站或者web应用的扩展性以及performance。想象一下,将你已经预先完成计算工作的网页上传到AWS S3或者Cloudfront或者其它的Content Delivery Network,那么当用户访问的时候,将会何等的响应迅速。

Async #2

再回到面包的场景:不幸的是,有时候顾客会有一些特殊的要求,例如生日蛋糕上写上‘生日快乐,史蒂夫’。面包店不可能预先判断到这些特殊的需求的,所以只能当顾客下单后才能开始这项工作并且告诉顾客明天再来取。对应于web服务来说,这是异步处理任务。

一个典型的工作流程如下:

一个用户访问你的网站并且触发了一个需要大量计算的任务,该任务估计要花费数分钟才能完成。所以你的网站前端将任务发送到后台的任务队列中去,并且立刻给用户发送一条提示:你的任务已经开始执行了,你现在可以继续浏览网页。后台的一些任务执行者会定期的去检查任务队列,看里面是否有了新任务。如果有了,那么就会开始执行并且在执行完毕后发送一条提示给用户。而前台会一直检查是否收到了这种‘任务完成’的提示,如果有的话就显示给用户。

我知道,这个例子有些简单。如果你想要获取更多的细节以及一些实际的技术设计,我推荐你去阅读RabbitMQ网站上的前三个教程。RabbitMQ是众多辅助实现异步处理的系统之一。你还可以使用ActiveMQ或者一个简单的Redis列表。基本的思想是有一个待处理的任务后台队列,后台的worker可以从里面获取任务并执行。异步看起来有些复杂,但是你觉得值得花时间去了解并实现异步。从而让你的网站后端变得无限可扩展,前端变得迅捷,从而整体上提高用户体检。

记住,如果你需要做一些耗时的工作,一定要用异步!

翻译手记

原文简洁明了,读完后收获很多,决定翻译过来。当然,如果你英文没问题的话,还是建议直接看原文。收获肯定会比看我的翻译大很多。原文链接