「低」代码有「高」性能:揭秘 Mendix 高性能优化方案!

文摘   2024-08-06 10:00   北京  

背景

最近,我们为一个客户开发的大型企业级 OA 应用进行了性能测试和优化,该应用是基于 Mendix 平台构建的,准备上线。

本应用面向整个企业,主要提供流程审批功能,每年约有 100 万次审批提交。共有 8000 多名员工使用该 OA 系统,在高峰期(上午 9:00 到 10:00)同时在线的用户数超过 800 个。

在进行性能优化之前,系统在 20 个并发用户的情况下就出现了无法承受的情况。经过优化后,该系统能够轻松应对 800 个并发用户。以下是优化前后一些关键指标的对比。

My Task 页面性能优化前后对比

从上述数据可以看出,优化显著提升了系统的性能。接下来,我们将具体分析优化过程是如何进行的。

该优化过程主要分为三个级别:

  1. 第一级,减少用户请求数量。在这一阶段,我们主要优化前后端接口通信逻辑,去除不必要的请求,减少前端对后端服务器的网络请求数。

  2. 第二级,减少数据库请求数量。在这一阶段,我们减少应用服务器向数据库服务器的请求次数。主要措施是优化应用服务器发送的 SQL 语句,将原本一次只请求一条记录的 SQL 语句改为一次请求多条数据记录的 SQL 语句。

  3. 第三级,减少数据库单次查询时间。在这一阶段,我们不再对应用逻辑进行优化,而是在确保返回结果一致的前提下,优化 SQL 查询语句,以减轻数据库的查询压力。例如,建立适当的索引并优化查询条件。

下文将详细阐述以上优化过程。

第一级:减少用户请求数量

在这一阶段,我们主要优化前后端接口通信逻辑,去除不必要的请求,减少前端对后端服务器的网络请求数。如下图所示,当用户登录成功进入首页之后,会进入My Task列表。在这个列表中,每一行代表一个待处理的审批单(Form),而每一个审批单又会关联多个历史审批记录(ApproveRecord)。

首页 My Task 页面截图

如下图所示,我们发现浏览器向服务器发送的请求就多达 21 次。

首页请求很多,加载时间很长

如果采用标准的低代码写法,Mendix 会首先通过一个请求获取整个列表,然后每一行数据再单独发一次请求以获取关联的数据。这样,总请求次数为 1+N,其中 N 是每页的数据数量。

为了减少浏览器向服务器发送的请求数量,我们可以利用 Mendix 中 Non-persistable Entity 的特性,将主表数据和关联数据在一个 HTTP 请求中返回。

首先,我们需要建立两个 Non-persistable Entity,如下图所示。请注意,这里的关联(Association)的 Owner 应该是表单实体(Form),而不是审批记录实体(ApproveRecord)。这样,当我们返回一个表单列表,并且这些表单列表与相应的审批人进行了关联时,Mendix 应用会将表单列表和关联的审批人列表一起返回给浏览器。这种方法能够有效减少请求次数,从而提高应用程序的性能和响应速度。

新的实体关系图

有了这两个实体之后,我们需要将 Data Grid 的数据源(datasource)设置为一个微流(Microflow)。在这个微流(Microflow)中,我们根据搜索条件从数据库中请求相应的数据,并将这些数据写入到之前创建的两个临时实体对象中,然后返回表单列表。这个过程简化示意如下:

改造后的 datasource 微流

通过上述操作,我们成功将打开My Task页面的请求数量从 21 个减少到 1 个,总花费时间也从过去的 5000ms 降至 300ms。这种优化显著提升了应用的性能和响应速度,表明了合理使用微流和非持久实体的有效性。

第二级:减少数据库请求数量

在这个级别,我们减少应用服务器向数据库服务器的请求数量。主要措施是优化应用服务器向数据库服务器发送的 SQL 语句,把一次只请求一条记录的 SQL 语句改为一次请求多条数据记录的语句。

在前文中,我们了解到这个业务主要处理两个实体:一个是审批表单实体(Form),另一个是审批记录实体(Approve Record)。我们需要处理的业务是返回当前用户待处理的所有审批表单(Form),并将这些表单的历史审批记录(ApproveRecord)一并返回给用户。

如果采用标准的低代码方式,其微流(Microflow)的逻辑简单示意如下:

标准的低代码请求方式

我们首先从数据库中取出当前用户的待处理审批列表,然后循环遍历该审批列表。对于每一个审批表单(Form),再去数据库请求该表单的历史审批记录(ApproveRecord)。假设当前用户的待处理审批列表每页有 20 条记录,那么这个循环就会执行 20 次,请求数据库的数量就达到 20 次。这些请求给服务器带来了额外的负担。

有没有方法可以通过一次数据库请求,就把上述数据全部返回给应用服务器呢?这就需要用到 Mendix 的高阶技巧:OQL

OQL 允许我们像使用 SQL 语句一样查询数据库。该查询的返回值和搜索条件都可以由我们自由定义,并且支持各种 JOIN 和关联查询。

通过使用 OQL,我们将从应用服务器向数据库服务器请求的数量从 21 个减少到 2 个。

如果你想了解更多关于 OQL 的用法,请参考以下链接:

  1. https://docs.mendix.com/refguide/oql/

  2. https://marketplace.mendix.com/link/component/66876

第三级:减少数据库单次查询时间

在这一阶段,我们不再对应用逻辑进行优化,而是侧重于在确保结果一致的前提下,优化 SQL 查询语句,以减少对数据库的查询压力。比如,可以通过建立合适的索引和优化查询条件来实现。

在该应用的首页,用户可以输入关键字来查询该用户可访问的所有审批表单。其界面示意图如下:

搜索审批表单页面

当应用接收到用户输入的搜索关键字后,需要搜索每一个审批单(Form)的标题(Subject),并将含有该关键字的审批单(Form)返回给用户。这里的搜索需要进行部分匹配,即常见的双向模糊匹配搜索。SQL 语句中的搜索条件类似这样:WHERE Subject LIKE "%keyword%"。然而,传统的关系型数据库对于双向模糊匹配搜索并不擅长,无法利用索引,因此必须对每一条记录进行模糊匹配计算才能得出最终答案。当审批表有一百万条数据时,性能将显著下降。

在这种情况下,我们通常有三种解决方案:

  1. 改需求

  2. 加条件

  3. 加服务

这三种方法的详细说明如下:

方案一:改需求

与需求方讨论,看看是否可以将需求改为前缀匹配搜索,这样数据库就能使用索引,从而提高查询效率。

方案二:加条件

在查询语句中增加更多的筛选条件,并对这些条件相关的属性建立索引。这样,数据库可以先利用索引排除大部分数据,再执行文本模糊搜索,从而减少需要进行模糊搜索的行数。在本项目中,我们增加了AND CreateAt < {2 weeks before}这样的条件,大大减少了文本模糊搜索的行数。

减少数据库请求数量
方案三:加服务

为应用增加独立的文本搜索服务,例如 Elastic Search。让文本搜索服务处理关系型数据库不擅长的文本搜索操作,从而减轻关系型数据库的压力。

增加额外的文本搜索服务

更多

通过上述优化,我们显著提高了 My Task 页面和 Search 页面的搜索效率。

My Task 页面性能优化前后对比

然而,这些优化方法仅适用于单表百万行、并发用户在千级别的应用。如果单表数据达到千万级,并发用户上万,或者需要更强大的文本搜索能力,那么就很难通过应用优化来满足需求,这时需要从架构层面进行优化。

总结

从上述过程我们可以看出,当我们的 Mendix 应用比较简单时,我们可以直接使用标准的低代码开发方式,这将在 90% 以上的情况下可以满足要求。

当我们的应用有超过 500 的并发用户数,或者单表数据超过百万条时,标准的低代码开发方式可能无法满足我们的性能要求。在这种情况下,我们可以采用本文提到的方法,对关键性能瓶颈进行针对性优化,以达到传统高代码同样的性能。根据我的经验,一个大型应用中,存在性能瓶颈的代码通常不会超过总代码量的 3%。也就是说,我们只需针对这 3% 的代码进行上述深度的优化,即可满足我们的目标!

这种优化策略确保我们在有限的时间和资源内,能够有效地提升应用的整体性能,而不必重构整个代码库。这不仅提高了开发效率,也为团队节省了大量的开发和维护成本。在后续的工作中,我们可以继续监测应用的性能,并根据反馈进行持续的优化和调整,以保持应用在高并发和大数据量场景下的稳定性和响应速度。



关于作者:


您好,朋友。我现就职于西门子工业软件,担任高级咨询顾问。成功领导 10 多个世界 500 强企业的数字化转型项目,跨越政府、零售、金融、汽车制造、生物医药等多个行业,创造巨大商业价值。如有任何与「数字化转型」有关的问题,欢迎用以下方式与我交流:

  1. 个人微信「zjh1943」。添加时请注明姓名、行业、交流的问题。

  2. 知乎专栏:https://www.zhihu.com/people/zhu-jin-heng 。

  3. LinkedIn:https://www.linkedin.com/in/jinheng-zhu/ 。


 最新文章