技术开发 频道

Java EE 迎合 Web 2.0

  要避免我们讨论的这些问题,一个可行的方法就是在设计应用程序时将延迟纳入到考虑事项中并使用由事件驱动的异步方法实现应用程序。如果应用程序处于空闲状态,则不会占用线程这样的有限资源。通过使用异步 API,应用程序将对外部事件进行轮询并在事件发生后执行相应的操作。通常,这种应用程序被分为若干个事件循环,每个循环都有自己独有的线程。

  事件驱动异步设计的一个明显优点就是,如果大量等待外部服务的操作之间没有数据依赖关系,则可以并行执行这些操作。即使根本没有发生并行操作,事件驱动的异步架构也提供了优于传统同步设计的强大的可伸缩性。

  异步 API 优点:概念证明模型

  可以通过一个简单的 servlet 流程模型演示异步 API 带来的可伸缩性优势。

  在我们的模型中,servlet 流程对到来的请求执行一些处理,对数据库进行查询,然后使用从数据库获取的信息调用 Web 服务。最后,根据 Web 服务的响应生成最终的响应。

  模型的 servlet 使用两种类型的资源,并伴有较高的延迟。在逐渐增加的负载之下,这些资源的特征和行为都互不相同:

  数据库连接。这种资源通常以 DataSource 的形式用于 Web 应用程序,它提供了数量有限的连接,通过这些连接实现同步处理。

  网络连接。这种资源用于编写对客户机的响应并调用 Web 服务。直到现在为止,这种资源在大多数应用服务器中都受到限制。然而,新一代应用服务器开始使用 nonblocking I/O (NIO) 实现这种资源,因此我们可以根据需要使用任意数量的同步网络连接。模型 servlet 在以下几种情形中使用这种资源:

  调用 Web 服务。尽管目标服务器每秒可以处理的请求的数量是有限制的,但是这个数量通常都很高。调用持续时间取决于网络通信量。

  从客户机读取请求。我们的模型忽视了这一开销,因为模型假定使用了一个 HTTP GET 请求。在这种情形下,从客户机读取请求所需的时间不会添加到 servlet 请求持续时间中。

  向客户机发送响应。我们的模型忽视了这一开销,因为,对于较短的 servlet 响应来说,应用服务器可以在内存中缓冲该响应,然后再使用 NIO 将它发送给客户机。并且我们假设这个响应非常短小。在这种情形下,向客户机发送响应所需的时间不会添加到 servlet 请求持续时间中。

  让我们假设 servlet 执行时间被划分为如表 1 所示的几个阶段:

  表 1. Servlet 操作时限(以抽象单位表示持续时间)

阶段持续时间操作
12 个单位解析 servlet 请求信息
28 个单位处理本地数据库事务
32 个单位处理数据库请求结果并准备远程调用
416 个单位使用一个 Web 服务调用远程服务器
54 个单位创建响应
总用时: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 操作时限

阶段持续时间操作
11 个单位解析 servlet 请求信息
24 个单位执行本地数据库事务处理
31 个单位处理数据库请求结果并为远程调用做准备
416 个单位使用 Web 服务调用远程服务器
52 个单位创建响应
总用时: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 平台构建事件驱动的异步应用程序。

0
相关文章