拒绝代码臃肿,这套计算引擎设计方法值得一看!
共 5122字,需浏览 11分钟
·
2021-08-19 08:24
导语 | 在庞大的数据系统中,往往会有大量的计算需求。传统的方式便是直接在代码写各种计算逻辑判断,这导致了代码非常臃肿,计算维护的成本变大。所以想着编写一套DSL,定义专用的语法去实现对数据的计算,并将其独立成为底层基础服务。
一、DSL 设计
(一)何为 DSL
领域特定语言(英语:domain-specific language、DSL)指的是专注于某个应用程序领域的计算机语言。不同于普通的跨领域通用计算机语言(GPL),领域特定语言只用在某些特定的领域。
简单来说,就是利用DSL,通过抽象构建模型,抽取公共的代码,以达到提高开发效率,减少重复的劳动的目的,比如经常使用的SQL。
同样的思路,我们要将复杂的逻辑判断与计算规则抽象化,构建计算DSL。
(二)如何通用化设计计算 DSL
值得庆幸的是,办公中经常使用的Excel就包含了许多计算规则。
让我直接举一个例子来说明,比如:要计算实际支出超出预算的金额,由于超出金额不可能为负数,所以逻辑条件为:如果实际支出大于预算,则结果为实际支出减预算,反之则取0。对应的Excel计算公式为:
IF (C2 > B2, IMSUB (C2, B2), 0)(C2 代表实际支出,B2 代表预算,IMSUB 代表减法)
有了一个这么专业的例子,那么对应我们的计算DSL就是:
IF ({budget} > {actual_expenses}, IMSUB ({budget}, {actual_expenses}), 0)
({}用于标示具体字段,budget、actual_expenses 代表数据库中对应的预算、实际支出字段)
(三)DSL 设计的优势
与Excel计算规则相似,减少用户学习成本。
按照专业的规则来定义,使计算DSL更规范。
由于规范的设计,更有利于后期扩展。
二、计算引擎的实现
(一)DSL 解析
对于这种有关键字并且无限嵌套的DSL,应该没有比堆栈更合适的方法来解析了。下面是具体例子的部分解析代码:
$dsl = 'IF({budget}>={actual_expenses},IMSUB({budget},{actual_expenses}),0,1)';
$stack = []; // 堆栈
$result = []; // 结果
$comparisonOperators = ['<', '>', '&', '|', '=']; // 比较运算符
$placeholders = [',', '(', ')']; // 占位符
for ($index = 0; $index < strlen($dsl); $index++) {
$key = $dsl[$index];
switch ($key) {
// 解析变量
case '}' :
$variable = '';
while (true) {
$item = array_pop($stack); // 出栈
if ($item === '{') {
break;
}
$variable = $item . $variable;
}
$result[] = $this->getVariable($variable); // 获取真实变量值
break;
// 解析方法
case '(' :
$method = '';
while (true) {
$item = array_pop($stack); // 出栈
if (is_null($item)) {
break;
}
$method = $item . $method;
}
$result[] = $method;
$result[] = $key;
break;
// 存储占位符,清空栈内变量(常量)
case in_array($key, $placeholders) :
$variable = '';
while (true) {
$item = array_pop($stack); // 出栈
if (is_null($item)) {
break;
}
$variable = $item . $variable;
}
$variable != '' && $result[] = $variable;
$result[] = $key;
break;
// 解析比较运算符
case in_array($key, $comparisonOperators) :
if ($dsl[$index + 1] == '=') { // 兼容 >=、<=
$result[] = $key . '=';
$index++;
} else {
$result[] = $key;
}
break;
// 入栈
default :
$stack[] = $key;
break;
}
}
(二)数据结构化
通过DSL解析可以得到“未赋值”的结构,再根据预先存储的数据模型对变量进行赋值,我们便可以得到如下结构:
这样一来,DSL就变成了机器所能识别的数据,将参数带入到指定的函数中便能得到计算结果。
(三)递归计算
从上图的结构中,我们可以分析出:每一个计算都包含了计算函数、占位符(开始符、分割符、结束符)以及函数对应的多个参数。其中参数可以是比较运算(IF函数第一个参数必为比较运算),也可以是另一个函数。这时候我们只需要使用递归的方式去不断往下运算便能得出结果。
/**
* IF 函数核心计算逻辑
*/
public function calculate()
{
// 计算比较结果
if ($this->getComparativeResult()) {
return $this->getResult($this->params[1]); // 返回真
} else {
return $this->getResult($this->params[2]); // 返回假
}
}
/**
* 获取计算结果
*/
public function getResult($params)
{
// 如果是函数,则继续计算
if (is_array($params)) {
return (new Calculate($params))->calculate(); // 递归计算
}
// 非函数,直接返回结果
return $params;
}
(四)架构梳理
首先对输入的DSL进行校验、解析并结构化数据;然后启动多个计算引擎同时并行处理;最终输出计算结果。
三、项目接入
(一)架构设计
整体架构分为五层,上层应用层提供给具体应用接入;通讯层负责对接收应用层的数据,及对支持应用层轮询获取计算结果;DSL解析层负责DSL校验、DSL解析以及数据结构化;处理完之后再到核心计算层,进行具体的计算执行;最后再将结果入库并将结果发送到消息队列中。
其中,DSL 解析层和核心计算层共同组成计算引擎。
四、问题与思考
(一)计算提升效率缓慢
在完成项目接入后,为了提升计算效率,采用并行执行的方式来执行计算。期望的效果便是:随着并行的数量增加,效率也随之增加。
但事实总是事与愿违,即使扩大计算的并行数量也无法成倍提升计算效率,并且当并行数达到一定量时,效率提升越不明显。
(二)计算依赖
在经过仔细的问题排查之后,发现数据计算之间是有依赖关系的。让我们直接看下图的例子:当同时计算A、B、C三个字段时,不管如何并行执行,B的计算永远依赖A计算的结果;同理,C的计算也永远依赖A和B的计算结果。总而言之,就是说计算效率是有瓶颈的。
那么,如何能够用最少的资源达到整体计算的最佳效率呢?
五、解决方案:寻找最优解
(一)策略优先算法
对于每个计算字段来说,我们是知道具体依赖的程度的:
对于A、D,只依赖常数,所以他们依赖程度为0。
对于B、E,分别依赖A、B,那么他们的依赖应该分别在A、B的基础上+1,所以他们的依赖程度为1。
对于C,同时依赖A、B,那么他的依赖程度应该为A+B+1=2。
所以,我们将每个字段排了优先级,对于同一优先级的字段并行计算,依次进行,便能以最少的资源达到整体计算的最佳效率。
(二)计算速度不一致
在实际的计算中,每个字段计算的速度是不一样的。比如:在第一优先级中的A需要不断的累加才能得出结果,需要比同一优先级的D花费更长的时间。假如此时D已经计算完成,那么E其实已经不需要再依赖其他计算了,应该立即被执行。但由于第一优先级还未算完,所以只能继续等待。这样一来,对于计算结果的反馈非常的不友好。
(三)更进一步:动态策略优先算法
为了能快速的响应计算结果,我们需要在计算的同时,对计算完成的字段触发完成事件,对依赖该字段的其他优先级字段,重新分配优先级,当获得第一优先级时,立即执行。
比如:在D计算完成后,去修改E的优先级,因为E只依赖D,而D已经计算完成,所以应该获得第一优先级,立即执行。
六、总结
(一)架构完善
在动态策略优先算法的思路下,我们在原先的结构中引入策略分配层。在DSL解析之后将数据传入到策略分配层中进行策略计算;然后,依次对各个优先级的字段进行计算任务调度;在计算完成后对事件进行处理,再依次进行任务调度;最终在完成整个计算后将数据入库。
其中,DSL解析层、策略分配层和核心计算层共同组成计算引擎。
(二)下一步:引入监控
在完成了一系列的开发与项目接入工作之后。对于整个底层计算服务来说,并不是已经无懈可击了。
在DSL的解析中需要实时监控解析结果,及时对错误进行拦截与记录,避免影响下层计算。
在策略分配层中,也需要对每一次的策略计算、任务调度、事件处理进行监控,因为每一次错误都将影响整个模型的计算结果。
在最终入库之前,还需要监控每个字段的计算结果是否符合预期。及时对错误结果进行修正。
作者简介
林楨淵
腾讯 CDC 团队应用开发工程师
腾讯 CDC 团队应用开发工程师,毕业于广东工业大学,负责腾讯投资决策信息平台开发。致力于低代码开发平台(包括流程引擎、表单配置、计算引擎等等)的架构设计与持续优化。在投资领域开发有着丰富的落地经验。
推荐阅读
程序员如何把你关注的内容推送到你眼前?揭秘信息流推荐背后的系统设计
在Exception的影响下,如何才能写出更高质量的C++代码?