查看原文
其他

配置化系统中的图模型

The following article is from 薯条的编程修养 Author 程序员薯条

读完本文的朋友可以根据自己组内的系统抽象个图demo,没准下个升p7的就是你~

目录:

  • 好处

    • 1. 通过引擎可以进行配置化编程

    • 2. 图节点职责单一,复用性极强

    • 3. 图的执行逻辑比较直观

  • 一些小细节

    • 图的节点问题

    • 同步与异步

    • 遍历方式

    • 减枝问题

    • 参数的并发安全问题

    • 节点自依赖

    • 图的内部查询语言

  • 现实很残酷

    • 上线问题

    • 展示问题

    • 接受度问题

    • 业务的复杂度问题

    • 参考


To C端的业务系统发展流程一般是:MVP版本快速上线验证猜想,然后大多数版本到这里就半死不活不再迭代了,少数效果不错的业务会继续迭代下去。在这个过程中,运营和PM的核心诉求是:研发团队可以快速实现功能,最好当天提需求当天实现,需求后续若有迭代系统还能进行灵活修改。

而研发团队接到这种有后续迭代的需求时,开发流程一般是:crud -> 更复杂的crud -> 继续不断的crud。一段时间过后,自己堆的屎终于把自己恶心到了,此时迭代效率严重降低,跟不上业务的步伐了。此时解法有两种:

  1. 找大老板申请HC,堆人继续crud,只要人够多,业务就跟不上我crud的步伐,只是代码可能会看不懂~
  2. 经历了足够多的curd后,逐渐摸索到了相关业务的套路,于是在一个乌漆墨黑的夜晚,几个研发歃血为盟,重构系统。

我们这里不谈论第一种解法,只分析第二种解法。

业务的理想与研发的现实为啥会有gap呢,这里要从研发周期开始说起,研发周期一般会包含:写代码、写自测,QA测试、上线等流程。在微服务盛行的当下,业务迭代还会跨团队合作,一旦跨团队,沟通成本就会迅速提高,整个流程是很耗时间。

重构系统解决不了跨团队合作的沟通成本问题,但是如果可以压缩自己团队的开发周期,那项目单点迭代速度必然加快。

于是开始重构系统,重构之前,业务系统中有一堆乱七八糟的函数,有的可能有10行,有的可能有100行,复用性根据函数的长度而降低,一个新需求并不会改变整体业务流程,而是会在某个地方增加一些新特性,于是研发就在对应函数里继续写新代码去实现业务。所以我们可以把系统视为:业务 = 一套流程,套用了不同特性

对于不同特性,本质上就是各种函数,重构时可以把原来的函数用统一的接口抽象一下形成一个lib库,里面有各种业务相关的UDF。

而一套流程就是一种数据结构,这种数据结构的特点是,灵活性强,且具备很强的流程表达能力。这个数据结构负责串联各种运算单元,形成最小可用实例去解决实际的业务问题。说到具有流程表达能力数据结构,反应快的朋友可能一下子想到了链表这种数据结构:

链表这种数据可以按照节点先后顺序遍历,非常适合 一条路走到黑 的流程系统,比如CI/CD系统的那条配置化的流水线。

但链表这种数据结构不是最高效的,有时会出现一个节点同时依赖多个上游,但多个上游相互不依赖。这里其实是可以并行获取多个上游的信息的。此时这个数据结构变成了树:

再进一步,树中的某个节点可能被多个节点同时依赖,如果还用两个节点表示会存在冗余计算,这里直接抽取公共依赖节点即可,于是树模型完成了闭环,变成了一个有向图: )

到这里,我们完成了系统的闭环,懂行的朋友都知道,一旦闭环,必然会诞生各种抓手抓各种你想要的东西,那么这个有向图能带来什么好处呢?

好处

1. 通过引擎可以进行配置化编程

我们有了一套闭环的数据结构作为模型指导,那么重构后的系统就可以变成配置化系统。这套系统的工作模式是:

  1. 新需求到来时,研发拆解需求写对应的配置
  2. 系统读取配置,渲染出一个图模型
  3. 系统遍历图模型,计算结果

这里可能抽象,可以联想一下MySQL,MySQL的数据存到表里之后,我们可以通过SQL获取,SQL描述了我们希望获取到的数据的逻辑,我们把SQL提交给MySQL,引擎层经过分析之后去下层数据层获取对应的数据。

系统很多时候会用静态语言去进行开发,我们组用的是Golang。对于这种静态语言想获得动态能力有两种方法:

  1. 读取配置文件,执行逻辑
  2. 用plugin搞插件化能力

第二种方法有很多不现实的地方,一般用第一种解法解决实际问题,这里的配置文件可以放在像MySQL这样的关系型数据库中。

2. 图节点职责单一,复用性极强

节点即operator,节点与节点之间完全解耦,几乎没有相关性,单个节点承担单一职责。每个组承接的业务是相似的,当我们把业务按照执行流程拆解成若干个节点后,下一个业务到来时,如果原有节点组合可以满足业务,那只需要配置节点即可,全程不用写代码;若原节点的功能不能承接业务,那可以基于业务在原来的节点上加代码丰富功能;若业务需求没有类似功能的节点,则新增节点承接业务。

找节点,搞配置 -> 增加节点功能,搞配置 -> 加节点,搞配置模式的循环往复下,我们的operator越来越多,operator内部的功能也越来越完善,最终可能会达到:业务不需要开发代码,只需要写配置即可上线的蓝图。

3. 图的执行逻辑比较直观

想象一下,如果我们代表业务的图模型渲染到平台上,用户就可以通过页面去了解这个业务的具体实现过程了:数据从哪里来,经过哪个节点经过了怎样的处理,都可以直观的看到,有了这种平台,可以把系统的能力对外开放。

实际上图模型在工作中非常常见,比如Golang中代码库之间的依赖,flink中节点拓扑图,spark中的执行计划,BPMN系统中的审批流程等。

如果你对这种理论感兴趣,可以继续往下看,下面继续介绍一些图的细节问题:

一些小细节

图的节点问题

在渲染一张图时,可以用所谓的邻接表法去存储,比如我们可以基于下面的依赖管理渲染出这个图:

旧的配置

这里我们对外的图节点是A,我们假设它叫instance实例。这里出现了一个有趣的场景,节点C同样满足很多业务,具有业务价值,可以暴露给业务方调用。

假设是先有C实例,再有A实例,那我们就没有必要在A实例中再配置一边C实例的实现了。那么在系统中让节点既可以依赖单纯,对内的operator,又可以依赖具有业务含义,对外的instance,就可以进一步减少研发同学的配置工作量了。

新的配置

同步与异步

如果这套模型对应实时系统,那需要在用户发送get请求时实时返回结果,我们可能只需要渲染出图模型后再用循环or递归的方式遍历图中的节点获取结果即可;但是有的系统是非实时的,这些系统每个节点的计算量巨大,没办法立刻产出结果,甚至需要map-reduce方式把任务下发到不同的主机上去执行。此时我们的引擎中需要增加一个监听器模块,监听器的功能是 用定期or定时的方式 监听节点的执行进度,执行进度信息可以放在DB中进行管理,这样服务层无状态,DB层统一管理状态,即使某些时候由于机器挂了导致任务执行过程中失败,也可以利用DB中的状态进行断点重传。

遍历方式

图的遍历方式可以有bfs、dfs、和拓扑顺序执行,这里dfs显然不能满足业务需求,下面来对比一下bfs和拓扑序执行的优劣。

bfs遍历

bfs即宽度优先遍历,就是按照节点的依赖层级去生成与遍历这个图,写过算法题的朋友都知道宽搜一般需要借助一个队列。构建图时要从上往下构建,而遍历图时要从下往上遍历求值。

比如下面这个例子:

顶部节点是A,A依赖于B、C节点的值,而C又依赖B、D节点的值,构建图时第一层为A,接着把B、C的值放入队列,层级为二层,接着遍历B的依赖,发现没有,继续遍历C的依赖,发现C依赖B、D,于是把B、D节点放在第三层,继续遍历B节点和D节点的依赖,均没有依赖,于是构造结束。

由于他们之前是强依赖的,所以遍历时只能自下往上遍历,先求出B、D节点的value,然后是求C节点,获取到A节点所有依赖后,最后求A节点的值,返回给用户。

拓扑排序

拓扑排序即根据节点之间的依赖关系构造出一个全局有序的序列,比如上面例子的一种拓扑排序可以是:B->D->C->A,这个序列有时并不是唯一的,刚刚的例子的拓扑序也可以是:D->B->C->A。但拓扑有一点问题是只能遍历有向无环图DAG,真实业务千奇百怪,在我们系统的迭代过程中还真遇到了这种场景,后面会介绍一下。

减枝问题

一个节点可能同时被多个节点依赖,我们在计算节点值时,应该把公共节点下沉,并把值缓存下来,这样就可以减少计算了。就比如这个B节点,如果是树模型,它会被计算两遍,而在图模型里只会计算一遍。

参数的并发安全问题

如果用golang去构造这个配置化系统,在有向图的执行过程中,参数传递大概率会使用map,这里要注意map由于并发读写导致系统panic的情况。比如同一层级的节点如果开goroutine做并发计算,要注意深拷贝一份map参数去进行实际节点执行。

节点自依赖

现在有一个场景是这样的:用户下单时,发送给下游营销系统用户上一次的下单时间,如果用户没有下过单则返回null。

这种情况下订阅下单mq, 在新消息到来时先获取用户的上次下单时间, 发送mq, 然后再更新用户的下单时间。

这里就会存在自依赖问题,用拓扑排序是行不通的。

图的内部查询语言

造一种DSL去做图模型的内部渲染,可选的方式可以有这几种:

  1. JSON

JSON的嵌套关系让它天生可以表示图、树这种数据结构,我们可以用json数组来表示节点之间的依赖关系。

  1. SQL

SQL这种语言太灵活了,而且是个程序员都会,这里把节点想象成表,把节点与节点之间的关系想象成连接查询,相信每个程序员都可以一天学会如何配置这套配置化系统。

  1. 自己造DSL

如果你想晋升冲一下kpi而苦于没有什么好点子,可以舍弃上面两种方式自己造一种DSL,晋升答辩的时候就可以用这套DSL向评委席中坐着的各位老板吹逼你当初做的利弊权衡了。这么做的缺点是后面接手你系统的萌新可能上手成本有点高。

现实很残酷

有了这套强大的图模型配置化系统就万事大吉了吗?NO,NO,NO。写代码的要相信没有银弹。代码的本质是函数链函数,图模型只是让你把提前写好的函数套在一起。但是业务就在那里,不会因为程序员的意愿而增加或减少,即使用了所谓灵活的图模型,底层该有的功能一个都少不了,甚至会增加其他工作量,下面来说一下这种系统的一些问题。

上线问题

即使系统有了所谓配置化的自动化流程,不用上线代码了,但仍然需要像上线代码一样去上线配置。该有的配置小流量发布、配置回滚机制,配置多副本机制一样都不能少。

展示问题

相信每个科班出身,学过数据结构的同学普遍会认为图是一个比较复杂的数据结构,用图去表示业务如果没有一个强大的展示平台,往外推广系统会比较抽象,没有了解过这套系统的外部同学想要使用系统的能力会比较困难。

接受度问题

图这种模型对于了解这套系统的同学同样有接受度的问题,理论上一个业务可以拆解成若干个节点组成的一个整体图去执行业务,也同样可以通过只有一个节点的运算单元去完成所有的业务操作。在一个复杂的业务到来时,如果没有足够的经验,比较不容易找到最优解,最后用一个大的、没有任何复用性的节点去完成所有任务。

业务的复杂度问题

最后,总有一些业务不适合用上面这套模型来表示,比如说一些非常业务的业务需求,用crud就是要比这套图模型更直接,强行把这种业务需要往图模型里面套貌似并没有比直接crud更happy,于是结果就变成研发生硬的套图模型,用一个大节点写相关逻辑。


参考

https://www.zhihu.com/question/68435360

https://zhuanlan.zhihu.com/p/34871092

https://xargin.com/feature-system-dev/


- EOF -

推荐阅读  点击标题可跳转

1、25 张图,一万字,拆解 Linux 网络包发送过程

2、2.3 万 Star!直追微软 Visio,这个简洁实用的在线绘图工具必须推荐给你

3、微软又一个数据可视化神器开源了!非常酷炫


觉得本文有帮助?请分享给更多人

推荐关注「算法爱好者」,修炼编程内功

点赞和在看就是最大的支持❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存