使用 WebSphere Operational Decision Management V8 运行大规模模拟

及时运行数百万个场景

本文将重点介绍 IBM® WebSphere® Operation Decision Manager V8 中引入的大规模模拟支持,该功能支持以及时的方式执行包含数亿个用例的模拟。我们将介绍如何实现大规模模拟支持,还将介绍为一个规则项目创建大规模模拟所遵循的流程,然后会演示一个运行模拟的完整示例,该模拟在一个关系数据库中存储了 1.2 亿个场景。 本文来自于 IBM Business Process Management Journal 中文版

Sebastian Brunot, 软件开发人员, IBM

Sebastien Brunot 是 Hursley 开发实验室的一名软件工程师。他在 Java 和测试领域有 11 年的工作经验。他负责 WebSphere ODM V8 中的大规模模拟支持的设计和开发。



Jose De Freitas, 系统测试员,Operational Decision Management, IBM

Jose de Freitas 是 IBM Operational Decision Management System Verification Testing (SVT) 团队的成员。在此职位上,他参与了 WebSphere ODM 大规模模拟特性的测试。Jose 以前曾在 Business Process Management (BPM) 工作过,担任实验室工作和面对客户的职位,他的职责包括模拟复杂的业务流程。



2013 年 1 月 04 日

背景知识

更改涉及大量实体(比如有数百万个客户或交易)的复杂流程中的业务规则是一个冒险之举。因此,在实际执行这些更改之前分析它们带来的影响就变得至关重要。使用模拟作为工具来实现此目标很有效,但对于具有非常多输入数据的模拟(我们称之为大规模模拟),可能无法及时获取模拟结果。为了解决这个问题,WebSphere Operational Decision Manager (WebSphere ODM) V8 引入了一项功能,将模拟划分为能在不同执行线程中运行的独立单元。每个执行线程与其他部分并行地执行自己的模拟部分。在完成最后一个执行的模拟部件时,会将结果汇集在一起。

WebSphere ODM 中的模拟

在 WebSphere ODM 中,可以将模拟定义为以下部分的组合:

  • 一个场景列表,其中一个场景被定义为执行某次模拟期间运行一个规则集所需的一组输入参数值。列表中的每个场景都通过它们的索引进行标识:列表中的第一个场景拥有索引 0,最后一个场景拥有索引 number of scenarios – 1
  • 一组关键绩效指标 (KPI),可使用列表中每个场景的执行输入和输出来计算。

场景服务提供程序 (Scenario Service Provider, SSP)(负责运行一次模拟的运行时组件)使用一种按序算法,该算法包含以下处理每次模拟执行的步骤:

  1. 使用一个临时名称部署要在模拟中使用的规则集。
  2. 创建并初始化场景提供程序,这是负责提供场景列表的组件。
  3. 创建和初始化负责计算模拟的 KPI 的组件。
  4. 从索引 0 开始迭代场景列表。对于每次迭代,SSP 会执行以下操作:
    • 使用场景提供程序获取当前索引的场景。
    • 通知 KPI 计算器一个场景即将开始运行,并向计算器提供场景数据。
    • 使用该场景提供的输入参数值执行规则集。
    • 通知 KPI 计算器该场景已完成运行,向计算器提供执行的输出。
  5. 解除部署规则集,从 KPI 计算器获取最终的 KPI 值,返回一个包含这些值的执行报告。

按上述方式计算出的 KPI 所花费的时间与模拟中的场景数量成正比。这是因为 SSP 使用了一个按序算法来运行场景。要使用及时方式运行大规模模拟(包含数百万个场景),则需要提供一种并行执行场景和计算 KPI 的方法。

在 WebSphere ODM V7.5 中,您并行运行大规模模拟场景的惟一选择是,在同一个规则集上定义不同的模拟,然后手动整合每次模拟的 KPI 结果。此方法很耗时,不是很方便。为了解决这个问题,WebSphere ODM V8 引入了一个执行大规模模拟的新功能。SSP 现在负责并行执行场景,使用在该产品的公共 API 中定义的一组新组件来整合 KPI 结果。


WebSphere ODM V8 中的大规模模拟支持

从 WebSphere ODM V8 开始,场景服务提供程序支持一种新的并行执行模式,减少了执行大规模模拟和计算相应的 KPI 值所花费的时间。

在过去,一次模拟的按序执行是在单个具有索引的场景列表中进行操作的,而对于大规模模拟的全新支持,可同时在多个场景列表上进行操作。例如,在 WebSphere ODM V7.5 中,要运行一次包含 10 000 000 个场景的模拟,SSP 必须迭代一个场景列表,从索引 0 迭代到索引 9 999 999。借助 WebSphere ODM V8 的新功能,现在 SSP 可以通过 5 个场景列表并行地迭代运行同一个模拟,这 5 个列表的索引范围分别为 0 到 1 999 999、2 000 000 到 3 999 999、4 000 000 到 5 999 999、6 000 000 到 7 999 999 和 8 000 000 到 9 999 999。每个场景子列表在 SSP 文档和公共 API 中称为一个模拟部分

在独立线程中并发运行的模拟部分列表由新的场景提供程序扩展(称为并行场景提供程序,Parallel Scenario Provider)在运行时提供。

因为 SSP 在一个专用线程中并发地运行每个模拟部分,所以它需要一种新的组件来整合从每个模拟部分获得的部分 KPI 结果,并将整合的值包含在最终报告中。这个新组件称为 KPI Result Aggregator。

当通过这两个新组件在并行执行模式下运行时,场景服务提供程序执行以下新的执行算法:

  1. 使用一个临时名称来部署要在模拟中使用的规则集。
  2. 创建并初始化并行场景提供程序,这是提供各个模拟部分的组件。不同于 WebSphere ODM V7.5 中的场景提供程序,并行场景提供程序支持多次实例化和多个实例的并行执行。
  3. 从并行场景提供程序获取一个模拟部分列表。无需按顺序运行场景列表,SSP 创建线程来并发地运行各个模拟部分。
  4. 创建并初始化 KPI 结果聚合器。此组件不仅执行对每个模拟部分计算的部分 KPI 结果的聚合,还提供 SSP 创建和初始化 KPI 计算器以用于每个模拟部分而需要的信息。
  5. 为各个模拟部分的按序执行创建工作者线程。
  6. 在每个模拟部分计算完成时从每个模拟部分获取部分 KPI 结果,将它的值转发给 KPI 结果聚合器。
  7. 完成所有模拟部分后,从 KPI 结果聚合器获取合并的 KPI 值,返回一个包含此值的报告。

管理资源

为了管理分配用于执行并行模拟的 CPU 资源量,SSP 现在支持资源分配策略。这些策略在 SSP Web 应用程序的部署描述符中配置(或者在使用一个本地 J2SE SSP 服务运行模拟时使用 SSP 公共 API 配置)。

本产品提供了一个默认的资源分配策略,它的行为如下所述:

  • 默认资源分配策略使用一个固定大小线程池向每个模拟部分分配一个执行线程,在池中没有任何可用的线程时,还会分配一个供模拟部分进行等待的队列。池大小可配置为默认资源分配策略的一部分。
  • 可自定义默认资源分配策略,为每个并行模拟分配一个小于池大小的最大线程数量。这可以预防某个并行模拟使用了池中的所有资源,而其他模拟都在等待。

可通过实现一个在 SSP 公共 API 中定义的接口来支持和开发自定义资源分配策略。对于进入 SSP 的每个模拟执行请求,自定义策略需要定义:

  • 分配用于执行模拟的最大线程数量。
  • 模拟的优先级。支持 3 个优先级:HIGH、NORMAL 和 LOW。模拟的优先级定义了创建类运行它的各部分的线程的优先级。

限制

WebSphere ODM V8 不支持在多个服务器上运行模拟部分。此限制不会影响 1 亿多个场景中涉及的模拟。在我们的示例中,场景获取(从数据库)显然是一个瓶颈。

另一个限制(可能会影响 Linux™ 用户)是一个与将 Java™ 线程优先级转换为 Linux OS 进程优先级(“nice” 级别)相关的 J9 JVM 限制。此限制会导致所有 WebSphere 工作管理器线程优先级映射到同一个 “nice” 级别 (0),这意味着,在 Linux 环境中,所有模拟作业都使用相同的优先级执行。


并行模拟特性的架构

与 WebSphere ODM V7.5 中一样,负责为模拟提供场景的组件和负责计算 KPI 的组件是以自定义场景套件的格式定义的,这种格式是使用 Rule Designer 中的测试和模拟工具来创作的。为了定义一个支持并行执行模拟部分的场景套件格式,SSP 公共 API 在 WebSphere ODM V8 中引入了两个新接口:IlrParallelScenarioProviderIlrKPIResultAggregator

IlrParallelScenarioProvider

ilog.rules.dvs.core.IlrParallelScenarioProvider 是一个新的场景提供程序接口,支持并行执行模拟部分。此接口扩展了现有的 ilog.rules.dvs.core.IlrScenarioProvider 接口,并定义一个新方法 getScenarioSuitePart(),该方法会返回将用于并行执行的部分列表。此接口还会在执行时定义一个新合约。在运行并行模拟时,SSP 中的并行模拟提供程序生命周期如下所示:

  1. IlrParallelScenarioProvider 的第一个实例是 SSP 通过反射功能、用一个没有参数的构造函数来创建的。
  2. 然后,initialize(IlrScenarioSuiteExecutionContext) 方法后由 SSP 在第一个实例上调用,使用的场景套件执行上下文的 getCurrentScenarioSuitePart() 方法返回 null。
  3. 当初始化执行完成后,SSP 随后会调用新的 getScenarioSuiteParts() 方法来获取模拟的场景部分列表。
  4. 获取场景部分列表后,SSP 会调用 getScenarioCount() 方法来获取模拟中的场景总数。
  5. 对于模拟的每个部分,SSP 随后会执行以下操作:
    1. 使用反射创建并行场景提供程序的一个新实例。
    2. 使用其 getCurrentScenarioSuitePart() 方法返回当前部分的场景套件执行上下文,并调用 Parallel Scenario Provider 实例的 initialize(IlrScenarioSuiteExecutionContext) 方法。
    3. 对于在该部分定义的范围中的每个索引,SSP 会调用并行场景提供程序实例的 getScenarioAt(int) 方法,从第一个索引开始,到最后一个索引结束。
    4. 这个场景部分中的所有场景都运行之后,SSP 会在并行场景提供程序实例上调用 close() 方法。
  6. 在模拟的所有部分都运行之后,SSP 调用会在第一个并行场景提供程序实例上调用 close() 方法。

IlrKPIResultAggregator

ilog.rules.dvs.core.IlrKPIResultAggregator 是新的 KPI 结果聚合器接口。它与一个 IlrKPI 实现结合使用,该实现负责计算并行模拟的每个部分的 KPI 值。IlrKPI 实现由 KPI 聚合器引用,并通过 IlrKPIResultAggregatorgetKPIClassName() 方法获得。在运行时,SSP 为每个模拟部分(使用反射)创建了 IlrKPI 实现的一个实例,使用它执行对模拟部分的 KPI 执行一次标准的非并行计算。在完成一个模拟部分的执行后,SSP 会使用 IlrKPI 实例的 getKPIResult() 来获取部分 KPI 计算值,并通过调用 IlrKPIResultAggregator 实例的 add(IlrKPIResult) 方法来通知该实例。当各个部分的结果都添加到 KPI 结果聚合器中之后,SSP 会调用聚合器的 getKPIResult() 方法,并将最终结果添加到执行报告中。

并发执行

为了实现模拟部分的并发执行,SSP 采用了两个不同的解决方案,具体采用哪个方案取决于运行时环境:

  • 当在 WebSphere Application Server 中运行 SSP 时,可以使用一个 WebSphere Application Server Work Manager 创建执行线程。工作管理器支持您使用 WebSphere Application Server Administration Console 来配置线程池属性。
  • 当在其他受支持的应用服务器中运行时,或者作为一个纯 J2SE 组件运行时,SSP 使用了一个 J2SE 线程池。

方法

创建和配置运行大规模模拟(满足性能需求)所需的工件的流程并不简单。出于这个原因,提供一组有助于确保此任务的成功和可重复性的准则很重要。下面提供的指南不是规范性的,但可以用作此流程的起点。

建立业务目标

开始初始化模拟工作之前,需要建立一个相关的业务目标。如果业务目标是 1) 可度量的且 2) 受业务规则支持,它可能成为业务规则模拟的候选者。例如,一个相关的业务目标可能是减少某些类型的财务交易的成本。一个与减少财务交易的成本相关联的更具体的目标可能是 “减少需要手动干预的交易数量”。假设业务规则认为在需要手动干预时,对这些规则或它们的输入参数的更改可能生成大量不同的场景,这带来的影响可能无法在不适用模拟工具时轻松确定。

确定模拟方法是否适用

在第一条准则中,我们确定业务目标可以通过更改业务规则来部分或完整地实现。如果这些规则更改所带来的影响是未知的,无法用一种简单方式来确定它们是否适用,或者是认为确认它们的成本超出了执行模拟的成本,那么有理由认为使用一个模拟工具能够更好地理解执行这些更改的风险。

定义如何度量业务目标

确定业务目标和模拟方法的适用性后,我们现在需要确定应如何度量业务目标。例如,要确定对业务规则的更改是否会减少需要手动干预的交易数量,我们只需统计手动交易数量。在莫些情况下,一个简单指标可能就足够了。但是,在大部分情况下,我们很有可能还需要考虑其他 “隐藏的” 指标。在我们的示例中,如果孤立地考虑手动交易,那么一个完全没有手动交易的场景就是一个理想场景。该示例的不足之处在于,我们的风险暴露面要大得多,这实际上会增加交易的成本。出于这个原因,我们还需要找到一种方式来度量更少的手动验证带来的更大的暴露面。例如,我们可度量不需要手动验证的交易的总体交易价值的增长。这些指标(手动交易数量和非手动交易金额)构成了我们的模拟的 KPI(这是一个过于简单化的示例,但它仅用于演示我们的观点)。

设置模拟目标

建立一个业务目标之后,常常需要设置模拟的其他目标。但是我们发现,以某种与手头任务相近的方式表达目标通常很有好处。在我们的示例中,将主要的模拟目标表达为 “建立会带来有利的手动验证/暴露面比率的业务规则变更”,这可明确地传达要执行的任务的用途(假设我们知道 “有利” 的含义)。

定义模拟的范围

和优化方法不同,在执行模拟时,我们不会寻找最佳的解决方案,而是寻找一个改进的解决方案。为此,我们将模拟的结果与现状进行对比,或与其他模拟的结果进行对比。这是一个不受限制的过程,但出于实际的原因,需要对它进行约束。实现此目标的一个好方法是以一个正式托管项目的形式执行大规模模拟。

许多因素都会影响到模拟中投入的时间和工作量,这些因素包括:

  • 要考虑的 “假设” 场景数量。
  • 执行模拟的硬件和软件环境的性能和可伸缩性特征。
  • 如果这是第一次开发模拟工件(比如场景提供程序和 KPI),那么还需要考虑开发工作所需的时间和成本。
  • 如何获取、格式化和访问输入数据。
  • 使用生成的数据还是来自某个数据库的真实数据。
  • 输入场景数量。

当定义模拟的范围时,您需要考虑这些因素和其他因素,以及项目的最后期限、时间和资源约束,还需要考虑从模拟工作中获得的预期收益。

在我们的示例中,假设我们要模拟 1.2 亿个基于历史数据的场景,我们应该预料到需要大量耗时的数据库设计和调节。我们还应预料到,每个模拟运行需要花费相对较长的时间才能完成。因此,我们可能决定将运行次数(假设场景数量)限制到一个可在项目时间段内管理的数字。设置此界限很重要,因为在实际场景中,人们总是禁不住不断寻求改进。如果我们对模拟结果不满意,或者结果没有说服力,那么最好证明另一个具有更大范围、更多时间和更多资源的模拟项目是合理的。

定义如何计算 KPI

在定义我们的业务和模拟目标时,我们确定了将帮助我们度量这些目标的 KPI。我们在这里指定了如何计算这些 KPI。在我们的示例中,这非常简单:需要手动干预的交易数量 和不需要手动干预的交易金额总和。在这种情况下,每个模拟部分的结果需要合并到一个值中。例如,如果我们有一个并行模拟有两部分组成,第 1 部分的手动交易数量为 20,第 2 部分为 30,那么手动交易总数将为 50。但是,每个模拟部分的结果的合并或聚合并不总是像这个示例中这样简单。举例而言,如果处理平均值,那么所有部分的 “直接” 平均值并不是正确的结果。相反,必须使用一个加权平均值。

显然,在这一步中,您还需要确定 KPI 计算需要哪些数据输入和输出。例如,要获取交易总额,需要将一个交易金额字段包含在场景输入数据中。

确定模拟输入

要确定模拟输入,除了检查 KPI 计算所需的数据之外,还需要检查将在模拟期间调用的规则集的参数。您可能还需要其他数据字段,比如用来确定数据如何在并行模拟中进行分区的字段。每组输入构成一场景。场景可能基于现有的历史数据,也有可能基于统计分布来生成的。执行在运行时生成的场景通常比执行基于历史数据的场景要快得多,因为不需要任何 IO 操作。方法的选择受业务目标(以及是否拥有历史数据)的影响。

如果使用历史数据,则必须安排时间来查找获取该数据的地方并提取必要的数据字段。如果使用统计分布,则需要访问主题专家,以确定合适的分布和分布参数。

确定将如何解释结果并将结果传达给业务用户

比较复杂的模拟(尤其是使用基于统计分布的输入的模拟)的结果常常会受到利益相关者的争议(或者被视为不太可信)。实际上,我们发现在运行第一次模拟之前,可以与利益相关者一起对业务目标、模拟目标和 KPI 的前期验证,这对提高支持率和减少怀疑很有效。在此阶段中,对结果的含义和它们的适用范围达成共识也很重要。

从与利益相关者一起召开的这些会议中获得的反馈可用于确定如何呈现结果(和 KPI 渲染器的设计)。此外,您可能确定支持和验证主要结果所需的额外 KPI(辅助 KPI)。例如,可以使用一个提供所有交易的总数的 KPI 与提供非手动交易总额的 KPI 进行对比。

确定数据分区方法

当使用一个关系数据库作为场景的数据来源时,确保场景数据已经适当地消除了规格化很重要。出于性能原因,场景数据筒换仓包含在单个表中,表中的每一行包含一个场景所需的所有数据。此外,当执行基于历史数据的并行模拟时,请仔细考虑如何将数据分配给每个模拟部分。例如,通常会基于日期(场景或交易日期/时间)来实现此分区操作。因为在任何两个给定日期之间可能发生不同数量的交易,所以各个模拟部分最终可能有完全不同的大小。各部分大小不同不是理想的情形,因为模拟持续时间等于最大那一部分的模拟的持续时间。

一种确保基于日期的分区具有相同大小的方法是使用 SQL 窗口函数(比如 ROW_NUMBER() OVER())。但是,我们使用此方法的经验是,模拟最终花费的时间比其他分区方法要多得多。为了克服此限制,可以为每个场景分配一个顺序编号,并在该编号上创建一个索引。另一种方法可能是手动将等长的日期间隔分配给每个模拟部分。无论决定采用哪种方法,一种不错的适用性早期确认是每个分区都获得固定的数据访问响应时间。换句话说,您在检索分区中的第一个场景时,应获得与检索最后一个场景大体相同的响应时间。

以下是针对依赖于历史数据的场景的一组一般建议:

  • 尽早让一位经验丰富的数据库管理员参与并行场景提供程序的开发,确保数据库经过了良好调节并且 SQL 经过了优化。
  • 对数据库系统使用高性能硬件,并拥有足够的临时空间。
  • 分析您的数据,以确定最佳的分区战略。
  • 避免使用 SQL “窗口函数”(比如 rownum() over)或任何会导致大型临时结果集或数据库排序的 SQL 语句(如果可能,请使用一个索引代替它)。
  • 规范化您的场景数据。
  • 在设计一个并行场景提供程序时,允许手动输入分区范围很重要,尤其在自动分区对性能具有重大影响的时候。
  • 在运行 SSP 且具有频繁的网络活动峰值的服务器上,稳定且较高的 CPU 使用率表明数据库能够更得上数据请求的进度,并且模拟运行处于稳定且可预测的状态。峰值 CPU 活动(其中峰值之间的差距逐渐增大)明确地表明数据库无法顺利应付。

开发场景提供程序、KPI 和 DVS 格式

采用 示例实现 中介绍的方式开发场景提供程序、KPI 和格式。

分析模拟性能

如果模拟运行的时间比预期的更长,您可能会发现通过改变执行线程(模拟部分)的数量和为每个查询抓取的行数,会得到一定程度的改进。但是,运行 SSP 的服务器上普遍的低 CPU 使用率和不频繁的网络活动,数据库临时空间的过量使用,以及数据库响应能力的降低,都是您需要改进数据库访问逻辑和/或数据库设计的征兆。


示例实现

本节中提供的示例实现基于一家金融服务公司的用例,该公司需要评估制度政策的更改给当前支付交易处理能力带来的影响。预计需要使用更多的手动验证(这由一个规则集控制),但手动工作的准确率增长程度仍然是未知的。

该规则集基于一个提供了两个主要类的 Java XOM:

  • com.bank.payment.Payment,定义一个具有以下属性的支付交易:
    • amount - 支付金额。
    • currency - 支付货币。
    • debitAccount - 拨出付款的银行帐号。
    • beneficiaryAccount - 接收付款的银行帐号。
    • date - 创建支付交易的日期。
    • valueDate - 支付交易的起息日。
    • manual - 一个表明支付交易是否应手动处理的标志。
  • com.bank.payment.AccountServices,定义一组服务,用于检索与某个银行帐号相关的信息。请注意,在本文中提供的示例 XOM 项目中,AccountServices 实现是一个不会通过连接到后台系统来获取信息的示例。

规则集签名包含一个类型为 PaymentIN_OUT 参数(如果有必要,会通过规则集来更新它的 manual flag 属性),还有一个类型为 AccountServicesIN 参数。

为了评估制度政策更改带来的影响,该公司决定对它过去的支付交易的数据库进行模拟。该公司在它的历史数据库上使用了一个商业智能工具,让统计两个给定日期之间的支付交易数量和金额变得非常简单。然后将模拟的范围定义为两个日期之间的支付交易数量。

在与数据库管理员讨论和测试不同的数据访问战略之后,该公司决定使用单个表来存储模拟数据。这个表名为 DATA,使用以下列来定义:

表 1. DATA 表的列
列名称列类型备注
ID INT 主键,在插入按升序日期排序的支付条目时生成
CURRENCY CHAR(3) 支付的货币
AMOUNT DECIMAL(15,2) 支付的金额
DEBTACC VARCHAR(27) 借方账户
BENEFACC VARCHAR(27) 受益人帐户
DATE BIGINT 支付日期
VALUEDATE BIGINT 付款起息日

因为支付交易日期的主要分区键是日期,所以可以确定,应该将模拟部分定义为一个日期列表,每个模拟部分都包含该列表中两个连续日期之间定义的支付。例如,要将在 DATE1 和 DATE2 之间做出的支付的模拟拆分为 3 个部分,用户可以使用商业智能工具获取 DATEa 和 DATEb 这两个日期,这样在 DATE1 和 DATEa 之间(第一个模拟部分)、DATEa 和 DATEb 之间(第二个模拟部分)以及 DATEb 和 DATE2 之间(第三个模拟部分)的数据库中就拥有大体相同的支付交易数量。

为了避免保持长时间连接到数据库,或者发出一个尝试一次获取数据库中的所有行的请求,每个场景提供程序实例都可以使用一个在必要时填充的 Payment 对象缓存,使用一个返回填满缓存所需的准确行数的数据库请求。缓存的大小是作为模拟的一个参数来提供的,如果它是空的,那么会在 SSP 请求一个新场景时重新填充缓存。

用于获取数据(和填充缓存)的 SQL 请求如下所示:

SELECT ID, CURRENCY, AMOUNT, DEBTACC, BENEFACC, DATE, VALUEDATE 
FROM DATA WHERE DATA.DATE between {0} and {1} AND ID >= {2} 
ORDER BY ID ASC FETCH FIRST {3} ROWS ONLY OPTIMIZE FOR {3} ROWS 
FOR READ ONLY WITH UR

其中 {0} 替换为用于该模拟部分的第一个支付交易的日期,{1} 替换为用于该模拟部分的最后一个支付交易的日期,{2} 替换为从缓存中处理的最后一个交易之后的支付 ID,{3} 替换为 Payment 对象的缓存大小。

以下这组 KPI 已经确认:

  • 模拟中的支付总数。
  • 需要进一步手动处理的支付数量。
  • 需要进一步手动处理的支付总(交易)额。
  • 需要进一步手动处理的支付的平均金额。
  • 不需要进一步手动处理的支付数量。
  • 不需要进一步手动处理的支付总额。
  • 不需要进一步手动处理的支付的平均金额。
  • 模拟的总执行时间(不是一个业务 KPI)。

实现此用例所需的所有示例材料都可以从本文的 下载 部分获得。payment.zip 归档文件包含:

  • 一个 Java 项目,用于创建一个示例支付数据库 (payment-database)。
  • Java XOM 项目 (payment-xom) 和规则项目 (payment-processing)。
  • 并行场景提供程序和 KPI 组件的实现 (sample-src)。

要安装和运行示例,则需要完成以下步骤。

步骤 1:将示例材料导入 Rule Designer 中

  1. 在一个干净的工作区中启动 Rule Designer,选择 File => Import 打开导入向导。在导入向导中,选择 General => Existing Projects into Workspace,如图 1 所示,然后单击 Next
    图 1. 导入项目
    导入项目
  2. 在导入向导中,选择 Select archive file 并单击 Browse,导航到您已 下载payment.zip 归档文件。向导的内容会使用该归档文件中包含的 3 个项目更新:
    • payment-database 是一个 Java 项目,包含示例数据库创建脚本和一个用于加载包含示例数据的数据库的 Java 实用程序。
    • payment-processing 是定义模拟的管制策略(规则)的规则项目。
    • payment-xom 是 payment-processing 的 Java XOM。
  3. 选择要导入的 3 个项目,如图 2 所示。
    图 2. 选择要导入的支付项目
    选择要导入的支付项目
  4. 单击 Finish 将 3 个项目导入您的工作区中,如图 3 所示。
    图 3. 导入后支付项目将会显示在 Rule Explorer 中
    导入后支付项目将会显示在 Rule Explorer 中

步骤 2:创建和填充示例数据库

第 1 步中导入 Rule Designer 中的 payment-database 项目包含以下工件:

  • create-schema.sql,用于在 DB2 数据库中创建示例数据库模式的 SQL 脚本。
  • com.bank.payment.database.DataLoader,一个 Java 类,定义了您为填充示例数据库而运行的主要方法。

如果在 Rule Designer 中切换到 Java 透视图,那么您可以轻松地访问这两个工件,如图 4 所示。

图 4. payment-database 项目的内容
payment-database 项目的内容

创建数据库的技巧

在创建数据库时,需要一名经验丰富的 DBA。大型的数据库必须进行仔细调优,而常见的默认配置设置可能不够用或不充分。例如,在 DB2® 中,您需要确保存储容器中拥有足够的空间,您的缓冲池和日志足够大,数据库配置参数(比如 BUFFPAGE 和 DBHEAP)拥有足够大的值,等等。

使用 create-schema.sql 脚本创建数据库模式后,需要将 DB2 JDBC 驱动程序 JAR 添加到 payment-database 项目的生成路径中,然后以 Java 程序形式运行 DataLoader 类来填充数据库,向该 Java 程序提供以下 6 个参数:

  • 用于访问您的数据库的 JDBC 驱动程序的名称。
  • 您的数据库的 JDBC URL。
  • 用于访问您的数据库的用户名。
  • 用于访问您的数据库的密码。
  • 在您的数据库中创建的行(支付)数。
  • 批次大小,这是在填充数据库时要在单个操作中创建的支付数量。

图 5 显示了我们用来向一个 DB2 示例数据库填充 1.2 亿次支付的参数值示例。加载该数据库花了 2 小时 41 分钟。

图 5. 运行配置,向一个 DB2 数据库填充 1.2 亿个条目
运行配置,向一个 DB2 数据库填充 1.2 亿个条目

步骤 3:创建自定义场景套件格式

要为大规模模拟创建自定义场景套件格式,需要在 Rule Designer 中创建一个新 DVS 项目,然后使用新场景套件格式向导定义并行场景提供程序,还要定义用于该自定义格式的 KPI。

  1. 在 Rule Designer 中,按 Ctrl+N 显示 New 向导。
  2. 选择 Rule Designer => Decision Validation Services => DVS Project,如图 6 所示。
    图 6. 新 DVS 项目创建向导
    新 DVS 项目创建向导
  3. 单击 Next 并为您的项目指定一个名称(在我们的示例中为 Payment Simulations),如图 7 所示。
    图 7. 为新 DVS 项目命名
    为新 DVS 项目命名
  4. 单击 Next,然后单击 Finish 创建新 DVS 项目。DVS Customization 编辑器现在已显示在 Rule Designer 中,如图 8 所示。
    图 8. DVS Customization 编辑器
    DVS Customization 编辑器

    查看图 8 的大图。)

  5. 下一步是为自定义添加一个运行时配置,该配置定义了 Decision Center 和 SSP 的目标应用服务器。为此,请单击 Configurations 部分中的 Create,选择目标应用服务器。然后将 payment-processing 规则项目添加到项目列表中(通过单击 Rule Projects 部分中的 Add 并选择该规则项目,如图 9 所示)。
    图 9. WebSphere Application Server V8 配置
    WebSphere Application Server V8 配置

    查看图 9 的大图。)

  6. 要创建将用于在 payment-processing 规则项目上运行并行模拟的新场景套件格式,可单击 Formats 部分中的 Create,这会打开 New DVS Format 向导,如图 10 所示。
    图 10. New DVS Format 向导
    New DVS Format 向导
  7. 为此格式指定一个名称(在我们的示例中为 payment database)并单击 Next
  8. 在下一个屏幕上,定义要用于该格式的场景提供程序实现。要创建一个新的并行场景提供程序,请单击 Create,这会显示 New DVS Scenario provider 向导。除了提供要生成的场景提供程序组件的包和类名,这里的一个重要的用户操作是选中 Generate a scenario provider that supports parallel execution of simulations,如图 11 所示。这可确保生成一个并行场景提供程序。
    图 11. 选中并行执行选项
    选中并行执行选项
  9. 单击 Finish 生成场景提供程序和场景套件渲染器类。这会将您带回 New DVS Format 向导,该向导现在已使用新场景提供程序的名称更新,如图 12 所示。
    图 12. 为 DVS 格式选择的新场景提供程序
    为 DVS 格式选择的新场景提供程序
  10. 单击 Finish 关闭 New DVS Format 向导,并为新创建的格式打开 DVS Format 编辑器,如图 13 所示。
    图 13. DVS Format 编辑器
    DVS Format 编辑器

    查看图 13 的大图。)

  11. 要完成新场景套件格式的定义,需要定义将在使用此格式运行模拟时使用的 KPI。为此,单击编辑器的 KPI 部分中的 New,打开 KPI Result Aggregator Definition 对话框,如图 14 所示。
    图 14. KPI Result Aggregator Definition 对话框
    KPI Result Aggregator Definition 对话框
  12. KPI Display Name 表示模拟报告中的 KPI(在我们的示例中为 Payment statistics)。要定义用于计算和聚合 KPI 值的实现类,请单击 KPI Result Aggregator Class 旁边的 Create,以便显示 New KPI Result Aggregator 向导,如图 15 所示。
    图 15. New DVS KPI Result Aggregator 向导
    New DVS KPI Result Aggregator 向导
  13. 这里提供了两个选择:从头创建一个新 KPI Result Aggregator,或者为一个已针对某个非并行模拟类型定义的 IlrKPI 组件创建一个聚合器。在我们的示例中,我们将创建一个新的 IlrKPI 实现来计算每个模拟部分的 KPI 值,所以我们接受了已选定的第一个选项并单击 Next。这会打开 New DVS KPI Result Aggregator 向导,您将在其中提供以下信息:
    • 要创建的 IlrKPI 实现(在我们的示例中为 com.bank.payment.PaymentKPI)。
    • 要创建的 KPI 结果聚合器实现(在我们的示例中为 com.bank.payment.PaymentKPIResultAggregator)。
    • 要创建的渲染器(在我们的示例中为 com.bank.payment.PaymentKPIRenderer>),用于在 Decision Center 中显示模拟报告中的 KPI 结果。
    • 计算的 KPI 结果的类型。在我们的示例中,我们使用的类型是 Map,它提供了一种在 KPI 结果中存储多个值的便捷方式,并且仅定义了一个 IlrKPI 实现和一个 KPI 结果聚合器。

    图 16 给出了在提供了所有输入后向导对话框的外观。

    图 16. 已完成的 New DVS KPI Result Aggregator 向导
    已完成的 New DVS KPI Result Aggregator 向导
  14. 单击 Finish 为 KPI 组件生成模板实现类,并更新 KPI Result Aggregator Definition 对话框,如图 17 所示。
    图 17. KPI Result Aggregator Definition 对话框
    KPI Result Aggregator Definition 对话框
  15. 单击 OK 关闭此对话框,并按 Ctrl+S 保存 Scenario Suite Format 编辑器中的 payment database 场景套件格式。

步骤 4:实现并行场景提供程序组件

向导为场景提供程序组件生成的模板已在下面列出,您现在需要实现它们:

  • com.bank.payment.PaymentParallelScenarioProvider 是并行场景提供程序。
  • com.bank.payment.PaymentScenarioSuiteResourcesRenderer 是负责获取一个模拟用户参数的 Decision Center 组件。这些参数值随后会在初始化时提供给并行场景提供程序。

图 18 在 Package Explorer(Rule Designer 中的 Java 透视图)中显示了这两个 Java 类模板。

图 18. 场景提供程序实现类的详细信息
场景提供程序实现类的详细信息

PaymentScenarioProvider 实现

com.bank.payment.PaymentParallelScenarioProvider 类的完整源代码可在本文的 下载 部分中提供的 payment.zip 归档文件的 sample-src 目录中找到。

让我们看看场景提供程序的实现细节。并行场景提供程序的每个实例都使用一个 JDBC Connection 对象来访问支付数据库。这个 Connection 对象通过一个存储在 SSP 应用服务器主机的 JNDI 目录中的 JDBC 数据源(在名称 jdbc/lssDB 下)连接到数据库,该对象可在场景提供程序的 initialize(...) 方法中找到。该连接可使用场景提供程序的 close() 方法来关闭。清单 1 显示了数据库连接生命周期

清单 1. 数据库连接生命周期
publicstaticfinal String DATASOURCE_JNDI_NAME = "jdbc/lssDB";
private Connection dbConnection = null;

/**
 * Callback method invoked by the runner in order to inject the execution
 * context into the component.
 */
publicvoid initialize(IlrScenarioSuiteExecutionContext context)
  throws IlrInitializationException {

  // Retrieve a connection to the payment database
  try {
    Context jndiContext = new InitialContext();
    DataSource datasource = 
    (DataSource) jndiContext.lookup(DATASOURCE_JNDI_NAME);
    this.dbConnection = datasource.getConnection();
  } catch (Throwable t) {
    thrownew IlrInitializationException(t);
 }

  // …
}

/**
 * Close the scenario provider.
 */
publicvoid close() {
  if (this.dbConnection != null) {
    try {
      this.dbConnection.close();
    } catch (SQLException e) {
 e.printStackTrace();
 }
 }
}

模拟用户在 Decision Center 中输入的两个模拟参数将从场景套件执行上下文中获取,该上下文由 SSP 提供给 initialize(...) 方法:

  • 缓存大小,该参数定义了 Payment 实例的内部缓存的大小,是一个 int。它的名称为 CACHE_SIZE
  • 日期范围,该参数定义了每个模拟部分的开始和结束日期列表,是一个 List<Long>。它的名称为 DATE_RANGES

两个值都存储在一个名为 ScenarioProviderParameters 的嵌套类中,该类提供了一个工厂方法,从一个场景套件上下文实例中获取这些参数值,如清单 2 所示。

清单 2. 获取场景参数
publicstaticfinal String DATE_RANGES = "DATE_RANGES";
publicstaticfinal String CACHE_SIZE = "CACHE_SIZE";

/**
 * Bean that stores the scenario provider parameters
 */
publicstaticfinalclass ScenarioProviderParameters {

  privateint cacheSize = -1;
  private List<Long> dateRanges = new ArrayList<Long>();

  publicstatic ScenarioProviderParameters fromScenarioSuiteDescriptor(
    // … 
  }

  // … 
}

private ScenarioProviderParameters parameters = null;

/**
 * Callback method invoked by the runner in order to inject the execution
 * context into the component.
 */
publicvoid initialize(IlrScenarioSuiteExecutionContext context)
  throws IlrInitializationException {
  // …
  // Extract the simulation parameters from the context
  this.parameters = ScenarioProviderParameters
    .fromScenarioSuiteDescriptor(context.getScenarioSuiteDescriptor());
  // …
}

模拟中的场景总数由支付数据库中在日期范围参数的第一个日期和最后一个日期之间的支付数量,如清单 3 所示。

清单 3. 获取模拟中的场景总数
/**
 * Return the count of scenarios for this provider.
 * 
 * @return the count of scenarios for this provider
 * @throws IlrTestingException
 * if an error occurred while computing the scenarios count
 */
publicint getScenarioCount() throws IlrScenarioProviderException {
  // Count the number of scenarios in the database
  long firstDate = parameters.getDateRanges().get(0);
  long lastDate = parameters.getDateRanges().get(
    parameters.getDateRanges().size() - 1);
  return countRecordsBetweenDates(firstDate, lastDate);
}

/**
 * Count the number of payments between two dates in the database.
 */
privateint countRecordsBetweenDates(Long startDate, Long endDate)
  throws IlrScenarioProviderException {
  int returnedValue = 0;
  Statement stmt = null;
  ResultSet result = null;
  try {
    stmt = this.dbConnection.createStatement();
    String request = MessageFormat.format(
    "SELECT COUNT(*) FROM DATA WHERE DATE BETWEEN {0} AND {1}",
      new Object[] { String.valueOf(startDate),
      String.valueOf(endDate) });
    result = stmt.executeQuery(request);
    if (result.next()) {
      returnedValue = result.getInt(1);
    }
  } catch (SQLException e) {
    thrownew IlrScenarioProviderException(e);
  } finally {
    // … (close resultset and statement)
  }
  return returnedValue;
}

每个模拟部分都由最终用户在 Decision Center 中输入的日期列表中两个连续的开始日期和结束日期来定义。对于一个模拟部分,并行场景提供程序将返回数据库中存储的在开始日期(含)和结束日期(不含,除了最后一个日期)之间的所有支付的场景。因为场景提供程序使用了一个 Payment 对象缓存,而不是通过查询数据库来获取某个部分的所有支付,所以它还会为每个部分跟踪将在查询数据库时加载到缓存中的下一个支付的 ID。方法 getScenarioSuiteParts() 定义了模拟的各个部分,并使用一个 long[] 将部分参数(开始日期、结束日期、该部分的下一个支付的 ID)存储为该部分的自定义数据,如清单 4 所示。

清单 4. 获取模拟部分
/**
 * Returns the list of parts to be used for the parallel execution of the
 * simulation.
 * 
 * @return The list of parts to be used by the SSP to execute a simulation
 * concurrently in multiple processors or threads. For each part of
 * the list, the SSP creates a new instance of the scenario provider
 * and iterates through the range of indexes defined for the part. The
 * KPI result calculated for each part is then consolidated by an
 * instance of {@link IlrMapReduceKPI}.
 */
public List<IlrScenarioSuitePart> getScenarioSuiteParts()
  throws IlrScenarioProviderException {
  ArrayList<IlrScenarioSuitePart> returnedValue =
    new ArrayList<IlrScenarioSuitePart>();
  // A simulation part is defined as the payments between two dates
  // from the range
  List<Long> dateRanges = this.parameters.getDateRanges();
  int numberOfDates = dateRanges.size();
  int currentScenarioIndex = 0;
  for (int partNumber = 0; partNumber < numberOfDates - 1; partNumber++) {
    // A part if defined by:
    // 1) a start date
    // 2) an end date
    // 3) the ID of the next payment to retrieve from the database
    // between these two dates (in order to refill the cache of
    // payments for the part)
    long startDate = this.parameters.getDateRanges().get(partNumber);
    long endDate = this.parameters.getDateRanges().get(partNumber + 1);
    // the payments with a date equal to the end date are not
    // included in the part, except for the last part
    if (partNumber < numberOfDates - 2) {
      endDate = endDate - 1;
    }
    // count the number of scenarios (payments) for this range
    int numberOfScenarios = countRecordsBetweenDates(startDate, endDate);
    // retrieve the next record ID for this part
    int nextRecordIDForThePart = -1;
    Statement stmt = null;
    ResultSet result = null;
    try {
      stmt = this.dbConnection.createStatement();
      String request = MessageFormat.format(
        "SELECT ID FROM DATA WHERE DATA.DATE >= {0} ORDER BY ID ASC",
        new Object[] { String.valueOf(startDate) });
      result = stmt.executeQuery(request);
      if (result.next()) {
        nextRecordIDForThePart = result.getInt(1);
      } else {
        thrownew IlrScenarioProviderException("No records for date >= "
          + String.valueOf(startDate));
      }
    } catch (SQLException e) {
      thrownew IlrScenarioProviderException(e);
    } finally {
      // … (close request and statement)
    }
    // Each part contains its inclusive range as custom data, as
    // well as the value of the next record ID to use to retrieve
    // data for the part
    IlrScenarioSuitePart part = new IlrScenarioSuitePart(
      currentScenarioIndex, currentScenarioIndex + numberOfScenarios - 1,
      newlong[] { startDate, endDate, nextRecordIDForThePart });
      returnedValue.add(part);
    currentScenarioIndex += numberOfScenarios;
  }
  return returnedValue;
}

当为一个模拟部分运行 initialize(...) 方法时,场景提供程序在该部分的自定义数据对象 (long[])) 中获取定义该部分的信息(开始日期、结束日期和将从数据库获取的下一个支付的 ID)。这些值存储为场景提供程序的属性,所以可以在 SSP 调用 getScenarioAt(...) 方法时获取它们,如清单 5 所示。

清单 5. 初始化当前的模拟部分
privatelong startDateForTheCurrentPart = -1;
privatelong endDateForTheCurrentPart = -1;
privateint nextRecordID = -1;
private Payment[] paymentCache = null;

/**
 * Callback method invoked by the runner in order to inject the execution
 * context into the component.
 */
publicvoid initialize(IlrScenarioSuiteExecutionContext context)
  throws IlrInitializationException {
  // …

  // If the scenario provider is initialized for a part, retrieve the
  // part parameters from the custom data of the part.
  IlrScenarioSuitePart currentPart = context.getCurrentScenarioSuitePart();
  if (currentPart != null) {
    long[] scenarioPartData = (long[]) currentPart.getCustomData();
    this.startDateForTheCurrentPart = scenarioPartData[0];
    this.endDateForTheCurrentPart = scenarioPartData[1];
    this.nextRecordID = (int) scenarioPartData[2];
    // Initialize the Payment cache
    this.paymentCache = new Payment[parameters.getCacheSize()];
  }
}

getScenarioAt(...) 方法从缓存获取 Payment 实例,负责在缓存为空时(重新)填充它,如清单 6 所示。

清单 6. 获取场景
public static final String PAYMENT_PARAMETER_NAME = "payment";
public static final String ACCOUNTSERVICES_PARAMETER_NAME = "accountServices";

private int currentCacheIndex = 0;
private int numberOfEntriesInCache = 0;
private AccountServices accountServices = new AccountServices();

/**
 * Return the scenario at specified index
 * 
 * @param scenarioIndex
 * the index of the scenario
 * @return the scenario at specified index
 * @throws IlrTestingException
 * if an exception occurred while retrieving the scenario
 */
public IlrScenario getScenarioAt(int scenarioIndex)
  throws IlrScenarioProviderException {
  // Retrieve scenario from the cache
  if (this.currentCacheIndex == 0) {
    // We need to refill the buffer
    ResultSet result = null;
    Statement getScenarioDataStatement = null;
    try {
      getScenarioDataStatement = this.dbConnection.createStatement();
      if (this.paymentCache.length > 1000) {
        getScenarioDataStatement.setFetchSize(this.paymentCache.length);
      }
      String request = MessageFormat.format(
        "SELECT ID, CURRENCY, AMOUNT, DEBTACC, BENEFACC, DATE, VALUEDATE" +
        " FROM DATA WHERE DATA.DATE between {0} and {1} AND ID >= {2} ORDER BY ID ASC" +
        " FETCH FIRST {3} ROWS ONLY OPTIMIZE FOR {4} ROWS FOR READ ONLY WITH UR",
        new Object[] { String.valueOf(this.startDateForTheCurrentPart),
        String.valueOf(this.endDateForTheCurrentPart),
        String.valueOf(nextRecordID),
        String.valueOf(this.paymentCache.length), 
        String.valueOf(this.paymentCache.length) });
      result = getScenarioDataStatement.executeQuery(request);
      this.numberOfEntriesInCache = 0;
      while (result.next() && this.numberOfEntriesInCache < this.paymentCache.length) {
        this.nextRecordID = result.getInt(1) + 1;
        Payment payment = new Payment();
        payment.setCurrency(Currency.valueOf(result.getString(2)));
        payment.setAmount(result.getBigDecimal(3));
        payment.setDebitAccount(result.getString(4));
        payment.setBeneficiaryAccount(result.getString(5));
        payment.setDate(new Date(result.getLong(6)));
        payment.setValueDate(new Date(result.getLong(7)));
        // Add the payment to the cache
        this.paymentCache[this.numberOfEntriesInCache] = payment;
        this.numberOfEntriesInCache++;
      }
    } catch (SQLException e) {
      throw new IlrScenarioProviderException(e);
    } finally {
      // … (close resultset and statement)
    }
  }
  IlrScenarioImpl returnedValue = new IlrScenarioImpl();
  returnedValue.setName(new StringBuffer("Scenario ")
    .append(scenarioIndex)
    .toString());
  Map<String, Object> scenarioInputParameters = new HashMap<String, Object>();
  scenarioInputParameters.put(PAYMENT_PARAMETER_NAME,
  this.paymentCache[this.currentCacheIndex]);
  scenarioInputParameters.put(ACCOUNTSERVICES_PARAMETER_NAME, accountServices);
  returnedValue.setInputParameters(scenarioInputParameters);

  if (++this.currentCacheIndex >= this.numberOfEntriesInCache) {
    this.currentCacheIndex = 0;
  }

  return returnedValue;
}

PaymentScenarioSuiteResourcesRenderer 实现

com.bank.payment.PaymentScenarioSuiteResourcesRenderer 类的完整源代码可在本文的 下载 部分中提供的 payment.zip 归档文件的 sample-src 目录中找到。

场景套件资源渲染器是 Decision Center 公共 API 定义的一个组件。Decision Center 将一个模拟的参数存储为 byte[] 对象,场景套件资源渲染器提供了以下 3 个方法来帮助 Decision Center 处理这些参数:

  • encodeAsEditor(...) 方法负责创建供最终用户用来提供模拟参数的 HTML 输入字段。每个参数的值(如果它们已定义)都由 Decision Center 提供,以 byte[] 对象形式存储在一个映射(参数 resources)中。在一个用户创建或编辑一个使用支付数据库格式的模拟时,此方法创建的 HTML 元素由 Decision Center 呈现。
  • encodeAsViewer(...) 方法负责创建一些 HTML 元素,当最终用户在 Decision Center 中查看模拟时,这些元素向他们显示模拟参数。
  • decode(...) 负责获取最终用户在 Decision Center 中输入的模拟参数值,并将它们存储在一个 byte[] 对象映射中。

步骤 5:实现 KPI 组件

以下是要实现的 KPI 组件,Rule Designer 向导为它们生成了模板:

  • com.bank.payment.PaymentKP 是负责计算一个模拟部分的 KPI 值的组件。
  • com.bank.payment.PaymentKPIResultAggregator 是负责聚合各部分的 KPI 结果并返回模拟的全局 KPI 结果的组件。
  • com.bank.payment.PaymentKPIRenderer 是在模拟报告中显示 KPI 结果的 Decision Center 组件。

图 19 在 Package Explorer(Rule Designer 中的 Java 透视图)中显示了这 3 个类模板。

图 19. KPI 实现类的详细信息
KPI 实现类的详细信息

PaymentKPI 实现

com.bank.payment.PaymentKPI 类的完整源代码可在本文的 下载 部分中提供的 payment.zip 归档文件的 sample-src 目录中找到。

KPI 值存储在该类的属性中。这些属性值会在每次执行一个场景后更新,除了平均值,平均值在以后的某个时间由 KPI 结果聚合器计算,如清单 7 所示。

清单 7. KPI 值的计算
privateint totalNumberOfPayments = 0;
privateint numberOfManualProcessings = 0;
private BigDecimal totalAmountManuallyProcessed = new BigDecimal(0);
privateint numberOfAutomatedProcessings = 0;
private BigDecimal totalAmountAutomaticallyProcessed = new BigDecimal(0);

/**
 * Called by the runner after the ruleset has executed.
 * 
 * This method is called for each scenario defined in the suite. When it is
 * called, the report element is not yet available from the suite report.
 * 
 * @param scenario
 * The scenario.
 * @param request
 * The request used to execute the ruleset to test.
 * @param response
 * The response returned by executing the ruleset.
 * @throws IlrKPIException
 * if an error occurs.
 */
publicvoid onScenarioEnd(IlrScenario scenario, IlrSessionRequest request,
  IlrSessionResponse response) throws IlrKPIException {
  this.totalNumberOfPayments++;
  Payment payment = (Payment) response.getOutputParameters().get("payment");
  if (payment.isManualProcessing()) {
    this.numberOfManualProcessings++;
    this.totalAmountManuallyProcessed = 
    this.totalAmountManuallyProcessed.add(payment.getAmount());
  } else {
    this.numberOfAutomatedProcessings++;
    this.totalAmountAutomaticallyProcessed =
      this.totalAmountAutomaticallyProcessed.add(payment.getAmount());
  }
}

KPI 结果是一个 Map,包含在执行期间计算的所有 KPI 值,如清单 8 所示。

清单 8. 创建 KPI 结果
publicstaticfinal String TOTAL_NB_OF_PAYMENTS =
  "Total nb of payments";
publicstaticfinal String NB_OF_MANUAL_PROCESSINGS =
  "Nb of manual processings";
publicstaticfinal String TOTAL_AMOUNT_MANUALLY_PROCESSED =
  "Total amount manually processed";
publicstaticfinal String NB_OF_AUTOMATED_PROCESSINGS =
  "Nb of automated processings";
publicstaticfinal String TOTAL_AMOUNT_AUTOMATICALLY_PROCESSED =
  "Total amount automatically processed";

/**
 * Returns the KPI result to be included in the full report.
 * 
 * This method is called after all of the parts of the simulation have been
 * executed.
 * 
 * @return The KPI result.
 * @throws IlrKPIException
 * if an error occurs while computing the result.
 */
public IlrKPIResult getKPIResult() throws IlrKPIException {
  IlrKPIResultMap returnedValue = new IlrKPIResultMap();
  returnedValue.setKPIClassName(PaymentKPI.class.getName());
  Map<String, Serializable> value = new HashMap<String, Serializable>();
  value.put(TOTAL_NB_OF_PAYMENTS, this.totalNumberOfPayments);
  value.put(NB_OF_MANUAL_PROCESSINGS, this.numberOfManualProcessings);
  value.put(TOTAL_AMOUNT_MANUALLY_PROCESSED, this.totalAmountManuallyProcessed);
  value.put(NB_OF_AUTOMATED_PROCESSINGS, this.numberOfAutomatedProcessings);
  value.put(TOTAL_AMOUNT_AUTOMATICALLY_PROCESSED,
  this.totalAmountAutomaticallyProcessed);
  returnedValue.setValue(value);
  return returnedValue;
}

PaymentKPIResultAggregator 实现

com.bank.payment.PaymentKPIResultAggregator 类的完整源代码可在本文的 下载 部分中提供的 payment.zip 归档文件的 sample-src 目录中找到。

模拟执行的总持续时间在 KPI 结果聚合器中计算,因为这是最先创建且最后调用来获取 KPI 结果的 KPI 组件,如清单 9 所示。

清单 9. 模拟持续时间的计算
privatelong startExecutionTimestamp = 0;

/**
 * Initializes this object with an execution context.
 * 
 * @param context
 * The execution context.
 * @throws IlrInitializationException
 * if an error occurs while initializing this object.
 */
publicvoid initialize(IlrScenarioSuiteExecutionContext context)
  throws IlrInitializationException {
  this.startExecutionTimestamp = System.currentTimeMillis();
}

/**
 * Returns the KPI result to be included in the full report.
 * 
 * This method is called after all of the parts of the simulation have been
 * executed.
 * 
 * @return The KPI result.
 * @throws IlrKPIException
 * if an error occurs while computing the result.
 */
public IlrKPIResult getKPIResult() throws IlrKPIException {
  IlrKPIResultMap returnedValue = new IlrKPIResultMap();
  // … 
  Map<String, Serializable> value = new HashMap<String, Serializable>();
  long durationInSeconds = (System.currentTimeMillis() -
  this.startExecutionTimestamp) / 1000;
  value.put("Total duration of the execution",
  String.valueOf(durationInSeconds) + " sec.");
  // … 
  returnedValue.setValue(value);
  return returnedValue;
}

PaymentKPI 实例计算的 KPI 值在每次出现一个结果是都会聚合,如清单 10 所示。

清单 10. 聚合 KPI 结果
privateint totalNumberOfPayments = 0;
privateint numberOfManualProcessings = 0;
private BigDecimal totalAmountManuallyProcessed = new BigDecimal(0);
privateint numberOfAutomatedProcessings = 0;
private BigDecimal totalAmountAutomaticallyProcessed = new BigDecimal(0);

/**
 * Adds the result of a KPI calculated for a part of the simulation, or
 * scenario suite.
 * 
 * This method is called at the end of the execution of each part.
 * 
 * @param kpiResult
 * The result of the simulation part, which is calculated by the
 * IlrKPI instance returned by the {@link #getKPIClassName()}
 * method.
 */
publicvoid add(IlrKPIResult kpiResult) throws IlrKPIException {
  Map<String, Serializable> values = ((IlrKPIResultMap) kpiResult).getValue();
  this.totalNumberOfPayments +=
    (Integer) values.get(PaymentKPI.TOTAL_NB_OF_PAYMENTS);
  this.numberOfManualProcessings +=
    (Integer) values.get(PaymentKPI.NB_OF_MANUAL_PROCESSINGS);
  this.totalAmountManuallyProcessed =
    this.totalAmountManuallyProcessed.add((BigDecimal)
    values.get(PaymentKPI.TOTAL_AMOUNT_MANUALLY_PROCESSED));
  this.numberOfAutomatedProcessings +=
    (Integer) values.get(PaymentKPI.NB_OF_AUTOMATED_PROCESSINGS);
  this.totalAmountAutomaticallyProcessed =
    this.totalAmountAutomaticallyProcessed.add((BigDecimal)
    values.get(PaymentKPI.TOTAL_AMOUNT_AUTOMATICALLY_PROCESSED));
}

平均值是最后的 KPI 结果计算,如清单 11 所示。

清单 11. 计算最后的 KPI 结果
/**
 * Returns the KPI result to be included in the full report.
 * 
 * This method is called after all of the parts of the simulation have been
 * executed.
 * 
 * @return The KPI result.
 * @throws IlrKPIException
 * if an error occurs while computing the result.
 */
public IlrKPIResult getKPIResult() throws IlrKPIException {
  IlrKPIResultMap returnedValue = new IlrKPIResultMap();
  returnedValue.setKPIClassName(PaymentKPIResultAggregator.class.getName());
  Map<String, Serializable> value = new HashMap<String, Serializable>();
  value.put(PaymentKPI.TOTAL_NB_OF_PAYMENTS, this.totalNumberOfPayments);
  value.put(PaymentKPI.NB_OF_MANUAL_PROCESSINGS,
    this.numberOfManualProcessings);
  value.put(PaymentKPI.TOTAL_AMOUNT_MANUALLY_PROCESSED,
    this.totalAmountManuallyProcessed);
  value.put("Average amount of a manually processed payment",
    this.totalAmountManuallyProcessed
    .divide(new BigDecimal(this.numberOfManualProcessings),
    2, BigDecimal.ROUND_HALF_DOWN));
  value.put(PaymentKPI.NB_OF_AUTOMATED_PROCESSINGS,
    this.numberOfAutomatedProcessings);
  value.put(PaymentKPI.TOTAL_AMOUNT_AUTOMATICALLY_PROCESSED,
    this.totalAmountAutomaticallyProcessed);
  value.put("Average amount of an automatically processed payment",
    this.totalAmountAutomaticallyProcessed
    .divide(new BigDecimal(this.numberOfAutomatedProcessings),
    2, BigDecimal.ROUND_HALF_DOWN));
  // … 
  returnedValue.setValue(value);
  return returnedValue;
}

PaymentKPIRenderer 实现

com.bank.payment.PaymentKPIRenderer 类的完整源代码可在本文的 下载 部分中提供的 payment.zip 归档文件的 sample-src 目录中找到。

PaymentKPIRenderer 实现在一个 HTML 表中呈现 KPI 聚合器结果的所有条目。第一列显示映射条目的键,第二列显示值,如清单 12 所示。

清单 12. 在一个 HTML 表中呈现 KPI
publicvoid encode(IlrKPIResultMap kpiResult, FacesContext context,
  UIComponent component) throws IOException {
 Map<String, Serializable> values = kpiResult.getValue();
  context.getResponseWriter().write("<table>");
  for (String key: values.keySet()) {
    context.getResponseWriter().write("<tr>");
    context.getResponseWriter().write("<td>");
 context.getResponseWriter().write(key);
    context.getResponseWriter().write(":&nbsp;");
    context.getResponseWriter().write("</td>");
    context.getResponseWriter().write("<td>");
    context.getResponseWriter().write(String.valueOf(values.get(key)));
    context.getResponseWriter().write("</td>");
    context.getResponseWriter().write("</tr>");
 }
  context.getResponseWriter().write("</table>");
}

步骤 6:部署和启用自定义场景套件格式

下一步是将新的场景套件格式封装在 Decision Center 和 SSP 企业应用程序归档文件中。然后需要将两个企业应用程序归档文件重新部署到应用服务器上。最后,您需要在 Decision Center 中为新的 ayment-processing 规则项目启用新的场景套件格式。

  1. 要将新场景套件格式封装在企业应用程序归档文件中,请单击自定义编辑器的 Actions 部分中的 Repackage the .ear/.war files,如图 20 所示。
    图 20. 在 DVS Customization 编辑器中选择 Repackage 选项
    在 DVS Customization 编辑器中选择 Repackage 选项
  2. 在下一个屏幕中,选择重新封装两个企业应用程序归档文件并单击 OK,如图 21 所示。
    图 21. 重新封装 SSP 和 Decision Center
    重新封装 SSP 和 Decision Center
  3. 等待对话框中表明重新封装的完成,如图 22 所示。
    图 22. Repackaging status 对话框
    Repackaging status 对话框
  4. 现在两个归档文件都已重新封装,在将 Decision Center 和 SSP 重新部署到应用服务器之前,您需要执行另外一个步骤。因为自定义场景提供程序在运行时通过访问 JNDI 目录来获取支付数据库的数据源,所以您必须更新 SSP 的支付描述符来声明此数据源(名为 jdbc/lssDB)。要更新的描述符是重新封装操作生成的 SSP 企业归档文件中的 jrules-ssp-server.war 文件的 WEB-INF 目录中的 web.xml 文件,如图 23 所示。
    图 23. Payment Simulations 项目中的 SSP ear 文件的位置
    Payment Simulations 项目中的 SSP ear 文件的位置

    在 web.xml 文件中,将以下 resources-ref 元素添加到该文件的资源引用部分中:

    <resource-ref>
    <res-ref-name>jdbc/lssDB</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
    <res-sharing-scope>Unshareable</res-sharing-scope>
    </resource-ref>

    请注意,该文件的资源引用部分在该文件中使用以下标头来标识:

    <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
    <!-- R E S O U R C E S -->
    <!-- -->
    <!-- R E F E R E N C E S -->
    <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
  5. 更新 web.xml 的内容后,您需要使用应用服务器主机的 JNDI 名称 jdbc/lssDB 定义一个数据源。有关如何在 WebSphere Application Server V8 中创建一个 DB2 数据源的信息,请参阅 WebSphere Application Server V8 信息中心
  6. 现在您可以将两个企业应用程序重新部署到应用服务器中。如果想获得有关如何重新部署 Decision Center 和 SSP 的更多信息,请参阅 WebSphere ODM V8 信息中心
  7. 重新部署完成后,您需要将 payment-processing 规则项目重新发布到 Decision Center 中(请参阅 WebSphere ODM 信息中心内的 此主题,了解有关的更多信息),在此规则项目上为模拟启用 Payment 数据库格式(在此处获取更多信息),如图 24 所示。
    图 24. 在 Decision Center 中启用 Payment 数据库模拟格式
    在 Decision Center 中启用 Payment 数据库模拟格式
  8. 要使用此格式创建一个模拟,请单击 Decision Center 中的 Compose 选项卡,然后选择 Simulation 并单击 OK。在模拟创建的第 3 个步骤中,选择 Payment database 格式来显示我们的渲染器创建的自定义模拟参数表单,如图 25 所示。
    图 25. 使用 Payment 数据库格式创建一个模拟
    使用 Payment 数据库格式创建一个模拟

模拟结果

当我们制定第一个包含 1.2 亿个场景的模拟运行时,我们立即观察到 CPU 和网络使用量在逐步下降,这通常表明该数据库的总体方面仍然不是太好。我们让模拟运行到结束,24 小时多一点的运行持续时间不太让我们满意。然后,我们打印出了对数据库的每次查询的持续时间,观察到(正如我们所怀疑的)随着运行的不断进行,查询执行时间也在不断增长。验证数据库配置参数后,检查 DB2 诊断消息并解释我们的查询,我们决定丢弃我们在 DATE 列上创建的一个索引。在这样做之后,查询执行时间开始保持固定,模拟在 2 小时内就完成了!(参见图 26)。在这种情况下,因为场景是从特定开始点顺序读取的,所以不需要索引,索引只会增加开销。

删除日期索引有一个小小的缺陷。当执行较小的模拟(比如 100 万个场景)时,您可能观察到在使用多个部分时,模拟运行的时间更长。原因是各部分是使用查询 SELECT ID FROM DATA WHERE DATA.DATE >= {0} ORDER BY ID ASC 来初始化的,该查询的执行性能比日期索引高。

在丢弃 DATE 上的索引后,第一次运行的模拟持续时间在预期范围内,如图 26 所示。

图 26. 一次包含 6 部分的执行操作的模拟报告
一次包含 6 部分的执行操作的模拟报告

我们为此运行使用了 6 部分和一个为 4000 的缓存大小。各部分使用日期(采用 Java long 格式)分隔,它们大小相同,分别包含 2000 万个场景,如表 2 所示。

表 2. 1.2 亿个记录的数据库中的支付数据分区
PARTITIONS
Scenario IDDate (long)
1 1296566652000
20000000 1296666651995
40000000 1296766651995
60000000 1296866651995
80000000 1296966651995
100000000 1297066651995
120000000 1297166651995

该运行花了 1 小时 37 分钟 30 秒(5850 秒)。

我们随后执行另一个模拟,在第一次运行与仅有一个部分(一个执行线程)的一个模拟的持续时间之间进行比较,结果如图 27 所示。

图 27. 仅包含一个部分的执行操作的模拟报告
仅包含一个部分的执行操作的模拟报告

此运行(它仅使用了一个执行线程)的持续时间为 3 小时 5 分钟 9 秒(11109 秒)。这接近前一次运行的两倍,而其那一次运行有 6 个执行线程。尽管取决于可用于模拟的计算资源和场景数据库的大小,对比的结果可能不同,但并行方法显然具有其优势。

最后,我们希望了解一下缓存大小对模拟持续时间的影响。我们将缓存大小从 4000 更改到 500,结果如图 28 所示。

图 28. 缓存大小为 500 的模拟报告
缓存大小为 500 的模拟报告

可以看到,运行程序时间(5861 秒)与第一次运行(5850 秒)相比的差别可忽略不计。在更改缓存大小时是否会看到任何变化主要取决于数据库配置和网络资源。

也可以考虑其他测试,包括改变部分数量来确定能提供最佳性能的数量。由于时间限制,我们没有执行任何进一步测试,但在向用户提供大规模模拟功能之前,可以执行足够的测试来确保计算资源的最佳使用,这通常是一种不错的做法。


结束语

在本文中,我们介绍了 WebSphere Operational Decision Management V8 中新的并行模拟功能,还介绍了此特性的工作原理,探讨了如何为执行可适应您公司需求的大规模模拟而创建必要的自定义工件。

总体而言,我们提出了一种方法来指导您解决一般的大规模模拟问题。我们还使用一个详细且全面的示例分析了实现和部署 API 所需的自定义工件的详细信息。此外,我们还了解了使用非常大的场景数据库的一些挑战。在示例中,我们展示了欠佳的数据库设计带来的影响以及可执行的调节,通过丢弃一个冗余的索引,我们将模拟时间从 24 小时缩短到了不到 2 小时。最后,通过使用示例项目和一个包含 1.2 亿个场景的数据库来执行模拟,我们演示了并行运行模拟相对于单个执行线程的优势,模拟时间减少了一半。


下载

描述名字大小
样例文件payment.zip40KB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=WebSphere
ArticleID=853952
ArticleTitle=使用 WebSphere Operational Decision Management V8 运行大规模模拟
publish-date=01042013