Java EE 迎合 Web 2.0
2009-11-05 00:00:00 来源:WEB开发网很多成功的企业应用程序都是使用 Java EE 平台构建的。但是,Java EE 的设计原理并不能够有效地支持 Web 2.0 应用程序。深入了解 Java EE 和 Web 2.0 原理之间的脱节可帮助您制定明智的决策,从而使用各种方法和工具在一定程度上解决这种脱节。本文将解答 Web 2.0 和标准 Java EE 平台缘何成为失败的组合,并演示为何由事件驱动的异步架构更适合 Web 2.0 应用程序。本文还介绍了一些框架和 API,它们通过支持异步设计使得 Java 平台更加适合 Web 2.0。
Java EE 原理和设想
Java EE 平台的创建目的就是为企业到客户(B2C)和企业到企业(B2B)应用程序提供支持。企业发现了 Internet 的价值之后就开始使用它增强与合作伙伴和客户之间的现有业务流程。这些应用程序通常要与一个现有企业集成系统(EIS)进行交互。大多数常见基准测试(测试 Java EE 服务器的性能和可伸缩性)— ECperf 1.1、SPECjbb2005 和 SPECjAppServer2004— 的用例都将这一点反映到了 B2C、B2B 和 EIS 中。类似地,标准的 Java PetStore 演示也是一个典型的电子商务应用程序。
很多有关 Java EE 架构可伸缩性的明显和暗含的设想都反映在基准测试中:
从客户机角度来看,请求吞吐量是影响性能的最重要特性。
事务持续时间是最重要的性能因素,并且,缩减所有个体事务的持续时间将改善应用程序的总体性能。
事务之间通常都是彼此独立的。
除长期执行的事务以外,只有少数业务对象会受事务影响。
应用服务器的性能和部署在同一管理域的 EIS 会限制事务的持续时间。
通过使用连接池可以抵消一定的网络通信成本(在处理本地资源时产生)
通过对网络配置、硬件和软件进行优化,可以缩短事务持续时间。
应用程序所有者可以控制内容和数据。在不依赖外部服务的前提下,向用户提供内容的最重要限制因素是带宽。
性能和可伸缩性问题
Java EE 平台最初的设计目的是使用部署在单个管理域中的资源操作服务。其设想的前提是 EIS 事务生存期较短并且请求处理较快,从而使平台能够支持较高的事务负载。
很多新兴架构方法和模式 — 例如对等(P2P)、面向服务架构和统称(非正式地)为 Web 2.0 的新型 Web 应用程序 — 不满足这些假设。在这些应用程序的使用场景中,请求处理将占用更长的时间。因此,当使用 Java EE 方法开发 Web 2.0 应用程序时,将出现严重的性能和可伸缩性问题。
这些设想产生了以下 Java EE API 构建原理:
同步 API。Java EE 在很多应用中都需要使用同步 API(重量级并且繁琐的 Java Message Service (JMS) API 基本上是惟一的例外)。这种需求更多地源于可用性的需要,而非性能需求。同步 API 易于使用并且开销较低。但需要处理大型多线程时,则会出现严重问题,因此 Java EE 严格限制未受控制的多线程处理。
有限的线程池。人们很快发现线程是种重要的资源,并且当线程数量超过某一界限后,应用服务器的性能将显著下降。然而,根据每个操作都很短暂的设想,这些操作可以分配到一组有限的线程中,从而维持较高的请求吞吐量。
有限的连接池。如果只使用一个数据库连接,则很难获得最优的数据库性能。虽然一些数据库操作可以并行执行,但是增加额外的数据库连接只能将应用程序提速到某一点。当连接数达到某一值后,数据库性能将开始下滑。通常,数据库连接的数量要小于 servlet 线程池中可用线程的数量。因此,连接池在创建时允许向服务器组件 — 例如 servlet 和 Enterprise JavaBeans (EJB) — 分配一个连接并在以后返回给连接池。如果连接不可用,组件将等待阻塞当前线程的连接。因为其他组件只对连接占用很短的时间,因此这种延迟通常较短。
固定的资源连接。应用程序被假设只使用很少一些外部资源。与各个资源的连接工厂通过 Java Naming and Directory Interface (JNDI)(或 EJB 3.0 的依赖性注入)获得。实际上,支持与不同 EIS 资源进行连接的主要 Java EE API 只有企业 Web 服务 API。其他 API 多数都假设资源是固定的并且只有诸如用户凭证这样的额外数据应该提供给开放连接操作。
在 Web 1.0 中,这些原理玩转得非常好。可以将一些独特的应用程序设计为遵守这些规则。但是,这些原理不能有效支持 Web 2.0。
Web 2.0 带来的巨变
Java EE 迎合 SOA
SOA 的引入首先对 Java EE 提出了挑战。在 SOA 中,交互通常产生很高的吞吐量,并且由于要跨多个域到达服务端点,因此很可能会产生较高的延迟。一些交互可能还需要得到操作人员的允许,而这种批准流程可能会产生几小时到几周的延迟。各种中间流程通常会使延迟情况进一步恶化,SOA 的出现就是为了支持这些中间流程。
通过利用事务消息传递 API 并引入业务流程概念,Java EE 平台已经解决了延迟带来的难题。SOAP-over-HTTP Web 服务调用模型和诸如 JMS 之类的消息传递服务之间一直不太匹配。HTTP 使用同步请求/响应模型并且没有提供任何内置的可靠特性。诸如 WS-Notification、WS-Reliability、WS-ReliableMessaging 和 WS-ASAP 这些规范试图针对部署在 B2B 环境中的 Web 服务解决这种错误匹配。但是对于 B2C 场景,通常部署的是富应用程序客户机,因为这种客户机可以使用特定于场景的交互模式(相对于 Web 应用程序)处理高延迟。
Web 2.0 应用程序具有很多独特需求,因此,不适合将 Java EE 用于 Web 2.0 实现。其中一个需求就是,Web 2.0 应用程序更多地通过服务 API 使用另一个 Web 2.0 应用程序,而不是使用 Web 1.0 应用程序。Web 2.0 应用程序的一个更为重要的因素是,极度倾向于用户到用户(C2C)交互:应用程序所有者只生成一小部分内容;用户负责生成大部分内容。
SOA + B2C + Web 2.0 = 高延迟
在 Web 2.0 环境中,聚合应用程序经常使用通过 SOA 服务 API 公开的服务和提要。这些应用程序需要在 B2C 环境中使用服务。例如,一个聚合应用程序可能从三个不同的数据源提取数据,如天气信息、交通信息和地图。检索这三种独特数据所需的时间延长了总的请求处理时间。不管数据源和服务 API 的数量是否增加,用户仍然期望得到具有高反应度的应用程序。
诸如缓存这类技术可以缓解延迟问题,但是不适用于所有场景。比如,可以缓存地图数据来减少响应时间,但通常并不适合将搜索查询结果或者实时交通信息进行缓存。
服务调用本来就是一种高延迟过程,在客户机和服务器上通常只分配很小一部分 CPU 资源。Web 服务调用的持续时间很大一部分用于建立连接和传输数据。因此,通常来讲,提升客户端或服务器端的性能对于减少调用持续时间效果甚微。
更好的交互性
Web 2.0 对用户参与的支持引发了另外一大挑战,因为应用程序要处理来自每个活动用户的更多数量的请求。下面这些理由证明了这一点:
因为大多数事件是由其他用户的操作引起的,因此会引发更多相关事件,并且用户具备更强大的能力来生成事件。这些事件通常使用户能够更加积极地使用 Web 应用程序。
应用程序为用户提供了更多的用例。Web 1.0 用户仅仅可以浏览类别、购买商品并跟踪他们的订单处理状态。现在,用户可以通过论坛、聊天、聚合等等方法与其他用户进行积极地交流,这将产生更高的通信负载。
如今的应用程序越来越多地使用 Ajax 改善用户体验。与普通 Web 应用程序的页面相比,使用 Ajax 的 Web 页面加载要慢一些,因为页面是由一些静态内容、脚本(可能会非常大)和一些发往服务器的请求组成。加载完成后,Ajax 页面通常会向服务器生成一些短小的请求。
高延迟和低带宽客户机
以手机和其他限制带宽的客户机为目标的应用程序日趋流行。即使服务器可以为特定客户机提供快速服务,客户机仍然不能迅速地使用数据,这要归咎于其低带宽连接和设备本身的物理限制。虽然客户机是通过低吞吐量连接加载数据,服务器在占用 servlet 线程时仍然未被充分利用或处于等待状态。随着越来越多的移动设备使用网络服务以及无线频段的充分利用,这类客户机的吞吐量和延迟性将逐渐减少,除非开发出一种通信机制来提供更好的可伸缩性。
与典型的 Web 1.0 应用程序相比,这些因素往往会生成更多的服务器通信量和请求数。在高负载期间,这种通信量难于控制(然而,Ajax 也提供了更多的机会对通信量进行优化;与支持相同用例的简单 Web 应用程序相比,Ajax 生成的通信量通常更少)。
更多内容
Web 2.0 的特征就是比上一代 Web 应用程序拥有更大量的内容和更大的规模。
在 Web 1.0 世界中,内容通常只有经过业务实体的明确允许后才被发布到公司网站。企业需要控制所显示的文本的每个字符。因此,如果计划发布的内容超出了框架的大小限制,则要对内容进行优化或将其分成几个较小的部分。
Web 2.0 站点的一个特性就是不会限制内容的大小或创建。大部分 Web 2.0 内容由用户和社区生成。组织和企业仅仅提供工具实现内容创建和发布。由于使用了大量图像、音频和视频,内容的大小也相应增加。
持久连接
建立客户机到服务器的新连接会耗费很多时间。如果某些交互在预期之中,则建立一次客户机/服务器通信,然后重复使用该连接,这样做会获得更高的效率。持久连接对于发送客户机通知也很有用。但是 Web 2.0 应用程序的客户机通常位于防火墙之后,一般很难或不能直接建立服务器到客户机的连接。Ajax 应用程序需要发送请求轮询特定事件。要减少轮询请求的数量,一些 Ajax 应用程序使用 Comet 模式:该服务器被设计为在某个事件发生以前保持等待状态,然后发送应答,同时保持连接打开。
对等消息传递协议,如 SIP、BEEP 和 XMPP,逐渐使用持久连接。流式直播视频也从持久连接中获益良多。
更容易发生 Slashdot 效应
Web 2.0 应用程序拥有大量的访客,这一点使某些站点更容易发生 “Slashdot 效应” — 如果某个流行的 blog、新闻站点或社会型网站提及某个站点时,该站点的通信量负载会猛增。所有 Web 站点都应该准备好处理比普通负载高几个数量级的通信量。这种情况下更重要的一点是,站点在如此高的负载下不会发生崩溃。
延迟问题
与操作吞吐量相比,操作延迟对 Java EE 应用程序的影响更大。即使应用程序使用的服务可以处理大量操作,延迟仍然保持不变或者进一步恶化。目前的 Java EE API 还无法很好地处理这一情况,因为这种情况违背了这些 API 设计中暗含的延迟假设。
在使用同步 API 时,为论坛或 blog 中的大型页面提供服务将开启一个处理线程。如果每个页面需要一秒钟的服务时间(例如 LiveJournal 这类应用程序,包含很多较大的页面),并且线程池包含 100 个线程,那么一秒钟的时间内无法为超过 100 个页面提供服务 — 这种性能无法接受。增加线程池中的线程数量收效甚微,因为当线程池中的线程数量增加时,应用程序-服务器性能将开始降低。
Java EE 架构无法利用 SIP、BEEP 和 XMPP 这样的消息传递协议,因为 Java EE 的同步 API 持续使用单个线程。由于应用服务器使用有限的线程池,持续使用一个线程将使应用服务器在使用这些协议发送和接收消息时无法处理其他请求。同样要注意,使用这些协议发送的消息可长可短(尤其在使用 BEEP 时),并且要生成这些消息,还需要使用 Web 服务或其他方法访问部署在其他组织内的资源。此外,BEEP 和 Stream Control Transmission Protocol (SCTP) 这样的传输协议在 TCP/IP 连接之上还需要建立一些同步的逻辑连接,这使线程管理问题变得更加严重。
要实现流式场景,Web 应用程序必须摒弃标准的 Java EE 模式和 API。因此,Java EE 很少用于运行 P2P 应用程序或流视频。对于经常使用 Java Connector Architecture (JCA) 连接器实现专有异步逻辑的协议,常常开发自定义组件来处理(正如后文将介绍的,新一代 servlet 引擎也支持使用非标准接口处理 Comet 模式。然而,就 API 和使用模式而言,这种支持与标准的 servlet 接口截然不同)。
最后,回想一下 Java EE 的一个基本原理,即对网络基础结构进行优化可以缩短事务持续时间。但是,对于现场直播的视频提要,提升网络基础结构的速度丝毫不会减少请求的持续时间,因为视频流是边生成边发送给客户机的。对网络基础结构进行优化只会增加流的数量,从而支持更多的客户机并以更高的分辨率处理流。
异步方法
要避免我们讨论的这些问题,一个可行的方法就是在设计应用程序时将延迟纳入到考虑事项中并使用由事件驱动的异步方法实现应用程序。如果应用程序处于空闲状态,则不会占用线程这样的有限资源。通过使用异步 API,应用程序将对外部事件进行轮询并在事件发生后执行相应的操作。通常,这种应用程序被分为若干个事件循环,每个循环都有自己独有的线程。
事件驱动异步设计的一个明显优点就是,如果大量等待外部服务的操作之间没有数据依赖关系,则可以并行执行这些操作。即使根本没有发生并行操作,事件驱动的异步架构也提供了优于传统同步设计的强大的可伸缩性。
异步 API 优点:概念证明模型
可以通过一个简单的 servlet 流程模型演示异步 API 带来的可伸缩性优势(如果您已经确信异步设计能够满足 Web 2.0 应用程序的可伸缩性需求,那么可以跳过本节内容,直接了解可用来解决 Web 2.0 / Java EE 问题的 解决方案讨论)。
在我们的模型中,servlet 流程对到来的请求执行一些处理,对数据库进行查询,然后使用从数据库获取的信息调用 Web 服务。最后,根据 Web 服务的响应生成最终的响应。
模型的 servlet 使用两种类型的资源,并伴有较高的延迟。在逐渐增加的负载之下,这些资源的特征和行为都互不相同:
数据库连接。这种资源通常以 DataSource 的形式用于 Web 应用程序,它提供了数量有限的连接,通过这些连接实现同步处理。
网络连接。这种资源用于编写对客户机的响应并调用 Web 服务。直到现在为止,这种资源在大多数应用服务器中都受到限制。然而,新一代应用服务器开始使用 nonblocking I/O (NIO) 实现这种资源,因此我们可以根据需要使用任意数量的同步网络连接。模型 servlet 在以下几种情形中使用这种资源:
调用 Web 服务。尽管目标服务器每秒可以处理的请求的数量是有限制的,但是这个数量通常都很高。调用持续时间取决于网络通信量。
从客户机读取请求。我们的模型忽视了这一开销,因为模型假定使用了一个 HTTP GET 请求。在这种情形下,从客户机读取请求所需的时间不会添加到 servlet 请求持续时间中。
向客户机发送响应。我们的模型忽视了这一开销,因为,对于较短的 servlet 响应来说,应用服务器可以在内存中缓冲该响应,然后再使用 NIO 将它发送给客户机。并且我们假设这个响应非常短小。在这种情形下,向客户机发送响应所需的时间不会添加到 servlet 请求持续时间中。
让我们假设 servlet 执行时间被划分为如表 1 所示的几个阶段:
表 1. Servlet 操作时限(以抽象单位表示持续时间)
阶段 | 持续时间 | 操作 |
1 | 2 个单位 | 解析 servlet 请求信息 |
2 | 8 个单位 | 处理本地数据库事务 |
3 | 2 个单位 | 处理数据库请求结果并准备远程调用 |
4 | 16 个单位 | 使用一个 Web 服务调用远程服务器 |
5 | 4 个单位 | 创建响应 |
总用时: | 32 个单位 |
图 1 展示了执行期间业务逻辑、数据库和 Web 服务之间的时间分布:
图 1. 执行步骤的时间分布
图片看不清楚?请点击这里查看原图(大图)。
这些选择的时限提供了一个可读的图表。在实际中,大多数 Web 服务进行处理使用的时间远远超过图表显示的时间。可以这样讲,Web 服务的处理时间要比业务逻辑 Java 代码的处理时间高出 100 到 300 倍。但是,为了演示同步调用模型,我们挑选了一些不太符合现实的参数,比如,Web 服务性能极其快,或者应用服务器速度很慢,或两者兼有。
让我们假设连接池的容量为 2。因此,同一时间内只能处理两个数据库事务。(对于真实的应用服务器,实际的线程数和连接数要比这个数大)。
我们还假设 Web 服务调用使用的时间相同并且全部可以并行处理。这一假设比较符合实际,因为 Web 服务交互过程包括来回发送数据。执行实际的处理只是 Web 服务调用的一小部分。
对于这种场景,同步和异步用例在低负载下表现相同。如果数据库查询和 Web 服务调用并行进行,异步用例表现更加良好。在发生超载时,比如访问量忽然达到峰值,将看到一个有趣的结果。我们假设同一时刻有 9 个请求。对于同步用例,servlet 引擎线程池有三个线程。而对于异步用例,我们只使用一个线程。
注意,在这两个用例中,所有 9 个连接在到达时全部被接受(大多数 servlet 引擎都会这样做)。然而,在处理前三个连接时,同步用例没有对接受的其他六个连接进行处理。
图 2 和图 3 是使用一个简单的模拟程序创建的,它分别模拟同步和异步 API 用例:
图 2. 同步用例
图片看不清楚?请点击这里查看原图(大图)。
图 2 中的每个矩形表示流程的一个步骤。矩形中的第一个数字是流程编号(1 到 9),第二个数字是流程内的阶段编号。每个流程使用惟一的颜色标记。注意,数据库和 Web 服务操作位于单独的行中,因为它们分别由数据库引擎和 Web 服务实现执行。servlet 引擎在等待结果期间不执行任何操作。浅灰色区域表示空闲(等待)状态。
图表底部的菱形标记表示在该点完成了一个或多个请求。标记的第一个数字表示以抽象单位计算的时间;第二个使用圆括号括起的可选数字表示在该点终止的请求数。在图 2 中可以看到,前两个请求在点 32 处完成,最后一个请求在点 104 处完成。
现在假设数据库和 Web 服务客户机运行时支持异步接口。并且假设所有异步 servlets 只使用一个线程(但是,如果提供了额外线程的话,异步接口非常适合使用额外线程)。图 3 显示了结果:
图 3. 异步用例
图片看不清楚?请点击这里查看原图(大图)。
图 3 中有几处需要注意。第一个请求要比同步用例中晚结束 23%。但是,最后一个请求则快了 26%。并且所使用的线程只是同步用例的三分之一。请求执行时间的分布更加有规律,因此用户可以以更加有规律的速度接收页面。第一个请求和最后一个请求的处理时间相差了 80%。在同步接口用例中,这个值达到了 225%。
现在假设我们对应用程序和数据库服务器进行了升级,它们的性能提升了两倍。表 2 展示了用时结果(使用与表 1 相关的单位):
表 2. 升级后的 Servlet 操作时限
阶段 | 持续时间 | 操作 |
1 | 1 个单位 | 解析 servlet 请求信息 |
2 | 4 个单位 | 执行本地数据库事务处理 |
3 | 1 个单位 | 处理数据库请求结果并为远程调用做准备 |
4 | 16 个单位 | 使用 Web 服务调用远程服务器 |
5 | 2 个单位 | 创建响应 |
总用时: | 24 个单位 |
可以看到,单个请求处理时间一般为 24 个时间单位,大概是原来的请求持续时间的 3/4。
图 4 展示了业务逻辑、数据库和 Web 服务之间的新的分布:
图 4. 升级后的步骤时间分布
图片看不清楚?请点击这里查看原图(大图)。
图 5 展示了同步处理后的结果。可以看到,总体持续时间减少了 25%。但是,步骤的分布模式没有发生很大变化,并且 servlet 线程处于等待状态的时间更长了。
图 5. 升级后的同步用例
图片看不清楚?请点击这里查看原图(大图)。
图 6 展示了异步 API 的处理结果:
图 6. 升级后的异步用例
图片看不清楚?请点击这里查看原图(大图)。
使用异步 API 得到的结果非常有趣。数据库和应用服务器的性能提高时,处理可以很好地进行相应扩展。结论已经得到证明,并且最差和最佳请求处理时间相差只有 57%。总的处理时间(截至最后一个请求完成)是升级之间所使用时间的 57%。与同步用例的 75% 相比,这是一个很显著的改进。最后一个请求(两种情况中的第 9 个请求)要比同步用例中早完成 40%,而第一个请求仅仅比同步用例晚 14%。此外,在异步用例中,可以执行更多数量的并行 Web 服务操作。而使用同步则无法达到这种并行程度,因为 servlet 线程池中的线程数是有限制的。即使 Web 服务能够处理更多的请求,servlet 也不会发送请求,因为它不处于活动状态。
实际的测试结果表明,异步应用程序具有更好的可伸缩性并且可以更从容地应对超载情况。延迟问题非常棘手,并且 Moore 定律也帮不了什么忙。大多数现代计算改进增加了所需的带宽。多数情况下,延迟可能维持不变,甚至进一步恶化。正因为如此,开发人员才尝试将异步接口引入到应用服务器中。
目前,可以使用很多方法实现异步系统,但是还未将其中任何一种方法确立为事实标准。每种方法都各有优缺点,并且它们在不同的情形中扮演不同的角色。本文后面的内容将对这些机制进行大致介绍,包括各种机制的优缺点,使您能够使用 Java 平台构建事件驱动的异步应用程序。
一般解决方案
Ad-hoc 并发性和 NIO
自 Java 1.4 起,Java 语言提供了一个非阻塞网络 I/O API(java.nio.*)。而从 Java SE 5 开始,Java 提供了更加标准的并发性工具(java.util.concurrent.*)。开发人员利用非阻塞 I/O 和并发性实现的应用程序能够通过可用的 API 和框架支持大量同步连接。
然而,这些 API 仍然处于较低的级别,并且通常只有在无法使用其他方式解决性能问题时才得到使用。NIO 选择器机制是一种非常低级的 API。使用它很难编写任何比复制流更复杂的操作。编写使用相同 NIO 选择器的独立模块也很困难。需要开发一个框架来封装 NIO 并简化这种类型的开发。
鉴于这些原因,很少直接使用 NIO API。应用程序通常使用一个更可用的接口将 NIO API 封装起来。和众多 API 相比,NIO API 有其独特的作用,但是不应该强制应用程序编程人员直接使用它。
使用并发性工具编写的应用程序则很少发生由于多线程处理引发的故障,因为 Java 5 的并发性工具提供了更高级的操作。然而,很容易发生死锁并且难于调试和查找错误根源
为了在 Java 平台上以通用的方式支持异步交互,人们作出了很多尝试。所有这些尝试都基于一个消息传递通信模型。大部分使用了 actor 模型的一个变体来定义对象。此外,这些框架在可用性、可用库和方法方面各有不同。
阶段式事件驱动架构
阶段式事件驱动架构(SEDA)是一种有趣的框架,它将异步编程和自主计算的原理结合在一起。SEDA 是 J2SE 1.4 对 Java NIO API 引入的最大一项补充。该项目本身已经被中断,但是 SEDA 为 Java 应用程序的可伸缩性和适应性设定了新的基准,并且其有关异步 API 的思想对其他项目也产生了影响。
SEDA 试图将异步和同步 API 设计结合起来,产生有趣的结果。这个框架具有比 ad-hoc 并发性 更加良好的可用性,但它还无法达到用户认可的程度。
使用 SEDA,应用程序被划分为若干个阶段。每个阶段表示的组件包含一定数量的线程。请求被分配到一个阶段然后进行处理。阶段可以通过以下几种方式管理自身的容量:
根据负载增加和减少使用线程的数量。这允许服务器动态适应组件的实际使用情况。如果某个组件的使用急剧上升,则会分配更多线程。如果为空闲状态,则减少线程的数量。
根据负载更改行为。例如,可以根据负载生成更加简单的页面。避免对页面使用图像,使用更少的脚本,禁用不必要的功能等等。用户仍然可以使用应用程序,但是生成的请求和通信量将变少。
对试图纳入请求或拒绝接受请求的阶段进行阻塞。
前两种方法非常不错,采用了智能应用程序实现自主计算的思想。然而,第三种方法揭示了为什么该框架至今无法得到广泛应用的原因。除非在设计应用程序时加倍小心,否则这样做会因为增加了死锁风险而引入一个故障点。下面介绍了致使该框架难于使用的其他一些原因:
阶段是一种非常粗粒度的组件。比如网络接口和 HTTP 支持。在将网络层作为整体处理时,很难解决诸如某些客户机带宽有限这样的问题。
无法使用简单的方法返回异步调用的结果。结果只是被分配给阶段,寄希望于阶段能自己找到相关的操作状态。
目前,大多数可用的 Java 库都是同步的。框架并没有尝试以一种一致的方式将同步代码从异步代码中分离开来,从而使编写出的代码很容易意外阻塞整个阶段。
贯彻 SEDA 项目思想的实现中部署最多的可能是 Apache MINA 框架。它用于 OSFlash.org Red5 流服务器的实现、Apache Directory Project 和 Jive Software Openfire XMPP Server。
E 编程语言
严格来讲,E 编程语言是一种动态输入的函数性编程语言,而非一种框架。它强调提供安全的分布式计算,它还为异步编程提供了一些有趣的概念。在异步编程方面,该语言仿效了其前辈 Joule 和 Concurrent Prolog,但是其并发性支持和整体语法更加自然,而且对于拥有主流编程语言(例如 Java 语言、JavaScript 和 C#)背景的编程人员来说也十分友好。
该语言目前通过 Java 和 Common-Lisp 实现。可以通过 Java 应用程序使用。但是,要将其应用于高负荷的服务器端应用程序,仍然存在着一些障碍。大多数问题源于其早期开发,但将来很可能会得到解决。其他一些问题则是由该语言的动态特性引起的,但是这些问题大部分与该语言提供的并发性扩展并无关系。
E 提供了以下核心语言概念来支持异步编程:
vat 表示对象的容器。所有对象都保存在一些 vat 的上下文中,并且不能从其他 vat 同步访问这些对象。
promise 变量用来表示某些异步操作的结果。它的初始状态为未解决状态,表示该操作还未结束。完成操作后,它会获得一个值或者出现 错误。
任何对象都可以接收消息并进行本地调用。本地对象可以通过即时的调用操作进行同步调用,也可以通过最终的发送操作进行异步调用。只能使用最终的发送操作对远程对象进行调用。最终的调用将生成一个 promise。. 操作符用于即时调用,而 <- 操作符用于最终调用。
Promise 也可通过显式方式创建。此时,将提供对解析器对象的引用,并传递给其他 vat。这个对象有两个方法:resolve 和 smash。
when 操作符允许您在 promise 执行 resolve 或 smash 时调用一些代码。when 操作符中的代码被处理为闭包,并且通过访问外围范围中的定义执行。这种方式类似于匿名的内部 Java 类访问一些方法范围内的定义。
这几个概念构成了一个功能强大的可用系统,允许轻松地创建异步组件。即使不是在生产环境中使用该语言,仍然可以使用它原型化复杂并发性问题。它执行一种消息传递规程并提供方便的语法来处理并发性问题。其操作符也十分简单,并且可以在其他编程语言中进行模仿,虽然产生的代码很可能不及原始代码那么优雅和简单。
E 增强了异步编程的可用性。该语言提供的并发支持与其他语言特性毫不相关,并且它可能对现有语言进行了改进。在 Squeak、Python 和 Erlang 开发环境中已经对这些语言特性进行了讨论。与更加特定于域的语言特性(如 C# 中的迭代器)相比,这种语言特性可能更为有用。
AsyncObjects 框架
AsyncObjects 框架项目侧重于使用纯 Java 代码创建可用的异步组件框架。该框架尝试将 SEDA 和 E 编程语言结合在一起。与 E 相同,它提供了基本的并发性机制。同样,它也仿效 SEDA 来提供机制集成同步 Java API。该框架的第一个原型版本发行于 2002 年。自此之后,该框架的开发变得消极起来,但是最近,该项目重新开始活跃。E 已经展示了异步编程的可用性,而这个框架将试图使用纯 Java 代码获得同样的可用性。
和 SEDA 相同,应用程序被分为若干个事件循环。但是,该项目目前还没有实现任何类似 SEDA 的自管理特性。与 SEDA 不同的是,它对 I/O 使用了更加简单的负载管理机制,因为组件更加细粒度化并且 promise 可用来接收操作结果。
框架实现了与 E 编程语言相同的异步组件概念、vat、和 promise。因为不能使用纯 Java 代码引入新的操作符,并不是任意一个对象都是异步组件。实现需要扩展某个基类,并且应该提供由框架实现的异步接口。由框架提供的异步接口实现将向组件的 vat 发送消息,而 vat 稍后将消息分配给组件。
该框架的当前版本(0.3.2)与 Java 5 兼容并且支持泛型。如果当前平台支持的话,也将使用 Java NIO。但是,框架能够返回到普通套接字。
该框架最大的一个问题是类库非常匮乏,因为很难实现与同步 Java API 的集成。目前,只实现了网络 I/O 库。但是,最近作出的一些改进 — 例如 Axis2 中的异步 Web 服务和前面描述的 Tomcat 6 的 Comet Servlet— 可以简化这种集成。
Waterken 的 ref_send
Waterken 的 ref_send 框架是使用 Java 编程语言实现 E 思想的又一尝试。它主要通过 Java 语言的一个子集(称为 Joe-E)实现。
该库支持最终的操作调用。然而,与 AsyncObjects 中的支持相比,这种支持的自主性较低。该框架的当前版本还存在线程安全问题。
它只发布了一些核心类和一些非常小的示例,并且没有提供重要的应用程序和类库。因此,关于如何在更大的范围内实现框架思想仍不明确。框架构建者宣称目前正在实现一个完整的 Web 服务器并且即将发布。等到发布之后在重新审视这个框架可能会更加有趣。
Frugal Mobile Objects
Frugal Mobile Objects 是另一种基于 actor 模型的框架。它以诸如 Java ME CLDC 1.1 这样的资源受限环境为目标,使用有趣的设计模式减少资源使用量,同时保持接口具有适当的简单性。
该框架表明应用程序在性能和可伸缩性方面会从异步设计中获益 — 甚至在一个资源受限的环境中。
该框架提供的 API 看似非常繁琐,但是框架的受限目标环境充分证明了这些 API 的有效性。
Scala actor
Scala 是另一种面向 Java 平台的编程语言。它提供了一个 Java 特性超集,但是却使用了稍有不同的语法。与普通的 Java 编程语言相比,它提供了一些可用性增强。
其中一个有趣特性就是基于 actor 的并发性支持,这一点模拟了 Erlang 编程语言。它的设计似乎还没有最终确定,但是这种特性基本可用并且得到该语言的语法支持。然而,与 E 的并发性支持相比,Scala 的跟 Erlang 类似的并发性支持的可用性和自主性较低。
Scala 模型还存在一些安全性问题,因为每条消息都传递对调用方的引用。这使得被调用的组件可以调用调用方组件的所有操作,而不仅仅是返回调用值。就这方面来说,E 的 promise 模型更具粒度化。这种机制用来与阻塞进行通信,目前还没有完全开发完毕。
Scala 的优点在于它可以编译为 JVM 字节码。理论上讲,它可以用于 Java SE 和 Java EE 应用程序,并且不会带来性能损失。然而,对于商业开发的适用性则另当别论,因为 Scala 的 IDE 支持有限,并且,与 Java 语言不同的是,它尚不具备供应商支持。因此,只能用于生命周期较短的项目(如原型),但是,如果对生命周期较长的项目使用该语言,则会添加很多风险。
特定于 Servlet 或特定于 I/O 的 API
由于我们讨论的这些问题只要针对 servlet、Web 服务和一般的 I/O 级别,因此使用了一些项目来专门解决这些问题。这些解决方案的最大缺陷就是它们只针对有限类别的应用程序解决问题。如果不能对本地和远程资源进行异步调用,即使能够实现异步 servlet 也毫无用处。它还应该能够编写一个异步模型和业务逻辑代码。另一个常见问题是解决方案的可用性,通常要低于普通的解决方案。
然而,作为为实现异步组件而作出的努力,这些尝试都值得关注。
JSR 203(NIO.2)
JSR 203 是 NIO API 的改进版。在撰写本文时,它仍然处于初期的草案阶段,在开发过程中可能发生了很多重大修改。其目标是将 API 纳入到 Java 7 中。
JSR 203 引入了异步通道(asynchronous channel)概念。目的是解决众多编程问题,但是似乎 API 仍然非常低级。它最终引入了以前版本所不具备的异步 File I/O API,并且 IoFuture 和 CompletionHandler 概念使它可以更轻松地使用其他框架中的类。一般来讲,新的异步 NIO API 要比上一代 API 中基于选择器的 API 更加易用。甚至可以将它直接用于简单的任务,而不需要编写自定义包装器。
然而,这种 JSR 的一大缺点就是,它高度特定于文件和套接字 I/O。它没有提供构建块来创建更高级的异步组件。可能提供了高级的类,但是必须提供自己的方法来执行相同的任务。这看似是一个不错的技术理念,因为在 Java 语言中仍然没有出现标准的异步组件开发方法。
Glassfish Grizzly NIO
Glassfish Grizzly NIO 支持类似于 SEDA 框架,并且继承了大部分 SEDA 问题。然而,它提供了对 I/O 任务的更加具体化的支持。所提供的 API 要比普通 NIO API 更加高级,但是使用起来仍然很枯燥。
Jetty 6 continuation
Jetty continuation 是一种与传统方法截然不同的方法。甚至可以将之称为一种快速补丁(quick hack)。servlet 可能会请求一个 continuation 对象并调用具有指定超时的 suspend() 方法。该操作将抛出一个异常。然后再对 continuation 调用一个恢复操作,或者 continuation 超过指定时间后自动重新开始执行。
因此 Jetty 尝试实现一个具有异步语义的同步查找 API。然而,这种行为将打断客户机的预测,因为 servlet 将从头执行方法,而不是从调用 suspend() 的位置执行。
Apache Tomcat 6 Comet API
Tomcat Comet API 专门为支持 Comet 交互模式而设计。servlet 引擎通知 servlet 关于其状态转换以及数据是否可读的信息。与 Jetty 使用的方法相比,这种方法更加健全和简单。它使用传统的同步 API 对流执行写入和读取操作。通过使用这种方式实现,如果谨慎使用,则不会出现 API 阻塞的情况。
JAX WS 2.0 和 Apache Axis2 Asynchronous Web Service Client API
JAX WS 2.0 和 Axis2 为 Web 服务的非阻塞调用提供了 API 支持。当 Web 服务操作完成后,Web 服务引擎将通知提供的侦听器。这为 Web 服务的使用提供了新的机会 — 即使来自 Web 客户机。如果一个 servlet 中发生若干独立的调用,它们将并行执行,因此客户机中的总延迟将更低。
结束语
现在,我们已经认识到了异步 Java 组件的必要性,并且,异步应用程序目前正在积极开发之中。两种大型的开源 servlet 引擎(Tomcat 和 Jetty)都至少针对最令开发人员头痛的 servlet 提供了一些支持。尽管 Java 库已开始提供异步接口,这些缺口还缺乏通用的结构,并且,由于线程管理和其他问题,彼此之间很难兼容。因此需要容器能够托管由不同来源提供的各种异步组件。
目前,用户面对着各种各样的选择,每种方法在不同情形下都各有优缺点。例如,Apache MINA 库为一些流行的网络协议提供了现成的支持,因此,在需要使用这些协议的情况下它将是一个不错的选择。Apache Tomcat 6 可以很好地支持 Comet 交互模式,如果要在这种模式中进行异步交互,那么则可以选择使用 Apache Tomcat 6。如果是从头构建应用程序,并且现有库明显不能提供足够支持,那么可以使用 AsyncObjects 框架,因为它提供了各种各样的可用接口。这种框架还可以用于围绕现有异步组件库创建包装器。
现在,是时候为 Java 语言创建一个通用的异步编程框架了。然后,还需要花费很多精力将现有异步组件集成到这个框架中,并为现有同步接口创建一个异步版本。每实现一个步骤,企业 Java 应用程序的可伸缩性都会得到改善,并且我们将能够应对比这更艰难的挑战。持续发展的 Internet 以及不断增生的各种网络服务必定将为我们带来更多这样的挑战。
赞助商链接