到现在为止,当您问我同样的问题时,我将告诉您Activiti如何以各种可能的方式最小化数据库访问,如何将流程结构分解为“执行树”,以便进行快速查询或如何利用十年的工作流框架开发知识。
您知道,尝试不回答问题就解决这个问题。 我们知道它很快,因为我们已经在此基础上建立了理论基础。 但是现在我们有了证明:实数……。 是的,这将是一个冗长的帖子。 但是请相信我,这值得您花时间!
免责声明:性能基准很难。 真的很难。 不同的机器,稍有不同的测试设置…很小的东西会严重改变结果。 此处的数字仅是为了证明Activiti引擎的开销非常小,同时还非常容易集成到Java生态系统中并提供BPMN 2.0流程执行。
Activiti基准项目
为了测试Activiti引擎的流程执行开销,我在github上创建了一个小项目: https : //github.com/jbarrez/activiti-benchmark
该项目目前包含9个测试过程,我们将在下面进行分析。 项目中的逻辑非常简单:
- 为每次测试运行创建一个流程引擎
- 每个进程都使用1到10个线程的线程池在此进程引擎上顺序执行。
- 将所有过程都放入一个包中,其中抽取了许多随机执行的内容。
- 收集所有结果,并生成带有一些漂亮图表HTML报告
要运行基准测试,只需按照github页上的说明来构建和执行jar。
基准结果
我用于测试结果的测试计算机是我的(相当旧的)台式计算机:AMD Phenom II X4 940 3.0Ghz,8 Gb 800Mhz RAM和运行Ubuntu 11.10的旧版本7200 rpm HD。 用于测试的数据库在运行测试的同一台计算机上运行。 因此请记住,在“真实”服务器环境中,结果甚至可能更好!
我上面提到的基准测试项目是在默认的Ubuntu MySQL 5数据库上执行的。 我只是切换到'large.cnf'设置(这会在数据库上抛出更多的RAM以及类似的东西),而不是默认配置。
- 每个测试过程使用一个从1个到10个线程的线程池运行2500次 。 用simpleton语言:仅使用一个线程执行2500个进程执行,使用两个线程执行2500个线程执行,使用三个线程执行2500个进程执行…是的,您知道了。
- 每次基准测试都是使用“默认” Activiti流程引擎完成的。 这基本上意味着以纯Java创建的“常规”独立Activiti引擎。 每个基准测试运行都在“ Spring”配置中完成。 在这里,流程引擎是通过将其包装在工厂bean中而构造的,数据源是Spring数据源,事务和连接池也由Spring管理(我实际上是使用经过调整的BoneCP线程池)
- 每次基准测试运行都是在默认历史记录级别(即“审核”)上使用历史记录,并且未启用历史记录(即历史记录级别“无”)时执行的 。
以下各节详细分析了这些过程,但是这里已经是测试运行的整体结果:
- Activiti 5.9 – MySQL –默认–历史记录已启用
- Activiti 5.9 – MySQL –默认–历史记录已禁用
- Activiti 5.9 – MySQL – Spring –历史记录已启用
- Activiti 5.9 – MySQL – Spring –历史记录已禁用
我使用Activiti的最新公共发行版Activiti 5.9运行了所有测试。 但是,我的测试运行为表面带来了一些潜在的性能修复(我还通过探查器运行了基准项目)。 很快就可以清楚地看到,大多数流程执行时间实际上是在流程结束时完成的。 基本上,触发更多的查询是不必要的,如果我们要在执行树中保存更多状态,则没有必要。 我与来自Camunda的 Daniel Meyer和我的同事Frederik Heremans坐在一起,他们已经设法为此进行了修复! 因此, 当前的Activiti 主干 (即Activiti 5.10-SNAPSHOT)比5.9快得多 。
- Activiti 5.10 – MySQL –默认–历史记录已启用
- Activiti 5.10 – MySQL –默认–历史记录已禁用
- Activiti 5.10 – MySQL – Spring –历史记录已启用
- Activiti 5.10 – MySQL – Spring –历史记录已禁用
从高级角度(向下滚动以进行详细分析),需要注意以下几点:
- 由于使用了更多的“专业”连接池,我期望默认配置和Spring配置之间会有一些差异。 但是,两种环境的结果都差不多。 有时默认值更快,有时是Spring。 很难真正找到一种模式。 因此,我在下面的详细分析中省略了Spring结果。
- 最佳的平均时间是使用四个线程执行进程时大多数时候找到的时间 。 这可能是由于拥有四核计算机。
- 当使用八个线程执行进程时,通常会找到最佳的吞吐量数字。 我只能假设这与拥有四核计算机有关。
- 当线程池中的线程数增加时,吞吐量(每秒执行的进程)增加,这两者均对平均时间产生负面影响。 当然,具有六个或七个以上的线程,您会非常清楚地看到这种效果。 这基本上意味着,虽然进程本身需要花费更长的时间来执行,但是由于有多个线程,您可以在相同的时间内执行更多这些“较慢”的进程。
- 启用历史记录确实会产生影响。 通常,启用历史记录会使执行时间加倍。 这是合乎逻辑的,因为当历史记录处于默认级别(即“审核”)时会插入许多额外的记录。
出于好奇,我进行了最后一个测试:在Oracle XE 11.2数据库上运行性能最佳的设置。 Oracle XE是“真实” Oracle数据库的免费版本。 无论多么努力,我都尝试过,无法在Ubuntu上正常运行。 因此,我在同一台计算机上使用了旧的Windows XP安装。 但是,操作系统是32位,这意味着系统仅具有3.2可用的8Gb RAM。 结果如下:
- Activiti 5.10 – Windows上的Oracle –默认–历史记录已禁用
结果不言而喻。 Oracle吹走了MySQL上的任何(单线程)结果 (它们已经非常快了!)。 但是,当使用多线程时,它比任何MySQL结果都要差得多。 我的猜测是,这些是由于XE版本的局限性 :仅使用一个CPU,仅使用1 GB RAM,等等。 我真的想在一个由Oracle托管的真实Oracle上运行这些测试, DBA…如果您有兴趣,请随时与我联系 !
在下一部分中,我们将详细研究每个测试过程的性能数字。 可以自行下载包含以下所有数字和图表的Excel工作表。
流程1:裸手(一笔交易)
第一个过程至少在业务方面不是一个非常有趣的过程。 开始该过程后,立即结束。 它本身不是很有用,但是它的数目使我们了解到一件重要的事情:Activiti引擎的光秃秃的开销。 以下是平均时间:
此过程在单个事务中运行,这意味着由于Activiti的优化禁用了历史记录后,没有任何内容保存到数据库中。 启用历史记录后,您基本上将获得在历史流程实例表中插入一行的成本,这大约是4.44毫秒。 同样很明显,我们对Activiti 5.10的修复在这里产生了巨大的影响。 在以前的版本中,有99%的时间用于流程的清理检查。 在这里查看最佳结果:使用4个线程执行2500次此过程时,为0.47 ms 。 仅半毫秒 ! 可以说Activiti引擎的开销非常小。
吞吐量数字同样令人印象深刻:
在最好的情况下,将执行8741个进程。 每秒。 当您到达此处阅读帖子时,您可能已经执行了数百万个过程 。 您还可以看到这里的4或8个线程之间几乎没有什么区别。 这里大多数执行时间是cpu时间,在这里不会发生诸如等待数据库锁定之类的潜在冲突。
在这些数字中,您还可以轻松地看到Oracle XE不能很好地在多个线程之间进行扩展(如上所述)。 在以下结果中,您将看到相同的行为。
流程2:相同,但时间更长(一笔交易)
此过程与上一个过程非常相似。 我们又只有一笔交易。 该过程开始后,我们将进行七个无操作通过活动,直到结束。
这里要注意一些事情:
- 最好的结果(同样是4个线程,禁用了历史记录)实际上比以前的简单过程更好。 但也请注意,单线程执行速度要慢一些。 这意味着该过程本身会比较慢,这是合理的,因为有更多活动。 但是,在进程中使用更多的线程并进行更多的活动确实允许更多潜在的交错。 在前一种情况下,该线程几乎没有诞生,然后再次被杀死。
- 启用/禁用历史记录之间的差异大于以前的过程。 这是合乎逻辑的,因为这里记录了更多的历史记录(对于每个活动,数据库中都有一条记录)。
- 同样,Activiti 5.10远远优于Activiti 5.9。
吞吐量数字遵循以下观察结果:在这里有更多的机会使用线程。 最好的结果徘徊在每秒12000个进程执行左右 。 再次,它演示了Activiti引擎的轻量级执行。
流程3:一次交易中的并行性
此过程执行一个派生的并行网关,而一个并行网关加入同一事务。 您会期望获得与先前结果类似的结果,但是您会感到惊讶:
将这些数字与上一个过程进行比较,您会发现执行速度较慢。 那么,即使活动较少,为什么此过程也会变慢? 原因在于并行网关的实现方式,尤其是联接行为。 在实现方面,最困难的部分是您需要应对多个执行到达联接时的情况。 为了确保行为是原子的,我们在内部进行一些锁定,并在执行树中获取所有子执行,以查明联接是否激活。 因此,与“常规”活动相比,这是一个“昂贵”的操作。
请注意, 我们在这里只谈论5毫秒的单线程和3.59毫秒的MySQL最佳情况 。 给定实现并行网关功能所需的功能,如果您要问我的话,这是花生。
吞吐量数字:
这是第一个实际上包含一些“逻辑”的过程。 在上述最佳情况下,这意味着可以在一秒钟内执行1112个过程。 如果您要问我,真是令人印象深刻! 。
流程4:现在我们到达某个地方(一笔交易)
在对真实的业务流程进行建模时,您已经看到了该流程。 但是,由于所有活动都是自动传递,因此我们仍在一个数据库事务中运行它。 在这里,我们还有两个分叉和两个联接。
看一下最低的数字:使用一个线程运行时,在Oracle上为6.88毫秒 。 考虑到这里发生的所有事情,这真是太快了。 此处的历史记录数量至少翻了一番(Activiti 5.10),这是有道理的,因为此处有大量活动审核日志记录。 您还可以看到,这导致此处四个线程的平均时间更长,这可能是由于联接的实现所致。 如果您对Activiti内部有一点了解,那么您将了解这意味着执行树中有很多执行。 我们有一个较大的并发根,但也有多个子级,有时它们也是并发根。
但是,尽管平均时间增加了,但吞吐量无疑会受益:
使用八个线程运行此过程,使您可以在一秒钟内执行411次此过程。
这里还有一些奇特的地方:Oracle数据库在执行更多线程并发时表现更好。 这与所有其他度量完全相反,在所有其他度量中,Oracle在该环境中总是较慢(请参见上面的说明)。 我认为这与我们在派生/加入时应用的内部锁定和强制更新有关,Oracle似乎可以更好地处理它。
流程5:添加一些Java逻辑(单个事务)
我添加了此过程,以了解在过程中添加Java服务任务的影响。 在此过程中,第一个活动生成一个随机值,将其存储为过程变量,然后根据该随机值在过程中上升或下降。 上升或下降的机会约为50/50。
平均时间非常好。 实际上,结果与上述过程1和2相同(没有活动或仅具有自动传递)。 这意味着将Java逻辑集成到您的进程中的开销几乎不存在 (当然免费是免费的)。 当然,您仍然可以使用该逻辑编写慢速代码,但是您不能为此而怪罪Activiti引擎
吞吐量数字与过程1和2相当:非常非常高。 最好的情况是每秒执行9000个以上的进程 。 这确实也意味着您自己的Java逻辑有9000次调用。
P rocess 6,7和8:加入等待状态和交易
先前的过程向我们展示了Activiti引擎的全部开销。 在这里,我们将研究等待状态和多个事务如何影响性能。 为此,我添加了三个包含用户任务的测试过程。 对于每个用户任务,引擎将提交当前事务并将线程返回给客户端。 由于结果与这些流程几乎完全兼容,因此我们将其分组。 这些过程是:
按上述过程的顺序,这是平均计时结果。 对于第一个过程,仅包含一个用户任务:
显然,具有等待状态和多个事务确实会对性能产生影响。 这也是合乎逻辑的:之前,引擎可以通过不将运行时状态插入数据库来进行优化,因为该过程是在一个事务中完成的。 现在,整个状态(即指向您当前位置的指针)需要保存到数据库中。 这样的过程可能会像这样“沉睡”很多天,几个月,几年……。 Activiti引擎现在不再将其保存在内存中,它可以释放出来以完全关注其他进程。
如果仅用一个用户任务检查过程的结果,就会发现在最佳情况下(Oracle,单线程– MySQL上的4个线程非常接近),此操作在6.27毫秒内完成。 这确实非常快,如果考虑到我们在这里进行了一些插入(执行树,任务),一些更新(执行树)和删除(清理)。
这里的第二个过程包含7个用户任务:
第二张图表告诉我们,从逻辑上讲,更多事务意味着更多时间。 最好的情况是,此过程在32.12 ms内完成。 这是针对七个事务的,每个事务给出4.6毫秒。 因此很明显,添加等待状态时,平均时间以线性方式缩放。 这当然是有道理的,因为交易不是免费的。
另请注意,启用历史记录确实会在此处增加一些开销。 这是由于将历史记录级别设置为“审核”,从而将所有用户任务信息存储在历史记录表中。 从禁用历史记录的Activiti 5.9和启用历史记录的Activiti 5.10之间的区别也可以注意到这一点:这是极少数情况,其中启用历史记录的Activiti 5.10比禁用历史记录的5.9慢。 但是考虑到这里存储的历史记录数量,这是合乎逻辑的。
第三个过程向我们学习了用户任务和并行网关如何交互:
第三张图告诉我们不多的新知识。 现在,我们有两个用户任务,以及更“昂贵”的fork / join(请参见上文)。 平均时间就是我们期望的时间。
吞吐量图表与您期望的平均时间一致。 每秒70至250个进程。 w!
为了节省空间,您需要单击它们以将其放大:
流程9:那么范围呢?
对于最后一个过程,我们将研究“范围”。 “范围”是我们内部在引擎中的调用方式,它与可变的可见性,指示进程状态的指针之间的关系,事件捕获等有关。BPMN2.0在这些范围内有很多情况,例如嵌入式子流程,如此处的流程所示。 基本上,每个子流程都可以具有边界事件(捕获错误,消息等),这些边界事件仅在其作用域处于活动状态时才应用于其内部活动。 无需过多讨论技术细节:要以正确的方式实现范围,您需要一些不太琐碎的逻辑。
这里的示例流程具有4个子流程,彼此嵌套。 内部过程使用并发性,这对于Activiti引擎本身也是一个作用域。 这里还有两个用户任务,因此意味着两个事务。 因此,让我们看一下它的性能:
您可以清楚地看到Activiti 5.9和5.10之间的巨大差异。 范围的确是一个领域,最后,围绕“流程清理”的修复程序具有巨大的优势,因为创建了许多执行对象并将其持久化以表示许多不同的范围。 在Activiti 5.9上,单线程性能不是很好。 幸运的是,从蓝色和红色条之间的间隙中可以看到,这些作用域确实允许高并发性。
Oracle的数量加上5.10测试的多线程结果,确实证明了引擎现在可以有效地处理范围。 吞吐量图表证明,该进程可以通过更多线程很好地进行扩展,正如您所看到的,倒数第二个块中的红线和绿线之间存在较大差距。 在最佳情况下,引擎会处理此更为复杂的过程中的64个过程。
随机执行
如果您已经单击了帖子开头的完整报告,则可能已经注意到还针对每种环境都对随机执行进行了测试。 在此设置中,完成了2500个流程执行,并且两个流程都是随机选择的。 如这些报告所示,这意味着超过2500次执行,每个进程执行的次数几乎相同(正态分布)。
最后一张图表显示了最佳设置(Activiti 5.10,禁用了历史记录)以及添加更多线程时这些随机进程执行的吞吐量如何:
正如我们在上面的许多测试中所看到的那样,一旦通过了四个线程,事情就不会改变太多了。 数字(每秒167个进程)证明在实际情况下(即,多个进程同时执行),Activiti引擎可以很好地扩展。 结论
平均时序图清楚地显示了两件事:
- Activiti引擎速度快,开销最小 !
- 启用或禁用历史记录之间的区别绝对明显。 有时甚至会减少一半的时间。 所有历史记录测试都是使用“审核”级别完成的,但是有一个更简单的历史记录级别(“活动”),可能对用例而言足够好。 Activiti在历史记录配置方面非常灵活,您可以为每个进程专门调整历史记录级别。 因此,请考虑一下您的流程需要具备的级别,如果它完全需要历史记录的话 !
吞吐量图表证明,当有更多线程可用时(即,任何现代应用程序服务器),引擎可以很好地扩展。 Activiti经过精心设计,可用于高吞吐量和可用性(集群)架构 。
正如我在引言中所说,数字就是它们:仅仅是数字。 我要在这里总结的主要观点是Activiti引擎非常轻巧。 使用Activiti自动化业务流程的开销很小。 通常, 如果您需要使业务流程或工作流自动化,则希望与任何Java系统进行一流的集成,并且您希望所有这些都快速且可扩展……所有这些都无需进一步!
参考:来自JCG合作伙伴 Joram Barrez 的Activiti Performance Showdown活动,来自“ 小脚走路”博客。
翻译自: https://www.javacodegeeks.com/2012/07/activiti-performance-showdown.html