数据中心:CPU 设计的现代挑战

标题:Data Center Computers: Modern Challenges in CPU Design

日期:2015/06/03

作者:Dick Sites

链接:https://www.youtube.com/watch?v=QBu2Ae8-8LM

注意:此为 AI 翻译生成 的中文转录稿,详细说明请参阅仓库中的 README 文件。

备注一:扫一眼没看懂会议主旨,我只是觉得幻灯片很牛逼就拿过来了。想要里面的可视化工具。

备注二:虽然年代已久,但是应该还有价值,TODO.

顺便提供 Gemini 总结版

TL;DR

这篇文章的核心主旨是:数据中心计算机的工作负载与传统的台式机(PC)有着根本性的不同,而当前主流的、为 PC 市场设计的通用处理器硬件,在应对数据中心的独特挑战时存在严重的瓶颈和设计缺陷。

作者通过四个关键领域(数据移动、高并发事务、程序间隔离、性能测量)的深入剖析,论证了行业需要为数据中心设计专用的、经过优化的 CPU 和系统架构,而不是简单地将为个人电脑设计的硬件堆砌在一起。这是一个呼吁,希望硬件制造商和软件工程师能够重新审视和设计面向未来数据中心的计算机体系。


这场讲座提供了大量具体、深刻且有数据支持的洞见,对于理解现代大规模计算系统的性能瓶颈非常有价值。主要信息可以分为以下四个方面:

  1. 数据移动是巨大的性能瓶颈

  • 问题的量化: 数据中心服务器约有 25% 的时间都消耗在移动数据上(如 memcpy、网络收发等),这是一个惊人的开销。

  • 硬件的无力: 作者以“每个时钟周期移动16字节”为目标,揭示了当前硬件的诸多不足:

    • 带宽不足: 从一级缓存到内存的整个链路带宽跟不上 CPU 的处理速度。

    • 缓存未命中的代价极其高昂: 他生动地演示了一次200个时钟周期的缓存未命中是多么漫长,这在拥有海量内存(TB级别)而缓存相对较小的数据中心里是常态。

    • 页面尺寸过小: 现代服务器仍在使用 4KB 的内存页,导致转译后备缓冲器(TLB)频繁未命中,某些程序甚至有 15% 的时间浪费在处理TLB未命中上。

    • 硬件指令集缺失: 缺少高效处理小块、非对齐数据的专用指令。

  1. 高并发环境下的“尾部延迟”是关键

  • 关注点不同: 与桌面应用不同,数据中心处理成千上万的并发请求,用户体验由最慢的那个请求决定,即 “尾部延迟”(Tail Latency),而不是平均延迟。

  • 性能分析的挑战:

    • 等待时间是魔鬼: 传统的 CPU 性能分析工具(Profiler)只会告诉你程序在 执行 时哪里耗时,但无法告诉你程序在 等待(等锁、等调度、等I/O)时浪费了多少时间,而后者往往是尾部延迟的罪魁祸首。

    • 锁和调度是元凶: 他通过一个详细案例展示,一个任务的执行时间可能只有几十微秒,但因为锁竞争或不佳的内核调度策略(如为了缓存亲和性而等待一个繁忙的CPU核,而不是立即调度到空闲的核上),等待时间可能长达数百微秒,使总耗时增加10倍。

  1. 程序间的隔离亟需硬件支持

  • “吵闹的邻居”问题: 在云环境或共享服务器上,一个行为不佳的程序(例如,疯狂读写内存)可以占满共享的L3缓存,导致其他所有程序的性能急剧下降。

  • 软件方案的局限性: 纯软件的资源隔离方案粒度太粗,效果不佳。

  • 需要硬件解决方案: 作者提出需要硬件级别的 缓存隔离 机制,例如给每个CPU核心分配一个缓存使用“预算”,当超出预算时,优先替换自己的缓存行,从而保护其他程序。但由于成本(每个缓存行增加几个比特的开销)和市场驱动力(PC市场为主)的原因,这个方案尚未被硬件厂商采纳。

  1. 测量和可视化的决定性力量

  • 猜测是无效的: 面对复杂的性能问题,工程师的直觉和猜测往往是错误的。

  • 全面的事件跟踪是王道: 解决疑难性能问题的唯一可靠方法是进行 低开销的、全面的事件跟踪,记录下系统中发生的每一件事(如任务切换、中断、系统调用、锁操作等)和其精确的时间戳。

  • 一个震撼的案例: 讲座以一个真实故事作为高潮——一个持续了 三年、影响了谷歌全球 25% 磁盘服务器的性能问题。该问题表现为磁盘读取延迟呈现250ms、500ms、750ms的周期性尖峰。在通过追踪和可视化所有磁盘的行为后,问题才被定位为内核的一个CPU节流(throttling)机制的错误触发。这个案例雄辩地证明了“先测量,再分析”和数据可视化的巨大威力。

总而言之,这篇讲座不仅指出了当前数据中心面临的严峻挑战,还提供了具体的技术分析和解决思路,对从事系统设计、软件性能优化、云计算和硬件架构的专业人士具有极高的启发价值。


能来到这里参加杰出校友系列讲座,我感到非常荣幸。校友办公室声称,从你踏入校园的第一天起,你就是校友了。

我父亲今年 95 岁,患有阿尔茨海默病。他已经不记得北卡罗来纳州在哪里了,但他仍然保有那种幽默感。所以我告诉他,我受邀来做这个杰出校友讲座。他想都没想就对我 smirked(得意地笑)了一下,然后说:“所以,你是替谁来的?”

好了,这是一个工程技术讲座。尽管这里不是一个工程学院校区,但我还是戴上了我的工程师帽子。

我将要谈论的是数据中心计算机。我的解读是,整个行业正开始出现两极分化:一边是手机,另一边是数据中心。而处于中间地带的东西将会大量消失。我想和大家聊聊数据中心计算机,以及它们与台式机有何不同。我想尝试描绘一些数据中心计算的生动画面,并揭示一些正在进行中的研究问题。或许能激励在座的一些学生投身于这个领域,因为我认为它在未来一段时间内会相当重要。顺便提一下,想找一张女性拿着灯泡而不是男性的图片,真的非常困难。

那么,做一个小小的类比。这是一栋加州牧场式平房和一栋公寓楼。这个类比是说,它们之间的关系,就如同台式电脑与什么的关系?

数据中心。大量的共享、大量的交互、大量的拥塞、大量的不同议程。哦,它奏效了。所以,我将从四个方面来谈谈我认为数据中心计算机与台式机的不同之处。首先是移动数据,无论大小。我们发现,我们所有服务器大约有四分之一的时间都花在了移动数据上,比如 mem copy(内存复制),或者 strlen(计算字符串长度),或者发送消息和接收消息,这些操作内部都嵌入了复制操作,将数据从网络移出内核,再移入用户空间等等。所以,我将花很多时间来谈论数据移动。然后谈谈当每秒有数千个事务时,到底发生了什么,以及这与运行两个桌面程序有何不同。再稍微谈谈程序之间的隔离问题。在这个领域,我们行业实际上需要更多的硬件帮助。最后,是我最喜欢的话题之一,谈论测量的基础。

移动数据:大的和小的

在我们的服务器中,我们在移动数据:从磁盘到内存(RAM),从网络到内存,从固态硬盘(SSD)到内存,还有大量的数据仅仅是为了物理上的拼接和并列。其中一些是批量数据,比如在这里或那里发送一兆字节。但有些只是短小的可变长度项。比如,以 UTF-8 编码存储 Unicode,这是一种覆盖 Unicode 中所有字母表的可变长度编码。其中有单字节字符、双字节、三字节和四字节字符。当你把它们拼接在一起时,你移动的是非常微小的数据片段。如果你必须做一个四路分支判断,比如“到这里移动一字节,到那里移动两字节”,那效率就非常糟糕。因为条件分支总会拖慢你的速度。或者你在组装数据包,里面有这个报头、那个报头、这个 VLAN 标签、还有其他东西和校验和。所有这些都需要物理上拼接在一起然后发送出去。我们还做了大量的压缩和解压缩工作,这些都涉及大量的数据移动。而解压缩从根本上说,就是从一个大文件中拾取小片段,然后按它们原始的顺序重新组合起来。我们还进行校验和计算。我们写入磁盘的每一项数据都在软件层面进行了校验,这还不包括所有的硬件校验,因为否则我们会丢失数据。所以我们只是在不断地遍历数据。我查看了我们的一台磁盘服务器并进行了统计,从磁盘读取一个数据块,该块中的每个字节在内存中实际上被访问了 11 次。

最早的 4004 处理器没有内存。而像英特尔 i7 这样的现代处理器则拥有 12MB 的缓存。但当你审视一台数据中心服务器时,它实际上是一个巨大的恐龙,所有的一切都是内存,然后只有一小部分是处理器。而它们之间的连接相当脆弱。如果你看钱都花在了哪里,钱并没有花在处理器芯片上,而是花在了海量的内存上,64GB,甚至高达 1TB。你面对的是巨量的内存,这会带来一个强烈的后果。如果你有大量内存并且为此付了钱,那就意味着你想使用它。你想把东西放进去,也想把东西取出来。我们稍后会看到,这意味着缓存(cache)不起作用,因为相对于缓存大小,你的内存实在太大了。所以这是一个有点奇怪的环境。我将通过一个假设的缓存结构,然后讨论一些问题。简单来说,假设你有一台 64 核的机器,也就是 16 个物理核心,每个核心有四个程序计数器(PC),也就是超线程(hyper-threaded)之类的技术。每个物理核心都有自己的一级缓存(L1 cache)。然后可能每四个物理核心共享一个二级缓存(L2 cache)。所有的核心都共享一个类似三级缓存(L3 cache)的东西。然后是大量的 DRAM。这基本上是对你今天能买到的机器的一种泛化。

我将以一个驱动函数(forcing function)为例,在接下来的 10 到 12 分钟里,我都会讨论这个驱动函数。如果我想每个时钟周期移动 16 字节——因为我 25% 的时间都在做这件事——那么实际的后果是什么?这对硬件设计意味着什么?“每个时钟周期移动 16 字节”这句简单的话,如果你认真对待它,就必须思考你需要构建什么样的东西。当我仔细研究了需要构建的东西后,我感到非常震惊。首先,你需要在一个加载/存储计数机器(load store counting machine)中,每个周期都加载 16 字节、存储 16 字节、测试是否完成,并执行一个条件分支。所以,仅仅为了开始,你就需要一台四路发射(four-way issue)的机器才能达到目标。你还需要一些 16 字节的寄存器之类的东西。如果你看看当今处理器的速度,大约是每秒 3 GHz,乘以 16,你就需要高达每秒 50GB 的读取流量。以及另外每秒 50GB 的写入流量。这意味着你需要以总共每秒 100GB 的速度持续访问一级缓存,每个周期,每一秒,连续数秒。如果你搞砸了,事实上,如果你购买任何今天的机器,你实际上说的是每秒 150GB,因为你正在写入的数据流,每个缓存行(cache line)实际上都会先被读取,然后被完全覆盖。所以这次读取完全是浪费时间,然后再写回内存。所以在你今天能买到的每一个处理器芯片中,数据移动都存在 1.5 倍的损失,这在我看来是很不幸的。

这里简单谈谈短字符串,我们处理数据包片段、单词、网页之类的东西。就是很多小片段。我想我跳过了一张幻灯片,请稍等。好的。如果你以 16 字节的块来看待这些东西,如果我们想以每周期约 16 字节的速度移动短数据,那就意味着你需要拾取大部分的 16 字节,在下一个周期拾取下一个块,再下一个周期拾取再下一个块。如果你看一下像 memcopy 这样的代码,它在开头会花大量时间来确定要做什么以及长度是多少等等。直到大约八个周期后,你才真正开始第一次移动。如果你要移动一兆字节,这没问题。但如果你只想移动四个字节然后就退出,那就太糟糕了。你把三分之二的时间都花在了开销上。如果你不把这些看作是小片段,而是对齐的缓存行,哦,事情就不会那么顺利。我在这里得到几个字节,但我需要把它们稍微移动一下,再从那里引入两个字符,两个字节,才能完成操作。这意味着,如果我要每周期移动 16 字节,而目标和源没有对齐到相同的 16 字节边界,我就需要拾取一些块,然后进行移位,再把它们存储起来。我需要在一个周期内完成拾取、移位和存储的所有操作,以某种流水线的方式。所以,这最终需要在一个 16 字节寄存器的机器上有一个移位操作,它能接收两个 16 字节的寄存器,对整个内容进行移位,然后取出 16 字节。如果你没有这个功能,你就无法每个周期移动 16 字节。而今天你也买不到这样的东西。所以这成了一个很好的驱动函数。我相信,在指令集方面,CPU 供应商(也许在座的一些学生会成为 CPU 供应商的员工)提供一个“部分加载”(load partial)指令和一个“部分存储”(store partial)指令是很有价值的。你所要做的就是给它一个地址和长度,它就能在一个周期内从那个地址加载 0 到 15 字节,或者向那个地址存储 0 到 15 字节。这些东西在硬件路径上很容易实现,因为非对齐加载(unaligned loads)的连接路径已经存在了。从长度中取最低的四位也很容易。但如果你用软件来实现,你就要做一堆与(AND)操作和分支,速度很慢。你可以在 IBM Power 系列中找到加载指令,但它加载的是最多八个四字节的寄存器。所以实际上是每周期四字节,而且需要好几个周期,是一个非常复杂的指令。你也可以在 Itanium 中找到部分存储指令,但你买不到任何能在每个周期内同时完成这两项操作的东西。

如果你有了这个,所有这些短距离移动就变成了加载-存储,然后就完成了。没有分支。

现在我要回过头来看看移动大量数据的下一个后果。我有一个数据缓存,有一些加载和存储。这个缓存,除了我每秒向这个处理器读入 50GB 并写出 50GB 之外,它的后端也必须以总共每秒 100GB 的速度进行填充和写回。L2 缓存的后端也必须以每秒 100GB 的速度移动数据,RAM 系统也必须做到这一点。如果我和其他核心共享这个 L2 缓存,这就好像是,哦,我需要乘以 16 吗?所有这些核心都在同时做这件事吗?幸运的是,你不需要。如果服务器总时间的四分之一都花在移动数据上,那么如果我们有 16 个核心中的 4 个全力运行来移动数据,其他的核心可以做其他事情,那就没问题了。所以,在这里,不是 100GB/秒乘以 16,实际上 100GB/秒乘以 4 会是一个相当不错的平衡。

所以,这种“我可以到达 L1 缓存并返回”的想法,制造商非常乐意与你谈论。但他们不太愿意谈论贯穿始终的持续带宽,以及在其他处理器做任何事情时的持续带宽。但这又回到了那个问题:我想每秒移动 16 字节,你需要做什么才能实现它?

如果你能做到这一点,你最终会得到一个单一的处理器芯片,它的性能能排进去年四月 STREAM COPY 带宽测试的前 20 名,达到 400GB/秒。它比一些 SGI 的机器稍慢,比 Oracle Sun 的那些机器稍快,而那些机器都是用大量处理器构建的,而不仅仅是一个 CPU 芯片。作为一个行业,高端数据中心处理器供应商并没有真正关注到这到底是一个多大的瓶颈。

然后是下一步。如果你有 256GB 的内存,正如我之前所说,只有在你打算使用它时才值得购买。如果你用大量的东西把它填满,那就意味着,如果你有 20 或 50GB 的缓存,你访问的几乎所有东西都不在缓存里。而在这些机器上,主内存乐观地讲,有 200 个时钟周期的延迟。悲观地讲,在现实世界中,可能需要 500 或 600 个周期。但我会坚持用 200 个周期。所以,我将带你们逐个周期地看接下来的两张幻灯片,以 4 赫兹的速度进行加载、加法、存储,加载、加法、存储,全部是缓存命中。然后再做一次,但第二次加载是缓存未命中(cache miss)。我想特别请教员们在这期间保持耐心。我以前讲这个的时候,教员们总是打断我。这是加载、加法、存储。大家都看清了吗?我再放一遍。这是 4 个周期的加载、加法、存储。现在我们用 200 个周期的缓存未命中再做一次。

(讲者在此处用非常缓慢、夸张的语调和停顿来模拟 200 个时钟周期的漫长等待,以达到戏剧性效果,这段文字无法直接翻译其表演性,但其意图是让听众“感受”到延迟)

好的……开始了……一、二、三、四……二、二、三……三、三、四……三、三、四……三、四……五、四、八、四、五……走!……五、四、五……该死……五、四、七……命中……三、四、五、五……四、七……八……九……二、三、四、五、十……四、五、四、五……四、五、九、九、九、十……五、九、九、二十……五……一百九十九……两百……命中。这就是一次缓存未命中。我希望你们能内化这个时间有多长,并思考一下在这段时间里你还能做些什么。这还是乐观情况。我们服务器的缓存未命中通常是 400 个周期。我可没那么有耐心。那 50 秒占了我这次演讲时间的 2%,是为了让你们内化缓存未命中到底是什么。

所以,你将会有很高的未命中率。你将会有延迟 200 个周期的东西。如果我真的要每个周期移动 16 字节,那就意味着我需要提前 200 个周期。我需要预取(prefetch)至少 3.2 KB 的数据,才能避免等待缓存未命中。这是针对长距离移动的。所以,仅仅为了这个简单的“请让我每个周期移动 16 字节”的要求,你就需要大约 4KB 的预取。你今天买不到这个。你可以买到预取一个缓存块,也就是 64 字节,但你买不到 4KB。但如果你要诚实地快速移动那么多数据,你就需要它。但还有更多,就像深夜电视广告里说的那样。在真实机器中,L1 缓存大小的上限是缓存的相联度(associativity)乘以页面大小(page size)。如果比这个大,你必须先进行从虚拟地址到物理地址的页面映射,然后再开始缓存查找,这需要两个周期而不是一个。或者你必须构建一个虚拟缓存,但没人这么做,因为硬件供应商对操作系统供应商没有足够的控制力来让它真正工作。所以他们不会制造这样做的芯片。所以如果你有 4K 的页面和一个八路相联的一级缓存,那么大小就是 32KB。故事结束。这就是你今天能买到的全部。无法再多了。我第一次看到这个是在 Amdahl 470 上,一个双路相联的缓存,4K 页面,8K 缓存,它并行进行 TLB(转译后备缓冲器)查找和缓存查找以找出接下来的位。所以我们真的需要远大于 4K 的页面。此外,如果你有一个包含 256 个条目的 TLB,每个条目 4K,那总共也就一兆字节。

你们笔记本电脑里的芯片可能有 20MB 的缓存。你甚至无法在不发生 TLB 未命中的情况下访问芯片上的缓存。我们有些数据中心的程序,有 15% 的时间都花在了 TLB 未命中服务(TBMS)的微码里。

如果我们有更大的页面,并且在 TBMS 微码上花费的时间少得多,那些机器的成本就能降低 15%。最后,如果你有 256GB 的内存,每个页面 4K,那你就有 6400 万个页面。这比你实际需要处理的球要多得多。如果你有一百万个页面,生活会很美好。如果你有 1000 个页面,生活会有点艰难。但 6400 万个页面纯属浪费。对于这个行业来说,转向更大的页面大小是一个艰难的过渡。这可能需要五到十年,也需要你们职业生涯的一部分时间。从磁盘上 512 字节的块过渡到 4K 块就花了很长时间。

抱歉。原因在于,Windows 分配的第一个分区不是 4K 的偶数倍。所以如果你制造一个使用 4K 块的磁盘,用户在个人电脑上访问的任何东西都不会在 4K 边界上对齐。每次读取都会变成两次读取,每次写入都会变成两次写入。

这花了 10 年时间,直到 Windows 7 最终将引导分区对齐到兆字节边界,你才能在磁盘上使用更大的块。所以,问题仍然是内存。我 20 年前写过一篇关于“关键是内存,笨蛋”(It’s the memory stupid)的文章,现在仍然是内存。我认为,在行业内,我们需要在指令和实现之间有更好的协同设计。这是一个有点跨界的事情。所以,挑战在于,对于数据中心来说,需要有大量的内存、大量的多路发射、到内存的全带宽、预取、更大的页面,以及也许更值得你们思考的:你们如何能做得更好?

好了,这是第一个四分之一,也是最长的一个四分之一。

实时事务处理

与台式机不同,我们每秒要处理数千个事务。我将通过四个视角向你们展示事务处理的序列。首先这个,这里的每一个都是一个处理器机架,大约和我一样宽,和我一样高,但比我重得多,里面装满了大概 50、60 块处理器板。这张图显示的是一个搜索查询进入一台机器,然后它将这个搜索查询的片段(黄色部分)分发给大约 100 台其他机器。然后这些机器中的每一台又将更多的工作(蓝色部分)分发给更多的机器。实际上还有第三层,我没有画出来,否则整张幻灯片就全黑了。这只是一个查询,它迅速地分发下去:砰、砰、砰、砰。第二层也迅速分发:砰、砰、砰、砰。所有第三层的机器都做一两毫秒的工作,然后所有的答案又迅速地返回:砰、砰、砰、砰,在返回树的途中被合并,最终在大约 20 毫秒内回到这里。与此同时,另外 20 到 100 个其他的搜索查询在同一组计算机上同时进行着。

所以,生活非常忙碌。这是同一搜索查询的另一个切片,以不同的方式分解。这是在顶层接收传入的查询,并在时间轴上显示答案返回所需的时间,大约是 160 毫秒,而不是 10 或 20 毫秒。在这其中,这是顶层的调用树(call tree),它调用了 93 个其他处理器,要求它们各自完成一部分工作。这个调用树中有一些异常。首先,有一个调用到另一台机器,直到它返回之前,其他任何事情都不会发生。然后有 93 个并行的调用到许多其他机器。然后这个家伙比其他的慢很多,而这个家伙更慢。正是最后一个返回的家伙决定了整个用户搜索查询需要多长时间。所以,进行大量并行计算的好处是,你可以让 100 台不同的机器来处理它。事实上,当它扩展开来时,是 2000 台不同的机器。坏处是,最慢的那一个决定了响应时间。

事实证明,我个人特别关注的是 99 百分位的延迟(99th percentile latency),即 99% 的响应所花费的时间都少于这个值,但有 1% 的响应时间更长。所以这 1% 的长延迟尾部是我喜欢研究和理解的问题类型。如果你并行处理 100 件事,并观察 99 百分位的净响应时间,你会发现几乎所有事情都花费了最长的时间,也就是 99 百分位的时间。一百次里有一次,你做了一百次,结果肯定会落在这里。对于每个用户、每个搜索、每个事务都是如此。

因此,控制这个尾部延迟变得非常重要,事实上,这非常有价值。那是对一个事务的时间调用图的切片。现在,这是在单个服务器上处理一个事务。这实际上是一个相当古老的 Gmail 投递过程。这个 Gmail 投递过程做了一堆事情,又一堆事情,这些都是为投递运行的不同子程序,不同的过程。这是另一个投递,第三个投递稍微长一些,第四个投递。你可以看到,如果我们看时间尺度,这些大部分都像是每个 10 毫秒。然后是这一个,整个底部的东西是一个。抱歉,不是 10 毫秒,是每个 60 到 70 毫秒。整个底部的东西是 1800 毫秒。一次电子邮件投递花了 1.8 秒。它的开始方式是一样的。用这种方式画图的好处是,你可以在每个部分的开头有一个“回车”,这样你就能看到这个家伙有什么不同。区别就在于所有这些蓝色的东西。事实证明,这些蓝色的东西是在重新索引刚刚收到的邮件中的单词,并且是在处理接收邮件的同一个处理器线程上进行的,而不是在其他某个线程上。这张图导致了早期的 Gmail 软件设计在接下来的一周内发生了改变,将这个重新索引的任务放到了另一个线程上。这使得尾部延迟从 1800 毫秒减少到大约 100 毫秒。

重点是,如果你做一些像性能剖析(profiling)之类的事情,很难发现那里发生了什么,因为所有东西都被混合在一起了。你必须获取单个事件的所有片段,看看长延迟的那些有什么不同。所以,我将展示一个服务器的另一个切片。这次我们看的是四个不同的 CPU,时间轴是横向的。这是 200 微秒。图中显示了在每个 CPU 上随时间运行的内容。不同颜色的矩形是不同的程序,而那些高高的家伙实际上是内核调用(kernel calls)或中断处理(interrupt processing)。所以,中等宽度的,最窄的东西,黑线是空闲任务。中等宽度的东西是用户模式的程序,比如橙色的。而那些又高又窄的矩形是内核模式的执行。我们在这里看到的是 200 微秒,CPU 0 的顶部大部分时间都很忙,主要在做这个用户模式的事情,不管它是什么,并且进行了相当多的小的内核调用和返回。CPU 1 大约一半时间是空闲的,但它在做相当多的事情,后来发现主要是中断处理。以及不管这个窄窄的橙色进程是什么。CPU 2 几乎是空的,除了一个后来发现是定时器中断的东西。CPU 3 完全是空的。这显示了一点网络流量进入并被处理的过程。CPU 1 上的橙色家伙实际上是 tcpdump。每个带有网络数据包的中断进来,都会运行 tcpdump,然后将数据包传递给发出接收消息调用的主线程。仔细观察后我们发现,在真实的数据中心运行,在生产负载下,一天中最繁忙的时刻,如果我们打开 tcpdump,它会消耗掉整个服务器 7% 的性能。

我不能去对我们数据中心的运营人员说:“我有一个测量工具,我想在生产环境中最繁忙的时刻打开它,而它会消耗 7% 的性能。”那会是一段非常简短的对话。他们会说:“不行。”

就是不能用 tcpdump,句号。太慢了。

如果我进去说 1%,他们会说:“当然可以。”如果在两者之间,那就会是一场漫长而尴尬的对话,我讨厌这种对话。所以,这是同一类图表,在一台 16 核处理器机器上,按时间显示了每个 CPU 核心上运行的所有内容。这里那里有一些细细的黑线表示空闲的 CPU 核心,但大部分时间都很忙。我将向你们展示完全相同数据的四个切片。这是按 CPU 划分的。这些绿色和橙色的东西在顶部运行,中间有一些空闲,一些中断处理,还有一些其他的东西,底部有一个小的红色块。下一部分是同样的处理过程,但不是按 CPU 编号排序,而是按它正在执行的远程过程调用(remote procedure call)排序,即哪个事务,哪个搜索。实际上,这些我认为是更新。不,这些是搜索。所以这里显示了在这段时间内跨越的 40 个搜索。这一个比上面的开始得晚一点,再晚一点,再晚一点。它们都花费了,在这种情况下,大约 50 微秒,除了这个家伙和少数几个其他的,它们花费了大约 10 倍的时间。而我就是那个对 1% 的尾部延迟感兴趣的人,想知道为什么那些会花费 10 倍的时间。如果我们看同样的数据,这是那 40 个事务所持有的 23 个锁。这些是相同的执行过程,但不是按 CPU 编号排序,也不是按事务编号排序,而是按软件线程(software thread)排序,即哪个线程在什么时候运行哪个搜索查询。在这种情况下,事实证明有 47 个不同的线程。那些细细的弧线表示这个家伙被阻塞了,正在等待那个锁。稍后,它再次被唤醒。然后它又被阻塞,等待那个锁。如果你想一想,这些都在同一个尺度上。如果你认为正常的执行时间只是屏幕上的一小段,大约十分之一,你就会看到这些锁的持有时间太长了。这就是这个特定数据切片的根本问题所在:软件锁的持有时间过长,导致你想完成的事情无法完成,因为它在等待其他东西。它可能要等待大约六个事务的时间才能取得一点进展。这就是根本的性能缺陷。但是,你知道,这都是些又大又乱的东西。所以,我将用同样的数据,只突出显示我们正在看的那个慢家伙。它运行了一小会儿。我们现在回到 CPU 视图,它在 CPU 9 上运行了一小会儿,这里一点,那里一点。而在这之间它根本没有运行。时间在流逝。如果你对这个事务进行 CPU 性能分析,你将一无所获。因为 CPU 性能分析不会告诉你你不运行的时候发生了什么,而这正是这个事务的全部故事。关键是它为什么在等待?CPU 性能分析告诉你的是,当你实际执行指令时发生了什么。所以,那是一个远程过程调用(RPC)。你大概可以看到周围的上下文,这当然很有用。这是它在等待的锁。哦,它几乎立刻又在等待同一个锁。

所以它运行了一小会儿,砰,等待锁。过了很长时间,再次被调度,运行了一小会儿,砰,等待锁。又等了很久。最后再次被调度,砰,完成了。这就是为什么它要花十倍的时间。但这里有一个非常奇怪的事情,我将在下一张幻灯片上展示。这条紫色的弧线是从……这是在持有锁的 CPU 上运行的进程。这是它释放锁的瞬间。上面那条代表锁的线停止了。当这个进程释放锁时,软件锁机制的一部分会说:“哦,有别人在等你。这是所有等待者的列表。去运行他们中的一个或多个。”所以,这是持有并释放锁的进程唤醒我关心的那个进程的时刻。但它实际上在 50 微秒后才开始执行。所以,这是“我完成了,去运行吧”。这是内核调度器最终开始运行它的时刻。这 50 微秒比这些事务的正常执行时间还要长。所以,我仅仅在唤醒过程中就失去了一个完整的事务时间。然后它运行了远少于 50 微秒,然后再次阻塞。这就是 Linux 内部的 Futex 调用。为什么它要等待 50 微秒?

现在,如果我们更仔细地看一下上下文,并把其他正在发生的事情放进去,内核调度器在这次唤醒时决定在它之前运行的同一个 CPU 上运行另一个家伙,而那个 CPU 当时正忙。

为什么它会说在你之前运行的同一个 CPU 上运行呢?

我不知道后面的学生是否需要知道答案。为什么 Linux 内核调度器会把一个就绪的进程放在它不久前运行过的同一个 CPU 核心上运行?亲和性策略(Affinity policies)。是的。基本思想是,哦,如果我让它在之前运行的同一个 CPU 上运行,它所有的内存访问都会缓存命中。哦,那太好了。所以我们等一下,然后运行它,生活就会很美好,但是应该等多久呢?哦,顺便说一句,在等待期间,事实证明 CPU 3 是空闲的。所以我们本可以更早开始。

所以,如果你在一个超线程(hyperthread)上等待,也就是一个与另一个程序共享物理核心的 CPU 程序计数器,如果你在另一个使用相同缓存的程序计数器上运行,你永远不应该等待。如果你要在一对超线程的另一个上重新调度。如果你要在一个与你共享 L2 缓存的地方重新调度,对于今天的大小,你可以等一会儿。通过等待并在你所在的核上运行,你最多可以节省大约 10 微秒。如果你要去一个不共享 L2 缓存但共享 L3 缓存的核,你大约可以节省 100 微秒。或者你可能会因为去了另一个处理器而损失 100 微秒,因为它需要多次缓存未命中。如果你有一个多 CPU 芯片,多插槽的处理器板,然后你去了另一个插槽,那速度会非常慢,因为现在所有东西都必须去主内存获取在错误芯片中的数据。但如果你本可以选择 L1 或 L2 缓存,那么 50 微秒就太长了。即使在 L3 缓存的情况下,这也不是一个值得等待的时间。所以,在一个大型环境中,我们真的需要一些更关注这个问题的东西。如果你在你的个人电脑上运行四个 SPEC 基准测试,这些都不会发生,这些都无关紧要。有些会发生。但要真正理解在这个复杂环境中发生了什么,我们需要做更多的跟踪,跨服务器、跨核心、跨线程、跨队列、跨锁地跟踪并发事务。我给你们看的这些图片,实际上,回过头看,它们代表了大约 10 年断断续续的工作成果。那张 Gmail 的图片是 2004 年的。当时谷歌还没有工具来收集这些数据、画出图表并看到底发生了什么。所以这花了一段时间。总结一下第二部分,关于大量事务:事情可以瞬间触达数千台服务器,砰、砰、砰、砰,然后得到答案并需要合并它们。最慢的路径决定了最终结果。所以你需要非常关注尾部延迟。而控制锁的持有时间,作为软件行业,我们并不擅长,除了一些非常狭窄的实时领域,但在任意程序员编写的、碰巧在同一台机器上运行的任意程序中并非如此。例如,云服务器,你向不同的人出售时间,你根本不知道谁会做什么。而编写大量使用锁的程序的程序员,对于什么是合理的持有时间、什么是不合理的持有时间,以及你可能会因此受到多大的伤害,概念非常模糊。然后,我们真的需要更智能的调度器和更聪明的人来思考这些问题。好了,这是第二部分。

程序间的隔离

我将谈谈程序之间的隔离。这是一张我研究过的磁盘服务器的时间直方图,显示了从这台磁盘服务器进行一次类似 64 KB 读取所需的时间。这是一台客户端机器,它发送一条消息到房间里某个地方的机器,比如四分之一英里外,说“读取这个块”,然后磁盘服务器最终会说“这是你的块”。大量的请求返回,如果我们只看磁盘服务器的时间,忽略网络时间,它们会在零毫秒内返回。

出现这个峰值是因为我们的磁盘服务器,最近读取过或写入过的数据会驻留在磁盘服务器的内存缓存(RAM cache)中,数据就在那里,砰,你就拿到了。然后这里在三毫秒左右有另一个峰值。三是个奇怪的数字。一个 7200 RPM 的磁盘转一圈大概需要多长时间?

是的,每秒 120 转,所以转一圈大约是 8.3 毫秒。所以这还不到半圈的时间。你怎么能去一个磁盘服务器说“给我东西”,而它内存里没有,结果在不到半圈的时间内就返回了呢?而且还有很多这样的情况。并且没有任何情况是四分之一圈或四分之三圈的。

这些是命中了最近经过读写头的数据,驱动器本身通常会缓存最近经过的一两轨所有数据。这只是通过 PCI Express 总线再返回的电子连接时间,以获取存储在驱动器内部内存中的数据。在我观察的机器上,这通常需要三毫秒。然后在这里,大约 25 毫秒处有一个大的驼峰,这是去磁盘、读取一个块、然后返回。然后是这个长长的尾部。这里的 99 百分位延迟是 696 微秒。哦抱歉,是毫秒。十分之七秒。所以 99% 的访问都在这里,相当不错,而 1% 在这里。仅仅读取一个磁盘块就超过了半秒。而且这些都是不可复现的。你稍后再去读同一个块,它就没问题了,速度很快。所以,当你看到不可复现的尾部延迟时,它基本上必定来自某种未知的干扰。有东西挡了道。问题是你不知道是什么挡了道。如果你知道是什么,你早就修复它了。所以,当我们研究程序的隔离时,同样的事情也会发生,如果某个其他程序干扰了你并拖慢了你,如果你能成功地将程序彼此隔离,使它们不再互相干扰,那么你就可以减少尾部延迟,有时减少的幅度相当大。如果你能减少尾部延迟,你实际上可以在一台给定的服务器上加载更多的工作,数量相当可观。如果你能在一台服务器上做更多的工作,你就不需要买那么多服务器了,而这 ternyata 能省下很多钱。

所以,尽管这有点像奇怪的谜题,但任何有商业头脑的公司肯定会付钱让你去做这件事。

大多数干扰来自软件,但也有一小部分来自硬件。在接下来的隔离部分,我将专注于硬件基础。我们将在最后一部分回到软件。我想你们中有些人肯定处理过共享公寓楼里那些难缠的邻居,但一些硬件基础的类比就像是住在一个墙壁特别薄或者厨房通风系统很差的公寓里,你的邻居煮卷心菜之类的,味道很难闻。这干扰了你的生活。如果我们看看服务器中共享的一些硬件,问题是一样的。我们有一些共享资源,但它们的共享方式并不合理。所以,我要回到我那个每周期 16 字节的例子。我要问,如果 CPU 0 全速每周期移动 16 字节,会发生什么?我们在这部分成功了。让我们看看缓存里发生了什么。CPU 0 在这里。CPU 0 在砰、砰、砰、砰地运行,很快 CPU 0 的所有 L1 缓存都充满了 CPU 0 的数据。事实上,所有的 L2 缓存也都充满了 CPU 0 的数据,而且几乎所有的 L3 缓存也是。而这些其他的处理器就被坑了。CPU 3 上的工作运行得很慢,因为 CPU 0 正在填满缓存。这种情况在你今天能买到的所有机器上都时有发生。你去一个谷歌云服务器,如果其他人,不是你,在猛烈地使用缓存,你的程序就会运行得很慢,因为你是按小时付费的,你在那个小时内得到的实际工作量就会少很多。亚马逊、微软或其他任何地方都一样。这种情况在我们的数据中心里一直发生,我们倾向于在同一台服务器上同时运行 20 或 30 个不同的不相关程序,因为否则我们就会有空闲时间,而你讨厌在有空闲时间的时候购买机器,特别是付钱的副总裁们讨厌这样做。所以这是个问题。在我们的行业里,我们没有缓存隔离(cache isolation)。一个处理器可以通过大量使用内存来完全摧毁所有其他处理器的性能。

而且我认为对此没有任何好的软件解决方案。有一些软件上的折衷办法,涉及到对缓存进行分区,你得到八分之一,你得到八分之一,你得到四分之一,但颗粒度很大。而你真正想要达到的是这样的状态。理想的情况是,你希望 CPU 0、1、2、3 的 L1 缓存中,大约四分之一的东西是给 CPU 0 的,大约四分之一给 CPU 1,CPU 2,CPU 3。在下一级缓存也是一样,每个 CPU 大约占有公平的份额,比如八分之一或十六分之一。我说“大约”是因为你需要一些弹性。你知道,如果 CPU 1、2、3 什么都不做,都在运行空闲循环,你希望 CPU 0 能够使用一些缓存,但也能很快地放弃它。但你希望在存在实际竞争时能达到那种环境。而这种分区,比如,如果你有一个八路相联的缓存,和 16 个不同的竞争线程,你不能给每个线程十六分之一。你可以给零,或者八分之一,或者八分之三,或者八分之二,等等。这个粒度就是不好。如果你给一个进程八路缓存的八分之一,你实际上给了他们一个小的单路缓存,一个直接映射的缓存。然后你就会遇到各种二阶性能异常,当两个地址恰好落在缓存的同一个位置并发生颠簸(thrash)。所以我推崇选择性分配(selective allocation),从 2004 年开始就有相关论文,但从未被实现过。所以这不是我的想法,我只是想推动它。你给每个线程或每个 CPU 核心一个目标缓存大小,只要那个 CPU 的缓存使用量低于其目标,你就自由地分配缓存行。如果超过了目标,你就偏向于缓存分配,让它优先替换自己的缓存行,而不去动别人的。然后你允许一些超预算的弹性,以避免在其他缓存未被使用时出现利用率不足的情况。所以看起来是这样的。你给每个 CPU 核心两个小的计数器,大概 10 或 15 位,一个是目标,比如,这个得到 25 个缓存行,这个 25 个,这个 25 个,总共 100 个。而这个目前正在使用 27 个,所以它超预算了。这个正在用 24 个,24 个,24 个。它们都在预算内。对于那个超预算的家伙,你说当它发生缓存未命中并需要缓存中的其他东西时,替换那个 CPU 已经拥有的东西,不要替换其他的。你在下一级缓存做同样的事情。你给它们每个都设定一个目标,让它们优先替换自己的。现在,这每个处理器会花费几个比特,任何芯片供应商如果考虑过这个问题并有动力,都会这么做。每个缓存行也需要花费几个比特来说明谁拥有它。因为如果我替换了 CPU 0 的一个缓存行,而这个缓存行里有 CPU 1 的数据,我需要去递减 CPU 1 的计数器,告诉它你不再拥有这个缓存行了。否则这些计数马上就全错了。所以,每个缓存行需要几个比特来跟踪最后是哪个处理器访问了它。到目前为止,所有看过这些论文的芯片供应商都不愿意花这些比特。所以我们得到的是,一个没有隔离的程序可以摧毁所有其他程序的性能。所以我认为这是一个我们确实需要硬件帮助的领域。我将跳过一些细节。特别是,我将跳过改进部分。所以,关于隔离的基础是,你需要好的篱笆才能有好的邻居。你需要一种有硬件支持的方式,来防止程序一拖慢程序二。

在硬件中所有共享的地方,而且这些地方比你想象的要多,但如果你列一个明确的清单,很快你的某个办公室同事之类的就会在清单上加上别的东西。哦是的,那个也是共享的。内存总线是共享的。TLB 条目是共享的。在最近的英特尔处理器上,有一个指令可以生成随机数。但你每秒只能做大约 30,000 次,因为为了获得随机性,实际上有一些物理过程在进行。而那个特定的指令,如果你连续快速地调用它来获取随机数,在大约 30,000 次之后,这个指令突然会花费很长时间,比如一毫秒左右。所以,如果我的那个有敌意的坏邻居程序在以过快的速度生成随机数,而我的简单的乖邻居程序只想要一个随机数,它就要额外等待一毫秒。它没预料到要等。如果这是一个实时应用,那一毫秒的代价可能会非常高。所以我们需要隔离事物。我们需要更多地关注共享的 CPU 调度、共享的缓存、共享的网络、共享的磁盘,所有的一切。我们需要在这个领域进行大量的创新。我的意思是,这些远非最佳的可能想法。而这是我们作为一个行业在未来五到十年内所需要的。

测量的基础

我将以第四部分,关于测量的基础来结束。我想你们中有些人用过 CPU 性能分析器,它会时不时地对程序计数器进行采样,然后画出一种散点图,显示你在这里,你在这里,你在这里,你在这里的次数更多,在那里的次数更少。你得到这些零散的样本。你对平均性能有了一些了解。但你会有一些盲点。你永远看不到那些异常值(outliers)。你永远看不到那些慢 10 倍、100 倍的情况。你永远看不到 99 百分位的慢速情况,因为它们被和另外 99 个快速情况平均掉了。所以,它是一个告诉你发生了什么的好工具,但对于告诉你为什么发生,它毫无用处。

当然,CPU 采样不会告诉你任何关于空闲时间的信息。这实际上是查克·克洛斯(Chuck Close)的作品。而真正的画作是这样的。这只是一个粗略的样本。这才是真正的画作。他创作了微小而细节丰富的像素。他实际上是一个患有,我想是面容失认症(prosopagnosia)的人。他无法识别人脸,除非他把他们画下来,然后他就能认出来了。所以,如果你对所有发生的事情进行时间序列的跟踪记录(traces),你就能得到所有这些细节。现在你就能看到异常情况中到底有什么不同了。不幸的是,这样做的盲点是你必须非常小心跟踪所带来的开销。如果你每个 CPU 核心每秒跟踪 20 万个事件,你的处理量会很大,而你只被允许有 1% 的额外开销。否则,你就要和数据中心的人进行那场艰难的对话了。那么,你记录一个事件的时间就远少于一次缓存未命中的时间。所以你必须非常小心地将它们分组,并在记录跟踪时为不同的 CPU 使用不同的缓存行。但所有这些都是可以做到的。那都只是直接的工程问题。我给你们举个例子。这是我们的直方图,但现在我标记了其中的七个峰值。有一个在零点我们解释过了,一个在两点,一个在三点我们也解释过了。然后是外面的这四个峰值。一个在 250 毫秒,一个在 500 毫秒,一个在 750 毫秒,一个在 1000 毫秒。这有什么奇怪的?

它们都是四分之一秒的倍数。究竟是什么可能导致数量异常多的磁盘事务,即读取,花费的时间恰好是四分之一秒的倍数?

猜猜看。节能。节能。时间尺度不对。节能可能会花费你 100 微秒。是的。你们每个人都想一个猜测,然后你们会发现你们都错了。

这是一张对一台磁盘服务器上的 13 个磁盘进行的跟踪记录。读取操作以蓝色显示。磁盘上实际的读取以蓝色显示。写入以红色显示。然后那些斜向的黑线显示的是处理器端的读取时间。由于有意的预读,处理器可能在磁盘上的读取实际完成之前就返回了答案。并且会有一些写入被缓存在内存中,根本没有接触磁盘。但在 700 毫秒的时间里,在 13 个磁盘上,我们得到了这样一幅图景:顶部的磁盘不是很忙,只是偶尔进行读取。我得请你们忽略那些红点,它们是无关的,那些品红色的点。你看到的是一些零散的读取和偶尔的写入。它们之间没有关联。而且密度不是 100%,可能只有 50%。好的,这是正常情况。

这是同样的 13 个磁盘,同一台服务器,稍后的一段时间。所有的磁盘都很忙。然后,砰,发生了一个相变(phase transition)。所有的磁盘都做了两三次读取。然后在接下来的 250 毫秒里什么也没发生。然后它们又都做了几次读取。然后在接下来的 250 毫秒里又什么也没发生。事实上,一个刚好在这个边缘之后进来的请求的延迟就是 250 毫秒。所有这些家伙的延迟都是 250,250,略低于 250,250。还有一些从这里开始的是 500 毫秒,更远一些的是 750 毫秒。

这种情况持续了九分钟。

然后又有一个相变,恢复正常了。所以,这里可能发生了什么?这与任何单个磁盘都无关。你知道,如果有一个磁盘突然变慢了,那么那个磁盘在这里会做一些奇怪的事情,而其他的都会是正常的。它同时发生在所有磁盘上。所以这是某个共享的东西,也就是说,是 CPU 或网络,但不是磁盘。所以你们可能猜到的所有关于,哦,磁盘在额外旋转或重新校准之类的东西,都不可能。但在你对每个磁盘上发生的事情进行这种跟踪,而不是对整体进行求和或平均之前,你是不知道的。那么,到底发生了什么?当我看到这个时,我在我们所有的代码库中搜索了 250、0.25 和一些其他数字。我没有找到 250 毫秒。然后我给很多人发了邮件。两天后,答案回来了。

哦,而且这九分钟意味着它不是某种临时性的事情。它必定是某种编程错误。有某种情况会让你进入这种状态,又有某种情况会让你退出。它持续多长时间其实并不重要。答案是,哦,内核因为该进程使用的 CPU 时间超过了它通过内部计费购买的时间,所以对 CPU 进行了节流(throttling)。所以这个特定的进程,也就是磁盘服务器进程,使用了太多的 CPU 时间。所以内核,为了保护那台机器上运行的所有其他程序,内核说:“哦,你超出了你的 CPU 配额。我将做的是,在下一个四分之一秒的倍数到来之前,我不会运行你的任何线程。然后我会运行你所有的线程。如果它们占用了太多的 CPU 时间,我会再次对你进行节流,再等四分之一秒。”而你摆脱这种情况的唯一方法是,侥幸地,在某个四分之一秒内没有很多新的请求进来。然后它们就停止对你进行节流了。砰,你就恢复正常了。

这种情况又发生了。你知道,那是在早上 7 点。晚上 6 点又发生了。于是另一个人开始查看我们整个机群,说:“这种情况多久发生一次?”现在我们知道要找什么了。他发现,这种情况在全球谷歌所有磁盘服务器的 25% 上都会发生,平均每天大约 30 分钟。他还发现有些机器上这种情况连续发生了 23 个小时。

而我们所处的环境是,我们希望在 200 毫秒内返回答案或搜索结果。而这是 250 毫秒起步。

他还发现,这只发生在那些更老、更慢的服务器上,这些服务器从内部计费的角度来看,根本就是配置不足。所以修复这个问题……哦,这种情况已经持续了三年了。

修复它所省下的钱足够支付我 10 年的薪水。

我给你们看的那张图,就是导致它被修复的图。

从来没有人看过这些跟踪记录。我们偶尔会采集磁盘跟踪记录。但从来没有人真正看过它们,把它们画成图,直到你能看到一个模式,然后思考、调查并找出原因。所以,永远不要把可以用愚蠢来充分解释的事情归咎于恶意。你们中有些人可能在这个行业里遇到过。我的推论是:永远不要把可以用软件复杂性来充分解释的事情归咎于愚蠢。这都是由不同团队拥有的、有着不同目标和不同议程的层层软件造成的。这在我们的行业里是正常的。

所以,要做的是努力去理解该测量什么。世界上所有的性能之谜,一旦被理解,就都变得简单了。但如果它是个谜,就意味着你不知道发生了什么。而解决这个问题的方法不是去猜测。你知道,这是针对难题的,不是那种 CPU 性能分析器告诉你哪里错了的问题。那种问题你直接去修复就好了。我感兴趣的是那些没人修复的问题,因为它们太难理解了。对于那些问题,你真的需要说:猜测不是一种策略。事实上,软件工程师在猜测性能问题上是出了名的无能。相反,你需要问这样一个问题:我需要什么数据才能告诉我到底发生了什么?然后去努力以某种方式或某种近似的方式收集那些数据。然后你就能看到真正发生了什么。然后我发现,这些问题中的大多数都是 20 分钟就能修复的。可能需要两个月来理解发生了什么,并最终得到正确的图表,然后说:“砰,就是它了。”然后它们就都变得简单了。但它们都各不相同。没有那种神奇的,“哦,让我们建一个能发现性能错误的软件工具”之类的东西。程序员太有创造力了。所以你需要低开销的工具。你需要能看到所有的动态,每个事务,每个远程过程调用。你真的需要为你正在发生的一切打上时间戳,并且能够进行长时间的跟踪,长时间的意思是像 30 秒。3 秒太短了。30 秒非常好。4 分钟你不需要。要看到所有正在发生的事情。然后你就能做得很好。

总结

简单总结一下,我们谈到了大数据移动,大的和小的有所不同。每秒数千个实时事务与台式机相比有些不同。程序之间的隔离有些不同。以及需要进行仔细测量的必要性也有些不同。这里有一些参考文献。我想幻灯片的副本会和演讲一起提供。我的第一个参考文献是香农的论文,因为我相信,作为一名计算机科学专业人士,你应该每隔几年就回去读一些基础性的论文。我大概每五年就回去读一遍香农的论文。同一篇论文。而我每次都能学到新的东西。除此之外,它与本次演讲无关。这是我 20 年前发现并引用的一篇关于内存的论文。问题仍然是内存。这里有几篇关于服务质量缓存共享思想的参考文献。

还有一些关于……我没有谈到 Z-cache,但这是更深一层的细节。Luis Berroso,哦,抱歉。这个引用错了。这是关于我们内部跟踪结构的一部分的公开论文,它处理远程过程调用树并为事物打上时间戳。还有一点关于细粒度缓存分区的。Luis 和 Urs 写了一本非常好的,几乎像一本书的东西,但在网上可以找到 PDF,我鼓励你们去看一下,是关于数据中心计算机的,作为仓库级机器设计的入门。在第二版中,他们真正地带你走过谷歌的数据中心,以及建造它们时需要考虑的各种因素。现在我将停下来,回答一些问题。

问答环节

提问者1: 好的。你展示的那些事务跟踪记录,是针对单个孤立事务的。如果运行一个真实的负载,比如说,在同一套硬件上同时运行 1500 个事务,会发生什么?它们是如何交错的?吞吐量如何?我对吞吐量与并发事务数量的曲线非常感兴趣。

Dick Sites: 是的。我们正在接近那个。哦,过了一页。所以,CPU 的顶部,所有的 CPU 在这里都很忙。它们在 600 微秒内处理了 40 个不同的事务,这些事务都是重叠的。这就是正在发生的事情。那是 40 个事务。如果你有耐心,你可以把那一部分拿出来,在上面找到它。你可以把那一段执行拿出来,在那里找到它,等等。所以它们都在 CPU 上交错执行。在软件线程上,它们也都是交错的。在锁上,它们……你知道,在某个特定的锁上它们不是交错的,但在不同的锁上,有很多事情在发生。

提问者2: 后面还有一个问题。当有人来找你说,“我们有一个长尾查询”,你是如何获得直觉,从哪里开始,以及如何开始这种性能分析的?你是怎么知道应该归咎于硬件还是软件?还是说这只是一个你开始用工具迭代的过程,所以这个过程才需要很长时间?你的结论非常具体,而且你说这是 10 年工作的成果。

Dick Sites: 这 10 年的工作是用来构建能画出这些图的工具。针对某个特定问题,比如“Gmail 很慢,怎么回事”的工作,时间要短得多。我不知道。我真的就是从“我需要什么信息来回答这个问题”开始。如果事情很慢,你想知道的就是“所有的时间都去哪了?”。如果你做这些性能分析,看直方图和平均值,你不会知道所有的时间都去了哪里。你只知道一个普通的有趣事务是这样的。所以我非常主张,缓慢地、有条不紊地构建开销极低的跟踪工具,来跟踪每一个事件。磁盘跟踪的时间尺度大概是毫秒级。而 CPU 对内核调用之类的跟踪,时间尺度更像是每个事件 100 纳秒。对于一个记录所有事件的长跟踪,你需要软件钩子来边走边记录。你需要大约 16 位来表示发生了什么事件。你需要大约 16 位的时间戳来表示发生的时间。你还需要把所有高位的比特都分离出去,否则你会花太多时间。所以每个事件大约需要 4 字节。你说,嗯,我不知道,每个 CPU 每秒有 20 万个内核调用和返回事件。你有 16 个 CPU,那么每秒就有 320 万个事件,每个 4 字节。所以大约是每秒 12MB。你说,好的,这是一个可管理的内存带宽,可以在不干扰的情况下进行写入。但你需要一个地方来存放它,你得算出来,如果你需要 30 秒的记录,那么你至少需要 360MB 的内存来存放东西。但你不需要 300GB 的内存来存放东西。

然后你需要去构建跟踪工具,再构建一些能写出,在这种情况下是 PostScript 图片之类的东西。然后你去看它们。人眼仍然是目前世界上最好的模式识别器。我非常努力地把 25 万个数据点放在一张纸上,因为我在纸上得到的分辨率比在屏幕上高得多。然后我去看它。当那张显示所有磁盘上 250 毫秒延迟的图从打印机里出来时,我立刻就看清了三年来没人理解的问题所在。他们能看到它时不时发生。好的,下一个问题。

提问者3: 你的三个主题都以下结论告终:我们需要硬件制造商做些什么。我想知道这背后的经济学是什么。我认为像谷歌、Facebook 和微软等数据中心提供了服务器处理器制造业务中相当大的一部分。是什么样的经济因素没有推动他们给你所需要的东西?而且我猜想你们谷歌的需求与其他数据中心的需求应该不会有太大差异。

Dick Sites: 我想那可能是对的,但我不知道。你问题的答案是:每年 1.6 亿台个人电脑。那才是设计目标。好的,所以,我对你们的假设是,随着行业分化为手机和数据中心,而中间地带消失,我们将需要更多地关注数据中心里发生的事情。而这是让你们都开始思考这个问题的一个早期步骤。

提问者4: 单个服务器是不是变得太大了?一个盒子里发生的事情太多了。

Dick Sites: 合理的假设。是的,我认为这基本上是摩尔定律必须进入另一个维度的结果。

提问者5: 有很多关于微型计算机的论文。基本上,与其试图让这些大型服务器作为大型服务器更好地工作,不如拥有许多没有这些固有的、难以处理的复杂性的小型服务器,这样会不会更好?

Dick Sites: 不会。好的。如果你有很多小型计算机,比如小型的 ARM 处理器,32 位处理器,它们无法寻址足够的内存。所以它们只能处理小问题的小部分。它们无法处理大问题中足够大的部分。当你拥有数量极其庞大的它们时,协调起来很困难。而且它们做不了太多事。你无法用一个羸弱的处理器来保持一个每秒 10 吉比特的网络链接繁忙。你没有足够的 CPU 周期来输送字节。所以,要能快速完成工作,有一个最低的基础。我不是说我们今天在行业里能买到的东西就是最佳选择,但我很确定,大量微小的处理器,比如速度只有今天的十分之一,是个坏主T意,因为你根本无法共享足够的东西。我的意思是,拥有几百 GB 内存的好处是,你可以让几个程序共享它。如果你只有 4GB 内存和一个羸弱的处理器,然后你买了一百万个这样的东西,然后某个付钱的高级副总裁审视整个机群,看到 80% 的内存都未被使用,因为你不得不把东西分解成大小不完全合适的小块。那是低效的。那很昂贵。

提问者6: 有没有哪些公司,无论大小,你看到正在解决这个硬件问题的?

Dick Sites: 没有,我希望你们中的一些人能在未来五年内做到。这些幻灯片大约是三周前做的。

提问者7: 你在后面某个地方有个问题?(示意某人)

Dick Sites: 好的,最后一个问题。最后两个问题。

提问者8: 我有一个问题是,你的数据移动工作负载中有多少可以通过采用零拷贝协议(zero copy protocols)并将事情推到 DMA 引擎(DMA engines)中来解决?

Dick Sites: 是的,零。零?好的,原因如下。我观察过这个零拷贝 DMA。让我先谈谈零拷贝网络的事情。每年都有人挥舞着手臂说,“哦,我们可以重新安排内核,实现零拷贝网络传输”,但它们从未实现。它们从未实现,因为如果我持有一堆我想通过网络发送的东西,而我是磁盘服务器软件,我不能把它交给内核,让内核拥有它,然后说“发送这个”,我必须一直持有那块内存直到它真正通过网线。如果网线堵塞了,发生的情况是磁盘服务器会堆积越来越多要发送但还没发送出去的东西,然后耗尽内存并崩溃。

这是真实发生过的。我见过这种情况。磁盘服务器耗尽内存然后崩溃。通过将数据复制到内核,并将所有权更改为内核所有,磁盘服务器就永远不会崩溃。内核实际上知道网络有瓶颈,而磁盘服务器不知道,并且内核拥有所有这些美妙的机制来排队和延迟事情,这是清晰的所有权问题。所以我观察过公司内部部署那些这样做的内核,它们总是失败,并且在最初的十几台机器之后就不会再被部署了。所以它就是没发生。至于 DMA 引擎,如果我能得到一个每个周期能移动 16 字节的 CPU,我为什么要去获取一个锁,在 16 个 CPU 核心之间共享一个单一的硬件——也就是 DMA 引擎,花一段时间启动它,让它运行,然后等待,以某种方式得到一个中断或信号说它完成了,最后解包所有东西,释放描述正在发生的事情的数据结构,然后再重新开始工作。我的意思是,西摩·克雷(Seymour Cray)在他的 7600 上展示过这个,他有一个移动指令,可以从大核心内存移动到小核心内存,而在移动期间它会锁定整个机器。

提问者8(追问): 是的,所以边缘缓存(edge caches)最终会大量使用这些东西,对吧?亚马逊就有一大堆。问题是为什么在你的工作负载中这行不通?

Dick Sites: 我不知道答案,但我认为整个 DMA 引擎的空谈不是一个好方法。如果相反,你能把设计精力投入到让所有 CPU 都能更快地移动数据上,那在编程上会是一个简单得多的问题。让我回答最后几个问题,然后我们很快就去招待会。你有一个。

提问者9: 是的,在我工作的地方,我可以获得对这类工作很有用的那种粒度的跟踪记录。我能从跟踪记录中看到长尾。我的问题是,要制作出你用来向他人解释并自己找出问题的那些漂亮图表,非常困难。那么,这些工具有没有可用的?

Dick Sites: 没有。它们不可用,部分原因是谷歌付钱让我为谷歌做这件事。它们不可用,部分原因是在一般情况下,人们对能处理 100MB 跟踪输入的工具没有太大兴趣。你知道,对于很多人来说,这个规模就是不对的。而那些可能感兴趣的人,往往是直接的竞争对手。

提问者10(最后一个问题): 你提到的一些挑战,难道不是源于你们想用像英特尔或……这样的通用处理器来构建你们的数据中心或超级计算机这个事实吗?

Dick Sites: 当然。我们买我们能得到的最便宜的硬件。我们不是硬件供应商。我们宁愿买三台个人电脑,也不愿买一台昂贵的、超高可靠性的什么东西,因为用三台个人电脑,我们可以让其中一两台出故障,但用同样的钱仍然能完成更多的工作。所以,经济学强烈偏爱商品化产品(commodity products)。好的。非常感谢大家。谢谢。