本文系笔者根据 2022 年 12 月 12 日在北京大学的演讲整理,首先将会议录音使用科大讯飞语音识别转换成口水稿,然后用 GPT-4 加以润色,修正语音识别的错误,最后人工加入一些新的思考)

非常感谢黄群教授和许辰人教授邀请,很荣幸来到北京大学为两位教授的计算机网络课程做客座报告。我听说你们都是北大最优秀的学生,我可是当年做梦都没进得了北大,今天能有机会来跟大家交流计算机网络领域学术界和工业界的一些最新进展,实在是非常荣幸。

图灵奖得主 David Patterson 2019 年有一个非常有名的演讲,叫做《计算机体系结构的新黄金时代》(A New Golden Age for Computer Architecture),它讲的是通用处理器摩尔定律的终结和领域特定体系结构(DSA)兴起的历史机遇。我今天要讲的是,计算机网络也进入了一个新黄金时代。

我们日常接触到的计算机网络主要由三大部分组成:无线网络、广域网和数据中心网络。它们为万物互联的智能世界提供了通信基石。

其中,无线网络的终端设备包括手机、PC、手表、智能家居、智能汽车等各种设备。这些设备通常是通过无线方式(如 Wi-Fi 或 5G)访问网络。经过 5G 基站和 Wi-Fi 热点之后,设备将进入广域网。广域网中还有一些 CDN 服务器,这些服务器属于边缘数据中心。接下来,设备将进入数据中心网络。在数据中心网络中,还有许多不同类型的设备,如网关、服务器等。

今天,我将分别从数据中心网络、广域网和终端无线网络这三个领域给大家做一些介绍。首先,让我们来看数据中心网络。数据中心网络最大的变化是从为简单的 Web 服务设计的简单网络,演变成为大规模异构并行计算所设计的网络,执行 AI、大数据、高性能计算等传统上超级计算机才能处理的任务。

从 Web 服务到大规模异构并行计算

传统的数据中心网络是为了易于并行处理的 Web 服务而设计的。当我们的手机访问数据中心网络时,首先会进入一个四层负载均衡器,然后再进入一个七层负载均衡器。四层负载均衡器负责将不同的连接分配到不同的服务器上,而七层负载均衡器则可以进一步按照一定策略将这些连接分发到业务的 Web 服务器上。Nginx 就是一个典型的七层负载均衡器。

我们业务的 Web 服务器通常使用 Python、Java 或 Node.js 等语言编写。Web 服务器在处理 HTTP 请求的业务逻辑时,可能会访问内存缓存服务器,也可能会访问数据库。为了容灾,数据库服务器一般有多个副本,例如在上图中,白色表示主节点,黑色表示备份节点。

事实上,传统的 Web 服务对于数据中心网络的延迟并不敏感。例如,我在学校开发的一个使用 Flask 制作的网站,打开一个网页的延迟基本上是秒级的。而处理网页的延迟在百毫秒量级,用户很难感知到百毫秒以内的延迟。广域网的延迟通常在几十毫秒到几百毫秒量级,数据中心内网的延迟即使是毫秒级,但相较于百毫秒级的数据处理延迟(包括数据库延迟以及业务处理延迟),其实都是一个较小的数量级。因此,数据中心网络的延迟在这种场景下并不是一个关键因素。

从上图中我们可以看到,整个网页的后端渲染时间花了 1000 多毫秒,其中 Flask 的后端业务逻辑占用了 134 毫秒的 CPU 时间,剩下的大部分时间都花在数据库查询,也就是 SQL 语句的执行上。有些数据库查询可能优化得不太好,时间较长,需要 720 毫秒;而有些查询可能只需要几十毫秒甚至几个毫秒。

刚才我们讲到,Web 服务对于数据中心网络的延迟并不敏感。另一方面,Web 服务也相对容易扩展,也就是容易通过增加机器数量的方式来提升整体的性能。

具体来说,当一个应用服务器不够用时,我们可以增加负载均衡器的数量。正如之前提到的,四层负载均衡器可以通过硬件实现负载均衡,而七层负载均衡器可以通过软件将不同的网络连接分散到不同的业务服务器上。这样一来,可以实现非常高的并发程度。在业务的 Web 服务器上,每一个 HTTP 请求实际上都可以并行处理。

而在内存缓存服务器上,由于单个服务器的容量和处理能力都无法满足所有用户的需求,所以实际上会有多个内存缓存服务器。一般来说,内存缓存服务器中存储了大量的键-值(key-value)映射,其中的键(key)和值(value)可以简单理解成是一个字符串。这样,内存缓存服务器就可以根据键(key)的哈希值进行分片,也就是每个键值服务器负责一个范围的键值映射。

数据库服务器在实际应用中,通常会有多台,一方面是为了扩展数据库容量与处理能力,另一方面是为了容灾。当一个数据库在单台服务器上容纳不下时,数据库管理员(DBA)需要把数据分配至多个服务器,也就是所谓的分库分表。具体而言,数据的划分方式可以是横向划分,也可以是纵向划分。通过对多个表的划分,数据库不仅扩大了容量,还增强了处理能力。

如果仅仅考虑传统的 Web 服务,现有的数据中心网络和内核的 TCP 协议栈已经足够应对需求。那为什么说计算机网络进入了一个新黄金时代呢?不管网络、系统还是体系结构,最大的两个驱动力始终是应用需求和硬件能力。

数据中心网络能够发展得这么快,根本原因是人工智能(AI)、高性能计算(HPC)、大数据、存储等新兴业务对带宽和延迟提出了更高要求。而我们的网络硬件性能还没有触到天花板,可以匹配上应用的需求。新的网络硬件又对上层的软件和系统带来新的需求,这就带领整个行业不断有新的事情可做。

以 2022 年的 stable diffusion 生成模型为例,这个计算机视觉(CV)生成模型令人叹为观止,能根据用户输入的提示词(prompt)生成精美图片。该模型的训练使用了 256 个英伟达 A100 GPU,耗费 15 万 GPU 小时,共计花费 60 万美元。

两周前(注:相对 2022 年 12 月 12 日),OpenAI 发布了 ChatGPT,一个基于 GPT-3.5 和 InstructGPT 的 AI 问答系统。对于 ChatGPT,许多人可能尚不了解。事实上,ChatGPT 具备强大功能,首先是其强大的思维链能力。给定提示词,它可以根据用户的思路生成回答。其次,它具备出色的代码逻辑思维能力,能编写如 Protocol Buffers 地址簿等代码。此外,它还能帮助修复代码中的 bug。相比传统的 NLP 大模型,一个显著优势是其卓越的记忆能力,能够回顾前面的内容,解决指代的难题。在过去的自然语言处理(NLP)领域,各种任务需分开处理,而在 ChatGPT 中,所有这些问题都得到了统一解决。尽管 ChatGPT 目前尚未广泛普及(注:2022 年 12 月 12 日原演讲内容),但未来它必将成为颠覆性的创新。

GPT-3.5 的上一代模型 GPT-3 已具备 1750 亿参数,单是推理就需要 350 GB 的 GPU 内存,单张 GPU 卡肯定是放不下的,需要多张 GPU 卡组成分布式推理集群。训练需要存储反向传播中的状态,需要的内存容量更大。这一模型的训练成本高达 1200 万美元,可能需要上万块 GPU 组成的集群来做训练。虽然 GPT-3.5 的规模尚不明确,但很可能也是一个庞大的模型。为什么模型需要这么大,其根本原因是存储的知识多,上知天文,下至地理,还懂多种语言。

我们来看一下这些 AI 模型对于计算能力和内存需求的增长趋势。在 2012 年,最早的 AlexNet 问世;到了 2014 年,有了 GoogleNet;之后是 2015 年的 Seq2Seq,2016 年的 ResNet。到了 2018 年出现了 AlphaGo 和 AlphaZero,最后在 2020 年出现了 GPT-3 等。我们可以看到,从 2012 年到 2020 年,这些模型对算力的需求增长速度非常快,翻了 30 万倍。这个增长速度并不像摩尔定律那样每 18 个月翻一倍,而是在 18 个月内翻 40 倍,非常惊人。

然而,我们的 AI 算力,即单个 GPU 的算力增长速度,实际上还是相对遵循摩尔定律的,因为它毕竟是硬件。这么巨大的需求差距要求我们必须采用分布式并行计算来满足这些需求。

从另一个角度来看,大模型的内存需求也非常大。我们可以看到,不同的模型对内存的需求也在增长,越大的模型通常意味着越高的内存需求。例如,我们之前提到的 ChatGPT,在单个 GPU 中根本放不下,需要更多的 GPU 资源才能完成推理。

训练需要的集群规模就更大了,ChatGPT 很可能是用成千上万张卡并行计算的。这些 GPU 之间需要的通信带宽是极高的,需要达到 100 GB/s 以上,远远超过目前以太网或者 RDMA 网络所能提供的带宽。因此,新型模型对数据中心网络的需求极高。目前,小规模 GPU 集群内的通信是通过超高带宽的 NVLink 专用硬件进行的,带宽最高可达 900 GB/s。但是 NVLink 只能用于几百块卡的小规模互联,大规模互联仍然要通过比 NVLink 慢几十倍的 Infiniband 网络,带宽只能达到 20 GB/s。

传统的网络协议设计于上世纪七八十年代,那时的处理器速度的发展速度超过网络的发展速度,而且应用对网络通信的时延和带宽并不敏感,因此网络协议都是 CPU 上的软件在处理。CPU 上的软件栈实际上相当复杂,开销非常大。

虽然数据中心网络硬件的延迟是微秒级,但是由于网络协议栈的复杂性,到了应用层,延迟可能会增加几十倍,甚至在 RPC 层次上达到百微秒级别。打个比方,从北京到天津,坐高铁只需要 30 分钟,但由于在市区内需要乘地铁花费两个小时,所以端到端的时间可能需要四个小时。谷歌曾经统计过,大约 25% 的 CPU 资源被浪费在了网络协议栈上,包括内存拷贝、RPC 调用、序列化、内存分配和压缩等。谷歌将其称为 “数据中心税”,意味着这个负担太重了。

实际上,即使我们将延迟降到微秒级,许多应用也无法充分利用这个低延迟。其中一个原因是延迟隐藏问题。当我们等待这些微秒级事件时,比如进行网络通信或者存储设备的读写,CPU 应该去执行什么操作?在传统的计算机体系结构中,CPU 的流水线能很好地隐藏纳秒级事件。也就是说,在等待这些事件时,CPU 可以执行其他指令,而不需要软件干预。然而,对于微秒级时延的事件,CPU 的流水线深度不足以隐藏时延,因此 CPU 就会卡住等待这个慢速的指令,其效率是很低下的。

对于毫秒级事件,操作系统可以处理这些任务切换,这是软件的职责。操作系统在进行进程或线程切换时,只需几个微秒就能完成,因此不会浪费太多时间。但是,如果事件本身就是微秒级的,比如访问存储花费 10 微秒或访问高速网络花费 3 微秒,这时候如果将 CPU 核切换到其他进程或者线程上,操作系统本身就要花费几个微秒的时间。操作系统切换到其他进程或者线程,再切换回来,这个过程花费的时间可能比直接等待事件完成还要长。所以,对于微秒级的时延,操作系统的任务切换实际上是在浪费时间。

体系结构和操作系统的机制都不能隐藏微秒级的时延,而如今最先进的网络和存储硬件时延都降到了微秒级,这就是微秒级时间隐藏难题的原因。

编程抽象:从字节流到内存语义

接下来,我会介绍一些解决上述问题的方法。首先我们从编程抽象的角度来考虑。编程抽象是任何系统的关键部分,它涉及到设计功能以及相应接口。有一条著名的建筑学上的设计原则,“form follow function”(形式遵循功能),也适用于软件工程领域,也就是接口设计一定要遵循功能的描述。

我们很多人,包括我自己,都比较习惯在别人的生态基础上做改进,总是想要保证兼容性,提升一点性能,但不敢提出自己的生态,这就是一种生态迷信。历史上有巨大影响力的系统大多数不是把现有系统的性能提升了 10 倍,而是提出了自己的系统和编程抽象,满足了新的业务需求或者用好了新的硬件能力。

原来我们用的都是基于字节流语义的 socket,这是一个很漂亮的编程抽象,体现了 UNIX 操作系统 “一切皆文件” 的设计原则,把网络通信管道也当成是文件了。但是,字节流的抽象在很多场景下并不好用,它没有消息边界的概念,在发送端 send 1024 字节,在接收端接收到的可能不是 1024 字节的一个整体,而是分成了两块。这样就需要应用自己去做消息重组的事情。

为了解决这个问题,一些新的编程抽象逐渐引入了消息边界的概念。例如,ZeroMQ 和 RabbitMQ 等消息队列系统,它们提供了基于消息的抽象。在这种抽象中,数据被组织成一个个独立的消息,每个消息都有明确的边界。这使得应用程序不需要关心如何保证消息边界的问题。这种新的抽象使得分布式系统的开发变得更加简单和高效。

如果我们最终的目的就是为了提供基于消息的语义,为什么需要先提供一个字节流的语义,再在上面封装一个消息语义呢?因此,一些最新的网络协议栈直接在不可靠的数据报文基础上构建消息语义。其中典型的一个例子就是 RDMA(远程直接内存访问)。RDMA 支持一种称为 “发送-接收”(Send-Receive)的通信模型,其中发送方将消息放入一个发送缓冲区,然后让网卡发送,接收方首先将接收缓冲区注册到网卡,然后网卡将数据写入接收缓冲区,并通知接收方。这种提前注册缓冲区的方法避免了基于字节流的消息语义中数据必须拷贝的问题,进一步提升了性能。

在消息传递之外,我们还有一个更进阶的编程抽象,那就是远程内存访问。无论是字节流还是消息语义,一个程序通信的对象一定是另一个程序,但为什么我们不能让程序直接访问远程的内存呢?也就是说,在网络上发出直接读写远程内存的指令。我们可能会问,这种远程内存访问的语义有什么作用呢?

首先,我可以让多个程序共享一块远程的内存,也就是将一块内存当作多个节点共享去访问,这样的一种方式被称为内存池化。其次,如果对面的应用程序里面存储了一些数据,比如一个哈希表,那么访问这块数据的过程中可能不需要引入对方的 CPU,也就是在不打扰对方 CPU 的情况下完成了数据结构的增删改查。当然,不打扰对方的 CPU 需要硬件的支持,这也就是 RDMA(远程直接内存访问)中的 read、write 以及 atomic(原子操作)等技术。例如,原子操作是我们可以在本地发起,但在远程完成的内存操作。

RDMA 的这些远程内存访问操作都是异步完成的,而本地内存访问操作是同步的。有人提出,既然本地操作可以同步,为什么不能把同步的内存操作引入到远程呢?当然,在 RDMA 的情况下,因为延时较高,不太适合把操作变成同步的。因为同步操作意味着阻塞 CPU 的流水线,也就是说,CPU 在等待访问时无法执行其他任务,这会导致效率低下。

在诸如 CXL 这样的新型互联总线中,因为实现了极低的延时,比如只有两三百纳秒,比 RDMA 的延时低一个数量级,这个时候,我们就可以引入 load、store 这样的同步操作。也就是说,让 CPU 直接访问远程的内存,就像访问本地内存一样,应用程序无需进行修改。这样的做法极大地方便了应用程序的编程,但它带来的后果是访问远程内存的粒度较小,因此它的效率可能会比异步访问的方式更低一些。

在字节流消息和远程内存访问之外,我们还有一种称为 RPC 的通信方式,实际上是程序与程序之间的一种交互,也就是远程调用。程序语言里的函数调用我们都很熟悉,它就是一个本地调用。如果我们想要调用另一个程序里的函数,就需要通过 RPC 的方式去实现。现在有很多的 RPC 框架,如 gRPC 和 Thrift 等。

我们将消息语义、远程内存访问和 RPC 统称为内存语义。这个内存语义的定义可能比一般人认为的更加广泛,也就是说,大部分人认为只有程序直接访问远程内存才叫做内存语义,但实际上,我们认为消息语义和 RPC 也属于内存语义,因为它们都涉及到程序与内存之间的交互。

那么,为什么我们需要内存语义而不是字节流呢?首先,刚才我们讲过,字节流的抽象层次太低,没有消息边界的概念,所以应用程序需要进行封包和拆包,增加额外的内存拷贝。

其次,字节流是一个有序的流,所以不能充分利用通信中的并行性,例如利用多条物理链路,数据中心里面有不同的物理路径,无线网络有 Wi-Fi 和 5G。

同时,字节流无法区分消息的重要性和优先级需求,所以在同一个流(同一条连接)内不能按照优先级进行服务。如果要区分优先级,那就只能建立多条连接,这就意味着需要维护大量的连接状态,并为每个连接预留缓冲区,导致内存占用量较大。内存占用量大不仅仅是内存开销的问题,同时也会占用大量缓存,导致缓存命中率降低,从而降低整个网络协议栈处理报文的性能。

RDMA 技术实际上是一种软硬结合的高效实现内存语义的方法。在同样的硬件下,如果使用传统的 TCP socket 协议栈,需要大约 30 微秒的时间,但如果使用 RDMA 协议,它只需要 2 微秒的时间,快了 15 倍。这是因为它将内核中原本由软件完成的很多工作放到了硬件中去完成,通过硬件加速的方式提高了速度。

尽管 RDMA 性能很高,但它的使用相对 TCP socket 较为复杂。主要原因有:首先,RDMA 内存需要事先注册到网卡才能访问,这些内存需要被绑定,也就是不能进行换入换出,因为网卡使用物理地址而不是虚拟地址来访问内存。第二,为了实现零拷贝,发送和接收操作都是异步的。在发送端,可以异步等待发送完成,而接收端需要事先指定接收缓冲区,并异步等待网卡生成的接收完成事件。为了实现内核旁路传输协议栈,还需要创建注册到网卡硬件的发送和接收队列,以在用户态与网卡直接交互。

从上图中,我们可以看到在使用 RDMA 时,在控制面上,首先需要创建很多的发送和接收队列以及资源,然后还需要通过一个带外通道来交换对方的信息。最后,还需要把队列状态设置为 ready to send,也就是准备好发送的状态。在数据面,还需要准备好很多内存缓冲区,然后才能发起请求。在发起请求之后,还需要等待完成事件。整个过程是一个异步的相对复杂的过程。最后,在使用完 RDMA 后,还需要释放所有的 RDMA 资源。

总之,内存语义相对于字节流具有更高的抽象层次和更好的性能,可以更好地适应现代高速网络通信的需求。尽管 RDMA 等技术的使用相对复杂,但它们仍然在很多场景下提供了显著的性能提升。为了降低 RDMA 的使用门槛,现在也有很多工具可以简化它们的使用,例如把 socket 转成 RDMA 的 SMC-R 和 rsocket 等技术,以及 FaSST RPC、eRPC 等基于 RDMA 的 RPC 库。

相比 RDMA 这种比较复杂的异步的远端内存访问,实际上 CXL 和 NVLink 这种 Load/Store 就是一种更简单的内存访问方式,也就是同步的访问方式。为什么它会更简单呢?我们从上面这张图可以看到,在 RDMA 中如果想发送一个数据,那么:

  1. 软件首先会生成一个 WQE(work queue element),就是工作队列里边的一个工作任务。
  2. 然后这个任务再下发一个 doorbell,就是按一个门铃到网卡告诉说我有事情要做了。
  3. 接着,网卡在收到这个门铃之后会从内存里面把这个工作任务取到网卡里面。
  4. 然后再根据工作任务当中的地址,访问内存中的数据,把它 DMA 到网卡。
  5. 再接下来,网卡会把这个数据封装成一个网络报文,从本地发送到远端。
  6. 然后,接收端的网卡在收到了这个数据之后,再把它写到远端的内存。
  7. 接着,接收端的网卡返回一个完成消息说我干完了。
  8. 发起端的网卡收到了这个完成消息之后,它就在本地内存中生成一个CQE。
  9. 最后,应用需要去 poll 这个CQE,也就是说它要获取这个完成队列里的完成事件才能够完成整个过程。

我们可以看到,整个过程非常复杂。

而 CXL 和 NVLink 就没有这么复杂了,因为它的 Load/Store 是一个同步的内存访问指令,也就是说 CPU(对 CXL 而言)或者 GPU(对 NVLink 而言)有一个硬件模块能够直接访问网络单元。那么这个指令就可以直接去访问远程的内存,而不需要经过 PCIe,这样就不需要 WQE、CQE 还有 doorbell 的这些开销,整个的时延可以降低到 0.5 us 以下。整个过程实际上只需要 4 步:

  1. 应用发一个 Load/Store 指令;
  2. CPU 中的网络模块发起一个 Load 或 Store 网络报文,在网络上面获取或者传送数据;
  3. 对方的网络模块会做一个 DMA,把对应的数据从内存里面拿出来;
  4. 通过网络回馈给发起的网络模块,然后CPU的这条指令就宣告完成,可以继续进行后续的指令了。

因为 CPU 有一条流水线,本来就能够去隐藏一些同步访问指令的延迟,所以它不需要把流水线卡死。但是,它会降低 CPU 流水线的并行度,因为 CPU 的流水线深度是有限的。

总的来说,同步和异步远程内存访问各有优缺点。同步远程内存访问,如 CXL 和 NVLink,使用简单的 Load 和 Store 操作,可以在不需太多复杂步骤的情况下实现远程内存访问。这种方式相较于异步远程内存访问,如 RDMA,更加简单,但也有一定的局限性。

同步远程内存访问的优势在于:

  1. 过程简单,交互流程简洁,使得访问延迟较低。
  2. 对应用程序来说是透明的,可以用来扩展本地内存,而不需要修改应用程序。
  3. 在访问较小数据量时,效率可能更高。
  4. 在硬件支持的情况下,可能支持缓存一致性。

同步远程内存访问的劣势包括:

  1. 对硬件要求较高,需要网卡与 CPU 紧密配合。
  2. 每次访问的数据量相对较小(通常是一个缓存行,如 64 字节),因此在访问大数据量时,效率可能不如异步远程内存访问。
  3. 同步远程内存访问的可靠性可能较差,因为一个节点故障可能会影响到使用了该节点所贡献的远端内存的所有节点。有一个所谓 “爆炸半径” 的概念,远端内存如果发生故障了,影响的不只是自己这个节点,这就会导致爆炸半径增大。
  4. 大规模下的缓存一致性开销很高。

异步远程内存访问的优势在于:

  1. 用户可以指定访问的数据量大小,从而在访问大数据量时,效率可能更高。
  2. 对硬件要求相对较低,网卡可以采用分离式形态,如 PCIe 接口的网卡。
  3. 可以通过应用捕获异常,从而将影响范围缩小到受影响的应用。

异步远程内存访问的劣势包括:

  1. 过程较复杂,涉及到与网卡的复杂交互,导致访问延迟相对较高。
  2. 对应用程序来说不是透明的,需要显式访问远程内存,因此如果用于扩展内存,需要修改应用程序。
  3. 不支持缓存一致性,需要靠软件在远端和本地内存之间进行拷贝,并在共享内存情况下配合分布式锁来保证一致性。

根据实际应用场景和需求,开发者可以选择适合的内存访问方式。对于需要访问较小数据量且对延迟要求较高的场景,同步远程内存访问可能更合适;而对于需要访问大数据量且对延迟要求不高的场景,异步远程内存访问可能更为有效。

值得注意的是,目前 NVLink 上的很多通信仍然是基于同步 Load/Store,这一方面是因为 GPU 的核数多,浪费一些核用于通信不太可惜,另一方面是因为 NVLink 规模小,时延低,只有几百纳秒。NVLink 虽然宣称支持一致性,但是它其实是以页面(page)为粒度、仅支持一个共享者的受限一致性,避开了分布式缓存一致性领域多个共享者如何存储共享者列表的经典难题。由于 GPU 主要用于 AI 和 HPC 场景,NVLink 上的通信主要是集合通信,而不是数据共享,因此受限一致性基本上是够用的。

接下来我们再来看一下 RPC,RPC 的全称是远程过程调用,它实际上就是跨主机、跨进程以及跨语言的函数调用。RPC 在数据中心内部的通信中非常重要,它其实是一个非常通用的东西,各种微服务之间的通信都是通过 RPC 的。谷歌有个统计,95% 的数据中心流量都是 RPC。

前段时间,我们看到埃隆·马斯克在 Twitter 上说 RPC 特别重要,甚至有一个工程师质疑他说 RPC 实际上根本不重要。然后马斯克直接就把他给炒了。后来他又补充说,就是在一次网络访问中,比如访问一个 Twitter 的首页,可能需要 1200 个 RPC 左右。这也就说明,我们对于 RPC 去做一些加速,是非常有价值的。

比如说我们现在的 gRPC,就是当前应用最广泛的一个 RPC 框架,它的性能比较差。比如说一个简单的 Hello World 程序需要 250 微秒,它实际上是达到了数据中心网络硬件时延的 50 倍。那么为什么它的开销这么高呢?传统上我们都认为可能是因为传输层的原因,但其实它有很多问题。

  • 首先,最大的一块,实际上是在序列化和反序列化这个地方,它实际上用的是 Protocol Buffers 来做序列化。这个序列化方案是为广域网设计的,采用了变长编码,所以效率比较低。我们在数据中心里,带宽其实是比较充足的,因此不需要为了节约带宽浪费很多 CPU 去做这样的编码压缩。
  • 接下来就是传输层里面,因为 gRPC 采用了 HTTP/2 作为传输层,在广域网中的可用性比较强,可以复用各种负载均衡等基础设施。但是 HTTP 协议的解析开销非常高,整个过程包括 HTTP 和 TCP 协议加起来可能达到了将近 200 微秒。
  • 再接下来还有一块开销,就是分发调度,首先用软件把请求从 TCP 协议栈里面收上来,然后再把它分发到各个工作线程,这个过程中存在很多瓶颈。

学术界在高性能的 RPC 方面已经做了很多探索。比如说一个较早的工作,叫做 FaSST RPC,它是基于 RDMA 的 RPC。我们知道 gRPC 实际上是仅仅支持 TCP/IP 协议的,而 FaSST RPC 使用了 RDMA 之后,把传输层卸载到了硬件,就可以达到 5 微秒级的端到端延迟,性能比 gRPC 高大约 50 倍。最近的一个工作是 eRPC,它可以基于 Packet I/O 运行,这是另外一种思路,相当于是把传输层放在软件里面,但是它不是在内核中实现,而是在用户态实现,这是一种用户态 I/O 的思路。这种方法其实也能达到很高的性能。

在序列化这一方面,我们有像 Flat Buffers、Cap’n Proto 这些序列化工具,它不需要对数据进行额外的编码压缩,而且在创建数据结构的时候就把数据连续地存储在内存中,从而数据不需要在不同的数据格式之间来回做转换,这样它的效率就比较高。Flat Buffers 和 Cap’n Proto 不仅免除了序列化的工作,也免除了反序列化的工作,在访问数据结构的时候直接从连续内存中读取,而无需创建大量零散的语言原生对象。

在传输层这一方面,刚才讲的像 FaSST RPC 和 eRPC,它们都是为数据中心优化的,或者基于 RDMA 网卡实现硬件卸载,或者基于 Packet I/O 的用户态协议栈,来消除内核 TCP/IP 协议栈的开销,从而使得延迟能够降低到微秒级。

而在分发调度这一方面,网卡硬件在 Packet I/O 模式下有 RSS(Receive-Side Scaling)的能力,使用 RDMA 的时候 QP 也天然自带分发的能力,这样我们可以使得请求从客户端直接到达 RPC 执行线程,避免软件集中式分发 RPC 请求。

把数据中心作为一台计算机

数据中心网络有了这样的一种内存语义的编程抽象,我们就可以把数据中心作为一台计算机。

在数据中心作为一台计算机里面,我们主要考虑的是两个方面,第一是让数据中心互联像一台计算机的内部总线一样高效,第二是数据中心内的分布式系统编程像单机编程一样便捷。

让数据中心互联像计算机内部总线一样高效

从以 CPU 为中心的体系结构开始,我们把它发展到以异构的对等互联的体系结构。目前,CPU 是计算机体系结构中绝对的老大哥,但在新的体系结构里面,我们会有各种各样的异构计算设备,比如说包括 CPU、GPU、NPU(神经网络处理器,即AI芯片),以及 DPU(数据处理器),FPGA 等等。同时,还有很多不同的异构存储设备,包括 HBM、DDR、非易失性内存(NVM)、Flash SSD 等等。

当前,数据中心的存储集群和计算集群已经实现了分离,在很多先进的数据中心当中,存储和计算集群之间使用 RDMA 进行高效通信,但是其他设备的异构对等互联尚未完全实现。

我们为什么要实现所谓的对等互联呢?这是因为以 CPU 为中心的体系结构在很多时候需要 CPU 进行中心化转发。例如,假设有两台机器,其中一个 GPU 需要与另一个 GPU 通信,如果这两个 GPU 之间没有直接的互联,那么它们可能需要通过 CPU 转发到网卡,然后再通过 InfiniBand 或者 RoCE 或者以太网等网络通信。如果两个 GPU 之间有 NVLink 互联,它们就可以直接互相通信。但是 NVLink 的互联范围是有限的,只能互联几百张 GPU 卡,超过了这个范围,又需要 CPU 和网卡去做转发。

而在对等互联的体系结构中,我们不再有 GPU 和 GPU 之间通过 NVLink 互联或者通过 Infiniband 互联这样的差异,它们会统一连接到一个高速的交换网络。实现设备直通后,可以大大扩展对等互联的规模。这样,计算集群在大规模任务下的性能将大大提高,对大模型训练有很大帮助。

另外一个重要的趋势是分离式内存,即 disaggregated memory。谷歌的统计显示,数据中心内存的成本占到服务器总成本的 50%;Meta 的统计也显示,数据中心内存的成本占到机架总成本的 40%。因此,内存分离以及多种内存介质的融合将成为降低硬件成本的一个重要趋势。

所谓的分离式内存是指每个机器都有一小块空间作为私有内存,然后通过高速网络连接到共享内存池,大部分的内存都在共享内存池中。然而,这种方案的可行性可能并不高,因为内存所需的带宽很高,时延很低,目前网络的带宽和时延比内存相去甚远,可能会导致性能下降。这就像是盖一座高楼,把所有的厕所都修在一楼。

相对来说,远端内存的方式可能更为可行。每个节点都有自己的 CPU 和内存,但可以贡献一部分内存供其他人使用。当本地内存不足时,可以向其他节点借用内存。我们知道,在公有云中内存的使用率是比较低的,很多内存都是空闲的,这些空闲内存就可以组成内存池。本地内存中存储热数据,远端内存池中存储冷数据。

在任何计算机体系结构设计中,局部性(locality)都是关键。之所以可以实现完全的分离式存储,是因为两个原因:

  1. 存储所需的带宽比数据中心网络的可用带宽低,而且存储介质的访问时延本身就高于数据中心网络的时延,因此性能损失不会很大。即使这样,数据中心存储的流量也已经占据了整个数据中心总流量的一半以上。
  2. 存储需要高可靠性,不能仅仅依赖于本地存储。当服务器出现故障时,本地存储的数据可能会丢失,因此需要将数据复制到多个节点以实现高可靠性。

分离式内存需要的带宽可能高于数据中心网络的可用带宽,而且一般不需要多副本,因此实现它的代价是高于分离式存储的。

除了使用远端内存来扩展本地内存容量外,另一个思路是使用分级内存,即 tiered memory。内存层次结构(memory hierarchy)是从计算机诞生的第一天起就存在的经典问题,可以包括本地内存、远程内存、非易失性内存(如 Intel 的 Optane 内存)以及 SSD、HDD 等。这实际上形成了一个庞大的存储金字塔,我们在计算机体系结构课程中应该都有所了解。在这个金字塔中,上层的性能较高、价格也较高,下层的性能较低、价格也较低。

Jeff Dean 列出了一个很有名的表格 Numbers Everyone Should Know(在 Designs, Lessons and Advice from Building Large Distributed Systems 这个报告里面),讲的就是存储金字塔,其中的一些数字可能今天并不适用,但其中的思想是深刻而永恒的。我们做计算机的一定要对数量级保持敏感,知道一些关键操作和系统组件的时延、吞吐、功耗、价格的数量级,并且能够快速估算一个系统各项指标的数量级,这叫做 Back of the Envelope Calculations,这样才能对系统有更好的感觉。

我们很关心的一个问题是,如果使用分离式内存或分级内存,而不是传统的本地内存,那么与单纯使用本地内存相比,它会对应用的性能造成多大的影响?实际上,造成的影响没有大家想象的那么严重。

有一篇经典的文章叫做 “Network Requirements for Resource Disaggregation”,它说明了在本地内存只有 25% 的情况下(也就是 75% 的数据都放在远端内存里),要达到性能下降不超过 5% 这样的指标,在访存不太密集的应用中,例如 Hadoop,GraphLab,Memcached 等,需要 5 微秒的网络往返时延和 40 Gbps 的带宽。而在对访问内存要求较高的应用中,例如 Spark,PageRank,HERD 等,需要 100 Gbps 的带宽和 3 微秒的往返时延。

我们可以看到,在热数据存储在本地内存、冷数据存储在远端内存的前提下,现有的硬件(如 Mellanox 的网卡)实际上已经满足了应用性能不显著下降的需求,所以在很多场景下应用性能并不是一个很大的问题。

如何将经常访问的数据存储在本地,而将不常访问的数据存储在远程成为了一个关键问题。这里又有两个子问题,首先是如何低开销地识别数据的冷热,其次是如何低开销地做冷热数据迁移,学术界和工业界都有很多人正在研究。

让分布式系统编程像单机编程一样便捷

前面我们讨论的是 “让数据中心的网络互联像单台计算机内部总线一样高效”,接下来我们讨论的是 “把数据中心作为一台计算机” 的另一个方面,也就是我们要让数据中心分布式系统的编程像单机一样便捷。

2009 年,伯克利针对这个云计算做了很多的预测,都成为现实了:

  • (理论上)无限可用的计算资源
  • 用户再也不需要承担服务器运维的工作和责任
  • 服务的按需付费成为可能
  • 超大型数据中心的使用成本显著降低
  • 通过可视化资源管理,运维操作的难度大大降低
  • 得益于分时复用,物理硬件的利用率大大提高

伯克利提出了一个很有影响力的观点:分布式系统的编程应该像单机编程一样便捷。为了实现这个目标,他们提出了很多新的编程模型和编程框架。其中一些比较知名的有 MapReduce、Hadoop、Spark 等。这些编程模型和框架的核心思想是:让程序员只需要关注数据处理的逻辑,而不需要关心分布式系统底层的细节,例如数据分片、并行计算、容错等。

这种编程模型在一定程度上确实简化了分布式系统的开发,但是也有一些问题。首先,这些模型通常只能很好地处理特定类型的任务,例如大规模数据处理任务。对于一些复杂的分布式系统,例如分布式数据库或者分布式事务,这些模型可能就不太适用了。其次,这些模型和框架往往有很高的学习成本,程序员需要学习一套全新的编程语言和概念,这对于很多人来说可能是一个挑战。

2019 年,伯克利又预言 Serverless 将成为下一个重要的应用范式。现在很多云服务商,包括华为云,都提供了 Serverless 函数服务。伯克利指出,Serverless 服务与传统的 IaaS 模式相比,有一些显著的差异。最大的区别在于系统管理方面,比如服务的实例扩缩、部署、容错、监控和日志等方面,Serverless 都由系统自动完成,而传统的 IaaS 模式全部需要程序员和运维工程师手工处理。

例如,在 IaaS 中,以下这些事情都是要程序员和运维工程师手工处理的,而如果使用 Serverless 服务,这些问题就完全是由 Serverless 服务解决的:

  • 为可用性做到冗余,这样一台机器的故障不会导致服务中断
  • 在发生灾难时保留服务的冗余拷贝的地理分布
  • 通过负载均衡、请求路由来高效利用资源
  • 根据负载变化自动伸缩系统
  • 监控服务确保它们一直健康地运作
  • 记录日志用于 debug 和性能调优
  • 系统升级,包括安全补丁
  • 在新实例可用时迁移到新实例

此外,在程序运行方面,Serverless 允许用户选择的事件触发执行,而传统模式必须持续运行,直到明确指出需要停止。

然而,目前的 Serverless 并不是一种万能解决方案,它的适用范围有一定的局限性。目前的 Serverless 更适合一些无状态、短时间运行的应用,同时这些应用可能具有很高的突发性,即访问量在某些时刻非常大,而在其他时候突然没有人访问。在这种情况下,使用 Serverless 可以很好地实现服务的自动扩缩。

在编程语言方面,传统模式可以使用任意的语言,而 Serverless 可能只能使用几种受限的编程语言。在程序状态方面,AWS 的 Serverless 是一个无状态的服务,而传统模式可以是有状态或无状态的。一些应用具有很复杂的中间状态,如机器学习、线性代数或数据库等,这时候 Serverless 的抽象会带来较大的挑战。正如伯克利的文章指出的,Serverless 的时代还刚刚开始,这些都是有待我们进一步探索和解决的问题。

本章小结

以上就是数据中心部分的内容。

数据中心网络传统上为容易并行的 Web 服务设计。但如今 AI、大数据、HPC 都是大规模异构并行计算系统,对通信性能都提出了很高的要求,厚重的软件栈造成巨大的开销,这就要求数据中心网络的通信语义从字节流演进到包括消息语义、同步和异步远端内存访问、RPC 在内的内存语义,软硬结合实现极致的时延和带宽。未来,我们期望把数据中心作为一台计算机,一方面实现异构计算、存储设备间的对等直通,让数据中心互联像主机内部总线一样高性能;另一方面通过 Serverless 让分布式系统编程像单机编程一样便捷。

前面讲到的问题我们基本上都在研究,也有一些初步成果,但还没有正式发布,因此今天讲的主要是学术界和工业界现有的一些技术。感兴趣的同学欢迎来我们计算机网络与协议实验室实习或者工作,我们拥有非常强的一支团队,承担公司战略项目的研发工作,我相信其中的技术是世界领先的。

接下来,我会讲讲广域网和终端无线网络领域的最新技术。

Comments