10.3 EJB会话Bean
本节考察 EJB 3.0 会话 Bean 的性能。Java EE 容器管理 EJB 生命周期的方法很特殊,本节中的准则有助于确保容器管理生命周期时不会影响应用的性能。
10.3.1 调优EJB对象池
因为 EJB 对象创建(和销毁)的代价很高,所以它们通常保存在对象池中。如果没有池化,调用 EJB 包括以下步骤:
创建 EJB 对象
处理标注并且将依赖的资源注入这个新 EJB 对象
调用标注为
@PostConstruct的方法如果是状态 Bean,则调用标注为
@Init的方法或者ejbCreate()方法执行业务方法
调用任何标注为
@PreRemove的方法如果是状态 Bean,则调用
remove()方法
如果从池中获取 EJB,则只需要调用执行业务方法——其余 6 个步骤都可以跳过。虽然通常情况下并不需要对象池(参见第 7 章),但如果初始化对象的代价高,就值得池化。
EJB 对象池化的代价
Java EE 应用服务器可以用不同大小的 EJB 池来进行测试,从而衡量从池中获取对象和因需创建对象的不同性能,所以 EJB 池可以发挥对象池的益处。
在这个例子中,我在 GlassFish 4.0 应用服务器中配置了标准的
StockServlet。应用中的无状态 Bean 完全没有初始化的开销。虽然有@PostConstruct方法,但是方法体是空的。
@PostConstruct方法通常用于初始化资源,比如,可以执行(代价相对较高的)Java 命名和目录接口(JNDI)查找。为了模仿这种情况,我把StorkServlet的@PostConstruct方法改为sleep,让它模拟时间消耗,或者执行一些初始化代码。表 10-2 是在不同 EJB 池大小、不同
@PostConstruct方法睡眠时间(模拟初始化时间)下模拟 64 个客户端访问应用时的响应时间。表10-2:对象池对EJB响应时间的影响
EJB池大小
初始化时间(毫秒)
平均响应时间(秒)
1
0
0.37
64
0
0.37
1
25
0.40
64
25
0.37
1
50
0.42
64
50
0.37
如果初始化不需要时间,EJB 池就没什么好处。当初始化需要 25 毫秒或者 50 毫秒,并且池的大小为 1 时——意味着每次调用都会创建一个 EJB 对象——不出所料,平均响应时间拉长了。
由于这个 EJB 池中只有 64 个(小)对象,因此不太可能发生 GC。这是好的对象池的另一个关键特性:小才是好。
只有在应用服务器池中还有可用的 EJB 对象时,性能才会提高,所以必须将应用服务器中的 EJB 对象数配置成应用同时使用的 EJB 数。如果应用使用 EJB 但没有池化的实例,应用服务器就会开启 EJB 对象的完整生命周期,从创建、初始化、使用到销毁 EJB 对象。
当然,应用所依赖的对象数取决于该应用如何被使用。通常情况下,由于一个请求最多只需要一个 EJB,所以在开始的时候一般需要确保 EJB 池中的对象数和应用服务器中的工作线程数一样多。请注意,EJB 池是按类型分的:如果应用有两个 EJB 类,应用服务器就会使用两个池(每个池都可以设置线程数)。
应用服务器不同,EJB 池的调优方式也不相同,不过通常来说,每个 EJB 池都有一个全局(或默认)配置,需要不同配置的 EJB 可以覆盖该选项(通常在它们的部署描述符中)。例如,对 GlassFish 应用服务器来说,EJB 容器默认每个池中有 32 个 EJB 实例,且在 sun-ejb-jar.xml 文件的以下段落中可以配置单个 bean 池的大小:
<bean-pool><steady-pool-size>8</steady-pool-size><resize-quantity>2</resize-quantity><max-pool-size>64</max-pool-size><pool-idle-timeout-in-seconds>300</pool-idle-timeout-in-seconds></bean-pool>
这个例子中 EJB 池的最大值扩大了一倍,是 64。
将 EJB 池大小设置为很大值的代价通常不是非常高。池中没有使用的实例会略微降低 GC 的效率,但通常来说,池不会很大,未使用的实例不会有很明显的影响。有个例外,即如果 EJB 占用了大量内存,GC 的影响就会变大。然而,从上面的 XML 可以看出,应用服务器通常用一个池的稳定值和最大值来管理池。在上面的例子中,如果流量主要来自 EJB 中的 10 个实例(比如 10 个并发请求),一直只有 10 个 EJB 实例,那么池就永远不会达到最大值 64。
如果有短暂的流量高峰,池会创建这 64 个实例,随着流量的衰减,这些 EJB 就会空闲。一旦空闲 300 秒,就会被销毁,内存就可以被 GC。这使得池对 GC 的影响最小。
因此,要更关心 EJB 池稳定值的调优,而不是最大值的调优。
快速小结
1. EJB 池是对象池的典型范例:初始化代价高,数量相对较少,所以池化更为有效。
2. 通常来说,EJB 池的大小包括稳定值和最大值。对于特定的环境,两种值都需要调优,但从长期来看,为了降低对垃圾收集器的影响,应更注重稳定值的调优。
10.3.2 调优EJB缓存
对于状态会话 Bean,还需要考虑另外一个因素,即它们有可能被钝化(Passivation):为了节约内存,应用服务器会选择将 Bean 的状态序列化并保存到磁盘上。这对性能会有很严重的影响,绝大多数情况下应该极力避免。
坦白说,我建议在所有情况下都避免这么做。关于钝化常见的争论是,会话空闲了几个小时或者几天,该怎么办。当用户重新回到系统时(几天后),你总希望他能找回完整的状态数据。这种情形的问题在于,它假定 EJB 会话是唯一重要的状态数据。但通常来说,EJB 与 HTTP 会话会有关联,而我们并不建议长时间保留 HTTP 会话。如果应用服务器的某种非标准特性可以将 HTTP 会话保存到磁盘,并且能配置成同时钝化 HTTP 会话和 EJB 会话(持续的时间也相同),这就有意义了。然而即便如此,其他的外部状态也可能会缺失。(比如,用户购物车中的物品失效了怎么办?)
如果需要长时间存活的状态,通常你需要绕过常规的 Java EE 状态机制。
与会话关联的状态 Bean 并没有保存在 EJB 池中,而是保存在 EJB 缓存中。因此,必须对 EJB 缓存进行调优,以便容纳应用中同时活跃的最大会话量。如果容纳不了,最近最少使用的会话将会被钝化。如前所述,不同的应用服务器实现的方式也不同。GlassFish 默认的缓存为 512,全局值可以通过域配置进行覆盖,或在 sun-ejb-jar.xml 文件中分别设置每个 EJB。
快速小结
1. EJB 缓存仅用于状态会话 Bean 与 HTTP 会话关联的时候。
2. 应该充分优化 EJB 缓存,以避免钝化。
监控 EJB 池
怎么才能知道 EJB 池和缓存的大小应该是多少?一种方法是根据应用在其预期的负载下的工作情况来进行合理的猜测。不过,想知道是否创建了太多 EJB(或钝化了太多状态会话 Bean)的唯一方法就是借助应用服务器的监控设备来进行。
图 10-1 是 GlassFish 中监控的示例。在这个例子中,EJB 累计的销毁数不为 0,表明有些 EJB 创建出来后就被销毁了,因为有些操作无法从池中获得可用的 Bean。相应地,EJB 累计的创建数大于池的最大值(这个例子中为 4)。这意味着 EJB 池过小了。
图 10-1:EJB 池监控示例
为了了解应用的性能,像这样监控统计值非常重要,但也得留意,监控本身也有代价。在这个例子中,我将 GlassFish 中的 EJB 容器监控级别设置为
HIGH,以便生成这些统计数据,结果总吞吐量就降低了约 5%。应用本身并没有太大影响,但对应用服务器来说,你就得注意如何进行配置和监控了。在 GlassFish 中,监控级别设为LOW的影响几乎可以忽略不计——绝大多数操作都可以进行这个级别的监控,而需要更多信息时,监控级别可以动态地设置为HIGH。
10.3.3 本地和远程实例
EJB 可以通过本地或远程接口访问。在标准的 Java EE 部署中,EJB 可通过 servlet 来访问,而 servlet 可以通过本地或远程接口访问 EJB。如果 EJB 在其他系统上,则必须使用远程接口,但如果 EJB 和 servlet 在一起(这是更常见的部署方式),servlet 通常应该使用本地接口访问 EJB。
由于远程接口包含网络调用,所以上述方式看起来很合理。但这并不是主要原因——当 servlet 和远程 EJB 部署在同一个应用服务器上时,大多数服务器都足够智能,可以旁路网络调用,从而通过常规的方法调用 EJB。
优先使用本地接口的主要原因是,两类接口处理参数的方法不同。传递(或返回)给本地 EJB 的参数合乎通常的 Java 语义:原生类型通过值传递,而对象则通过引用传递。(或者,严格来说,对象句柄也仍然是通过值传递,只不过对象的引用使对象看起来是通过引用传递的。)
而传递(或者返回)给远程 EJB 的参数则总是值传递。这种通过网络的传送只有一种方式:发送方将对象序列化后以字节流的方式传输出去,而接收方则反序列化字节流后重建对象。即使服务器优化本地调用,避免了网络开销,它也不能绕过序列化 / 反序列化步骤。(大多数服务器传输对象不可变时——字符串或者原生值——都能跳过序列化的步骤,但这不是一般情况。)无论服务器写得多好,使用远程 EJB 接口总是比本地接口慢。
Java EE 还包括其他部署场景。例如,可以将 servlet 和 EJB 部署在不同层上,且普通应用可以通过远程接口访问 EJB。也常常会有业务或者功能上的原因而使网络结构受限,例如,假设 EJB 需要访问企业数据库,你可能想将数据库放在防火墙后面的机器上,而防火墙则隔开了 servlet 容器和数据库。这些因素都是性能问题中需要重点考虑的。但严格地从性能角度来说,将访问 EJB 的组件和 EJB 部署到一起并使用本地接口总是比使用远程协议要快。
说到远程协议,所有的远程 EJB 都必须支持 IIOP(CORBA)协议。这十分有利于互通性,特别是对那些不是用 Java 编写的程序来说。对于远程访问来说,Java EE 服务器供应商也可以使用其他协议,包括专用协议。通常来说,这些专用协议都比 CORBA 快(这就是为什么供应商开发它的首要原因)。所以,如果必须使用远程 EJB 调用(不考虑不同语言之间的互通性),可以考虑那些应用服务器供应商所提供的访问协议选择。
快速小结
即便在同一个服务器中,调用 EJB 远程接口也对性能有很大的影响。
快速小结