导读:作者William Hertling的业余爱好是写科幻小说,目前就职于HP。他在博客中谈到了如何在三天内让一个Web应用程序承载拓展1,000x的实时并发访问量。对此他分享了自己的经验,包括怎么做到、从中学到了什么,以及从中吸取的经验。
环境:由NgniX,Ruby on Rails和MySQL构成。注:这个Web应用只是一个旅行指南。
当用户进入我们的网站时,会通过TripIt导航或者选定目标,再依据详细信息进行下面的操作。他们可以根据自己的喜好选择不同类别(比如餐厅的菜式风格、评级指标等吸引点进行筛选)。或者也可以通过浏览器查看生活指南做一些预订。
尽管我们的网站还处于beta版中,但是每天仍有几百万的访问量。周一下午2点我们接到一则通知,网站将在周四早上9:30(东部时间)一个非常受欢迎的电视节目上出现,根据以往的经验,我们料想这一天将会有20,000至150,000的需求访量,峰值可达到10,000,而服务器将承受100x至1,000x的需求请求数。
我们估计将会有10,000人同时访问,也就意味着要创建每分钟约40,000页的请求数。
我们所具备的:
- 运行于Amazon Web Services,采用EC2云平台。
- 当CPU负载超过网站服务器端时采用auto-scale规则检测,EC2实例将增加一倍。
- 部署过程(尽管这成为一种约束)
- 一个月前,花了一周时间将网站性能进行优化,将网页加载时间缩减了30%,大部分是通过减少数据库调用。
周一,我们在几台电脑上安装了JMeter(JMeter是Apache组织的开放源代码项目,它是功能和性能测试的工具)。你可以使用JMeter编写脚本从而模拟一个典型的用户访问。在以往案例中,会通过脚本来加载首页,通过选定目标,选择自己的喜好进入浏览,但我们没有发表任何留言(如果时间充足的话,我们很乐意去做)。利用模拟用户将大部分的数据库资料来构建用户界面。(这一点很重要)。
经过几次试验,我们发现当线程超过125个时,就无法在单一的JMeter实例上运行了。因此,如果我们要模拟真正的高负荷,这需要将JMeter同时运行在几台电脑上。同时,我们还要确定本地网络不在饱和状态,登陆VPN进入另一个网络,由一个客户端切换到另一个远程客户端。
我们很快了解到:
- 需要改变最初的auto-scale团队由2台服务器变成4台;
- 需要将CPU运行加载由80%降至50%
- 需要将加载持续的时间由5分钟减少至1分钟
经过这些改变后,让应用服务端规模扩大至16台,但这受到数据库的约束。数据库很快达到100%负荷,尽管我们扩大了AWS(Amazon Web Services)数据库的规模(超大的CPU和内存)但是仍然受约束。于是我们采用JMeter运行测试,设置平均响应时间为30秒,我们清楚的知道,如果这个问题无法解决,那我们便失败了。这一天是星期二。
我说过“我们会失败”,此前,我们采取了很多解决措施,团队中的大部分成员认为这样做未免太冒险了,会酿成大错。因为根据以往100%正确经验:在高负荷加载情况下会导致页面无法显示,这是100%有风险的。经过几个小时深思熟虑之后,我们深信,必须要解决可扩展性问题。
我们简要的探讨了几个不同的且动作较大的方法,但这些风险很大:
- 复制整个堆栈,并将他们之间的线程改为使用DynDNS或者Amazon's Route 53,因为我们没有用户账户或者是其他的共享数据,因此只能这么做。当然,我们也可以通过脚本将所有用户数据收集在一起来同步到所需数据库。但前提是,如果我们有一周的时间或者有(Ops)专业人士,或许我们会采取此方案。
- 卸载数据库加载的一部分,通过创建一个只读副本,分散数据库调用主要是静态数据(如目的地或者那些目的地中有吸引力的部分)来作为只读数据。我们并没有采取此做法,主要是因为在一台数据库服务器和其他不同数据库服务器之间无法找到数据的需求点。
- 使用一种机制memcache或是其他(mechanisms)机制用只读存储器取代数据库,这将导致大量的代码被更改。由于时间太短,我们认为风险很大。
因此,我们决定采取行动:
我们将一些易于识别的DB需求运行在每个页面上,这样能够满足每次加载相同的数据。我们在存储器中重新编写代码进行缓存,所以,当服务器重新启动加载时,不会重复。
周三早上,我们开始灵活运用JMeter做压力测试。为了防止由于来自于客户端成千上万的并发线程访问使服务器不堪重负,并保证监控到的(并发)请求次数在可控范围之内。我们让线程缓慢增加,保持着服务器的稳定性,统计每一秒总吞吐数,同时核查CPU的平均负载。接着,我们继续增加线程,核查(的步骤)。通过截图,我们记录了相关数据并让每个人都能看到。我们的目标是不仅要承受住高峰期的RPM,同时不额外增加响应时间。
随着时间的推移,我们通过改变代码来缓存数据库的一些需求。同时,在周四之前,我们不断更新最新需求来满足我们的需要。这就是我们的部署过程。
我们所具备的环境:本地机器,开发,示例,分期,生产(local machines, dev, demo, staging, production)。通常情况下,在进入开发之前,会有一个脚本测试运行(大约40分钟)。
脚本部署大概需要15—30分钟(80%的人把时间花在git clone上)我们需要重复这一过程,与生产逐渐靠近。
而生产最主要的一个环境正在上演。如果我们将代码优化,我们需要测试性能,这将花费几个小时部署过程才能有所变化。
相反,我们可以临时进入服务器,手动套用变更(利用ssh进入服务器,vi编辑文件,重新启动mongrel),但是我们要在4台服务器上重复这一过程,取消auto-scale服务器,重建auto-scale AMI,然后在连接auto-scale服务器。即使你采取捷径过程,仍需要20—30分钟。
我们重复了好几次,很小心的保存数据,这时我们发现,我们正进入一个错误的方向。因为每秒(900rpm)能维持15个请求,而数量也随之一天天的变少。聪明的数据库管理员看到这里也许会偷笑,知道是什么原因造成的。但是我们没有专门的DBA团队,我的数据库方面技能也有点生疏。
于是我们安装了New Relic,自从几年前我利用New Relic来开发Facebook应用程序后,便爱上了它。这是一个针对Rails和Web应用环境的性能监控解决方案。我只把它当做一款开发工具,因为它可以让你看到应用程序所花费的时间。它同样适用于生产监控,比如我们需要为职业规化付款,它还可以确定页面请求运行慢的原因并且便于数据库查询并且对已执行的数据可提供很多细节。
因此,我们迫切希望得到更多的数据以便升级至New Relic Pro计划中。我们有一些丰富的数据(数据库请求缓慢原因和应用所要花费的时间)。
一个数据库查询时间花费了整个数据库的95%。当我看到这一点时,我知道我们的表现变得很糟糕,这时我意识到在查询上还有个未索引的领域。也许是因为JMeter测试在数据库列表中创建了新条目,或许是因为已经运行了几个小时的缘故,在数据库中有成千上万行,因此我们需要对整个条目进行扫描。
于是我检查了数据库中schema.rb文件,发现有人错误的创建了一个multi-column索引(我想说避免创建multi-column索引,除非你知道需要一个特别的索引)。在多列索引中,如果你在查询中指定A,B,C,你可以用它来查询A,或者A和B,或者A和B和C。前提是必须依赖列,比如你无法脱离A列。
这在我们的案例中是真实存在的,所以,我立即为索引创建一个迁移。当然我们是在服务器上迁移,这样可以使服务器有5分钟时间去创建牵引,然后重新运行测试。每秒扩展到120个请求数(7.200 rpm),此时数据库加载时间只有16%。
周三下午大约3点钟,这时数据库服务器可以承受的负荷为每分钟约36,000页的请求数。我们再次突破服务器CPU加载的时间。
就在这一天,我们已经刷爆了EC2实例,我们还有一支团队一直在忙于扩展性测试,突破了极限。我们去除了AMIs一些不必要的运行,并要求其他人停止测试,我们反复的请求并添加AWS(Amazon Web Services)限制,然后进入客户代表的账号,并让他们提出升级需求。
周三晚上很晚我们才离开,此时我们信心倍加,相信能处理好周四早上的超负荷。
周四,我们所有人都在使用Google分析New Relic,它涵盖了所有屏幕的后端接口,因为它将在PST(太平洋标准时间Pacific Standard Time)上午5:30开始举行,我们遍布在酒店的每个角落,用篝火进行实时聊天。
第一次加载的时间在5:38分,此时此刻发生了极大的变化:在短短的一分钟时间里从原有0访量变成上千人访问。页面响应时间稳定在500ms,在服务器客户端页面,没出现过任何延误。
当然,我们也出现Bug,这就要求在加载的时候必须要对此进行修补,然后重新启动,(可以同时进行),还要确保页面通畅。加载高峰值我们已测试过,当然,运行的非常好,这也远远超过了我们周一下午前所做的改变。因此,这两天的辛苦,没白费,我们获得了回报。
经验与教训:
- 如果我们早些使用使用New Relic Pro计划,那么我们将节省很多时间,但是无论用哪种方法,我相信是New Relic拯救了我们的漏洞,如果我们在周三下午还没有得到所需要的数据,那么周四早上的一切都是扯谈。
- 加载测试需要定期进行,而不是在大事件发生时才想起测试。在任何时候也许某人使数据库发生改变,比如忘记了索引或是注入了一些其他的异常性能而又未被发现,直到系统加载时才知道。
- 需要一个相对安全的、自动化的方式便于提供执行加快部署分段和生产次数。必需制定捷径的常规过程。而不是依赖于某人用vi编辑文件,不会造成任何错误。
- 如果不使用云服务,我们根本不会如此之快扩展应用程序实例和数据库服务器。
- 团队的作用很重要,通常情况下,如果不是有危机发生我们是分散的,各司其职,也不会想到做些遥不可及的事情。