拒绝代码臃肿,这套计算引擎设计方法值得一看!

Posted QcloudCommunity

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了拒绝代码臃肿,这套计算引擎设计方法值得一看!相关的知识,希望对你有一定的参考价值。

导语 | 在庞大的数据系统中,往往会有大量的计算需求。传统的方式便是直接在代码写各种计算逻辑判断,这导致了代码非常臃肿,计算维护的成本变大。所以想着编写一套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++代码?

自动的内存管理系统实操手册——Java和Golang对比篇


以上是关于拒绝代码臃肿,这套计算引擎设计方法值得一看!的主要内容,如果未能解决你的问题,请参考以下文章

这几本值得你一看再看的程序员素养必备好书

简易的站内搜索引擎 (万字长文!!绝对值得一看!!)

简易的站内搜索引擎 (万字长文!!绝对值得一看!!)

简易的站内搜索引擎 (万字长文!!绝对值得一看!!)

简易的站内搜索引擎 (万字长文!!绝对值得一看!!)

值得一看,响应式网页设计的9条基本原则!