乐趣区

关于开源:C-Workflow异步调度框架-性能优化网络篇

C++ Workflow 异步调度框架 – 性能优化网络篇

时隔 (鸽) 一年半,Workflow 架构系列又回来惹~尽管搁笔许久,但 咱们我的项目简直每天都在更新代码

GitHub 是主战场,欢送大家在 github 关注一手信息,这段时间的海量性能更新,都在散落在 文档、issue、以及我起初的其余文章和答复 中了。

过来一年的提交动静!真的!没有!偷懒!👇

明天整的活,是被催更最多的,Workflow 中最重要的优化——网络。

通信器能够说是 Workflow 的前身,是我老大从自研分布式存储模块中演变进去的,并且因为老大很少看其余我的项目的做法,因而个人感觉其中有许多翻新点值得分享,如果大家看腻了千篇一律的做法,兴许这里能够和你产生一些思维的碰撞。因而本篇欢送大家探讨、交换,以及指出执笔的我写得不对的中央~

P.S. 这个系列是我在 2020 年 7 月刚开源时积攒的一些鶸鶸的思路,省得一些起初意识的开发者不太理解,这里附上一些鶸鶸的链接:

C++ Workflow 异步调度框架 – 根本介绍篇
C++ Workflow 异步调度框架 – 架构设计篇
C++ Workflow 异步调度框架 – 性能优化上篇

而后咱们从底向上,开始明天的话题——网络优化。

我的项目地址 GitHub👉 https://github.com/sogou/workflow
我的项目地址 Gitee👉 https://gitee.com/sogou/workflow

一、和事件循环不一样的全新玩法

乏味的新货色放第一局部说:Workflow 应用 epoll 的形式有什么不同?

答案是 线程模型

咱们罕用 epoll 提供的三个接口:create、ctl、wait。连贯多了的时候,异步要做的就是用尽可能少的线程去治理 fd,以节俭创立销毁线程的 overhead 以及线程所占用的内存和对资源的争抢。

所以高性能网络框架,都要治理着本人的多个线程(或者 nginx 的多过程)对 epoll 进行操作,并对下层提供原子性的语义。

好,咱们当初给 n 个网络线程去操作 epoll,全局这么多 fd 怎么调配和治理呢?

咱们以前都见过的通用的做法是 事件循环 ,用one loop per thread 的形式进行调配和治理的。

以下我形容一下我弱弱的几点了解:

  1. 如果是 server,是被动方,那么要做好 accept 工作
  2. 如果是 client,是被动方,那么要做好 connect 工作
  3. 这些都是要从全局的角度来散发 fd
  4. 而后依照这 n 个线程以后的负载量分发给一个人,这个人来全面负责这个 fd 的:吃(增)喝(删)拉(改)撒(等)

Workflow 的形式不一样:

  1. 散发局部咱们先简略地对 fd 进行 n 取模,毕竟建设连贯大家也是异步做的呀,连贯的响应曾经能够交给网络线程去做了
  2. 而后这个网络线程就持续做期待这个被调配的 fd 以及响应它的所有事件
  3. 并且,敲黑板~,如果一个线程在 epoll_wait,另一个线程向 epoll 里增加,删除或批改 fd 这在 Workflow 里都是惯例操作,因为 epoll、kqueue 都是反对这个特色的

所以看到这里边最大的区别是什么了吗?

事件循环是通过 eventfd 或者其余形式打断 epoll_wait 来增加 fd。显然,这个做法在很多场景下其实对性能是有影响的

如果对一个的操作有变动,Workflow 怎么做呢?咱们会通过一个 pipe 事件告诉这个 poller thread

举个例子。如果要删除一个 fd,那么如果他人把 fd 从 epoll 删除,删除之后就没有契机通知该 poller thread 去做它要做的事件(最典型的,比方,删掉对应的上下文或者调用钩子等)。所以要借助 pipe 事件来告诉“删除”这件小事儿,而这个等这个 poller thread 下次有正事儿要做的时候,再一并处理就完了,无需当初叫醒它就为了干点小事儿。

好奇宝宝你可能会问:fd 间接取模难道不会不平均吗?

这里有个很重要的设计上的优化理念。

Workflow 从来不做空跑 QPS 之王,Workflow 做的是一个跑得又快又稳的通用企业级框架,所以贯通整个我的项目一个设计理念就是 面向全局优化

即,比起尽可能优化一个申请失去最优性能,咱们更偏向于优化整体的申请失去最优性能

如果零碎自身很忙,那么其实连进来的大部分 fd 都会比拟忙碌,因而临时还不须要去做散发,取模就够用了。毕竟每个优化步骤都是有点小开销,到底优化谁,这是个十分 compromise 的事件。

这个优化思路前面还会继续看到~

尽管这种线程模型的新做法,不肯定会成为 Workflow 高性能的最决定性因素,但却是我集体感觉最值得分享的新思路,能够让咱们这些临时还没有把底层吃透、没方法上来就翻新的入门开发者,也看看业内当初有了不一样的眼前一亮的乏味计划,也让咱们能够不要那么塌实,不要为了疾速出问题节约了本人的思考机会,而应该大胆设计,小心实现。

二、比 proactor 走得更远:音讯的语义设计

上一部分讲的,除了封装多线程以外,网络库还要提供咱们所设计的接口。

而 Workflow 的另一个不同点在于,它不是网络库,而是从网络模块到下层具体协定、工作流都有的成型框架 。所以提供的接口语义并不是proactorreactor,Workflow 的语义是 以音讯为单位的

为了简略起见,这里以收音讯为例:

  • Reactor 是有事件来了,我通知你,你负责去读出来;(epoll 所提供的性能)
  • Proactor 是你给我一片内存,我把数据读出来了之后通知你,一次通信的音讯可能你是要读好几次能力读完的;(iocp,以及很多网络库的做法)
  • Workflow 是别管事件来了和读多少几次,我会帮你把你要的残缺音讯都收好了,再叫你;(也就是下层的每一个工作)

这显然更加合乎人类的天然思维,接口的简洁和易用也是咱们对 Workflow 始终以来的保持。

咱们仍然从底向上,看看一个音讯长什么样:

1.pollet_message_t

struct __poller_message poller_message_t;

struct __poller_message
{int (*append)(const void *, size_t *, poller_message_t *);
    char data[0];
};

最底层很简略,一个钩子,以及一片内存。

2.CommMessageIn

class CommMessageIn : private poller_message_t
{
private:
    virtual int append(const void *buf, size_t *size) = 0;
    …

这个派生类是收到的音讯,减少了一个 append 接口:
数据来了下层能够拿走,并通过 ret 通知底层外围之后的状态。这里的 size 是个双向参数,你甚至能够通知我你当初要拿走到哪里,剩下的我帮你接管、下次再给你。

其中:

  • ret 返回 1 示意音讯收完;
  • ret 返回 0 示意还没收完;
  • ret<0 示意音讯谬误。

正是这个返回值让底层外围晓得如何切出一份残缺的音讯。

3.CommMessageOut

class CommMessageOut
{
private:
    virtual int encode(struct iovec vectors[], int max) = 0;
    …

发送接口也很简略。

到此就是外围通信器所做能看到的音讯接口。但作为一个成熟的框架,咱们认为还远远不够不便,因而音讯的语义咱们持续往下看:

ProtocolMessage会从 CommMessageI n 和CommMessageOut 派生,因为对于 server 来说,in就是 requestout 就是 response,而对于client 来说,out就是 requestin 就是response,咱们会须要收发两种性能。

class ProtocolMessage : public CommMessageOut, public CommMessageIn;

而音讯收完了没有,是由协定开发者来做的:具体协定须要派生 ProtocolMessage 来实现方才说的 appendencode接口。因而 Workflow 的 HttpRedisMySQLKafkaDNS 等协定 都是本人手写解析的,这样能力真正做纯异步、收发不受原生模块的线程模型影响,这也是性能足够快的关键点之一

Workflow 的很多用户是在用作异步 MySQL 客户端,尽管咱们认为 MySQL 性能瓶颈应该在 server 才对,然而只有想晋升并发,原生 client 就会顾此失彼,而且随着目前 MySQL 协定各集群的崛起,Workflow 伪装成 MySQL client 的时候,对方也经常并不是原生 MySQL server,而是 tidb 之类的其余集群版伪装者(开源世界就是如此有意思~

三、非凡的异步写实现

如果你是后端开发,你就会有同感,咱们解决的绝大部分 tcp 申请场景其实都是一来一回的,这是 Workflow 最善于的畛域,以至于在这在场景下,Workflow 又呈现了一个新思路:高效的异步写实现。

咱们晓得 fd 天生是能够同时监听 EPOLLOUTEPOLLIN的,如果这样做,咱们的网络库能够在有事件处理的时候,既看看是不是要读、又看看是不是要写,这些都在这次 loop 被叫醒的一次中做。

因而 Workflow 无论是 client 或者 server,fd 长期都放弃 EPOLLIN 状态。须要写数据时,先同步的写,如果数据能够全副写入 tcp buffer,则无需扭转 fd 的状态;如果数据无奈全副写入,通过 epoll 的 MOD,原子性的把 fd 从 EPOLLIN 状态改为 EPOLLOUT 状态开始异步发送。异步发送实现(fd 会从 epoll 里删除),再把 fd 以 EPOLLOUT 状态重新加入 epoll。

而且为了性能思考,咱们的 poller_node 是一个以 fd 为下标的数组,而每个 node 只能关注一种事件,READ 或 WRITE。而后咱们会通过 operation 来判断调用哪个处理函数(而非用 event 来判断)。

这种做法实现进去的通信器,其实比任何全双工都要快。

因为大多数状况下,没有必要进行异步写。操作系统会动静调整 TCP send buffer 的大小,从 100 多 K 逐步减少至多 10M。须要异步写的场景很少,所以 epoll 里的 fd 根本不必动不会有额定开销。如果真的须要异步写,基于一来一回的模式下这个 fd 只写的模式那也是炒鸡快的。

四、超时解决是一门学识

超时难不难?超~难。

难在哪里呢?我集体了解是难在于 准确地响应 高效的治理 、和 严格的原子性

咱们先看看 准确地响应

首先要基于一个足够准确的机制,所以咱们用了 linux 的 timerfd(kqueue 的话用了 timer 事件,没错仍然辣么对立~)。每一个负责操作 poller 的线程,都有一个 timerfd 去负责以后该线程所有在监听的 fd 的超时事件。

这就够准确了吗?那可太天真了,毕竟网络那么多步骤,而且咱们不心愿用户每个申请都去关怀 connect、request、response 这么多阶段。

那怎么办呢?咱们先对超时提出了几个阶段,最底层是全局给几个配置:

  • connect_timeout: 与指标建设连贯的超时。
  • receive_timeout: 接管一条残缺申请的超时。
  • response_timeout: 期待指标响应的超时。代表胜利发送到指标、或从指标读取到一块数据的超时。

这个每读一块数据的超时值得注意,在理论网络链路不好的状况下须要阶段划分进去,否则会很惨烈(置信我我是过来人

再往上一层,咱们开发者还须要对连贯层有超时治理的权力:

  • keep_alive_timeout: 连贯放弃工夫。默认 1 分钟。redis server 为 5 分钟。

再往上,每一个工作。

如果连贯曾经达到下限,默认的状况下,client 工作就会失败返回。然而咱们容许通过工作上的一个超时值,来配置同步期待的工夫。如果在这段时间内,有连贯被开释,则工作能够占用这个连贯:

  • wait_timeout: 全局惟一一个同步期待超时。

再往上?那就不是框架须要管的事件了,相熟 Workflow 框架的开发者应该晓得,咱们提供了 timer_task 与工作流,所以咱们能够用 timer_task 和咱们的业务逻辑的 task 一起随便搭配组装应用~让你写代码都能感触到拼乐高的乐趣~~

以上的架构设计,足以满足咱们对网络申请中准确的超时管制。

咱们再看看 高效的治理

目前的超时算法利用了 链表 + 红黑树 的数据结构,工夫复杂度在 O(1)和 O(logn)之间,其中 n 为 poller 线程的 fd 数量。

超时解决目前看不是瓶颈所在,因为 Linux 内核 epoll 相干调用也是 O(logn)工夫复杂度,咱们把超时都做到 O(1)也区别不大。

最初,严格的原子性,设计咱们配合第五局部的代码一起感受一下。另外波及到 Communicator 的状态转换,所以就须要放到独自的代码解析中了(并不是这篇文章写到这里写累了._.

五、循环局部的代码解说

咱们把 src/kernel/poller.c 中,一个网络线程所执行的外围函数拿进去一行一行解读:

static void *__poller_thread_routine(void *arg) // 线程函数入口
{poller_t *poller = (poller_t *)arg;
    __poller_event_t events[POLLER_EVENTS_MAX]; // 方才提到的以 fd 为下标的超级快的 poller_node 数组
    struct __poller_node time_node;
    struct __poller_node *node;
    ...

    while (1)
    {__poller_set_timer(poller);   // 持续开始期待之前,须要更新本轮要等的下一个超时工夫,就是这里设置的 timerfd
        nevents = __poller_wait(events, POLLER_EVENTS_MAX, poller); // 2000 years later ...
        clock_gettime(CLOCK_MONOTONIC, &time_node.timeout);
        has_pipe_event = 0;
        for (i = 0; i < nevents; i++) // 对于每个本线程要解决的曾经 ready 的事件
        {node = (struct __poller_node *)__poller_event_data(&events[i]);
            if (node > (struct __poller_node *)1)
            {switch (node->data.operation) // 一个正在监听的 node(一个会话、或文件读写操作等)只会有一种状态
                {
                case PD_OP_READ:
                    __poller_handle_read(node, poller);
                    break;
                case PD_OP_WRITE:
                    __poller_handle_write(node, poller);
                    break;
                ... // 还有很多其余异步操作,比方连贯也是异步的,以及 ssl 的简单操作。倡议大家先看 nossl 分支学习
            }
            else if (node == (struct __poller_node *)1)
                has_pipe_event = 1; // 咱们应用了 node== 1 标记 pipe 事件,毕竟 fd 为 1 不可能是非法地址,用之~
        }

        if (has_pipe_event) // 这里阐明本轮有 pipe 事件,pipe 用来告诉各种 fd 的删除和 poller 的进行
        {if (__poller_handle_pipe(poller))
                break;
        }

        __poller_handle_timeout(&time_node, poller); // 解决超时事件,如上所述从红黑树和链表里把所有超时的节点都解决掉
    }
    ...
}

六、还有什么 Workflow 目前不做

实质上,workflow 优化的次要方向都是:通用地用尽量少的系统资源,去做尽可能多的事件。

所以,Workflow 的通信器是全世界最快的通信器吗?
很显然不是。

Workflow 只是一个够快够稳够简略好用的、并且携带很多新思路的企业级框架,而且其异步特点在于能够做到调度无损耗

这个得益于很多方面,上述这些折中的抉择其实十分重要,另外还得益于架构层面的:

  • 对资源的厚此薄彼、
  • 接口设计的对称性、
  • 工作流的编程范式

等等。并且,方才有提到,Workflow 做的优化决策,都是面向全局的,这样的例子还有很多。

许多高性能优化方向,Workflow 目前没有用到,并不是货色不好,而是很多时候目前还没有必要,或者通过理论测试得出比照数据,发现必要性没有设想中的高,又或者优化思路不太通用。

包含以下这些:

  1. 音讯发出来,能够不切一次线程。
    但 Workflow 目前都会切,并过一次音讯队列(某些空跑场景下显然不切会更好,但 Workflow 还是走实用路线~
  2. workstealing 队列
    很多调度零碎包含 go 内核都在应用,但 Workflow 的队列仍然只是一个简略的队列,我原先有写过多种简略队列的比照,目前用的双队列模式,是简略队列里实测最快的!
  3. 其余无锁技术,
    以及有人会应用 eventfd 去代替 cond,或者对 cond 进行 cacheline 优化(这也是一个很乏味的方向,但前两者我还没尝试。后者实际过,cacheline 优化听下来很漂亮,但简略场景下没有测出理论的性能晋升🤔
  4. cpu 亲和性相干的优化
    在服务器曾经够忙碌的时候。这个优化并不是那么有必要,但后续有空我会去学习一下~
  5. 各种用户态的优化
    用户态协程、用户态协定栈~~~
  6. 各种内核态的优化
    比方早就进去十几年、最近忽然又火了的技能树 eBPF

七、最初

心愿这篇优化,除了 新的 epoll 应用形式、新的异步写形式等新思路 以外,更多地是 分享一些做事件的办法

一方面,咱们在做优化的时候,既要放弃对新技术的好奇心,又不能对新技术趋之若鹜。有些花里胡哨的做法听下来超级棒,实际上真的有用吗?很多时候,操作系统曾经帮你做得很好的事件,就不要自我打动拍脑袋去做好吗。

但另一方面,反过来说,又须要 保持足够的自我思考,多推敲多看看新思路新做法,从而晋升本人的思路,去发明出更多有价值的精品,从做题家进化为代码艺术家

其实原本这次还想写写连贯治理,毕竟也做了很多事件,然而真的写不完了╥﹏╥好不容易提笔,我必须!当初!立即!马上!把这篇文章收回!!!因而连贯治理和其余的管制逻辑,我会放到下一篇~(# /ω\#)

主页上有吞吐和长尾的优良的 Benchmark,欢送大家到点击 [浏览原文] 到主页围观,另外附上 GitHub 上我的项目主作者对某个 issue 中一个问题的用心答复截图:

图长正告⚠️这只是对一个问题的答复,截四段是因为答复太长👇

咱们小团队真的是做用心血去开发与推动这个我的项目,Workflow 的倒退尽管与 kpi 无关,但至多和我的集体技术信奉无关~心愿大家能够喜爱我积攒的新思路,以及不厌弃我一丢丢啰嗦的心得体会。
并且!在等不到我的文章的时候,乃们能够去 issue 找主作者答疑解惑嘤嘤嘤~

GitHub – sogou/workflow: C++ Parallel Computing and Asynchronous Networking Engine

七、最初

心愿这篇优化,除了 新的 epoll 应用形式、新的异步写形式等新思路 以外,更多地是 分享一些做事件的办法

一方面,咱们在做优化的时候,既要放弃对新技术的好奇心,又不能对新技术趋之若鹜。有些花里胡哨的做法听下来超级棒,实际上真的有用吗?很多时候操作系统曾经帮你做得很好的事件,就不要自我打动拍脑袋去做好吗。

但另一方面,反过来说,又须要 保持足够的自我思考,多推敲多看看新思路新做法,从而晋升本人的思路,去发明出更多有价值的精品,从做题家进化为代码艺术家

其实原本这次还想写写连贯治理,毕竟也做了很多事件,然而真的写不完了╥﹏╥好不容易提笔,我必须!当初!立即!马上!把这篇文章收回!!!因而连贯治理和其余的管制逻辑,我会放到下一篇~(# /ω\#)

主页上有吞吐和长尾的 benchmark,欢送大家到主页围观,另外附上 GitHub 上我的项目主作者对某个 issue 中一个问题的用心答复截图:

咱们小团队真的是做用心血去开发与推动这个我的项目,Workflow 的倒退尽管与 kpi 无关,但至多和我的集体技术信奉无关~心愿大家能够喜爱我积攒的新思路,以及不厌弃我一丢丢啰嗦的心得体会。

并且!在等不到我的文章的时候,乃们能够去 issue 找主作者答疑解惑嘤嘤嘤~~~

GitHub – sogou/workflow: C++ Parallel Computing and Asynchronous Networking Engine

退出移动版