https://zhuanlan.zhihu.com/p/531101733
https://book.douban.com/subject/35934902/
应电子工业出版社编辑的邀请,我怀着忐忑的心情,斗胆写下这篇推荐语,以下是带链接的原稿。
性能测量的水相当深,斯坦福大学的 John Ousterhout 教授[wiki]在《Always Measure One Level Deeper》一文(《ACM 通讯》杂志 2018 年第 7 期)中提到,在他亲历的几十次系统性能评估中,没有哪次的首批测量结果是正确的。都是先掉进坑里,再慢慢爬出来。
就拿文件 IO 来说,你测得的是真实的磁盘性能,还是操作系统中文件系统的性能?存储是分级的。操作系统有 page cache,高档的磁盘控制器(阵列卡)有带电池备份的 write cache,机械硬盘上有 cache RAM,混合型机械硬盘带有 SSD 缓存,消费级 TLC SSD 上带有 SLC 缓存。文件 IO 的性能恐怕主要取决于的读写操作击中的是哪一级 cache,而 cache 颠簸(thrashing)是系统过载时造成性能急剧下降的重要因素。
如今的计算机系统极度复杂,性能优劣往往违反直觉。不少人可能先入为主地认为 mmap() 比 read()/write() 读写文件更快,因为前者减少了系统调用和内存拷贝。这听上去很有道理,也正是 80 年代 Unix 引入 mmap() 的理由。但是卡内基·梅隆大学的 Andrew Crotty 等人通过分析与测量,对此提出相反的观点。他们的论文《Are You Sure You Want to Use MMAP in Your Database Management System?》发表在 CIDR 2022 会议上。
性能瓶颈时常发生在意想不到的地方,我们遇到过服务器硬件升级导致性能下降的案例。新的服务器有更大的网络带宽,更快的 CPU 和更多的核,我们原本预计它每秒能处理更多的计算密集型请求。但很快我们发现其上运行的一个网络服务程序的响应时间偶尔会飙升,甚至出现超时。初步探查发现此期间 CPU 和网络带宽的使用率都不高,远未饱和。进一步分析发现,这个服务程序会按比例抽取一些客户请求写在本地磁盘,以供日后分析。由于新机器更强大,它收到的请求也比原来多,但新磁盘的速度没有多大提升,这就使得磁盘带宽吃紧。如果同一台机器上同时有别的程序大量读写磁盘,会造成我们的服务程序阻塞在磁盘 IO 上,CPU 无所事事,响应时间也自然会增加。更糟糕的是,我们的负载均衡器主要依据 CPU 繁忙度来分配新任务,试图让多台服务器的 CPU 使用率一致,以避免出现延迟波动。这个服务程序因为等在磁盘 IO 上,反映出 CPU 使用率低的假象,结果反而分到了更多的请求,排在队列里等候处理,最终造成请求超时。一开始谁能料到我们这个计算密集型网络服务程序的性能最终受限于磁盘带宽?排查性能故障就像阅读侦探小说,当谜底层层揭开的时候,读者会恍然大悟,反思自己怎么没有早点想到凶手是谁呢?系统的性能瓶颈可能出现在 CPU、内存、硬盘、网络甚至总线上,这本书第 6 到 10 章详细介绍了各种情况的应对方法。
现代 CPU 不再是常速运行的。功耗是限制 CPU 主频提高的主要因素,多核 CPU 在整体功耗允许的情况下,会把其中某一个或几个核自动“超频”,以提高程序性能。这使得在空闲机器上做的单线程基准测试的结果一般不能简单推广到多核。比如说,消费级 12 核 AMD 5900X 的单线程 memcpy() 速度可达 20GB/s,但是双通道 DDR4 内存能提供的总带宽约为 50GB/s,平摊到每个核上大约才 4GB/s,是单核峰值的 1/5。我还曾经试验把一个单线程的程序改成两个线程交替计算,在多核台式机上测试发现性能下降 50% 以上,远超我的预期。后来发现 CPU 为了节能,在工作量不饱和的情况下,自动降频运行。两个慢速的 CPU 核来回接力运算当然远差于一个全速的核。本书第 12 章介绍了更多性能基准测试的陷阱与防范措施。
体系结构领域的图灵奖得主,加州大学伯克利分校的 David Patterson 教授在《ACM 通讯》2004 年第 10 期发表的《Latency lags bandwidth》文章[slides]指出,带宽的增长远远快于延迟的进步,因为把公路修宽比把路修短容易得多。内存带宽大约每 5 年翻倍。2020 年的 DDR5 内存带宽是 2000 年 DDR1 带宽的 16 倍,但访问延迟变化不大,还是 30ns 上下,是现在 CPU 时钟周期的 100 倍左右,而且 DDR5 的延迟比 DDR4 不降反升。跟硬盘相比,内存当然是很快的,但是跟 CPU 运算速度相比,内存访问可以算得上相当慢,遍历链表如今可以入选头号 CPU 性能“大杀器”。在必要的情况下,程序员可以通过调整内存布局来提高 locality,以求降低 CPU cache miss。Google 开源的自家 C++ 基础库中的 absl::flat_hash_map 正是利用了这一点,实现了比标准库的开链 unordered_map 快 2~3 倍的性能。
互联网先驱,TCP 拥塞控制算法的发明人 Van Jacobson 博士在 2018 年 netdev 0x12 会议的开场主题演讲中提到,从 1990 年算起,以太网带宽的增速甚至超过了摩尔定律。康奈尔大学的博士生蔡其哲等人发表在 SIGCOMM ’21 的论文《Understanding Host Network Stack Overheads》的数据表明,经过调优,Linux 上单个 TCP 连接的网络吞吐量可达 5GB/s,只比 memcpy() 慢几倍。为了实现高效安全可靠的网络传输,消息在发送之前,常经过压缩(Zstd)、加密(AES)、校验(SHA1)等算法处理。而尽管有 AES/SHA 指令加速,CPU 执行这些算法的吞吐量还不及 TCP 速度的一半。如果设计不当,加密与计算校验和这样的前期准备工作会成为收发网络消息的性能瓶颈,而 TCP/IP 协议栈反而乐得清闲,这就有点儿尴尬了。我们当然不能因噎废食,为了速度而放弃安全性。一个办法是把这些耗费 CPU 和内存带宽的工作下放(off load)到内核和网卡里去做。Netflix 从 2015 年开始往 FreeBSD 内核增加 TLS 功能,然后与网卡厂商合作,把 TLS 硬件加速做到网卡里。在 2019 年的 EuroBSDCon 会议上他们汇报了成果《Kernel TLS and hardware TLS offload in FreeBSD 13》,然后在 2021 年汇报了最新进展《Serving Netflix Video at 400Gb/s on FreeBSD》。注意这里的 400Gb/s 是多个连接汇总的吞吐量,这里也没有提及压缩,因为 Netflix 传输的视频数据本身已经是高度压缩过了。受此激励,Linux 也不甘落后,从 4.13 版开始逐步支持内核 TLS 与硬件加速。
总而言之,提升系统性能与代码性能优化是完全不同的领域,它不是找到热点函数加以改写,而是通过对系统的总体把握与观测,精确定位软硬件性能瓶颈,设法化解并突破。前面的例子说明,克服性能瓶颈的手段除了修改应用程序和调整配置,必要时可以改进操作系统内核,甚至引入硬件加速器。拿我比较熟悉的 Linux 网络协议栈来说,20 年前,为了解决高并发网络编程的瓶颈,加入了 epoll。10 年前,为了批量收发 UDP 消息,增加了 recvmmsg/sendmmsg 系统调用,后来基于 UDP 的 QUIC 协议实现也得益于此。为了解决 Web 服务器接受新 TCP clients 的瓶颈,增加了 SO_REUSEPORT 选项。为了降低 Web 服务器首次请求的延迟,加大了 TCP 初始窗口 (RFC6928),并且实现了 TCP Fast Open (RFC7413)。5 年前,为了减少收发消息时内存拷贝的开销,增加了 MSG_ZEROCOPY 和 TCP_ZEROCOPY_RECEIVE 选项。去年,为了加快 TCP 丢包检测,有了 Tail Loss Probe (RFC8985)。这些改进也逐步被别的操作系统实现。
本书作者 Brendan Gregg 是全球知名的实战派性能专家,他发明的火焰图是分析 CPU 开销的有力工具,如今已是我们日常性能分析的标配。这本书是他多年经验的总结,既有理论深度,又有丰富案例,在同类书籍里是最全面深入的,非常值得深入阅读学习。先后两个版本我都是第一时间购买,常备案头,随时查阅。
——陈硕 2022年5月
原创文章,作者:6024010,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/274692.html