找回密码
 立即注册
查看: 17|回复: 0

C++高性能年夜 范围 办事 器开发实践

[复制链接]

9365

主题

0

回帖

2万

积分

论坛元老

积分
28103
发表于 2024-3-1 13:01:16 | 显示全部楼层 |阅读模式
本文摘录自腾讯高等 工程师在「全球C++及系统软件技术年夜 会」上的专题演讲。
01Lego简介

首先介绍一下 CDN。异常 早期的时候有一个年夜 牛创建了一个公司叫阿卡曼,他把办事 器安排 到全球各地,然后把源站的内容缓存在就近的办事 器。比如我们广东,可能在深圳有个机房,我们就缓存了一份跟源站一模一样的内容。这样用户在拜访 这个内容的时候不消 “四处奔忙 ”到北京的办事 器去拿需要的视频了,只要在本地就能办事 。所以 CDN 其实就是网络世界的快递工。
那么为什么说 CDN 年夜 范围 且需要高性能呢?首先,我们 CDN 每时每刻都邑 跑年夜 概 100t 流量,我相信这个流量级别在国内也算是数一数二的。腾讯,包含 腾讯云自己 的拜访 量都异常 年夜 ,QPS 基本上都是千万级,所以我们办事 的是一个流量这么年夜 ,并且 QPS 并发量这么高的办事 器。存储量更不消 说,都是10万级其余 ,量级异常 年夜 。腾讯 CDN 支持的场景也异常 多,业务纷纷 多样,各类 类型都有。
但今天我不讲怎么去运营这种年夜 范围 办事 器,我主要会讲单机层面怎么去设计办事 器,来尽可能地提升单机的性能和办事 能力。

办事 器肯定要支持高性能、高QPS、高能量,但除了这一部分,我们更要考虑的是可扩展性这一块。特别要提前讲一点,其实传统的办事 器基于异步调用是异常 难以去写、难以去维护的,写过异步回调的同事应该知道。所以我们希望这个办事 器是所有新人,包含 所有开发者,都能够尽快上手的。
那怎么去衡量呢?其实就是一个写 Nginx 办事 器的人需要多久能够精通,能够去把控完整的代码让它跑起来,并且出了问题能够快速排查处理?一般这种都要按月算,然则 用我们的架构,我们希望新人过来,按周、按天就能够上手去写代码,去学习,去尽快投入到工作,说白了就是尽可能高地产效。
另外就是CDN友好,它的功能很多,特别是可能年夜 家觉得除了缓存之外也没什么器械 ,其实产品异常 多。比如直播带货,在CDN里会有很多的功能需求,比如怎么尽快地把流、把内容发放给用户,尽可能减少延迟;比如抢红包,你可能要等个10秒钟,但有人可能一瞬间、1毫秒就看到了。所以在CDN上面会一直 地迭代很多新的功能特效,比如编解码,比如直播的一些特性,比如电商需要的一些边沿 计算能力的开放特性。
02传统Web框架
回过火 来看一下传统Web框架有哪些呢?这里抽取了三个比较典范 的去做比较 。

比如像 Nginx 就是经典的异步回调。异步回调实质 上就是一个状态机,有很多的事件。比如很多网络收发的事件、网络磁盘的事件来了之后,内部怎么去流转这个状态,怎么尽快处理。这种框架的利益 是性能异常 高,现在年夜 家都认为 Nginx 是办事 器里性能最高的一个代表。
另外,最早协程没有 C++,所以我们找的是 Libco,是微信做的一个协程框架。我后面会讲一下协程,在考虑选型的时候有考虑这个协程,但它有其他方面的一些问题,包含 怎么去开销的部分。
最后一个是 ATS。ATS 里有个 Continuation,是 CPS 编程模型,C++ 里其实也有,叫 Future/Promise。

我们先来具体看一下传统异步框架。
首先它是基于事件的,另外它事件的转换去维护的时候很庞杂 ,比如 Nginx 内部就有11个阶段,再加上各类handler,你要去处理的事件异常 多,所以为什么异步回调异常 难处理,就是需要人工去维护这个状态机。你要去处理每个事件,要预想到每个流程,要知道它的每个状态,来哪个事件都要能够处理。
另外就是 Coroutine,它的主要问题在哪呢?不管任何协程都有开销,比如我们有很多的 stackless、stackful。stackful 需要去存这个协议、存这个栈的内容,那么这时候内存就有问题了。你的编程模型会受限于占空间的年夜 小,你要写多年夜 ?会不会溢出?比如 Libco 就是用 128k。stackless 会限制你开发的模式,比如 C++ Coroutine 现在就是 stackless 的一种模式,但 stackless 对编程者也是有要求的。
还有在协程自己 做切换的时候,我们认为它还是有不小的开销,切换的时候会拷贝很多内存,就会有很多 memory copy;比如这个栈可能要恢复,一些 register 可能也要重新赋值,这些恢复的开销我们测试下来还是挺可不雅 的,特别在高 QPS 的时候。

然后回到我们的主题,Continuation-Passing Style 是什么概念?你可以认为是一些 Callback 的组合,Callback 一个一个去做,但在 ATS 里并不克不及 完全实现。所以其时 我们认为基于 ATS 去做这个事情的庞杂 度也很高,但它的想法异常 好。基于回调的话它的性能相对来说是比较好的,同时它的方法 对开发者异常 友好。其实就是一个一个函数往后调,用法接近于串行的法度模范 写法。早期在 C++ 里做这个事情异常 庞杂 ,到了 C++ 11 之后有了一些特性的支持才使得这个事情变得异常 简单自然,包含C++11 的一些 Lamdba, declytype 的概念。所以早期我们也没有发明 比较好的,到后面才决定去做这个事情。
相关视频推荐
6种epoll的做法,从redis,memcached到nginx的网络模型实现
协程!协程!协程!给你一个吊打面试官的机会
学习地址:C/C++Linux办事 器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
需要C/C++ Linux办事 器架构师学习资料加qun812855908获取(资料包含 C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

03Lego架构实现

然后看一下年夜 的架构图年夜 概是这样子,上面有一些模块,就是乐高的积木,下面很多 Epoll 的线程,基于这个线程,上面也会分层。当然这个是传统架构图,我们还有一些异步的事情。

先看一下为什么异步回调这个事情会比较麻烦。比如我处理一个事情要写很多 handler,这是预状态机里的事件,怎么跳的线,要转到什么状态。异步回调里面就写这个状态机里的这个线。这个时候就很麻烦,状态异常 多,每个事件都要去处理。很多时候你是不知道异常怎么跳过来的,你可能没考虑到或者考虑不完全,这时候突然就陷到了异常状态。特别在网络事件异常 多的时候,比如客户端封闭 、超时,比如甚至到源站,比如到远端的办事 器超时等等,你完全没法考虑全,所以导致你写的时候可能认为有几个事件但写漏了几个,再去做调试就会异常 庞杂 ,并且 这个状态跳转的进程 也很难去维护。
另外一个异常 明显的问题是代码异常 疏散 。比如我这个事件写在哪了,每个事件都要去处理的话,我的代码是按事件去划呢,还是按状态去划分。所以我们后来就选用了 Future 和 Promise 的方法 。

Future 是什么概念?就是有一个事件我们认为未来会完成,但我不关怀 这个器械 怎么完成或什么时候完成。Promise 就是这个器械 能够复制回去,然后回调回 Future。
Future/Promise 跟 Continuation-Passing 有什么联系呢?Continuation-Passing 的意思是通事后 续逻辑可以一个个函数一直 地往后去叠加。比如我有一个处理请求的函数,接着可以再往后面传 Lambda 函数,这样在读完 Body 之后,ReadBody 返回 Future,Future在完成之后调 .Then 后面的函数。我完全不需要关怀 你下面的状态怎么扭转的,我只关怀 你业务的逻辑。这是一个串行的流程,读完 Body 写 Response,返回一些数据。然后我通过一直 地去写毛病 、写 .Then,把整个逻辑串联起来。这就是 Continuation-Passing 的概念,也是最基础的一部分。
我们还设计了一个 Finally,我们在写代码的时候有一些异常希望在最后处理,这在 Python、Java 都有类似的概念,就是在最后 Cache 所有的异常,这样法度模范 运行到任何状态都能够正常处理一些资源,关失落 一些链接,关失落 一些 file、handler,这些事情都在 Finally 去做处理。

Future 和 Promise 比较 是什么样子?一个直不雅 比较 ,是异步回调提出一个状态机,在一直 地写连线的时候去写代码,我的连线就是我的代码,然后我在代码里面会触发这个事情到什么样的状态,一直 地去写,这么一个逻辑,当然维护就比较庞杂 。我们希望做到的方法 是通过 CPS 能够把整个事情拉平,把逻辑酿成 串行,让用户或者编程人员不需要关怀 我下面的事情是怎么完成的,只要我帮你完成这个事情,让你能往后继续走,就有点像写应用法度模范 一样,完全不需要关怀 办事 器内部是怎么做的。这样子,新的法度模范 员不需要像在状态机那样了解很多,比如办事 器内部实现跳转怎么做,每个接口的寄义 是什么,办事 器自己 内部的编程方法 等等。我只要给到你一些函数,并且你自己通过 .Then Callback 的串联方法 就把整个逻辑串联起来。这是两个编程模式的直不雅 比较 。

异常部分,刚也讲到了 Finally,但其实我小我 认为 C++ 自己 的异常不该 该酿成 常态。我自己的不雅 点是Fail-Fast,如果涌现Exception 异常 严重就直接让办事 器当失落 ,当然这跟应用场景有关系。像 CDN 是一个完全散布 式系统,有几十万台机器,当失落 一台也能够通过其他系统恢复,所以我们却是 希望办事 器正常的一些异常,就是特别 Critical 的异常能够尽可能出来,并且 异常自己 的性能开销是异常 年夜 的。当然我不知道现在怎么样,C++ 20 应该没对这部分做优化。原来的异常会一直 地去 Unwind 这个 stack,里面会存很多信息,导致其性能比较重,并且 特别在网络经常涌现 异常的情况下,异常是常态,更不克不及 用性能开销这么年夜 的器械 。
所以我小我 在写办事 器的时候不赞同去做try、cache,而是用更传统的一些方法 对 Exception 结构重新做设计,在 .Then Callback 时一直 地往下层抛,所以在设计方法 上就有点不一样。就是我们希望后面的人去处理异常时,比如读取一个 http 请求的 body 或者读取头部时涌现 异常,在往回发响应数据的时候,可以自己判断涌现 这个异常周围应该怎么处理,而不是往上层提议 这个请求的处所 去做,因为我们后面会更理解这个逻辑,更理解涌现 异常之后你应该怎么去做处理。所以我们在做异常的时候定制了比较轻量化的异常的一个类,然后一直 地往下面去抛,下面的去做处理。
适才 也说到我们有个 Finally 去做这一部分的完整清理,包含 一些数据的清理,不管涌现 任何异常,在最后一步都能包管 资源的释放,不会涌现 内存泄露,句柄泄漏,包含 忘了关 socket 的这种异常,这是我们希望做的一个比较轻量化、高性能的 Exception 框架,可能跟 C++ 原有的 Exception 区别就比较年夜 。

那做完这个事情之后,我们最开始看到的异步回调会在你处理完一个请求和要做的其他事情后就把所有的逻辑浓缩到一个函数里面,这样比原来分成三个函数去看的时候会加倍 清晰。当一个请求来的时候我去做请求处理,再跳转另外一个事件,之后再去提议 一些业务逻辑,这里写的是像源站 CDN 的一个场景,比如我要去另外一个处所 获取一些数据的逻辑,最后我甚至要写一个毛病 处理,所有器械 都放到一个函数里面,这样逻辑其实是更清晰的,新人一眼就能看懂这个代码逻辑是干什么的,不消 随处 去看。这就是我们选取 Future/Promise 时达到的一个效果。

Future/Promise 讲起来异常 简单,那它背后的实现是什么样子?虽然说起来它只是一个函数,但这个函数到底层怎么去包管 。它其实就是一个代码块,我们在做的时候就酿成 了对代码块的调剂 ,虽然法度模范 员看到 ReadRequest 这么一个函数会认为它是异步的,但异步怎么执行呢?它到底层是怎么样实现的呢?一个异步代码块在一个框架底层的时候,我们会做一个调剂 器,把所有生成的 ReadRequest 的 Future 酿成 一个个 task,从底层去做调剂 ,这样就达到了在上面写完之后,下面自动去扭转这个状态去做这些事情。

另外我们还做了很多的性能优化,包含Future Folding 这一部分,跟我们调剂 器是异常 相关的。因为我们以前遇到过 Future 去一直 地做 .Then 的时候,最开始是以批量压入的方法 去做的,后面发明 这么写的话,比如你的 copy 异常 多的时候,一直 压栈就会发明 一些内存的溢出,这个跟框架就比较相关了。然则 刚说到 Folding 的部分也提到一点就是 Future 在代码里面会年夜 量的创建,所以我们选取了这个编程模型之后就发明 有个问题,年夜 量创建 Future 之后内存是会炸的,比如并发1万个请求,业务逻辑有十个,内部可能要创建10万个 Future/Promise 的对象,所以导致内存一直 的申请示范,开销异常 年夜 。所以在这进程 中我们就发明 需要去做一些解决计划 ,经过研究之后发明 有两个,一个最简单的是替换 malloc库,后来发明jemalloc 是比较好的,特别在这个场景下。另外,我们针对每一个进程去分派 一个 ThreadLocal 的内存池,让内存在本地就有些缓存,不需要去内核申请很多内存。如果对内存分派 比较熟悉的话就知道malloc,tmalloc有很多的内存治理 ,包含 去内存,用 brk 申请年夜 一点的内存,所以我们的做法是提前申请一年夜 批,自己去管,完全不消 库去关怀 这些事情。

另外一个Future/Promise 遇到的问题是局部变量 holder 的问题。这里有个 case 。法度模范 员写代码时,在局部变量创建完之后可能做很多事情,可能去解析请求的用户来源是谁,解析完后面肯定要用,那我肯定会往后面的 copy 去传,但这样其实是有问题的。就是因为这些代码全是异步的,是一个个代码块在底层的调剂 器做调剂 ,那这就有一个问题是代码块在底层做调剂 的 stack 上分派 的变量可能已经被释放了,所以会导致这个 Future 在调剂 的时候 task 变量已经没有了。所以在做这个事情的时候我们就发明 写 Future/Promise 对法度模范 员有一个要求是尽可能用栈上的变量。这是我们后来才发明 的一个比较小的区隔方法 ,它会导致你没办法跟原来一样写栈变量往下传,很多时候要用堆上的一些变量去传递。

上面讲的是我们编程模型的选择,下面我想分享一下怎么达到高性能。昨天有些演讲嘉宾也说了,现在英特尔已经发明 搞不定单 CPU 去赫兹一直 往上升,摩尔定律终不终结其实年夜 家心里都是有一个小小的问号了。就单 CPU 来看,它的性能已经很难再往上提了,但现在厂商怎么做呢,一个 CPU 尽可能 pack 更多的 CPU 进去,比如早些年我看这个24核、32核、64核感到 挺多了,现在 AMD 直接有两三百核来提升单机办事 器的性能,然则 我们在实践的时候却发明 我们在用这么高端、这么年夜 量 CPU 的机器的时候,性能却是没有呈线性提升的,就是在办事 器 Scale Up 之后,性能不是平行扩展的。
后来我们发明 一个比较年夜 的问题,之前也有很多年夜 牛说的一些Cache 问题,就是这个内存和 CPU 之间的高速通路其实是异常 拥堵的,特别在 CPU 变多了之后这条路就更堵了,所以在写高性能法度模范 的时候,Cache 异常 的重要。另外就是锁,当 CPU 变多了之后,这些锁之间的竞争开销会导致机器没办法完全施展 出硬件的性能。那现在业界算是公认比较好的能够解决现在单机性能瓶颈的一个模型就是 Shared-Nothing。每个 CPU 核上尽可能只干自力 完整的一件事,当然这个事情说起来比较简单。年夜 家写法度模范 都邑 发明 有很多需要共享,就是全局的一些变量,比如我的用户信息肯定是全局共享,弗成 能每一个 CPU 存一份,因为这样会导致很多不一致的情况。比如最简单的金融付费,一个用户付完费了,其他核心肯定也要看到这个结果,那么这就有一个问题:全局的器械 怎么办?

这个是 Shared-Nothing 简单的框架图,就是每个进程和线程之间都尽可能规避这个器械 。每个器械 做一个事情怎么做呢?其实我们现在网络层就是通过 Reuse Port 去做,异常 简单,直接这么去导就行了,通过这样我们网络收发就可以线性了,但全局变量就不可 ,那我们怎么做?就是有一些异步的进程去处理这一部分的能力,通过 Reuse Port,网络部分有自力 的线程去收发,但异常 重、异常 庞杂 的器械 就通过自力 的 worker 去处理这些全局的内容,但在这个进程 中是没有锁的,中间会通过无锁队列去传递我想要的信息。比如我需要获取一个用户的信息,通过无锁队列这边网络收发完之后,再向这边发送一个消息,之后全局的数据处理完之后再通过无锁队列返回来,这样中间是没有任何的锁和增强的开销的,这样就达到全局数据的共享,这就是我们解决全局数据的办法 理念。
但如果读取异常 频繁的情况下怎么去解决呢?我们在每一个收发线程上还会做一些 ThreadLocal 的快照,发曩昔 之后拿回来的器械 在本地做缓存,每个线程上去做缓存,这样就解决了需要频繁读写的问题。熟悉 Kernel Reuse Port 的同学可能知道 Reuse Port在处理很多 case 的时候是没办法完整地连续 分发的,它只会依据IP 和 Port 去做。但这有一个问题是,比如现在在4G和wifi之间做切换,IP 和 Port 会变对吧,那变了之后再去做分发的时候,做Shared-Nothing 很重要的一个事情是用户请求一定要转发到同一个线程进程里面。那如果网络一变,它通过 Reuse Port 的方法 就不会转发到同一个里面去了,那这种问题在正常 TCP 链接的部分不会涌现 ,因为 TCP 就是依据IP 和 Port去界说 链接的,但在新的一些业务场景比如 Quic、UDP,会涌现 一个问题:它不是通过 IP 和 Port 来界说 一个链接,而是通过协议自带的信息去做,所以在原有的内核的 Reuse Port 方法 就完全失效了,没办法做 Shared-Nothing,于是我们在中间内核做了一些模块,依据 一些 UDP 协议信息做转发,选择一些 ID 去做。这也是我们在做 Shared-Nothing 时遇到的链接转发的一个问题。

这里我还想讲我们在写的时候遇到另外一个 Shared-Nothing 的问题是原子变量。Share Pointer 年夜 家应该都不陌生,但其实问题挺多的,就是我去做 Atomic Pointer 的时候,它只包管 了引用计数的原子性,但在多个线程同时去读写 Share Pointer 时,它不是原子的,不掩护 那个指针,只掩护 它的 reference counter。C++ 后来也出了那个 Atomic Share Pointer,早期是通过 atomic_load 的方法 来掩护 。这是现有的做 Pointer 在一个线程之间能够达到平安 共享的两种方法 。
但虽然有这些器械 ,用了之后我们发明 问题异常 多:第一,它的 referencecounter 会导致 Cache 涌现 问题。比如用 Share Pointer 时一个典范 的场景:多个代码块之间都要读取这个指针,但在用这个指针时,原子变量增加和读写都要刷 Cache,导致 Cache 失效,那么就算我在单一线程里面做共享的时候也会发明 这个器械 导致 CPU 性能年夜 范围 地下降,所以在没有做跨线程这种 case 的时候,我们把 Reference Pointer 改成了正常的数据和变量,就不消 那个原子的引用计数了,只用一个简单的 int 去做引用计数。
那我怎么规避不合进程之间共享呢?其实我不规避,因为我会包管 它只在本进程的不合的上下文去共享,然后针对不合进程的共享,我们会采取跟 atomic_load 一样的方法 ,会去加一个锁,然则 这个锁是有点类似于全局的部分,全局有一个 Local 的池,这个池子里我会尽可能规避这个开销。
04 未来展望

简单总结一下,性能部分我们主要是通过 Shared-Nothing 去解决,中间也解决了很多问题,包含Shared_Ptr、内存治理 ,包含Exception Handle 的问题,这一部分是要一直 地去深挖每一个细节。性能是需要一直 地去优化一个个 case 解决的,不是有一个架构就能解决所有问题。另外是可维护性,我们是通过 Future/Promise 来解决。我们抛弃了原有异步回调的方法 ,通过 Future/Promise 去一直 地去串联代码,尽可能达到线性写代码的方法 。

当然 Future/Promise 有一个问题是,年夜 家都这么串行写代码,最后代码会异常 长,可能会集中在一个代码块里面,这样对代码的规范治理 是要求异常 高的,所以在选择对象 的时候,年夜 家可能要考虑一下配套的代码治理 对象 ,包含 代码的规范。我们认为这个在写 Future/Promise 的时候会写很多,即使有异常 好的代码规范和运营方法 也会涌现 很难维护的问题,所以我们希望做到的方法 是它能够完全没有任何器械 ,有点像现在 C++ Coroutine 做的事情,就是在一个代码块里就把事情全部解决。

但现在 Coroutine 问题也挺多,C++ Coroutine 的代码除了前面加 co_await 之外,跟正常的代码没有差别,但这对开发人员要求异常 的高,你需要去实现 Awaitable 的成员函数,更像是说针对库开发者提供的,而不是向法度模范 员开发提供的。比如你去实现四个异步函数,就要实现四遍 Awaitable 的对象,那我要实现10个、20个呢?这个庞杂 度异常 高。另外一个问题是做 Awaitable 的时候,比如我的调剂 函数里面有一个 func 1,再去失落func 2,再去失落func 3。假设 func 3 要去做成 C++20 Coroutine 的时候,反过来可能 func 2 用户也不需要知道我这器械 也能去 co_await,那 func 2 即使内部没有其他器械 是 Coroutine,我也需要去实现 Awaitable,相当于整条挪用 链上的器械 都需要去实现,即使我只有一个函数需要做异步。

可能我们未来办事 器上有很多器械 要去做,包含 现在云原生比较火的一些概念,就是这办事 器怎么去做到开包即用,怎么去给到用户就能用上。上午冯富秋老师也说我们怎么尽可能开放硬件上的能力,软件跟硬件怎么去做结合,怎么尽可能去提升这部分的最年夜 化收益。这些是现在包含Kernel 和社区也都在讨论的 Kernel Bypassing,就是怎么绕过内核,尽可能直接触达硬件。另外就是新的一些硬件也在一直 涌现 , 比如解密卡,包含FPGA 这一部分,就是我可以在硬件上写我法度模范 ,写完之后你直接去用。
最后,很多时候我们希望这个办事 是优化部分,就是一些可中断的办事 ,可能有一些器械 不是特其余 重要,这一部分是可以抛弃的,可以中断失落 的,就比如我日常离线的一些计算,离线的一些压缩,在正常办事 器执行进程 中,假设低负载可以尽可能利用的时候,我可能会去跑,但如果没有事情的时候我就希望他尽早停失落 ,就是可中断办事 的支持。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|小黑屋|货拉客微商论坛 |网站地图|网站地图

GMT+8, 2024-9-22 04:06 , Processed in 0.108317 second(s), 20 queries , Gzip On.

Powered by Huolake! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表