应用程序扩展性不好?来看基于Java开发的云应用程序
最近,我们受命开发一个用于分析大数据的软件即服务(SaaS)应用程序。为了进行数据挖掘,系统需要将数十亿个公开帖子存储在数据库中,并且对这些帖子进行分类处理。
这个环境下的分类是一个缓慢、耗费资源、又让人痛苦的过程,需要为数据库中的任何记录赋予主题或情绪。对我们的测试数据进行分类的过程持续时间长达24个小时。
为了应对这些要求,显然摆在我们面前的选择就是在亚马逊网络服务(AWS)上构建一个云应用程序。接手这个项目一段时间后,我想交流一下本人在开发基于Java的云应用程序方面的心得、认识和方法。
何谓云计算?
不妨先来看看维基百科给出的定义:
“云计算涉及网络上的分布式计算,某个程序或应用软件可能同时在多个连接的计算机上运行。” |
这个定义可能有点模糊不清,但是可以理解,因为云计算本身与其说是个技术术语,还不如说是个营销术语。对新手而言,要是我们用一种更实际的方法来定义云计算,理解起来就比较容易:
传统Web应用程序与云Web应用程序的唯一区别在于完美扩展的功能。如果给予无限制的硬件,云应用程序应该能够处理无限制的工作量。 |
如今云应用程序大行其道,这是由于人们对现代应用程序提出了更高的要求。在过去,谷歌以开发含有互联网上几乎所有可用信息的具有高扩展性的应用程序而出名。而如今,另外许多企业需要开发能够处理类似规模数据和计算的应用程序(Facebook、Youtube、LinkedIn、Twitter,还有像我们这样搜索和处理数据的人员)。
借助开发应用程序的传统方式,不可能处理这么庞大的数据量。这促使我们采用了一种全然不同的方法,开发可扩展性非常好的应用程序。那就是云应用程序。
为何开发Web应用程序的传统方法其扩展性不够好?
开发Web应用程序的传统方法
不妨看一下为什么传统应用程序无法处理规模庞大的数据。
如果你开发过一款传统的Web应用程序,它应该与上图非常相似。存在另外一些细小的差异,比如合并应用服务器和Web服务器或者多台企业服务器。不过大多数时候,数据库是关系型数据库。Web服务器通常是有状态(stateful)服务器,而企业服务器能同时提供无状态服务和有状态服务。
有一些重大的缺点导致这种架构不具备足够好的可扩展性。我们不妨先从定义完美的可扩展性开始分析。
如果给予双倍数量的带宽和双倍数量的硬件,某个系统总是能为双倍数量的工作提供一样的响应时间,就能实现完美的可扩展性(perfect scalability)。 |
完美的可扩展性在实际环境中实现不了。确切地说,开发人员只是旨在实现近似完美的可扩展性。比如说,DNS服务器就不在我们的控制范围之内。因而,从理论上来说,我们无法处理数量超出DNS服务器的请求。这就是任何系统的上限,连谷歌也不例外。
SQL
回到上面那个图,最大的弱点在于数据库的可扩展性。请求数量和数据大小都足够小时,开发人员应该不会注意到负载增加后性能受到的任何影响。继续进一步增加负载,如果处理器的使用率达到100%或者内存全部被占用,影响可能非常明显。这时候,最实际的办法就是为数据库系统提供更多的内存和处理器资源。之后,系统可能会再度顺畅运行起来。
遗憾的是,这种方法不可能只要问题出现就可以永远重复。总是会存在一个限制:不管你拥有多少内存和处理器资源,性能总会慢慢变得越来越差。这是预料之中的事,因为会有许多请求需要创建、读取、更新和删除(CRUD)某一些记录。不管你是决定缓存记录、将记录存储在内存中还是采取其他任何手段,它们都是独特的记录,存在于单一机器中,多少数量的访问请求可以发送到某一个内存地址是有限制的。
这又是不可避免的限制,因为SQL是为确保完整性而开发的。为了确保完整性,SQL服务器中的任何信息都应该具有独特性,这点必不可少。即便在进行了数据隔离或复制之后,这个特点仍然适用(至少对主实例来说是这样)。
相比之下,NoSQL并不试图对数据进行规范化。相反,它选择存储聚合对象,这些对象里面可能含有重复的信息。因此,NoSQL只有在数据完整性并非必需的情况下才适用。
来自couchbase.com的上面这个例子表明了数据如何存储在文档数据库中以及如何存储在关系数据库中。如果某户人家有多个成员,关系数据库只为所有家庭成员存储一个地址,而NoSQL数据库只是复制住址。某家人搬家后,所有家庭成员的住址可能在一个事务中未被更新,这违反了数据完整性。
不过,对我们的应用程序及另外许多应用程序而言,这种暂时的违反是可以接受的。比如说,你可能不需要你的社交网页页面浏览量或社交网站的公开帖子数做到百分之百准确。
数据重复实际上消除了我们上面提到的针对单单一个内存地址的并发访问,让开发人员可以选择将数据存储在自己希望的任何地方,只要一个节点中的变化内容可以慢慢同步到其他节点。这种架构的可扩展性强得多。
有状态
下一个问题是有状态服务。有状态服务需要同一批硬件来服务同一客户机提出的请求。客户机数量增加后,最明智的举动就是将更多的应用服务器和Web服务器部署到系统中。不过就有状态服务而言,资源分配无法做到完全优化。
就传统应用程序而言,负载均衡系统没有系统负载的任何信息,通常使用循环配置(Round Robin)手法,将请求分散到不同的服务器。这里的问题在于,并非所有的请求都一个样,也并非所有的客户机都发送数量一样的请求。这势必导致有些服务器不堪重负,而有些服务器仍然处于闲置状态。
混合数据检索和数据处理
就传统应用程序而言,从数据库检索数据的服务器最终要处理数据。处理数据和检索数据之间没有明确的分离。这两项任务都会给系统带来瓶颈。如果瓶颈来自数据检索,数据处理自然未得到充分使用,反之亦然。
重新考虑开发可扩展应用程序的最佳方法
看一下最近我们IT领域采用的方法,我发现它们根本不是什么新发明。确切地说,IT领域只是采用了已在实际生活中成功运用的方法来解决可扩展性问题。为了阐明这一点,不妨设想处理可扩展性问题的实际情形。
医院
假设我们有一家小型医院。就这个医院而言,我们服务的对象主要是本地客户。每个忠诚的客户都有自己青睐的医生,医生跟踪记录病人的病历。正由于如此,客户只要出示IC病历卡,他们青睐的医生就会处理病历。
让情况颇具挑战性的是,我们这家医院在互联网时代之前就在运作了。
有状态与无状态
上述描述是不是看起来与有状态服务足够相似?现在,你的医院开始有了名气,客户数量突然激增。假设你拥有足够的硬件基础设施,一个明显的选择就是招聘更多的医生护士。不过,客户不愿意换新来的医生。这导致新来的医工作员很空,而原来的工作人员很忙。
为了确保优化,你决定改变医院政策,以便客户必须保留其病历,医院将把他们分派给任何有空的医生。这个新做法有助于解决医院的所有头痛问题,让医院有办法调派更多的季节性人员,以处理客户数量突然激增的局面。
这个政策可能无法让客户满意,但是对IT领域来说,有状态服务和无状态服务提供了同样的结果。
数据复制
假设客户数量在不断激增,你开始考虑开设更多的分院。与此同时,出现了一个新的问题:客户不断抱怨医院规定看病就诊时要带病历的做法。
为了解决这个问题,你重新沿用了原来的政策:将病历储存在医院。不过,当你拥有不止一家分院,每家分院都需要储存用户病历的副本。一天或一周下来,记录的任何变化都需要同步到每一家分院。
服务分离
医院运作了几个月后,你认识到资源分配没有得到非常理想的优化。比如说,你在分院A和分院B都设有验血科和X光科。不过,许多客户在分院A进行验血,许多人在分院B拍X光片。
这导致客户在一家分院不断等待,另一家分院却无人光顾。为了优化资源,你关闭了没充分利用起来的科室,建立了独特的验血中心和X光中心。客户将从这些分院分派到提供特殊服务的特设中心。
特定的资源
很难为医院进行资源规划,因为季节性疾病只在一年当中的某个时间段才发生。此外,灾难有可能随时都会发生。它们导致短期内病房病人突然激增。为了应对这种情况,你可能想与市政当局签订协议,以便需要时可以暂时租赁设施和场地,招聘更多的兼职人员。
运用这些想法来开发云应用程序
现在,你在分析了上述例子后可能觉得,大多数想法颇有道理。开发人员很快就可以开始运用这些想法来开发Web应用程序。
然后,我们就进入了云应用程序时代。
如何开发云应用程序?
为了开发云应用程序,我们就要想方设法,运用上述的想法来开发应用程序。下面是我建议采取的方法。
基础设施
如果你开始考虑开发云应用程序,基础设施是需要关注的头一个问题。如果你的平台不支持特定的资源(动态提升现有服务器的硬件规格或启用新实例),想开发应用程序就很困难。
眼下,我们之所以选择AWS是因为它是市面上最成熟的平台。一年前,由于AWS具有的一些重大好处,我们从内部托管模式转为AWS托管模式。
- 多个位置:我们的客户来自五大洲;使用亚马逊区域(Amazon Region),我们就能让部署的实例更靠近客户位置;这样一来,可以缩短响应时间。
- 监控和自动扩展:亚马逊为其平台提供了一种相当有效的监控服务。面对服务器负载,可以实现自动扩展。
- 内容分发网络:亚马逊CloudFront为我们提供了这一选项,即将静态内容从主部署环境卸载过来,这将缩短页面装入时间。类似平常的实例,静态内容从最近的实例提供给客户。
- 同步与分布式缓存:这些年来,MemCache一直是我们青睐的缓存解决方案。不过,一大问题是缺少对节点之间同步的支持。亚马逊弹性缓存(Amazon Elastic Cache)为我们提供了使用MemCache的选择,而不必担心节点同步问题。
- 管理型API:这是一大优点。最近,我们开始充分使用管理型API,以便短时间启用实例,从而运行集成测试。
数据库
假设你已选择了用来开发云应用程序的平台,下一步应该是为你的系统选择合适的数据库。你需要做出的第一个决策就是哪个适合你的系统,是SQL还是NoSQL?如果系统不是数据密集型,SQL应该可以;如果系统是数据密集型,那么你应该考虑NoSQL。
有时候,多个数据库可以一起使用。比如说,如果我们想实施Facebook之类的社交网络应用程序,就可以将系统设置、或者甚至用户配置文件存储在SQL数据库中。相比之下,由于数据量庞大,用户帖子必须存储在NoSQL数据库中。此外,我们可以选择具有强大搜索功能的SOLR来存储公共帖子,并选择Mongo DB用来存储用户活动。
可能的话,务必选择支持集群、数据隔离和负载均衡等功能的数据库系统。要不然,你可能到头来得自行实施所有这些功能特性。比如说,相比Lucene,SOLR应该是更合适的选择,除非我们想自己进行数据隔离。
计算密集型还是数据密集型
要是我们知道系统是数据密集型还是计算密集型,就比较好。比如说,Facebook等社交网络几乎完全是数据密集型,而我们的大数据分析既是数据密集型又是计算密集型。
就数据密集型系统而言,我们可以让云中的任何一个节点检索数据、同时处理数据。如果是计算密集型节点,最好还是将数据检索和数据处理分开来。
数据密集型系统通常处理实时数据,而计算密集型系统运行后台任务来处理数据。将这两种繁重任务结合在同一个环境中可能最后会降低系统效果。
就计算云而言,最好是有一个框架来监控负载、分发任务以及计算完毕后收集结果。如果你不需要处理是实时的,Hadoop是市面上的最佳选择。如果需要实时计算,那么可以考虑Apache Storm。
云应用程序的设计模式
想开发一个成功的云应用程序,我们应该牢记以下几个方面。
1. 无状态
让你的所有服务和服务器都是无状态,这点必不可少。如果服务需要用户数据,就把它们作为参数添加到API中。
值得注意的是,想在Web服务器上实施无状态会话(Stateless Session),我们有几个选择可以考虑:
- 基于Cookie的会话
- 分布式缓存会话
- 数据库会话
上述解决方案从上往下排列,可扩展性较差,但可管理性较强。
2. 幂等性
就云应用程序而言,大多数API调用会通过网络而进行,而不是通过内部方法调用而进行。因此,如果我们能确保方法调用安全,比较好。如果你坚持使用上述的无状态原则,那么你实施的服务已经具有幂等性。
3. 远程外观
远程外观有别于外观模式。它们实际上看起来似乎一样,但旨在解决不同的问题。由于你的大多数API调用是通过网络进行的,网络延迟会对响应时间大有影响。借助远程外观模式,开发人员应该可以构建粗粒度API,那样就可以减少调用数量。
通俗地说,跑一趟超市,一次性购买10件商品比跑20趟超市、每趟购买1件商品来得明智。
4. 数据访问对象
你在传输数据时,要注意所传输的数据量。最好只传输所需的最少数据。
5. 稳扎稳打
这不是设计模式,但你会在将来庆幸稳扎稳打。由于分布式计算的特性,一旦哪里出了问题,就很难查明具体是哪个部分出了岔子。可能的话,为系统中的每个部分实施运行状况检查、ping检测、全面日志、调试模式等安全机制。
结束语
我希望开发云应用程序的这个方法能为大家带来一点帮助。要是你有其他什么观点或经验,欢迎留言交流。