异步编程框架:Workflow 的计算调度算法

让大家好奇了这么久,今天终于写被cue到最多的话题:Workflow的计算调度,包括独创的调度算法与相关数据结构。
P.S. 原文写于2022年5月份~

C++ Workflow作为一个异步调度编程范式,对调度的拆解是有几个层次的:

  • 用户代码层:提供任务流级别的结构化并发,包括串并联、DAG和复合任务等,用于管理业务逻辑,组织要做的事情的依赖关系;

  • 资源管理层:对不同资源内部做了协调和管理,比如网络、CPU、GPU、文件IO等,用最少的代价、做最高效、最通用的资源复用。

今天就重点介绍一下,Workflow内部独创的计算调度算法,包括Executor模块(仅200行代码)及相关模块,整体是如何管理计算资源、协调不同计算任务,从而做到无论任务耗时长短,都可以尽可能均衡调度的最通用的方案。

而且看完之后,也可以对上一篇《一个逻辑完备的线程池》中一直强调的生命周期有一个更好的理解。架构设计一直要强调每一个模块本身做到完备和自洽,是因为更有利于演化出上层模块。

https://github.com/sogou/workflow/blob/master/src/kernel/Executor.cc

1. 计算调度面临的问题

无论是用何种计算硬件,计算调度要解决的问题是:

  1. 充分使用资源;
  2. 不同类别的任务的资源分配;
  3. 优先级管理;

第一点很好理解,以CPU为例,只要来任务了就要尽量跑满CPU上的若干核。

第二点,不同类别是比如:每件事情由3种步骤** A->B->C** 组成,耗费的计算资源是1:2:3,简单的做法是可以分别给予3个线程池,线程比例1:2:3(假设24核的机器,我们可以分别把3个线程池中的线程数配置为4,8,12)。

但由于线上环境是复杂多变的如果耗费资源变成了7:2:3,固定线程数方案显然不可取,不改动代码是难以匹配这样的状况。

这么做的另一个弊端是,如果一批提交100个a,那么显然只有4个线程可以工作,难以做到“资源应用即用”;

还有没有解决办法呢?更复杂地,可以引入动态监测耗时,然而引入任何复杂方案都会有新的overhead,绝大部分情况下这些都是浪费。

继续看第三点,优先级管理是比如:还是 A->B->C 三种任务。现在增加了一个D,我想要尽快被调起来,简单的做法往往是给所有任务一个优先级编号,比如1-32。

但这并不是长久的解决办法,编号是固定的总会往更高优的用完,而且任务自己都是贪心的,只要有最高优先级,最终大家都会卷起来(不是

我们需要的,是一个灵活配置线程比例充分调度CPU、且可以公平处理优先级的方案。

2. 创新的数据结构:多维调度队列

Workflow内部几乎所有的方案都是往通用了做,对于CPU计算,则是:全局一个线程池,和统一的管理方式。使用上,计算任务只需要带一个队列名,即可交给Workflow帮你做到最均衡的调度。

基本原理图如下:

Executor内部,有一个线程池和一个基本的主队列。而每个任务本身,同时也是属于一个ExecQueue,可以看到其结构是一组以名字区分的子队列。

这种数据结构的组合,可以做到以下三点:

  1. 首先,只要有空闲计算线程可用,任务将实时调起,计算队列名不起作用。

  2. 当计算线程无法实时调起每个任务的时候,那么同一队列名下的任务将按FIFO的顺序被调起,而队列与队列之间则是平等对待。

    例如,先连续提交n个队列名为A的任务,再连续提交n个队列名为B的任务。那么无论每个任务的cpu耗时分别是多少,也无论计算线程数多少,这两个队列将近倾向于同时执行完毕。

  3. 这个规律可以扩展到任意队列数量以及任意提交顺序。

分别来看看算法是什么。

第一点:Executor的线程不停从Executor内部的主队列中拿任务出来执行;

第二点:线程从主队列把任务取走、并准备执行任务之前,也把任务从它自己的子队列里拿走。并且,如果该子队列后面还有任务,就把下一个任务出来,放到主队列中。

第三点:外部用户给Workflow提交任务的时候,Workflow会先把任务按名字放到子队列。并且如果发现这是子队列中的第一个任务(说明刚才子队列是空的),便立刻提交到主队列中。

算法本身相当简单,而提交任务时,只需要给调度器轻微的指导,既队列名(对应Executor的一个ExecQueue),无需指定优先级或计算时间预估等信息。

当我们收到的A, B, C任务数足够多而且数量相等,无论任务以什么顺序到达,也无论每个(注意是每个而不是每种)任务的计算时间多少,A, B, C三个子队列将同时计算完成。

而主队列长度,永远不超过子队列的个数,且主队列中,每个子队列的任务永远只有一个,这是算法的必然结果。

3. 源码简析

我们用最简单的WFGoTask为例子,把抽象的调度算法从外到里一层层落实到代码上。

1) 用法示例

void func(int a);

// 使用时
WTGoTask *task = WFTaskFactory::create_go_task("queue_name_test", func, 4); 
task->start();

2) 派生关系

了解过Workflow任务的小伙伴一定知道,Workflow任何任务都是行为派生,而中间有一层,是基本单元,即由SubTask和具体执行单元双派生,这样既可以让上层任务被SubTask串到任务流里、也可以做具体执行单元做的事情。

对计算调度来说,具体执行单元那肯定是每个可以被线程调度起来的计算任务。

我们可以看到WFGoTask是从ExecRequest派生的,而ExecRequest就是执行的基本单元。(复习到网络层面,基本单元是CommRequest,一个代表执行,一个代表网络,对称性无处不在~)
打开src/kernel/ExecRequest.h文件可以找到它,这里只看dispatch()里做了什么:

 // 这里可以看到,具体执行单元是ExecSession,它负责和Executor等打交道
class ExecRequest : public SubTask, public ExecSession 
{
public:
    virtual void dispatch()
    {
        if (this->executor->request(this, this->queue) < 0)
        ...
    }

dispath()做的事情,就是把自己和自己的队列,通过request()接口提交到Executor。

3) Executor的生产接口:request()

int Executor::request(ExecSession *session, ExecQueue *queue)
{
    ...
    // 把任务加到对应的子队列后面  
    list_add_tail(&entry->list, &queue->task_list);

    if (queue->task_list.next == &entry->list)
    { // 子队列刚才是空的,那么可以立刻把此任务提交到Executor中
        struct thrdpool_task task = {
        .routine = Executor::executor_thread_routine,
        .context = queue
        };

        if (thrdpool_schedule(&task, this->thrdpool) < 0)  //提交
            ...
    }  
    return -!entry;  
}

4) Executor的消费接口:routine()

刚才看到线程真正执行一个任务的时候,是调用的executor_thread_routine(),传进去的context就是这个任务所在的子队列。

void Executor::executor_thread_routine(void *context)
{
    ExecQueue *queue = (ExecQueue *)context;
    ...
    entry = list_entry(queue->task_list.next, struct ExecTaskEntry, list);  
    list_del(&entry->list); // 从子队列里删掉当前正要执行的任务
    ... 
    if (!list_empty(&queue->task_list))
    { // 如果子队列后面还有任务(也就是同名任务),放进来主队列中
        struct thrdpool_task task = {
        .routine = Executor::executor_thread_routine,
        .context = queue
        }; 
        __thrdpool_schedule(&task, entry, entry->thrdpool);
    }
    ...
    session->execute(); // 执行当前任务... 跑啊跑...
    session->handle(ES_STATE_FINISHED, 0); // 执行完,调用接口,会打通后续任务流
}

4. 改造案例分享:用任务流替代传统流水线模式

在公司内部,最经典的改造案例就是用Workflow的任务调度替换掉传统的流水线模式,和开头介绍的按资源比例分配不同模块的线程数量是类似的,这对于某一个步骤突发增加的耗时、想要额外增加另一个步骤/module等,都是非常不灵活的方案。

而Workflow的方案相比起来,则可以完美避开传统做法的许多弊端。

除此之外,实际的计算调度中还有一些问题,是非常考验框架的实现细节的。比如,错误处理好不好做,依赖和取消好不好做,生命周期好不好管理。虽然这些不是计算模块本身的事情,但Workflow的任务流这层都提供了很好的解决方案。

在上一篇线程池的文章po上网时,也有小伙伴问到:

做法如下简单分享一下:

5. 最后

如上总结一些心得吧。

我们做计算调度时往往忘了,根本要解决问题是A->B->C ,而不是关心1:2:3。如果常常要担心其他问题,往往是因为调度方案本身做得不够通用。只有一个最通用、最回归问题本质的架构方案,才可以让开发者不用关心其他问题,专注于提升自己的模块本身,也更方便上层做二次开发,为开发者提供充满想象力的无穷可能性。

另外也是作为上一篇逻辑完备的线程池的场景补充吧,把Executor等上层代码拿出来分析,才能真正感受到底层线程池中让任务本身可以调起下一个任务等做法的重要性。

Workflow内部有许多创新的做法,也许我本身有许多表达不好或者技术不到位的地方,但技术文章都是抱着一种分享新思路新做法的心态~ 那么,很希望可以看到大家的不同意见,欢迎发到项目的issue中~~~

https://github.com/sogou/workflow

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352

推荐阅读更多精彩内容