CONAN ZONE

你越挣扎我就越兴奋

BlogJava 首页 新随笔 联系 聚合 管理
  0 Posts :: 282 Stories :: 0 Comments :: 0 Trackbacks
原文地址:http://www.theserverside.com/tt/articles/article.tss?l=ScalingYourJavaEEApplicationsPart2

当并发用户数明显的开始增长,你可能会不满意一台机器所能提供的性能,或者由于单个JVM实例gc的限制,你没法扩展你的java应用,在这样的情况下你可以做的另外的选择是在多个JVM实例或多台服务器上运行你的系统,我们把这种方法称为水平扩展。

请注意,我们相信能够在一台机器的多个JVM上运行系统的扩展方式是水平扩展方式,而非垂直扩展方式。JVM实例之间的IPC机制是有限的,两个JVM实例之间无法通过管道、共享内存、信号量或指令来进行通讯,不同的JVM进程之间最有效的通讯方式是socket。简而言之,如果Java EE应用如果扩展到多个JVM实例中运行,那么大多数情况下它也可以扩展到多台服务器上运行。

随着计算机越来越便宜,性能越来越高,通过将低成本的机器群组装为集群可以获得超过那些昂贵的超级计算机所具备的计算能力。不过,大量的计算机也意味着增加了管理的复杂性以及更为复杂的编程模型,就像服务器节点之间的吞吐量和延时等问题。

Java EE集群是一种成熟的技术,我在TSS上写了一篇名为“Uncover the Hood of J2EE Clustering”的文章来描述它的内部机制。

从失败的项目中吸取的教训

采用无共享的集群架构


Figure 3: share nothing cluster

最具备扩展性的架构当属无共享的集群架构。在这样的集群中,每个节点具备完全相同的功能,并且不需要知道其他节点存在与否。负载均衡器(Load Balancer)来完成如何将请求分发给这些后台的服务器实例。由于负载均衡器只是做一些简单的工作,例如分派请求、健康检查和保持session,因此负载均衡器很少会成为瓶颈。如果后端的数据库系统或其他的信息系统足够的强大,那么通过增加更多的节点,集群的计算能力可以得到线性的增长。

几乎所有的Java EE提供商在他们的集群产品中都实现了HttpSession的failover功能,这样即使在某些服务器节点不可用的情况下也仍然能够保证客户端的请求中的session信息不丢失,但这点其实是打破了无共享原则的。为了实现failover,同样的session数据将会被两个或多个节点共享,在我之前的文章中,我曾经推荐除非是万不得已,不要使用session failover。就像我文章中提到的,当失败发生时,session failover功能并不能完全避免错误,而且同时还会对性能和可扩展性带来损失。

使用可扩展的session复制机制

为了让用户获得更友好的体验,有些时候可能必须使用session failover功能,这里最重要的在于选择可扩展的复制型产品或机制。不同的厂商会提供不同的复制方案 - 有些采用数据库持久,有些采用中央集中的状态服务器,而有些则采用节点间内存复制的方式。最具可扩展性的是成对节点的复制(paired node replication),这也是现在大部分厂商采用的方案,包括BEA Weblogic、JBoss和IBM Websphere,Sun在Glassfish V2以及以上版本也实现了成对节点的复制。最不可取的方案是数据库持久session的方式。在我们实验室中曾经测试过一个采用数据库持久来实现 session复制的项目,测试结果表明如果session对象频繁更新的话,节点在三到四个时就会导致数据库崩溃。

采用collocated部署方式来取代分布式

Java EE技术,尤其是EJB,天生就是用来做分布式计算的。解耦业务功能和重用远程的组件使得多层的应用模型得以流行。但对于可扩展性而言,减少分布式的层次可能是一个好的选择。

在我们实验室曾经以一个政府的项目测试过这两种方式在同样的服务器数量上的部署 - 一种是分布式的,一种是collocated方式的,如下图所示:


Figure 4: distributed structure

Figure 5: collocated structure

结果表明collocated式的部署方式比分布式的方式更具备可扩展性。假设你应用中的一个方法调用了一堆的EJB,如果每个EJB的调用都需要load balance,那么有可能会因为需要分散到不同的服务器上进行调用导致你的应用崩溃,这样的结果就是,你可能做了很多次无谓的跨服务器的调用。来看更糟糕的情况,如果你的方法是需要事务的,那么这个事务就必须跨越多个服务器,而这对于性能是会产生很大的损害的。

共享资源和服务

对于用于支撑并发请求的Java EE集群系统而言,其扩展后的性能取决于对于那些不支持线性扩展的共享资源的操作。数据库服务器、JNDI树、LDAP服务器以及外部的文件系统都有可能被集群中的节点共享。

尽管Java EE规范中并不推荐,但为了实现各种目标,通常都会采用外部的I/O操作。例如,在我们实验室测试的应用中有用文件系统来保存用户上传的文件的应用,或动态的创建xml配置文件的应用。在集群内,应用服务器节点必须想办法来复制这些文件到其他的节点,但这样做是不利于扩展的。随着越来越多节点的加入,节点间的文件复制会占用所有的网络带宽和消耗大量的CPU资源。在集群中要达到这样的目标,可以采用数据库来替代外部文件,或采用SAN作为文件的集中存储,另外一个可选的方案是采用高效的分布式文件系统,例如Hadoop DFS(http://wiki.apache.org/hadoop/)。

在集群环境中共享服务很常见,这些服务不会部署到集群的每个节点,而是部署在专门的服务器节点上,例如分布式的日志服务或时间服务。分布式锁管理器 (DLM)来管理集群中的应用对这些共享服务的同步访问,即使在网络延时和系统处理失败的情况下,锁管理器也必须正常操作。举例来说,在我们的实验室中测试的一个ERP系统就碰到了这样的问题,他们写了自己的DLM系统,最终发现当集群中持有锁的节点失败时,他们的lock system将会永远的持有锁。

分布式缓存

我所碰到过的几乎所有的Java EE项目都采用了对象缓存来提升性能,同样所有流行的应用服务器也都提供了不同级别的缓存来加速应用。但有些缓存是为单一运行的环境而设计的,并且只能在单JVM实例中正常的运行。由于有些对象的创建需要耗费大量的资源,我们需要缓存,因此我们维护对象池来缓存对象的实例。如果获取维护缓存较之创建对象而言更划算,那么我们就提升了系统的性能。在集群环境中,每个jvm实例维护着自己的缓存,为了保持集群中所有服务器状态的一致,这些缓存对象需要进行同步。有些时候这样的同步机制有可能会比不采用缓存的性能还差,对于整个集群的扩展能力而言,一个可扩展的分布式缓存系统是非常重要的。

如今很多分布式缓存相关的开源java产品已经非常流行,在我们实验室中有如下的一些测试:

  • 1个基于JBoss Cache的项目的测试;
  • 3个基于Terracotta的项目的测试;
  • 9个基于memcached的项目的测试;
测试结果表明Terracotta可以很好的扩展到10个节点,并且在不超过5个节点时拥有很高的性能,但memcached则在超过20个服务器节点时会扩展的非常好。

Memcached

Memcached是一个高性能的分布式对象缓存系统,经常被用于降低数据库load,同时提升动态web应用的速度。Memcached的奇妙之处在于它的两阶段hash的方法,它通过一个巨大的hash表来查找key = value对,给它一个key,就可以set或get数据了。当进行一次memcached查询时,首先客户端将会根据整个服务器的列表来对key进行 hash,在找到一台服务器后,客户端就发送请求,服务器端在接收到请求后通过对key再做一次内部的hash,从而查找到实际的数据项。当处理巨大的系统时,最大的好处就是memcached所具备的良好的水平扩展能力。由于客户端做了一层hashing,这使得增加N多的节点到集群变得非常的容易,并不会因为节点的互连造成负载的增高,也不会因为多播协议而造成网络的洪水效应。

实际上Memcached并不是一款java产品,但它提供了Java client API,这也就意味着如果你需要在Java EE应用中使用memcached的话,并不需要做多大的改动就可以从cache中通过get获取值,或通过put将值放入cache中。使用 memcached是非常简单的,不过同时也得注意一些事情避免对扩展性和性能造成损失:

  • 不要缓存写频繁的对象。Memcached是用来减少对数据库的读操作的,而非写操作,在使用Memcached前,应先关注对象的读/写比率,如果这个比率比较高,那么采用缓存才有意义。
  • 尽量避免让运行的memcached的节点互相调用,对于memcached而言这是灾难性的。
  • 尽量避免行方式的缓存,在这样的情况下可采用复杂的对象来进行缓存,这对于memcached来说会更为有效。
  • 选 择合适的hashing算法。在默认的算法下,增加或减少服务器会导致所有的cache全部失效。由于服务器的列表hash值被改变,可能会造成大部分的 key都要hash到和之前不同的服务器上去,这种情况下,可以考虑采用持续的hashing算法(http://weblogs.java.net /blog/tomwhite/archive/2007/11/consistent_hash.html) 来增加和减少服务器,这样做可以保证你大部分缓存的对象仍然是有效的。

Terracotta

Terracotta(http://www.terracottatech.com/)是一个企业级的、开源的、JVM级别的集群解决方案。JVM级的集群方案意味着可以支撑将企业级的Java应用部署部署到多JVM上,而且就像是运行在同一个JVM中。 Terracotta扩展了JVM的内存模型,各虚拟机上的线程通过集群来与其他虚拟机上的线程进行交互(Terracotta extends the Java Memory Model of a single JVM to include a cluster of virtual machines such that threads on one virtual machine can interact with threads on another virtual machine as if they were all on the same virtual machine with an unlimited amount of heap.)。



Figure 6: Terracotta JVM clustering


采用Terracotta来实现集群应用的编程方式和编写单机应用基本没有什么差别,Terrocotta并没有特别的提供开发者的API,Terracotta采用字节码织入的方式(很多AOP软件开发框架中采用的技术,例如AspectJ和AspectWerkz)来将集群方式的代码插入到已有的java语言中。

我猜想Terrocotta是通过某种互连的方式或多播协议的方式来实现服务器和客户端JVM实例的通讯的,可能是这个原因导致了在我们实验室测试时的效果:当超过20个节点时Terracotta扩展的并不是很好。(注:这个测试结果仅为在我们实验室的测试结果,你的结果可能会不同。)

并行处理

我之前说过,单线程的任务会成为系统可扩展性的瓶颈。但有些单线程的工作(例如处理或生成巨大的数据集)不仅需要多线程或多进程的运行,还会有扩展到多节点运行的需求。例如,在我们实验室测试的一个Java EE项目有一个场景是这样的:根据他们站点的日志文件分析URL的访问规则,每周产生的这些日志文件通常会超过120GB,当采用单线程的Java应用去分析时需要耗费四个小时,客户改为采用Hadoop Map-Reduce使其能够水平扩展从而解决了这个问题,如今这个分析URL访问规则的程序不仅运行在多进程模式下,同时还并行的在超过10个节点上运行,而完成所有的工作也只需要7分钟了。

有很多的框架和工具可以帮助Java EE开发人员来让应用支持水平扩展。除了Hadoop,很多MPI的Java实现也可以用来将单线程的任务水平的扩展到多个节点上并行运行。

MapReduce

MapReduce由Google的Jeffrey Dean和Sanjay Ghemawat提出,是一种用于在大型集群环境下处理巨量数据的分布式编程模型。MapReduce由两个步骤来实现 - Map:对集合中所有的对象进行操作并基于处理返回一系列的结果,Reduce:通过多线程、进程或独立系统并行的从两个或多个Map中整理和获取结果。Map()和Reduce()都是可以并行运行的,不过通常来说没必要在同样的系统同样的时间这么来做。

Hadoop是一个开源的、点对点的、纯Java实现的MapReduce。它是一个用于将分布式应用部署到大型廉价集群上运行的Lucene-derived框架,得到了全世界范围开源人士的支持以及广泛的应用,Yahoo的Search Webmap、Amazon EC2/S3服务以及Sun的网格引擎都可运行在Hadoop上。

简单来说,通过使用“Hadoop Map-Reduce”,"URL访问规则分析"程序可以首先将日志文件分解为多个128M的小文件,然后由Hadoop将这些小文件分配到不同的Map()上去执行。Map()会分析分配给它的小文件并产生临时的结果,Map()产生的所有的临时结果会被排序并分配给不同的Reduce(),Reduce()合并所有的临时结果产生最终的结果,这些Map和Reduce操作都可以由Hadoop框架控制来并行的运行在集群中所有的节点上。

MapReduce对于很多应用而言都是非常有用的,包括分布式检索、分布式排序、web link-graph reversal、term-vector per host、web访问日志分析、索引重建、文档集群、机器智能学习、statistical machine translation和其他领域。

MPI

MPI是一种语言无关、用于实现并行运行计算机间交互的通讯协议,目前已经有很多Java版本的MPI标准的实现,mpiJava和MPJ是其中的典型。mpiJava 基于JNI绑定native的MPI库来实现,MPJ是100%纯java的MPI标准的实现。mpiJava和MPJ和MPI Fortran和C版本提供的API都基本一致,例如它们都对外提供了具备同样方法名和参数的Comm class来实现MPI的信息传递。

CCJ是一个类似MPI通讯操作的java库。CCJ提供了barrier、broadcast、scatter、 gather、all-gather、reduce和all-reduce操作的支持(但不提供点对点的操作,例如send、receive和send- receive)。在底层的通讯协议方面,CCJ并没有自己实现,而是采用了Java RMI,这也就使得CCJ可以用来传递复杂的序列化对象,而不仅仅是MPI中的原始数据类型。进一步看,CCJ还可以从一组并行的processes中获取到复杂的集合对象,例如实现了CCJ的DividableDataObject接口的集合。

采用不同的方法来获取高扩展能力

有很多的书会教我们如何以OO的方式来设计灵活架构的系统,如何来使服务透明的被客户端使用以便维护,如何采用正常的模式来设计数据库schema以便集成。但有些时候为了获取高扩展性,需要采用一些不同的方法。

Google设计了自己的高可扩展的分布式文件系统(GFS),它并不是基于POSIX API来实现的,不过GFS对于用户来说并不完全透明。为了使用GFS,你必须采用GFS的API包。Google也设计了自己的高可扩展的分布式数据库系统(Bigtable),但它并不遵循ANSI SQL标准,而且其中的概念和结构和传统的关系数据库几乎完全不同,但最重要的是GFS和Bigtable能够满足Google的存储要求、良好的扩展性要求,并且已经被Google的广泛的作为其存储平台而使用。

传统方式下,我们通过使用更大型的、更快和更贵的机器或企业级的集群数据库(例如RAC)来将数据库扩展到多节点运行,但我有一个我们实验室中测试的social networking的网站采用了不同的方式,这个应用允许用户在网站上创建profiles、blogs,和朋友共享照片和音乐,此应用基于Java EE编写,运行在Tomcat和Mysql上,但不同于我们实验室中测试的其他应用,它只是希望在20多台便宜的PC Server上进行测试,其数据模型结构如下:


Figure 7: Users data partitions


这里比较特殊的地方子碍于不同的用户数据(例如profile、blog)可能会存储在不同的数据库实例上,例如,用户 00001存储在服务器A上,而用户20001存储在服务器C上,分库的规则以一张元信息的表的方式存储在专门的数据库上。当部署在Tomcat的 Java EE应用希望获取或更新用户信息时,首先它会从这张元信息的表中获取到需要去哪台服务器上获取这个用户,然后再连到实际的服务器上去执行查询或更新操作。

用户数据分区和这种两步时的动作方式可以带来如下的一些好处:

  • 扩展了写的带宽:对于这类应用而言,blogging、ranking和BBS将会使得写带宽成为网站的主要瓶颈。分 布式的缓存对于数据库的写操作只能带来很小的提升。采用数据分区的方式,可以并行的进行写,同样也就意味着提升了写的吞吐量。要支持更多的注册用户,只需 要通过增加更多的数据库节点,然后修改元信息表来匹配到新的服务器上。
  • 高可用性:如果一台数据库服务器down了,那么只会有部分用户被影响,而其他大部分的用户可以仍然正常使用;
同时也会带来一些缺点:
  • 由于数据库节点可以动态的增加,这对于在Tomcat中的Java EE应用而言要使用数据库连接池就比较难了;
  • 由于操作用户的数据是两步式的,这也就意味着很难使用ORMapping的工具去实现;
  • 当要执行一个复杂的搜索或合并数据时,需要从多台数据库服务器上获取很多不同的数据。

这个系统的架构师这么说:“我们已经知道这些缺点,并且准备好了应对它,我们甚至准备好了应对当元信息表的服务器成为瓶颈的状况,如果出现那样的状况我们将会把元信息表再次划分,并创建出一个更高级别的元信息表来指向众多的二级元信息表服务器实例。“

参考

  1. Scalability definition in wikipedia: http://en.wikipedia.org/wiki/Scalability
  2. Javadoc of atomic APIs: http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/atomic/package-summary.html
  3. Alan Kaminsky. Parallel Java: A unified API for shared memory and cluster parallel programming in 100% Java: http://www.cs.rit.edu/~ark/20070326/pj.pdf
  4. OMP-an OpenMP-like interface for Java: http://portal.acm.org/citation.cfm?id=337466
  5. Google MapReduce white paper: http://labs.google.com/papers/mapreduce-osdi04.pdf
  6. Google Bigtable white paper: http://labs.google.com/papers/bigtable-osdi06.pdf
  7. Hadoop MapReduce tutorial: http://hadoop.apache.org/core/docs/r0.17.0/mapred_tutorial.html
  8. Memcached FAQ: http://www.socialtext.net/memcached/index.cgi?faq
  9. Terracotta: http://www.terracotta.org/

关于作者

Wang Yu目前在Sun的ISVE Group小组工作,担任的职位为Java工程师和架构咨询师,他承担的职责包括支持本地的ISVs,为一些重要的Java技术例如Java EE、EJB、JSP/Servlet、JMS和web services技术提供咨询,可以通过wang.yu@sun.com联系他。
posted on 2008-07-10 19:36 CONAN 阅读(209) 评论(0)  编辑  收藏 所属分类: J2EE