C++ Workflow 异步调度框架 - 性能优化上篇
最近在努力同步维护 InfoQ 的文章积累~方便后续持续更新~
原文是 2019 年 7 月底开源后陆续 po 的,这里对近况进行了调整和补充。
希望自己和项目都可以持续进步 (๑╹ヮ╹๑)ノ 欢迎多多交流!!
搜狗 C++ workflow 异步调度框架 github 地址:
https://github.com/sogou/workflow
先来和大家 update 一下,这一周以来 workflow 又有哪些成长呢:
新版更简洁的 README.md
server 默认使用 ipv4 启动(为了兼容 windows 与 unix 的行为
加了全局配置项的文档 about-config.md (正好和今天的话题相关!
开源了两周,备受小伙伴们关注,真的很开心~~ >_< ~~ 特别感谢各位关注和支持我们的大神们小朋友们,希望能够持续和大家交流,一起进步~
(上图更新于 2022 年 7 月,开源马上两年了,留个纪念~)
我也在整理开源项目规范化相关的事情,如果有哪里做得不到位,希望能帮我指出~
今天这个话题是非常通用的入门话题:写完代码我们需要做什么最基本的系统性能优化。
由于 workflow 是个异步调度引擎,workflow 的职责就是让系统各资源尽可能地利用起来,所以我的日常工作,除了写 bug 之外,还要配合开发小伙伴现场 debug、分析用了 workflow 之后的各项指标是否还能进一步提升。
我还是结合具体几类资源为线索来介绍:
CPU:多路复用相关的线程数、计算相关线程数、多进程
网络:长连接短连接、连接数控制、超时配置、压缩
计时器:timerfd 的优化
计数器:命名计数器与匿名计数器
文件 IO:实际场景用得少,先不写了
GPU:目前我只做了 demo 版,所以没有放出来,也先不写了
其中计时器和计数器相对简单一些,我会这里介绍下内部实现,其他的内部实现做了很多优化,每个话题都值得以后单独写一下。
一、CPU
先来看看我们的配置项:
1. 基本网络线程
一般用 epoll 的框架都需要对其进行类似 proactor 式的封装,那么就要负责做以下事情,以及决定具体哪个线程去分工:
对 epoll 具体某个 fd 进行读写
读写时把完整数据包切下来
数据包切完之后的解析(即反序列化)
执行用户的操作
Workflow 当前的做法,poller_threads
线程是去操作 epoll 读写和做 fd 读的切消息的事情,而handler_threads
是做基本用户操作的,比如 callback 和作为 server 的话,我们的 process 函数所在的线程。
brpc 是不区分的,我个人理解有几个原因,比如:
它套了一层 bthread 做换线程的调度;
fd 上拉了写链表:没人在写你就写,有人在写你就把数据扔下就行了,这个人会帮你写,不存在类似 handler 线程还要回去管 poller 线程的异步写的事情;
Workflow 没有做这样的优化,主要还是因为一个进程内网络读写和业务操作压力比例基本是差不多确定的,业务上线前的调优调整一下 poller 和 handler 线程比例基本足够了。而且 workflow 才 1 岁多,很多优化可能会往后放。
这里也顺带说一句,对于把数据包切下来和切完之后的解析,其实有些协议是不太能分得开的。
我鶸鶸地给大家列一下,从协议设计上,可以分以下三类:
收到消息就能知道我怎么完整地切一条消息出来;
收一点之后判断一下才能知道我怎么切一条消息出来;
一边收数据流一边解析,不到最后一刻都不知道是不是收完。
第 1 种就很简单,一般做 RPC 协议我们都会友好地在头部告诉你大概多长。
第 2 种有点类似 HTTP 这样,大概收完头部你就知道后边还有多少了,这个时候你收 header,是要自己边收边 parse 的。
第 3 种比如 MySQL 这种吐血的协议,大概它在设计的时候就没有想过你要多线程去操作一个 fd 读消息,你得根据当前哪种包的状态再判断,这种必须写个状态机去完整收完了才能交给用户。而这个收的期间,我已经把每个 field 和每个 ResultSet 给解析出来了,收完基本等于数据反序列化也做完了。所以第 2 种、第 3 种,对于切完整消息和解析消息的反序列化操作其实并不会太分得开,workflow 都会在poller_threads
里做。
2. 计算线程
我们内部会有独立的计算线程池,默认是和系统 cpu 数一致的数量,这个是基本可以减少线程数过多而频繁切换的问题,当然如果用不到计算任务,此线程池不会创建。
和 cpu 数一致,那么不同时期不同类型的计算任务占比不同,这个 workflow 怎么解决呢?我们内部用了一个谢爷发明的多维队列调度模型,已经申请专利,以后有机会让谢爷写一篇给大家讲讲>_<
简单来说,workflow 的计算任务都是带名字的,对于业务开发来说,基本只需要把同一类任务以同一个名字去创建,那么 start 之后是基本可以保证不同名字的任务被公平调度,并且整体尽可能用满计算线程数,这是一种比优先级和固定队列要灵活得多的做法。P.S. 我们也有独立的 DNS 线程池,但是 DNS 目前的路由模式我觉得要并发去更新真的非常粗暴非常不喜欢,有空了路由机制是我第一个要动刀改进的地方!(认真立 flag 中 o( ̄▽ ̄)o
3. 多进程
一般来说我们不太需要多进程,但是不可避免的情况下,先前有个场景确实需要小伙伴拆多进程:使用 Intel QAT 加速卡多线程会卡 spinlock,这个前几篇文章有个系列已经提到过。
通用点说多进程,一般来说我们作为 server 的做法是先 bind 再 listen,然后 fork 多个进程,然后,重点是在于,你这个时候再去 epoll_create,那么操作系统来保证连进来同一个端口的连接不会惊群 accept。
这个我个人的理解是:
首先我们 bind 并 listen,是保证多个进程拿到同一个 listen_fd。
然后先 fork 再 epoll_create,意思是由多个 epoll 去 listen 同一个 listen_fd。由于 epoll 不是用户态的,操作系统来保证同一个 listen_fd 的 accpet 只会被一个 epoll 来响应,所以不会有惊群。
说回 workflow~workflow 的 server 想做成多进程就很简单了:用 WFServer::serve()接口,做以上 fd 自己 bind、listen,再 fork 多次的事情就可以了。
也给大家列一下 demo 测试中多进程操作加速卡的性能。绿色的点是 nginx(只能打到 8w),nginx 本身就是多进程单线程的,但是由于 QAT 只以多进程纬度来处理并发,因此我们只以进程数对比,基本轻松上 10w 了。并且说一下,QAT 加速卡如果只做 RSA 计算,极限 QPS 也就是 10w 左右。
以及短连接、长连接情况下多进程、多线程在我们小伙伴调用 QAT 加速卡每个请求做 2 次 RSA 解压时的 QPS 对比情况:
这里的短连接长连接,就当作给大家抛砖引玉网络调优话题,但今天来不及,明天继续写下篇~~~
本系列的前两篇:
下一篇:
本系列其他文章:
版权声明: 本文为 InfoQ 作者【1412】的原创文章。
原文链接:【http://xie.infoq.cn/article/27cc433f49991c08f7d299a9e】。文章转载请联系作者。
评论