不久前，识别硬件瓶颈还需要深厚的专业知识。今天的开源 GUI 性能监视器使它变得相当简单。计算机是一个集成的系统，它的性能取决于最慢的硬件组件。如果一个组件的能力比其他组件差，性能落后而不能跟上，它就会拖累你的整个系统。这就是一个 性能瓶颈。消除一个严重的瓶颈可以使你的系统飞起来。本文解释了如何识别 Linux 系统中的硬件瓶颈。这些技术同时适用于个人的电脑和服务器。我强调的是个人电脑 —— 我不会涉及局域网管理或数据库系统等领域的服务器特定的瓶颈。这些通常涉及专门的工具。我也不会多谈解决方案。这对本文来说是个太大的话题。相反，我将写一篇关于性能调整的后续文章。我将只使用开源的图形用户界面（GUI）工具来完成这项工作。大多数关于 Linux 瓶颈的文章都相当复杂。它们使用专门的命令，并深入研究神秘的细节。开源提供的 GUI 工具使得识别许多瓶颈变得简单。我的目标是给你一个快速、简单的方法，你可以在任何地方使用。从哪里开始一台计算机由六个关键的硬件资源组成。处理器内存存储器USB 端口互联网连接图形处理器如果任何一个资源表现不佳，就会产生一个性能瓶颈。为了识别瓶颈，你必须监测这六种资源。开源提供了大量的工具来完成这项工作。我会使用 GNOME 系统监视器。它的输出很容易理解，而且你可以在大多数软件库中找到它。启动它并点击“资源”标签。你可以马上发现许多性能问题。图 1. 系统监控器发现问题。(Howard Fosdick, CC BY-SA 4.0)在“资源”面板上显示三个部分：CPU 历史、内存和交换历史，以及网络历史。一眼就能看出你的处理器是否不堪负荷了，还是你的电脑没有内存了，抑或你的网络带宽被用光了。我将在下面探讨这些问题。现在，当你的电脑速度变慢时，首先检查系统监视器。它可以立即为你提供最常见的性能问题的线索。现在让我们来探讨一下如何识别特定方面的瓶颈。如何识别处理器的瓶颈要发现瓶颈，你必须首先知道你有什么硬件。开源为这个目的提供了几个工具。我喜欢 HardInfo，因为它的屏幕显示很容易阅读，而且广泛流行。启动 HardInfo。它的“计算机->摘要”面板可以识别你的 CPU 并告诉你它的核心数、线程数和速度。它还能识别你的主板和其他计算机部件。图 2. HardInfo 显示了硬件细节。(Howard Fosdick, CC BY-SA 4.0)HardInfo 显示，这台计算机有一个物理 CPU 芯片。该芯片包含两个处理器（或称为核心）。每个核心支持两个线程（或称为逻辑处理器）。这就是总共四个逻辑处理器 —— 正是图 1 中系统监控器的 CPU 历史部分所显示的。当处理器不能在其时间内对请求做出反应时，就会出现 处理器瓶颈，说明它们已经很忙了。当系统监控器显示逻辑处理器的利用率持续在 80% 或 90% 以上时，你就可以确定这一点。这里有一个例子，四个逻辑处理器中有三个被淹没在 100% 的利用率中。这是一个瓶颈，因为它没有留下多少 CPU 用于其他工作。图 3. 一个处理器的瓶颈。(Howard Fosdick, CC BY-SA 4.0)哪个程序导致了这个问题？你需要找出是哪个程序在消耗所有的 CPU。点击系统监视器的“进程”标签。然后点击“CPU 百分比”标头，根据它们消耗的 CPU 的多少对进程进行排序。你将看到哪些应用程序正在扼杀你的系统。图 4. 识别违规的进程。(Howard Fosdick, CC BY-SA 4.0)前三个进程各消耗了 总 CPU 资源的 24%。由于有四个逻辑处理器，这意味着每个进程消耗了一整个处理器。这就像图 3 所示。在“进程”面板上，一个名为“analytical\_AI”的程序被确定为罪魁祸首。你可以在面板上右键单击它，以查看其资源消耗的更多细节，包括内存使用、它所打开的文件、其输入/输出细节，等等。如果你的登录会话有管理员权限，你可以管理这个进程。你可以改变它的优先级，并停止、继续、结束或杀死它。因此，你可以在这里立即解决你的瓶颈问题。图 5. 右键点击一个进程来管理它。(Howard Fosdick, CC BY-SA 4.0)如何解决处理瓶颈问题？除了实时管理违规的进程外，你也可以防止瓶颈的发生。例如，你可以用另一个应用程序来代替违规进程，绕过它，改变你使用该应用程序的行为，将该应用程序安排在非工作时间，解决潜在的内存问题，对该应用程序或你的系统软件进行性能调整，或升级你的硬件。这里涉及的内容太多，所以我将在下一篇文章中探讨这些方式。常见的处理器瓶颈在用系统监控器监控你的 CPU 时，你会遇到几种常见的瓶颈问题。有时一个逻辑处理器出现瓶颈，而其他所有的处理器都处于低利用率。这意味着你有一个应用程序，它的代码不够智能，无法利用一个以上的逻辑处理器，而且它已经把正在使用的那个处理器耗尽了。这个应用程序完成的时间将比使用更多的处理器要长。但另一方面，至少它能让你的其他处理器腾出手来做别的工作，而不会接管你的电脑。你也可能看到一个逻辑处理器永远停留在 100% 的利用率。要么它非常忙，要么是一个进程被挂起了。判断它是否被挂起的方法是，是看该进程是否从不进行任何磁盘活动（正如系统监视器“进程”面板所显示的那样）。最后，你可能会注意到，当你所有的处理器都陷入瓶颈时，你的内存也被完全利用了。内存不足的情况有时会导致处理器瓶颈。在这种情况下，你要解决的是根本的内存问题，而不是体现出症状的 CPU 问题。如何识别内存瓶颈鉴于现代 PC 中有大量的内存，内存瓶颈比以前要少得多。然而，如果你运行内存密集型程序，特别是当你的计算机没有很多的随机存取内存（RAM）时，你仍然可能遇到这些问题。Linux 使用内存 既用于程序，也用于缓存磁盘数据。后者加快了磁盘数据的访问速度。Linux 可以在它需要的任何时候回收这些内存供程序使用。系统监视器的“资源”面板显示了你的总内存和它被使用的程度。在“进程”面板上，你可以看到单个进程的内存使用情况。下面是系统监控器“资源”面板中跟踪总内存使用的部分。图 6. 一个内存瓶颈。(Howard Fosdick, CC BY-SA 4.0)在“内存”的右边，你会注意到 交换空间。这是 Linux 在内存不足时使用的磁盘空间。它将内存写入磁盘以继续操作，有效地将交换空间作为你的内存的一个较慢的扩展。你要注意的两个内存性能问题是：内存被大量使用，而且你看到交换空间的活动频繁或不断增加。内存和交换空间都被大量使用。情况一意味着更慢的性能，因为交换空间总是比内存更慢。你是否认为这是一个性能问题，取决于许多因素（例如，你的交换空间有多活跃、它的速度、你的预期，等等）。我的看法是，对于现代个人电脑来说，交换空间任何超过象征性的使用都是不可接受的。情况二是指内存和交换空间都被大量使用。这是一个 内存瓶颈。计算机变得反应迟钝。它甚至可能陷入一种“咆哮”的状态，在这种状态下，除了内存管理之外，它几乎不能完成其他任务。上面的图 6 显示了一台只有 2GB 内存的旧电脑。当内存使用量超过 80% 时，系统开始向交换空间写入，响应速度下降了。这张截图显示了内存使用量超过了 90%，而且这台电脑已经无法使用。解决内存问题的最终答案是要么少用内存，要么多买内存。我将在后续文章中讨论解决方案。如何识别存储瓶颈如今的存储有固态和机械硬盘等多个品种。设备接口包括 PCIe、SATA、雷电和 USB。无论有哪种类型的存储，你都要使用相同的程序来识别磁盘瓶颈。从系统监视器开始。它的“进程”面板显示各个进程的输入/输出率。因此，你可以快速识别哪些进程做了最多的磁盘 I/O。但该工具并不显示每个磁盘的总数据传输率。你需要查看特定磁盘上的总负载，以确定该磁盘是否是一个存储瓶颈。要做到这一点，使用 atop 命令。它在大多数 Linux 软件库中都有。只要在命令行提示符下输入 atop 即可。下面的输出显示，设备 sdb 达到 busy 101%。很明显，它已经达到了性能极限，限制了你的系统完成工作的速度。图 7. atop 命令识别了一个磁盘瓶颈。(Howard Fosdick, CC BY-SA 4.0)注意到其中一个 CPU 有 85% 的时间在等待磁盘完成它的工作（cpu001 w 85%）。这是典型的存储设备成为瓶颈的情况。事实上，许多人首先看 CPU 的 I/O 等待时间来发现存储瓶颈。因此，要想轻松识别存储瓶颈，请使用 atop 命令。然后使用系统监视器上的“进程”面板来识别导致瓶颈的各个进程。如何识别 USB 端口的瓶颈有些人整天都在使用他们的 USB 端口。然而，他们从不检查这些端口是否被最佳地使用。无论你是插入外部磁盘、U 盘，还是其他东西，你都要确认你是否从 USB 连接的设备中获得了最大性能。这个图表显示了原因。潜在的 USB 数据传输率差异 很大。图 8. USB 速度变化很大。(Howard Fosdick，根据 Tripplite 和 Wikipedia 提供的数字，CC BY-SA 4.0)HardInfo 的“USB 设备”标签显示了你的计算机支持的 USB 标准。大多数计算机提供不止一种速度。你怎么知道一个特定端口的速度呢？供应商对它们进行颜色编码，如图表中所示。或者你可以在你的计算机的文档中查找。要看到你得到的实际速度，可以使用开源的 GNOME 磁盘 程序进行测试。只要启动 GNOME 磁盘，选择它的“磁盘基准”功能，然后运行一个基准测试。这将告诉你在一个端口插入特定设备时，你将得到的最大实际速度。你可能会得到不同的端口传输速度，这取决于你将哪个设备插入它。数据速率取决于端口和设备的特定组合。例如，一个可以以 3.1 速度运行的设备如果使用 2.0 端口就会以 2.0 的速度运行。（而且它不会告诉你它是以较慢的速度运行的！）相反，如果你把一个 USB 2.0 设备插入 3.1 端口，它能工作，但速度是 2.0 的速度。所以要获得快速的 USB，你必须确保端口和设备都支持它。GNOME 磁盘为你提供了验证这一点的方法。要确定 USB 的处理瓶颈，使用你对固态和硬盘所做的同样程序。运行 atop 命令来发现 USB 存储瓶颈。然后，使用系统监视器来获取违规进程的详细信息。如何识别互联网带宽瓶颈系统监控器的“资源”面板会实时告诉你互联网连接速度（见图 1）。有 很好的 Python 工具 可以测试你的最大网速，但你也可以在 Speedtest、Fast.com 和 Speakeasy 等网站进行测试。为了获得最佳结果，关闭所有东西，只运行 速度测试；关闭你的虚拟私有网络；在一天中的不同时间运行测试；并比较几个测试网站的结果。然后将你的结果与你的供应商声称的下载和上传速度进行比较。这样，你就可以确认你得到的是你所付费的速度。如果你有一个单独的路由器，在有和没有它的情况下进行测试。这可以告诉你，你的路由器是否是一个瓶颈。如果你使用 WiFi，在有 WiFi 和没有 WiFi 的情况下进行测试（通过将你的笔记本电脑直接与调制解调器连接）。我经常看到人们抱怨他们的互联网供应商，而实际上他们只是有一个 WiFi 瓶颈，可以自己解决。如果某些程序正在消耗你的整个互联网连接，你想知道是哪一个。通过使用 nethogs 命令找到它。它在大多数软件库中都有。有一天，我的系统监视器突然显示我的互联网访问量激增。我只是在命令行中输入了 nethogs，它立即确定带宽消耗者是 Clamav 防病毒更新。图 9. Nethogs 识别带宽用户。(Howard Fosdick, CC BY-SA 4.0)如何识别图形处理瓶颈如果你把显示器插在台式电脑后面的主板上，你就在使用 板载显卡。如果你把它插在后面的卡上，你就有一个专门的图形子系统。大多数人称它为 视频卡 或 显卡。对于台式电脑来说，附加显卡通常比主板上的显卡更强大、更昂贵。笔记本电脑总是使用板载显卡。HardInfo 的“PCI 设备”面板告诉你关于你的图形处理单元（GPU）。它还显示你的专用视频内存的数量（寻找标有“可预取”的内存）。图 10. HardInfo提供图形处理信息。(Howard Fosdick, CC BY-SA 4.0)CPU 和 GPU 非常密切地 一起工作。简而言之，CPU 为 GPU 准备渲染的帧，然后 GPU 渲染这些帧。当你的 CPU 在等待 100% 繁忙的 GPU 时，就会出现 GPU 瓶颈。为了确定这一点，你需要监控 CPU 和 GPU 的利用率。像 Conky 和 Glances 这样的开源监控器，如果它们的扩展插件支持你的图形芯片组，就可以做到这一点。看一下 Conky 的这个例子。你可以看到，这个系统有很多可用的 CPU。GPU 只有 25% 的使用率。想象一下，如果这个 GPU 的数量接近 100%。那么你就会知道 CPU 在等待 GPU，你就会有一个 GPU 的瓶颈。图 11. Conky 显示 CPU 和 GPU 的利用率。 (图片来源：AskUbuntu论坛)在某些系统上，你需要一个供应商专属的工具来监控你的 GPU。它们可以从 GitHub 上下载，并在 GPU 监控和诊断命令行工具 这篇文章中有所描述。总结计算机由一系列集成的硬件资源组成。如果它们中的任何一个在工作量上远远落后于其他资源，就会产生性能瓶颈。这可能会拖累你的整个系统。你需要能够识别和纠正瓶颈，以实现最佳性能。不久前，识别瓶颈需要深厚的专业知识。今天的开源 GUI 性能监控器使它变得相当简单。在我的下一篇文章中，我将讨论改善你的 Linux 电脑性能的具体方法。同时，请在评论中分享你自己的经验。via: https://opensource.com/article/21/3/linux-performance-bottlenecks作者：Howard Fosdick 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
在 Linux 中如何使用 iotop 和 iostat 监控磁盘 I/O 活动？
你知道在 Linux 中我们使用什么工具检修和监控实时的磁盘活动吗？如果 Linux 系统性能变慢，我们会用 top 命令 来查看系统性能。它被用来检查是什么进程在服务器上占有如此高的使用率，对于大多数 Linux 系统管理员来说很常见，现实世界中被 Linux 系统管理员广泛采用。如果在进程输出中你没有看到很大的不同，你仍然有选择查看其他东西。我会建议你在 top 输出中检查 wa 状态，因为大多数时间里服务器性能由于在硬盘上的高 I/O 读和写降低了性能。如果它很高或者波动，很可能就是它造成的。因此，我们需要检查硬盘上的 I/O 活动。我们可以在 Linux 中使用 iotop 和 iostat 命令监控所有的磁盘和文件系统的磁盘 I/O 统计。什么是 iotop？iotop 是一个类似 top 的工具，用来显示实时的磁盘活动。iotop 监控 Linux 内核输出的 I/O 使用信息，并且显示一个系统中进程或线程的当前 I/O 使用情况。它显示每个进程/线程读写 I/O 带宽。它同样显示当等待换入和等待 I/O 的线程/进程花费的时间的百分比。Total DISK READ 和 Total DISK WRITE 的值一方面表示了进程和内核线程之间的总的读写带宽，另一方面也表示内核块设备子系统的。Actual DISK READ 和 Actual DISK WRITE 的值表示在内核块设备子系统和下面硬件（HDD、SSD 等等）对应的实际磁盘 I/O 带宽。如何在 Linux 中安装 iotop ？我们可以轻松在包管理器的帮助下安装，因为该软件包在所有的 Linux 发行版仓库中都可以获得。对于 Fedora 系统，使用 DNF 命令 来安装 iotop。$ sudo dnf install iotop对于 Debian/Ubuntu 系统，使用 API-GET 命令 或者 APT 命令 来安装 iotop。$ sudo apt install iotop对于基于 Arch Linux 的系统，使用 Pacman Command 来安装 iotop。$ sudo pacman -S iotop对于 RHEL/CentOS 的系统，使用 YUM Command 来安装 iotop。$ sudo yum install iotop对于使用 openSUSE Leap 的系统，使用 Zypper Command 来安装 iotop。$ sudo zypper install iotop在 Linux 中如何使用 iotop 命令来监控磁盘 I/O 活动/统计？iotop 命令有很多参数来检查关于磁盘 I/O 的变化：# iotop如果你想检查那个进程实际在做 I/O，那么运行 iotop 命令加上 -o 或者 --only 参数。# iotop --only细节：IO：它显示每个进程的 I/O 利用率，包含磁盘和交换。SWAPIN： 它只显示每个进程的交换使用率。什么是 iostat？iostat 被用来报告中央处理单元（CPU）的统计和设备与分区的输出/输出的统计。iostat 命令通过观察与它们平均传输率相关的设备活跃时间来监控系统输入/输出设备负载。iostat 命令生成的报告可以被用来改变系统配置来更好的平衡物理磁盘之间的输入/输出负载。所有的统计都在 iostat 命令每次运行时被报告。该报告包含一个 CPU 头部，后面是一行 CPU 统计。在多处理器系统中，CPU 统计被计算为系统层面的所有处理器的平均值。设备头行后紧跟显示每个配置的设备一行的统计。iostat 命令生成两种类型的报告，CPU 利用率报告和设备利用率报告。在 Linux 中怎样安装 iostat？iostat 工具是 sysstat 包的一部分，所以我们可以轻松地在包管理器地帮助下安装，因为在所有的 Linux 发行版的仓库都是可以获得的。对于 Fedora 系统，使用 DNF Command 来安装 sysstat。$ sudo dnf install sysstat对于 Debian/Ubuntu 系统，使用 APT-GET Command 或者 APT Command 来安装 sysstat。$ sudo apt install sysstat对于基于 Arch Linux 的系统，使用 Pacman Command 来安装 sysstat。$ sudo pacman -S sysstat对于 RHEL/CentOS 系统，使用 YUM Command 来安装 sysstat。$ sudo yum install sysstat对于 openSUSE Leap 系统，使用 Zypper Command 来安装 sysstat。$ sudo zypper install sysstat在 Linux 中如何使用 sysstat 命令监控磁盘 I/O 活动/统计？在 iostat 命令中有很多参数来检查关于 I/O 和 CPU 的变化统计信息。不加参数运行 iostat 命令会看到完整的系统统计。# iostat Linux 4.19.32-1-MANJARO (daygeek-Y700) Thursday 18 April 2019 _x86_64_ (8 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 29.45 0.02 16.47 0.12 0.00 53.94 Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd nvme0n1 6.68 126.95 124.97 0.00 58420014 57507206 0 sda 0.18 6.77 80.24 0.00 3115036 36924764 0 loop0 0.00 0.00 0.00 0.00 2160 0 0 loop1 0.00 0.00 0.00 0.00 1093 0 0 loop2 0.00 0.00 0.00 0.00 1077 0 0运行 iostat 命令加上 -d 参数查看所有设备的 I/O 统计。# iostat -d Linux 4.19.32-1-MANJARO (daygeek-Y700) Thursday 18 April 2019 _x86_64_ (8 CPU) Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd nvme0n1 6.68 126.95 124.97 0.00 58420030 57509090 0 sda 0.18 6.77 80.24 0.00 3115292 36924764 0 loop0 0.00 0.00 0.00 0.00 2160 0 0 loop1 0.00 0.00 0.00 0.00 1093 0 0 loop2 0.00 0.00 0.00 0.00 1077 0 0运行 iostat 命令加上 -p 参数查看所有的设备和分区的 I/O 统计。# iostat -p Linux 4.19.32-1-MANJARO (daygeek-Y700) Thursday 18 April 2019 _x86_64_ (8 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 29.42 0.02 16.45 0.12 0.00 53.99 Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd nvme0n1 6.68 126.94 124.96 0.00 58420062 57512278 0 nvme0n1p1 6.40 124.46 118.36 0.00 57279753 54474898 0 nvme0n1p2 0.27 2.47 6.60 0.00 1138069 3037380 0 sda 0.18 6.77 80.23 0.00 3116060 36924764 0 sda1 0.00 0.01 0.00 0.00 3224 0 0 sda2 0.18 6.76 80.23 0.00 3111508 36924764 0 loop0 0.00 0.00 0.00 0.00 2160 0 0 loop1 0.00 0.00 0.00 0.00 1093 0 0 loop2 0.00 0.00 0.00 0.00 1077 0 0运行 iostat 命令加上 -x 参数显示所有设备的详细的 I/O 统计信息。# iostat -x Linux 4.19.32-1-MANJARO (daygeek-Y700) Thursday 18 April 2019 _x86_64_ (8 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 29.41 0.02 16.45 0.12 0.00 54.00 Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz d/s dkB/s drqm/s %drqm d_await dareq-sz aqu-sz %util nvme0n1 2.45 126.93 0.60 19.74 0.40 51.74 4.23 124.96 5.12 54.76 3.16 29.54 0.00 0.00 0.00 0.00 0.00 0.00 0.31 30.28 sda 0.06 6.77 0.00 0.00 8.34 119.20 0.12 80.23 19.94 99.40 31.84 670.73 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.13 loop0 0.00 0.00 0.00 0.00 0.08 19.64 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 loop1 0.00 0.00 0.00 0.00 0.40 12.86 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 loop2 0.00 0.00 0.00 0.00 0.38 19.58 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00运行 iostat 命令加上 -d [设备名] 参数查看具体设备和它的分区的 I/O 统计信息。# iostat -p [Device_Name] # iostat -p sda Linux 4.19.32-1-MANJARO (daygeek-Y700) Thursday 18 April 2019 _x86_64_ (8 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 29.38 0.02 16.43 0.12 0.00 54.05 Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd sda 0.18 6.77 80.21 0.00 3117468 36924764 0 sda2 0.18 6.76 80.21 0.00 3112916 36924764 0 sda1 0.00 0.01 0.00 0.00 3224 0 0运行 iostat 命令加上 -m 参数以 MB 为单位而不是 KB 查看所有设备的统计。默认以 KB 显示输出。# iostat -m Linux 4.19.32-1-MANJARO (daygeek-Y700) Thursday 18 April 2019 _x86_64_ (8 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 29.36 0.02 16.41 0.12 0.00 54.09 Device tps MB_read/s MB_wrtn/s MB_dscd/s MB_read MB_wrtn MB_dscd nvme0n1 6.68 0.12 0.12 0.00 57050 56176 0 sda 0.18 0.01 0.08 0.00 3045 36059 0 loop0 0.00 0.00 0.00 0.00 2 0 0 loop1 0.00 0.00 0.00 0.00 1 0 0 loop2 0.00 0.00 0.00 0.00 1 0 0运行 iostat 命令使用特定的间隔使用如下的格式。在这个例子中，我们打算以 5 秒捕获的间隔捕获两个报告。# iostat [Interval] [Number Of Reports] # iostat 5 2 Linux 4.19.32-1-MANJARO (daygeek-Y700) Thursday 18 April 2019 _x86_64_ (8 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 29.35 0.02 16.41 0.12 0.00 54.10 Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd nvme0n1 6.68 126.89 124.95 0.00 58420116 57525344 0 sda 0.18 6.77 80.20 0.00 3118492 36924764 0 loop0 0.00 0.00 0.00 0.00 2160 0 0 loop1 0.00 0.00 0.00 0.00 1093 0 0 loop2 0.00 0.00 0.00 0.00 1077 0 0 avg-cpu: %user %nice %system %iowait %steal %idle 3.71 0.00 2.51 0.05 0.00 93.73 Device tps kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd nvme0n1 19.00 0.20 311.40 0.00 1 1557 0 sda 0.20 25.60 0.00 0.00 128 0 0 loop0 0.00 0.00 0.00 0.00 0 0 0 loop1 0.00 0.00 0.00 0.00 0 0 0 loop2 0.00 0.00 0.00 0.00 0 0 0运行 iostat 命令与 -N 参数来查看 LVM 磁盘 I/O 统计报告。# iostat -N Linux 4.15.0-47-generic (Ubuntu18.2daygeek.com) Thursday 18 April 2019 _x86_64_ (2 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 0.38 0.07 0.18 0.26 0.00 99.12 Device tps kB_read/s kB_wrtn/s kB_read kB_wrtn sda 3.60 57.07 69.06 968729 1172340 sdb 0.02 0.33 0.00 5680 0 sdc 0.01 0.12 0.00 2108 0 2g-2gvol1 0.00 0.07 0.00 1204 0运行 nfsiostat 命令来查看 Network File System（NFS）的 I/O 统计。# nfsiostatvia: https://www.2daygeek.com/check-monitor-disk-io-in-linux-using-iotop-iostat-command/作者：Magesh Maruthamuthu 选题：lujun9972 译者：warmfrog 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Anthony Starks 使用他出色的 Deck 演示工具重构了我原来的基于 Google Slides 的幻灯片。你可以在他的博客上查看他重构后的幻灯片, mindchunk.blogspot.com.au/2014/06/remixing-with-deck。我最近被邀请在 Gocon 发表演讲，这是一个每半年在日本东京举行的 Go 的精彩大会。Gocon 2014 是一个完全由社区驱动的为期一天的活动，由培训和一整个下午的围绕着生产环境中的 Go 这个主题的演讲组成.（LCTT 译注：本文发表于 2014 年）以下是我的讲义。原文的结构能让我缓慢而清晰的演讲，因此我已经编辑了它使其更可读。我要感谢 Bill Kennedy 和 Minux Ma，特别是 Josh Bleecher Snyder，感谢他们在我准备这次演讲中的帮助。大家下午好。我叫 David.我很高兴今天能来到 Gocon。我想参加这个会议已经两年了，我很感谢主办方能提供给我向你们演讲的机会。我想以一个问题开始我的演讲。为什么选择 Go？当大家讨论学习或在生产环境中使用 Go 的原因时，答案不一而足，但因为以下三个原因的最多。这就是 TOP3 的原因。第一，并发。Go 的 并发原语 Concurrency Primitives 对于来自 Nodejs，Ruby 或 Python 等单线程脚本语言的程序员，或者来自 C++ 或 Java 等重量级线程模型的语言都很有吸引力。易于部署。我们今天从经验丰富的 Gophers 那里听说过，他们非常欣赏部署 Go 应用的简单性。然后是性能。我相信人们选择 Go 的一个重要原因是它 快。在今天的演讲中，我想讨论五个有助于提高 Go 性能的特性。我还将与大家分享 Go 如何实现这些特性的细节。我要谈的第一个特性是 Go 对于值的高效处理和存储。这是 Go 中一个值的例子。编译时，gocon 正好消耗四个字节的内存。让我们将 Go 与其他一些语言进行比较由于 Python 表示变量的方式的开销，使用 Python 存储相同的值会消耗六倍的内存。Python 使用额外的内存来跟踪类型信息，进行 引用计数 Reference Counting 等。让我们看另一个例子：与 Go 类似，Java 消耗 4 个字节的内存来存储 int 型。但是，要在像 List 或 Map 这样的集合中使用此值，编译器必须将其转换为 Integer 对象。因此，Java 中的整数通常消耗 16 到 24 个字节的内存。为什么这很重要？ 内存便宜且充足，为什么这个开销很重要？这是一张显示 CPU 时钟速度与内存总线速度的图表。请注意 CPU 时钟速度和内存总线速度之间的差距如何继续扩大。两者之间的差异实际上是 CPU 花费多少时间等待内存。自 1960 年代后期以来，CPU 设计师已经意识到了这个问题。他们的解决方案是一个缓存，一个更小、更快的内存区域，介入 CPU 和主存之间。这是一个 Location 类型，它保存物体在三维空间中的位置。它是用 Go 编写的，因此每个 Location 只消耗 24 个字节的存储空间。我们可以使用这种类型来构造一个容纳 1000 个 Location 的数组类型，它只消耗 24000 字节的内存。在数组内部，Location 结构体是顺序存储的，而不是随机存储的 1000 个 Location 结构体的指针。这很重要，因为现在所有 1000 个 Location 结构体都按顺序放在缓存中，紧密排列在一起。Go 允许您创建紧凑的数据结构，避免不必要的填充字节。紧凑的数据结构能更好地利用缓存。更好的缓存利用率可带来更好的性能。函数调用不是无开销的。调用函数时会发生三件事。创建一个新的 栈帧 Stack Frame ，并记录调用者的详细信息。在函数调用期间可能被覆盖的任何寄存器都将保存到栈中。处理器计算函数的地址并执行到该新地址的分支。由于函数调用是非常常见的操作，因此 CPU 设计师一直在努力优化此过程，但他们无法消除开销。函调固有开销，或重于泰山，或轻于鸿毛，这取决于函数做了什么。减少函数调用开销的解决方案是 内联 Inlining 。Go 编译器通过将函数体视为调用者的一部分来内联函数。内联也有成本，它增加了二进制文件大小。只有当调用开销与函数所做工作关联度的很大时内联才有意义，因此只有简单的函数才能用于内联。复杂的函数通常不受调用它们的开销所支配，因此不会内联。这个例子显示函数 Double 调用 util.Max。为了减少调用 util.Max 的开销，编译器可以将 util.Max 内联到 Double 中，就象这样内联后不再调用 util.Max，但是 Double 的行为没有改变。内联并不是 Go 独有的。几乎每种编译或及时编译的语言都执行此优化。但是 Go 的内联是如何实现的？Go 实现非常简单。编译包时，会标记任何适合内联的小函数，然后照常编译。然后函数的源代码和编译后版本都会被存储。此幻灯片显示了 util.a 的内容。源代码已经过一些转换，以便编译器更容易快速处理。当编译器编译 Double 时，它看到 util.Max 可内联的，并且 util.Max 的源代码是可用的。就会替换原函数中的代码，而不是插入对 util.Max 的编译版本的调用。拥有该函数的源代码可以实现其他优化。在这个例子中，尽管函数 Test 总是返回 false，但 Expensive 在不执行它的情况下无法知道结果。当 Test 被内联时，我们得到这样的东西。编译器现在知道 Expensive 的代码无法访问。这不仅节省了调用 Test 的成本，还节省了编译或运行任何现在无法访问的 Expensive 代码。Go 编译器可以跨文件甚至跨包自动内联函数。还包括从标准库调用的可内联函数的代码。 强制垃圾回收 Mandatory Garbage Collection 使 Go 成为一种更简单，更安全的语言。这并不意味着垃圾回收会使 Go 变慢，或者垃圾回收是程序速度的瓶颈。这意味着在堆上分配的内存是有代价的。每次 GC 运行时都会花费 CPU 时间，直到释放内存为止。然而，有另一个地方分配内存，那就是栈。与 C 不同，它强制您选择是否将值通过 malloc 将其存储在堆上，还是通过在函数范围内声明将其储存在栈上；Go 实现了一个名为 逃逸分析 Escape Analysis 的优化。逃逸分析决定了对一个值的任何引用是否会从被声明的函数中逃逸。如果没有引用逃逸，则该值可以安全地存储在栈中。存储在栈中的值不需要分配或释放。让我们看一些例子Sum 返回 1 到 100 的整数的和。这是一种相当不寻常的做法，但它说明了逃逸分析的工作原理。因为切片 numbers 仅在 Sum 内引用，所以编译器将安排到栈上来存储的 100 个整数，而不是安排到堆上。没有必要回收 numbers，它会在 Sum 返回时自动释放。第二个例子也有点尬。在 CenterCursor 中，我们创建一个新的 Cursor 对象并在 c 中存储指向它的指针。然后我们将 c 传递给 Center() 函数，它将 Cursor 移动到屏幕的中心。最后我们打印出那个 ‘Cursor` 的 X 和 Y 坐标。即使 c 被 new 函数分配了空间，它也不会存储在堆上，因为没有引用 c 的变量逃逸 CenterCursor 函数。默认情况下，Go 的优化始终处于启用状态。可以使用 -gcflags = -m 开关查看编译器的逃逸分析和内联决策。因为逃逸分析是在编译时执行的，而不是运行时，所以无论垃圾回收的效率如何，栈分配总是比堆分配快。我将在本演讲的其余部分详细讨论栈。Go 有 goroutine。 这是 Go 并发的基石。我想退一步，探索 goroutine 的历史。最初，计算机一次运行一个进程。在 60 年代，多进程或 分时 Time Sharing 的想法变得流行起来。在分时系统中，操作系统必须通过保护当前进程的现场，然后恢复另一个进程的现场，不断地在这些进程之间切换 CPU 的注意力。这称为 进程切换。进程切换有三个主要开销。首先，内核需要保护该进程的所有 CPU 寄存器的现场，然后恢复另一个进程的现场。内核还需要将 CPU 的映射从虚拟内存刷新到物理内存，因为这些映射仅对当前进程有效。最后是操作系统 上下文切换 Context Switch 的成本，以及 调度函数 Scheduler Function 选择占用 CPU 的下一个进程的开销。现代处理器中有数量惊人的寄存器。我很难在一张幻灯片上排开它们，这可以让你知道保护和恢复它们需要多少时间。由于进程切换可以在进程执行的任何时刻发生，因此操作系统需要存储所有寄存器的内容，因为它不知道当前正在使用哪些寄存器。这导致了线程的出生，这些线程在概念上与进程相同，但共享相同的内存空间。由于线程共享地址空间，因此它们比进程更轻，因此创建速度更快，切换速度更快。Goroutine 升华了线程的思想。Goroutine 是 协作式调度 Cooperative Scheduled 的，而不是依靠内核来调度。当对 Go 运行时调度器 Runtime Scheduler 进行显式调用时，goroutine 之间的切换仅发生在明确定义的点上。编译器知道正在使用的寄存器并自动保存它们。虽然 goroutine 是协作式调度的，但运行时会为你处理。Goroutine 可能会给禅让给其他协程时刻是：阻塞式通道发送和接收。Go 声明，虽然不能保证会立即调度新的 goroutine。文件和网络操作式的阻塞式系统调用。在被垃圾回收循环停止后。这个例子说明了上一张幻灯片中描述的一些调度点。箭头所示的线程从左侧的 ReadFile 函数开始。遇到 os.Open，它在等待文件操作完成时阻塞线程，因此调度器将线程切换到右侧的 goroutine。继续执行直到从通道 c 中读，并且此时 os.Open 调用已完成，因此调度器将线程切换回左侧并继续执行 file.Read 函数，然后又被文件 IO 阻塞。调度器将线程切换回右侧以进行另一个通道操作，该操作在左侧运行期间已解锁，但在通道发送时再次阻塞。最后，当 Read 操作完成并且数据可用时，线程切换回左侧。这张幻灯片显示了低级语言描述的 runtime.Syscall 函数，它是 os 包中所有函数的基础。只要你的代码调用操作系统，就会通过此函数。对 entersyscall 的调用通知运行时该线程即将阻塞。这允许运行时启动一个新线程，该线程将在当前线程被阻塞时为其他 goroutine 提供服务。这导致每 Go 进程的操作系统线程相对较少，Go 运行时负责将可运行的 Goroutine 分配给空闲的操作系统线程。在上一节中，我讨论了 goroutine 如何减少管理许多（有时是数十万个并发执行线程）的开销。Goroutine故事还有另一面，那就是栈管理，它引导我进入我的最后一个话题。这是一个进程的内存布局图。我们感兴趣的关键是堆和栈的位置。传统上，在进程的地址空间内，堆位于内存的底部，位于程序（代码）的上方并向上增长。栈位于虚拟地址空间的顶部，并向下增长。因为堆和栈相互覆盖的结果会是灾难性的，操作系统通常会安排在栈和堆之间放置一个不可写内存区域，以确保如果它们发生碰撞，程序将中止。这称为保护页，有效地限制了进程的栈大小，通常大约为几兆字节。我们已经讨论过线程共享相同的地址空间，因此对于每个线程，它必须有自己的栈。由于很难预测特定线程的栈需求，因此为每个线程的栈和保护页面保留了大量内存。希望是这些区域永远不被使用，而且防护页永远不会被击中。缺点是随着程序中线程数的增加，可用地址空间的数量会减少。我们已经看到 Go 运行时将大量的 goroutine 调度到少量线程上，但那些 goroutines 的栈需求呢？Go 编译器不使用保护页，而是在每个函数调用时插入一个检查，以检查是否有足够的栈来运行该函数。如果没有，运行时可以分配更多的栈空间。由于这种检查，goroutines 初始栈可以做得更小，这反过来允许 Go 程序员将 goroutines 视为廉价资源。这是一张显示了 Go 1.2 如何管理栈的幻灯片。当 G 调用 H 时，没有足够的空间让 H 运行，所以运行时从堆中分配一个新的栈帧，然后在新的栈段上运行 H。当 H 返回时，栈区域返回到堆，然后返回到 G。这种管理栈的方法通常很好用，但对于某些类型的代码，通常是递归代码，它可能导致程序的内部循环跨越这些栈边界之一。例如，在程序的内部循环中，函数 G 可以在循环中多次调用 H，每次都会导致栈拆分。 这被称为 热分裂 Hot Split 问题。为了解决热分裂问题，Go 1.3 采用了一种新的栈管理方法。如果 goroutine 的栈太小，则不会添加和删除其他栈段，而是分配新的更大的栈。旧栈的内容被复制到新栈，然后 goroutine 使用新的更大的栈继续运行。在第一次调用 H 之后，栈将足够大，对可用栈空间的检查将始终成功。这解决了热分裂问题。值，内联，逃逸分析，Goroutines 和分段/复制栈。这些是我今天选择谈论的五个特性，但它们绝不是使 Go 成为快速的语言的唯一因素，就像人们引用他们学习 Go 的理由的三个原因一样。这五个特性一样强大，它们不是孤立存在的。例如，运行时将 goroutine 复用到线程上的方式在没有可扩展栈的情况下几乎没有效率。内联通过将较小的函数组合成较大的函数来降低栈大小检查的成本。逃逸分析通过自动将从实例从堆移动到栈来减少垃圾回收器的压力。逃逸分析还提供了更好的 缓存局部性 Cache Locality 。如果没有可增长的栈，逃逸分析可能会对栈施加太大的压力。感谢 Gocon 主办方允许我今天发言twitter / web / email details感谢 @offbymany，@billkennedy\_go 和 Minux 在准备这个演讲的过程中所提供的帮助。相关文章：听我在 OSCON 上关于 Go 性能的演讲为什么 Goroutine 的栈是无限大的？Go 的运行时环境变量的旋风之旅没有事件循环的性能作者简介：David 是来自澳大利亚悉尼的程序员和作者。自 2011 年 2 月起成为 Go 的 contributor，自 2012 年 4 月起成为 committer。联系信息dave@cheney.nettwitter: @davecheneyvia: https://dave.cheney.net/2014/06/07/five-things-that-make-go-fast作者：Dave Cheney 译者：houbaron 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
了解应用程序的输入/输出（I/O）模型意味着理解应用程序处理其数据的载入差异，并揭示其在真实环境中表现。或许你的应用程序很小，在不承受很大的负载时，这并不是个严重的问题；但随着应用程序的流量负载增加，可能因为使用了低效的 I/O 模型导致承受不了而崩溃。和大多数情况一样，处理这种问题的方法有多种方式，这不仅仅是一个择优的问题，而是对权衡的理解问题。 接下来我们来看看 I/O 到底是什么。在本文中，我们将对 Node、Java、Go 和 PHP + Apache 进行对比，讨论不同语言如何构造其 I/O ，每个模型的优缺点，并总结一些基本的规律。如果你担心你的下一个 Web 应用程序的 I/O 性能，本文将给你最优的解答。I/O 基础知识: 快速复习要了解 I/O 所涉及的因素，我们首先深入到操作系统层面复习这些概念。虽然看起来并不与这些概念直接打交道，但你会一直通过应用程序的运行时环境与它们间接接触。了解细节很重要。系统调用首先是系统调用，其被描述如下：程序（所谓“ 用户端 user land ”）必须请求操作系统内核代表它执行 I/O 操作。“ 系统调用 syscall ”是你的程序要求内核执行某些操作的方法。这些实现的细节在操作系统之间有所不同，但基本概念是相同的。有一些具体的指令会将控制权从你的程序转移到内核（类似函数调用，但是使用专门用于处理这种情况的专用方式）。一般来说，系统调用会被阻塞，这意味着你的程序会等待内核返回（控制权到）你的代码。内核在所需的物理设备（ 磁盘、网卡等 ）上执行底层 I/O 操作，并回应系统调用。在实际情况中，内核可能需要做许多事情来满足你的要求，包括等待设备准备就绪、更新其内部状态等，但作为应用程序开发人员，你不需要关心这些。这是内核的工作。阻塞与非阻塞上面我们提到过，系统调用是阻塞的，一般来说是这样的。然而，一些调用被归类为“非阻塞”，这意味着内核会接收你的请求，将其放在队列或缓冲区之类的地方，然后立即返回而不等待实际的 I/O 发生。所以它只是在很短的时间内“阻塞”，只需要排队你的请求即可。举一些 Linux 系统调用的例子可能有助于理解：read() 是一个阻塞调用 - 你传递一个句柄，指出哪个文件和缓冲区在哪里传送它所读取的数据，当数据就绪时，该调用返回。这种方式的优点是简单友好。分别调用 epoll_create()、epoll_ctl() 和 epoll_wait() ，你可以创建一组句柄来侦听、添加/删除该组中的处理程序、然后阻塞直到有任何事件发生。这允许你通过单个线程有效地控制大量的 I/O 操作，但是现在谈这个还太早。如果你需要这个功能当然好，但须知道它使用起来是比较复杂的。了解这里的时间差异的数量级是很重要的。假设 CPU 内核运行在 3GHz，在没有进行 CPU 优化的情况下，那么它每秒执行 30 亿次 周期 cycle （即每纳秒 3 个周期）。非阻塞系统调用可能需要几十个周期来完成，或者说 “相对少的纳秒” 时间完成。而一个被跨网络接收信息所阻塞的系统调用可能需要更长的时间 - 例如 200 毫秒（1/5 秒）。这就是说，如果非阻塞调用需要 20 纳秒，阻塞调用需要 2 亿纳秒。你的进程因阻塞调用而等待了 1000 万倍的时长！内核既提供了阻塞 I/O （“从网络连接读取并给出数据”），也提供了非阻塞 I/O （“告知我何时这些网络连接具有新数据”）的方法。使用的是哪种机制对调用进程的阻塞时长有截然不同的影响。调度关键的第三件事是当你有很多线程或进程开始阻塞时会发生什么。根据我们的理解，线程和进程之间没有很大的区别。在现实生活中，最显著的性能相关的差异在于，由于线程共享相同的内存，而进程每个都有自己的内存空间，使得单独的进程往往占用更多的内存。但是当我们谈论 调度 Scheduling 时，它真正归结为一类事情（线程和进程类同），每个都需要在可用的 CPU 内核上获得一段执行时间。如果你有 300 个线程运行在 8 个内核上，则必须将时间分成几份，以便每个线程和进程都能分享它，每个运行一段时间，然后交给下一个。这是通过 “ 上下文切换 context switch ” 完成的，可以使 CPU 从运行到一个线程/进程到切换下一个。这些上下文切换也有相关的成本 - 它们需要一些时间。在某些快速的情况下，它可能小于 100 纳秒，但根据实际情况、处理器速度/体系结构、CPU 缓存等，偶见花费 1000 纳秒或更长时间。而线程（或进程）越多，上下文切换就越多。当我们涉及数以千计的线程时，每个线程花费数百纳秒，就会变得很慢。然而，非阻塞调用实质上是告诉内核“仅在这些连接之一有新的数据或事件时再叫我”。这些非阻塞调用旨在有效地处理大量 I/O 负载并减少上下文交换。这些你明白了么？现在来到了真正有趣的部分：我们来看看一些流行的语言对那些工具的使用，并得出关于易用性和性能之间权衡的结论，以及一些其他有趣小东西。声明，本文中显示的示例是零碎的（片面的，只能体现相关的信息）; 数据库访问、外部缓存系统（ memcache 等等）以及任何需要 I/O 的东西都将执行某种类型的 I/O 调用，其实质与上面所示的简单示例效果相同。此外，对于将 I/O 描述为“阻塞”（ PHP、Java ）的情况，HTTP 请求和响应读取和写入本身就是阻塞调用：系统中隐藏着更多 I/O 及其伴生的性能问题需要考虑。为一个项目选择编程语言要考虑很多因素。甚至当你只考虑效率时，也有很多因素。但是，如果你担心你的程序将主要受到 I/O 的限制，如果 I/O 性能影响到项目的成败，那么这些是你需要了解的。“保持简单”方法：PHP早在 90 年代，很多人都穿着 Converse 鞋，用 Perl 写着 CGI 脚本。然后 PHP 来了，就像一些人喜欢咒骂的一样，它使得动态网页更容易。PHP 使用的模型相当简单。虽有一些出入，但你的 PHP 服务器基本上是这样：HTTP 请求来自用户的浏览器，并访问你的 Apache Web 服务器。Apache 为每个请求创建一个单独的进程，有一些优化方式可以重新使用它们，以最大限度地减少创建次数（ 相对而言，创建进程较慢 ）。Apache 调用 PHP 并告诉它运行磁盘上合适的 .php 文件。PHP 代码执行并阻塞 I/O 调用。你在 PHP 中调用 file_get_contents() ，其底层会调用 read() 系统调用并等待结果。当然，实际的代码是直接嵌入到你的页面，并且该操作被阻塞：<?php // blocking file I/O $file_data = file_get_contents(‘/path/to/file.dat’); // blocking network I/O $curl = curl_init('http://example.com/example-microservice'); $result = curl_exec($curl); // some more blocking network I/O $result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?> 关于如何与系统集成，就像这样：很简单：每个请求一个进程。 I/O 调用就阻塞。优点是简单可工作，缺点是，同时与 20,000 个客户端连接，你的服务器将会崩溃。这种方法不能很好地扩展，因为内核提供的用于处理大容量 I/O （epoll 等） 的工具没有被使用。 雪上加霜的是，为每个请求运行一个单独的进程往往会使用大量的系统资源，特别是内存，这通常是你在这样的场景中遇到的第一个问题。注意：Ruby 使用的方法与 PHP 非常相似，在大致的方面上，它们可以被认为是相同的。多线程方法： Java就在你购买你的第一个域名，在某个句子后很酷地随机说出 “dot com” 的那个时候，Java 来了。而 Java 具有内置于该语言中的多线程功能，它非常棒（特别是在创建时）。大多数 Java Web 服务器通过为每个请求启动一个新的执行线程，然后在该线程中最终调用你（作为应用程序开发人员）编写的函数。在 Java Servlet 中执行 I/O 往往看起来像：public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream("/path/to/file"); // blocking network I/O URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println("..."); } 由于我们上面的 doGet 方法对应于一个请求，并且在其自己的线程中运行，而不是每个请求一个单独的进程，申请自己的内存。这样有一些好处，比如在线程之间共享状态、缓存数据等，因为它们可以访问彼此的内存，但是它与调度的交互影响与之前的 PHP 的例子几乎相同。每个请求获得一个新线程，该线程内的各种 I/O 操作阻塞在线程内，直到请求被完全处理为止。线程被池化以最小化创建和销毁它们的成本，但是数千个连接仍然意味着数千个线程，这对调度程序是不利的。重要的里程碑出现在 Java 1.4 版本（以及 1.7 的重要升级）中，它获得了执行非阻塞 I/O 调用的能力。大多数应用程序、web 应用和其它用途不会使用它，但至少它是可用的。一些 Java Web 服务器尝试以各种方式利用这一点；然而，绝大多数部署的 Java 应用程序仍然如上所述工作。肯定有一些很好的开箱即用的 I/O 功能，Java 让我们更接近，但它仍然没有真正解决当你有一个大量的 I/O 绑定的应用程序被数千个阻塞线程所压垮的问题。无阻塞 I/O 作为一等公民： Node当更好的 I/O 模式来到 Node.js，阻塞才真正被解决。任何一个曾听过 Node 简单介绍的人都被告知这是“非阻塞”，可以有效地处理 I/O。这在一般意义上是正确的。但在细节中则不尽然，而且当在进行性能工程时，这种巫术遇到了问题。Node 实现的范例基本上不是说 “在这里写代码来处理请求”，而是说 “在这里写代码来开始处理请求”。每次你需要做一些涉及到 I/O 的操作，你会创建一个请求并给出一个回调函数，Node 将在完成之后调用该函数。在请求中执行 I/O 操作的典型 Node 代码如下所示：http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); }); 你可以看到，这里有两个回调函数。当请求开始时，第一个被调用，当文件数据可用时，第二个被调用。这样做的基本原理是让 Node 有机会有效地处理这些回调之间的 I/O 。一个更加密切相关的场景是在 Node 中进行数据库调用，但是我不会在这个例子中啰嗦，因为它遵循完全相同的原则：启动数据库调用，并给 Node 一个回调函数，它使用非阻塞调用单独执行 I/O 操作，然后在你要求的数据可用时调用回调函数。排队 I/O 调用和让 Node 处理它然后获取回调的机制称为“事件循环”。它工作的很好。然而，这个模型有一个陷阱，究其原因，很多是与 V8 JavaScript 引擎（Node 用的是 Chrome 浏览器的 JS 引擎）如何实现的有关 注1 。你编写的所有 JS 代码都运行在单个线程中。你可以想想，这意味着当使用高效的非阻塞技术执行 I/O 时，你的 JS 可以在单个线程中运行计算密集型的操作，每个代码块都会阻塞下一个。可能出现这种情况的一个常见例子是以某种方式遍历数据库记录，然后再将其输出到客户端。这是一个示例，展示了其是如何工作：var handler = function(request, response) { connection.query('SELECT ...', function (err, rows) { if (err) { throw err }; for (var i = 0; i < rows.length; i++) { // do processing on each row } response.end(...); // write out the results }) }; 虽然 Node 确实有效地处理了 I/O ，但是上面的例子中 for 循环是在你的唯一的一个主线程中占用 CPU 周期。这意味着如果你有 10,000 个连接，则该循环可能会使你的整个应用程序像爬行般缓慢，具体取决于其会持续多久。每个请求必须在主线程中分享一段时间，一次一段。这整个概念的前提是 I/O 操作是最慢的部分，因此最重要的是要有效地处理这些操作，即使这意味着要连续进行其他处理。这在某些情况下是正确的，但不是全部。另一点是，虽然这只是一个观点，但是写一堆嵌套的回调可能是相当令人讨厌的，有些则认为它使代码更难以追踪。在 Node 代码中看到回调嵌套 4 层、5 层甚至更多层并不罕见。我们再次来权衡一下。如果你的主要性能问题是 I/O，则 Node 模型工作正常。然而，它的关键是，你可以在一个处理 HTTP 请求的函数里面放置 CPU 密集型的代码，而且不小心的话会导致每个连接都很慢。最自然的非阻塞：Go在我进入 Go 部分之前，我应该披露我是一个 Go 的粉丝。我已经在许多项目中使用过它，我是一个其生产力优势的公开支持者，我在我的工作中使用它。那么，让我们来看看它是如何处理 I/O 的。Go 语言的一个关键特征是它包含自己的调度程序。在 Go 中，不是每个执行线程对应于一个单一的 OS 线程，其通过一种叫做 “ 协程 goroutine ” 的概念来工作。而 Go 的运行时可以将一个协程分配给一个 OS 线程，使其执行或暂停它，并且它不与一个 OS 线程相关联——这要基于那个协程正在做什么。来自 Go 的 HTTP 服务器的每个请求都在单独的协程中处理。调度程序的工作原理如图所示：在底层，这是通过 Go 运行时中的各个部分实现的，它通过对请求的写入/读取/连接等操作来实现 I/O 调用，将当前协程休眠，并当采取进一步动作时唤醒该协程。从效果上看，Go 运行时做的一些事情与 Node 做的没有太大不同，除了回调机制是内置到 I/O 调用的实现中，并自动与调度程序交互。它也不会受到必须让所有处理程序代码在同一个线程中运行的限制，Go 将根据其调度程序中的逻辑自动将协程映射到其认为适当的 OS 线程。结果是这样的代码：func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, err := db.Query("SELECT ...") for _, row := range rows { // do something with the rows, // each request in its own goroutine } w.Write(...) // write the response, also non-blocking } 如上所述，我们重构基本的代码结构为更简化的方式，并在底层仍然实现了非阻塞 I/O。在大多数情况下，最终是“两全其美”的。非阻塞 I/O 用于所有重要的事情，但是你的代码看起来像是阻塞，因此更容易理解和维护。Go 调度程序和 OS 调度程序之间的交互处理其余部分。这不是完整的魔法，如果你建立一个大型系统，那么值得我们来看看有关它的工作原理的更多细节；但与此同时，你获得的“开箱即用”的环境可以很好地工作和扩展。Go 可能有其缺点，但一般来说，它处理 I/O 的方式不在其中。谎言，可恶的谎言和基准对这些各种模式的上下文切换进行准确的定时是很困难的。我也可以认为这对你来说不太有用。相反，我会给出一些比较这些服务器环境的整个 HTTP 服务器性能的基本基准。请记住，影响整个端到端 HTTP 请求/响应路径的性能有很多因素，这里提供的数字只是我将一些样本放在一起进行基本比较的结果。对于这些环境中的每一个，我写了适当的代码在一个 64k 文件中读取随机字节，在其上运行了一个 SHA-256 哈希 N 次（ N 在 URL 的查询字符串中指定，例如 .../test.php?n=100），并打印出结果十六进制散列。我选择这样做，是因为使用一些一致的 I/O 和受控的方式来运行相同的基准测试是一个增加 CPU 使用率的非常简单的方法。有关使用的环境的更多细节，请参阅 基准说明 。首先，我们来看一些低并发的例子。运行 2000 次迭代，300 个并发请求，每个请求只有一个散列（N = 1），结果如下：时间是在所有并发请求中完成请求的平均毫秒数。越低越好。仅从一张图很难得出结论，但是对我来说，似乎在大量的连接和计算量上，我们看到时间更多地与语言本身的一般执行有关，对于 I/O 更是如此。请注意，那些被视为“脚本语言”的语言（松散类型，动态解释）执行速度最慢。但是，如果我们将 N 增加到 1000，仍然有 300 个并发请求，相同的任务，但是哈希迭代是 1000 倍（显着增加了 CPU 负载）：时间是在所有并发请求中完成请求的平均毫秒数。越低越好。突然间， Node 性能显著下降，因为每个请求中的 CPU 密集型操作都相互阻塞。有趣的是，在这个测试中，PHP 的性能要好得多（相对于其他的），并且打败了 Java。（值得注意的是，在 PHP 中，SHA-256 实现是用 C 编写的，在那个循环中执行路径花费了更多的时间，因为现在我们正在进行 1000 个哈希迭代）。现在让我们尝试 5000 个并发连接（N = 1） - 或者是我可以发起的最大连接。不幸的是，对于大多数这些环境，故障率并不显着。对于这个图表，我们来看每秒的请求总数。 越高越好 :每秒请求数。越高越好。这个图看起来有很大的不同。我猜测，但是看起来像在高连接量时，产生新进程所涉及的每连接开销以及与 PHP + Apache 相关联的附加内存似乎成为主要因素，并阻止了 PHP 的性能。显然，Go 是这里的赢家，其次是 Java，Node，最后是 PHP。虽然与你的整体吞吐量相关的因素很多，并且在应用程序之间也有很大的差异，但是你对底层发生什么的事情以及所涉及的权衡了解更多，你将会得到更好的结果。总结以上所有这一切，很显然，随着语言的发展，处理大量 I/O 的大型应用程序的解决方案也随之发展。为了公平起见，PHP 和 Java，尽管这篇文章中的描述，确实 实现了 在 web 应用程序 中 可使用的 非阻塞 I/O 。但是这些方法并不像上述方法那么常见，并且需要考虑使用这种方法来维护服务器的随之而来的操作开销。更不用说你的代码必须以与这些环境相适应的方式进行结构化；你的 “正常” PHP 或 Java Web 应用程序通常不会在这样的环境中进行重大修改。作为比较，如果我们考虑影响性能和易用性的几个重要因素，我们得出以下结论：语言线程与进程非阻塞 I/O使用便捷性PHP进程否 Java线程可用需要回调Node.js线程是需要回调Go线程 （协程）是不需要回调线程通常要比进程有更高的内存效率，因为它们共享相同的内存空间，而进程则没有。结合与非阻塞 I/O 相关的因素，我们可以看到，至少考虑到上述因素，当我们从列表往下看时，与 I/O 相关的一般设置得到改善。所以如果我不得不在上面的比赛中选择一个赢家，那肯定会是 Go。即使如此，在实践中，选择构建应用程序的环境与你的团队对所述环境的熟悉程度以及你可以实现的总体生产力密切相关。因此，每个团队都深入并开始在 Node 或 Go 中开发 Web 应用程序和服务可能就没有意义。事实上，寻找开发人员或你内部团队的熟悉度通常被认为是不使用不同语言和/或环境的主要原因。也就是说，过去十五年来，时代已经发生了变化。希望以上内容可以帮助你更清楚地了解底层发生的情况，并为你提供如何处理应用程序的现实可扩展性的一些想法。via: https://www.toptal.com/back-end/server-side-io-performance-node-php-java-go作者：BRAD PEABODY 译者：MonkeyDEcho 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
应用程序的性能决定了软件能多快完成预期任务。这回答有关应用程序的几个问题，例如：峰值负载下的响应时间与替代方案相比，它易于使用，受支持的功能和用例运营成本（CPU 使用率、内存需求、数据吞吐量、带宽等）该性能分析的价值超出了服务负载所需的计算资源或满足峰值需求所需的应用实例数量的估计。性能显然与成功企业的基本要素挂钩。它揭示了用户的总体体验，包括确定什么会拖慢客户预期的响应时间，通过设计满足带宽要求的内容交付来提高客户粘性，选择最佳设备，最终帮助企业发展业务。问题当然，这是对业务服务的性能工程价值的过度简化。为了理解在完成我刚刚所描述事情背后的挑战，让我们把它放到一个真实的稍微有点复杂的场景中。现实世界的应用程序可能托管在云端。应用程序可以利用非常大（或概念上是无穷大）的计算资源。在硬件和软件方面的需求将通过云来满足。从事开发工作的开发人员将使用云交付功能来实现更快的编码和部署。云托管不是免费的，但成本开销与应用程序的资源需求成正比。除了 搜索即服务 Search as a Service （SaaS）、 平台即服务 Platform as a Service （PaaS）、 基础设施即服务 Infrastructure as a Service （IaaS）以及 负载平衡即服务 Load Balancing as a Service （LBaaS）之外，当云端管理托管程序的流量时，开发人员可能还会使用这些快速增长的云服务中的一个或多个： 安全即服务 Security as a Service （SECaaS），可满足软件和用户的安全需求 数据即服务 Data as a Service （DaaS），为应用提供了用户需求的数据 登录即服务 Logging as a Service （LaaS），DaaS 的近亲，提供了日志传递和使用的分析指标 搜索即服务 Search as a Service （SaaS），用于应用程序的分析和大数据需求 网络即服务 Network as a Service （NaaS），用于通过公共网络发送和接收数据云服务也呈指数级增长，因为它们使得开发人员更容易编写复杂的应用程序。除了软件复杂性之外，所有这些分布式组件的相互作用变得越来越多。用户群变得更加多元化。该软件的需求列表变得更长。对其他服务的依赖性变大。由于这些因素，这个生态系统的缺陷会引发性能问题的多米诺效应。例如，假设你有一个精心编写的应用程序，它遵循安全编码实践，旨在满足不同的负载要求，并经过彻底测试。另外假设你已经将基础架构和分析工作结合起来，以支持基本的性能要求。在系统的实现、设计和架构中建立性能标准需要做些什么？软件如何跟上不断变化的市场需求和新兴技术？如何测量关键参数以调整系统以获得最佳性能？如何使系统具有弹性和自我恢复能力？你如何更快地识别任何潜在的性能问题，并尽早解决？进入容器软件容器以微服务设计或面向服务的架构（SoA）的优点为基础，提高了性能，因为包含更小的、自足的代码块的系统更容易编码，对其它系统组件有更清晰、定义良好的依赖。测试更容易，包括围绕资源利用和内存过度消耗的问题比在宏架构中更容易确定。当扩容系统以增加负载能力时，容器应用程序的复制快速而简单。安全漏洞能更好地隔离。补丁可以独立版本化并快速部署。性能监控更有针对性，测量更可靠。你还可以重写和“改版”资源密集型代码，以满足不断变化的性能要求。容器启动快速，停止也快速。它比虚拟机（VM）有更高效资源利用和更好的进程隔离。容器没有空闲内存和 CPU 闲置。它们允许多个应用程序共享机器，而不会丢失数据或性能。容器使应用程序可移植，因此开发人员可以构建并将应用程序发送到任何支持容器技术的 Linux 服务器上，而不必担心性能损失。容器生存在其内，并遵守其集群管理器（如 Cloud Foundry 的 Diego、Kubernetes、Apache Mesos 和 Docker Swarm）所规定的配额（比如包括存储、计算和对象计数配额）。容器在性能方面表现出色，而即将到来的 “serverless” 计算（也称为 功能即服务 Function as a Service （FaaS））的浪潮将扩大容器的优势。在 FaaS 时代，这些临时性或短期的容器将带来超越应用程序性能的优势，直接转化为在云中托管的间接成本的节省。如果容器的工作更快，那么它的寿命就会更短，而且计算量负载纯粹是按需的。作者简介：Garima 是 Red Hat 的工程经理，专注于 OpenShift 容器平台。在加入 Red Hat 之前，Garima 帮助 Akamai Technologies＆MathWorks Inc. 开创了创新。via: https://opensource.com/article/17/2/performance-container-world作者：Garima 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
在本文中，我们将解释 Linux 系统中最关键的管理任务之一——关于系统 / CPU 的 负载 （ load ） 和 平均负载 （ Load average ） 的性能监控。首先来看所有的类 UNIX 系统中两个重要的表述:系统负载 / CPU 负载 – 衡量 Linux 系统的 CPU 过载或利用率低的指标，即处于运算状态或等待状态的 CPU 核心数。平均负载 – 通过固定的时间周期如 1、5、15 分钟计算出的平均的系统负载。Linux 中，平均负载一般指在内核运行队列中被标记为运行或不可打断状态的进程的平均数。注意：几乎没有 Linux 或类 Unix 系统不为用户展示平均负载的值。完全空闲的 Linux 系统平均负载为 0，不包括空闲进程。绝大多数类 Unix 系统只统计运行和等待状态的进程。但是在 Linux 中，平均负载也包括处于不可打断的睡眠状态的进程——它们是在等待其它系统资源如磁盘 I/O 等的进程。如何监测 Linux 系统平均负载有诸多方式监测系统平均负载，如 uptime，它会展示系统运行时间、用户数量及平均负载：$ uptime 07:13:53 up 8 days, 19 min, 1 user, load average: 1.98, 2.15, 2.21 平均负载的数字从左到右的含义依次为:最近 1 分钟的平均负载为 1.98最近 5 分钟的平均负载为 2.15最近 15 分钟的平均负载为 2.21高平均负载意味着系统是过载的：许多进程在等待 CPU 时间。下一节将介绍平均负载和 CPU 核数的关系。此外，常用的工具 top 和 glances 可以实时显示 Linux 系统的运行状态：Top命令$ top 显示运行中的Linux进程：top - 12:51:42 up 2:11, 1 user, load average: 1.22, 1.12, 1.26 Tasks: 243 total, 1 running, 242 sleeping, 0 stopped, 0 zombie %Cpu(s): 17.4 us, 2.9 sy, 0.3 ni, 74.8 id, 4.6 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 8069036 total, 388060 free, 4381184 used, 3299792 buff/cache KiB Swap: 3906556 total, 3901876 free, 4680 used. 2807464 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 6265 tecmint 20 0 1244348 170680 83616 S 13.3 2.1 6:47.72 Headset 2301 tecmint 9 -11 640332 13344 9932 S 6.7 0.2 2:18.96 pulseaudio 2459 tecmint 20 0 1707692 315628 62992 S 6.7 3.9 6:55.45 cinnamon 2957 tecmint 20 0 2644644 1.035g 137968 S 6.7 13.5 50:11.13 firefox 3208 tecmint 20 0 507060 52136 33152 S 6.7 0.6 0:04.34 gnome-terminal- 3272 tecmint 20 0 1521380 391324 178348 S 6.7 4.8 6:21.01 chrome 6220 tecmint 20 0 1595392 106964 76836 S 6.7 1.3 3:31.94 Headset 1 root 20 0 120056 6204 3964 S 0.0 0.1 0:01.83 systemd 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd 3 root 20 0 0 0 0 S 0.0 0.0 0:00.10 ksoftirqd/0 5 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H .... Glances 工具$ glances Glances – Linux系统监测工具：TecMint (LinuxMint 18 64bit / Linux 4.4.0-21-generic) Uptime: 2:16:06 CPU 16.4% nice: 0.1% LOAD 4-core MEM 60.5% active: 4.90G SWAP 0.1% user: 10.2% irq: 0.0% 1 min: 1.20 total: 7.70G inactive: 2.07G total: 3.73G system: 3.4% iowait: 2.7% 5 min: 1.16 used: 4.66G buffers: 242M used: 4.57M idle: 83.6% steal: 0.0% 15 min: 1.24 free: 3.04G cached: 2.58G free: 3.72G NETWORK Rx/s Tx/s TASKS 253 (883 thr), 1 run, 252 slp, 0 oth sorted automatically by cpu_percent, flat view enp1s0 525Kb 31Kb lo 2Kb 2Kb CPU% MEM% VIRT RES PID USER NI S TIME+ IOR/s IOW/s Command wlp2s0 0b 0b 14.6 13.3 2.53G 1.03G 2957 tecmint 0 S 51:49.10 0 40K /usr/lib/firefox/firefox 7.4 2.2 1.16G 176M 6265 tecmint 0 S 7:08.18 0 0 /usr/lib/Headset/Headset --type=renderer --no-sandbox --primordial-pipe-token=879B36514C6BEDB183D3E4142774D1DF --lan DISK I/O R/s W/s 4.9 3.9 1.63G 310M 2459 tecmint 0 R 7:12.18 0 0 cinnamon --replace ram0 0 0 4.2 0.2 625M 13.0M 2301 tecmint -11 S 2:29.72 0 0 /usr/bin/pulseaudio --start --log-target=syslog ram1 0 0 4.2 1.3 1.52G 105M 6220 tecmint 0 S 3:42.64 0 0 /usr/lib/Headset/Headset ram10 0 0 2.9 0.8 409M 66.7M 6240 tecmint 0 S 2:40.44 0 0 /usr/lib/Headset/Headset --type=gpu-process --no-sandbox --supports-dual-gpus=false --gpu-driver-bug-workarounds=7,2 ram11 0 0 2.9 1.8 531M 142M 1690 root 0 S 6:03.79 0 0 /usr/lib/xorg/Xorg :0 -audit 0 -auth /var/lib/mdm/:0.Xauth -nolisten tcp vt8 ram12 0 0 2.6 0.3 79.3M 23.8M 9651 tecmint 0 R 0:00.71 0 0 /usr/bin/python3 /usr/bin/glances ram13 0 0 1.6 4.8 1.45G 382M 3272 tecmint 0 S 6:25.30 0 4K /opt/google/chrome/chrome ... 这些工具中的平均负载是从 /proc/loadavg 文件中读取的，也可以直接使用 cat 命令查看：$ cat /proc/loadavg 2.48 1.69 1.42 5/889 10570 想要图形样式监测平均负载，请戳：ttyload – 终端中颜色编码图形显示 Linux 平均负载。在桌面计算机中，可以使用图形用户接口工具查看系统平均负载。理解系统平均负载和 CPU 核心数的关系考虑了 CPU 核心数的影响，才能解释系统负载。多处理器 Vs 多核处理器多处理器 – 一个计算机系统中集成两个或多个物理 CPU多核处理器 – 单个物理 CPU 有两个或多个单独的核并行工作（也叫处理单元）。双核意味着有两个处理单元，4 核有 4 个处理单元，以此类推。此外，Intel 引入了超线程技术用来提高并行计算能力。通过超线程技术，在操作系统中，单个物理 CPU 表现的和两个逻辑 CPU 一样。（实际在硬件上只有一个 CPU）。注意，单个 CPU 核同一时间只能执行一个任务，于是产生了多 CPU/处理器、多核 CPU，以及多线程技术。多 CPU 时，多个程序可以同时执行。如今的 Intel CPU 使用了多核心和超线程技术。可以使用 nproc 或 lscpu 命令查看系统中的处理器单元数量。$ nproc 4 # 或者 lscpu 也可以使用 grep 命令：$ grep 'model name' /proc/cpuinfo | wc -l 4 为了进一步理解系统负载，需要做一些假设。假设系统负载如下：23:16:49 up 10:49, 5 user, load average: 1.00, 0.40, 3.35 在单核系统中意味着：CPU 被充分利用（100%）；最近的 1 分钟有 1 个进程在运行。CPU 有 60% 处于空闲状态；在最近的 5 分钟没有进程等待 CPU 时间。CPU 平均过载了 235%；最近的 15 分钟平均有 2.35 个进程在等待 CPU 时间。在双核系统中意味着：有一个 CPU 处于完全空闲状态，另一个 CPU 被使用；最近的 1 分钟没有进程等待 CPU 时间。CPU 平均 160% 处于空闲状态；最近的 5 分钟没有进程等待 CPU 时间。CPU 平均过载了 135%；最近的 15 分钟有 1.35 个进程等待 CPU 时间。也许你还会喜欢：20 个监控系统性能的命令行工具（一）13 个 Linux 性能监控工具（二）Perf：一个 Linux 上的性能监控分析工具使用 Nmon 监控 Linux 的系统性能总而言之，如果你是系统管理员，你应该关注高的平均负载。平均负载高于 CPU 核心数意味着需要增加 CPU，反之则意味着 CPU 未被充分利用。作者简介：Aaron Kili 是 Linux 和自由软件的热心者，热衷于分享知识，现在是 TecMint 网站的内容创作者，不久之后将成为 Linux 系统管理员，web 开发者。via: https://www.tecmint.com/understand-linux-load-averages-and-monitor-performance/作者：Aaron Kili 译者：kylecao 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
cpustat 是 Linux 下一个强大的系统性能测量程序，它用 Go 编程语言 编写。它通过使用 “用于分析任意系统的性能的方法（USE）”，以有效的方式显示 CPU 利用率和饱和度。它高频率对系统中运行的每个进程进行取样，然后以较低的频率汇总这些样本。例如，它能够每 200ms 测量一次每个进程，然后每 5 秒汇总这些样本，包括某些度量的最小/平均/最大值（min/avg/max）。推荐阅读： 监控 Linux 性能的 20 个命令行工具cpustat 能用两种方式输出数据：定时汇总的纯文本列表和每个取样的彩色滚动面板。如何在 Linux 中安装 cpustat为了使用 cpustat，你的 Linux 系统中必须安装有 Go 语言（GoLang），如果你还没有安装它，点击下面的链接逐步安装 GoLang：在 Linux 下安装 GoLang（Go 编程语言）安装完 Go 以后，输入下面的 go get 命令安装 cpustat，这个命令会将 cpustat 二进制文件安装到你的 GOBIN 变量（所指的路径）：# go get github.com/uber-common/cpustat 如何在 Linux 中使用 cpustat安装过程完成后，如果你不是以 root 用户控制系统，像下面这样使用 sudo 命令获取 root 权限运行 cpustat，否则会出现下面显示的错误信息：$ $GOBIN/cpustat This program uses the netlink taskstats interface, so it must be run as root. 注意：想要像你系统中已经安装的其它 Go 程序那样运行 cpustat，你需要把 GOBIN 变量添加到 PATH 环境变量。打开下面的链接学习如何在 Linux 中设置 PATH 变量。学习如何在 Linux 中永久设置你的 $PATH 变量cpustat 是这样工作的：在每个时间间隔查询 /proc 目录获取当前进程 ID 列表，然后：对于每个 PID，读取 /proc/pid/stat，然后计算和前一个样本的差别。如果是一个新的 PID，读取 /proc/pid/cmdline。对于每个 PID，发送 netlink 消息获取 taskstat，计算和前一个样本的差别。读取 /proc/stat 获取总的系统统计信息。根据获取所有这些统计信息所花费的时间，会调整每个休息间隔。另外，通过每次取样之间实际经过的时间，每个样本也会记录它用于测量的时间。这可用于计算 cpustat 自身的延迟。当不带任何参数运行时，cpustat 默认会显示以下信息：样本间隔：200ms；汇总间隔：2s（10 个样本）；显示前 10 个进程；用户过滤器：all；pid 过滤器：all。正如下面截图所示：$ sudo $GOBIN/cpustat cpustat – 监控 Linux CPU 使用在上面的输出中，之前显示的系统范围的度量字段意义如下：usr - 用户模式运行时间占 CPU 百分比的 min/avg/max 值。sys - 系统模式运行时间占 CPU 百分比的 min/avg/max 值。nice - 用户模式低优先级运行时间占 CPU 百分比的 min/avg/max 值。idle - 用户模式空闲时间占 CPU 百分比的 min/avg/max 值。iowait - 等待磁盘 IO 的 min/avg/max 延迟时间。prun - 处于可运行状态的 min/avg/max 进程数量（同“平均负载”一样）。pblock - 被磁盘 IO 阻塞的 min/avg/max 进程数量。pstat - 在本次汇总间隔里启动的进程/线程数目。同样还是上面的输出，对于一个进程，不同列的意思分别是：name - 从 /proc/pid/stat 或 /proc/pid/cmdline 获取的进程名称。pid - 进程 ID，也被用作 “tgid” （线程组 ID）。min - 该 pid 的用户模式+系统模式时间的最小样本，取自 /proc/pid/stat。比率是 CPU 的百分比。max - 该 pid 的用户模式+系统模式时间的最大样本，取自 /proc/pid/stat。usr - 在汇总期间该 pid 的平均用户模式运行时间，取自 /proc/pid/stat。sys - 在汇总期间该 pid 的平均系统模式运行时间，取自 /proc/pid/stat。nice - 表示该进程的当前 “nice” 值，取自 /proc/pid/stat。值越高表示越好（nicer）。runq - 进程和它所有线程可运行但等待运行的时间，通过 netlink 取自 taskstats。比率是 CPU 的百分比。iow - 进程和它所有线程被磁盘 IO 阻塞的时间，通过 netlink 取自 taskstats。比率是 CPU 的百分比，对整个汇总间隔平均。swap - 进程和它所有线程等待被换入（swap in）的时间，通过 netlink 取自 taskstats。Scale 是 CPU 的百分比，对整个汇总间隔平均。vcx 和 icx - 在汇总间隔期间进程和它的所有线程自动上下文切换总的次数，通过 netlink 取自 taskstats。rss - 从 /proc/pid/stat 获取的当前 RSS 值。它是指该进程正在使用的内存数量。ctime - 在汇总间隔期间等待子进程退出的用户模式+系统模式 CPU 时间总和，取自 /proc/pid/stat。 注意长时间运行的子进程可能导致混淆这个值，因为只有在子进程退出后才会报告时间。但是，这对于计算高频 cron 任务以及 CPU 时间经常被多个子进程使用的健康检查非常有帮助。thrd - 汇总间隔最后线程的数目，取自 /proc/pid/stat。sam - 在这个汇总间隔期间该进程的样本数目。最近启动或退出的进程可能看起来比汇总间隔的样本数目少。下面的命令显示了系统中运行的前 10 个 root 用户进程：$ sudo $GOBIN/cpustat -u root 查找 root 用户正在运行的进程要想用更好看的终端模式显示输出，像下面这样用 -t 选项：$ sudo $GOBIN/cpustat -u root -t root 用户正在运行的进程要查看前 x 个进程（默认是 10），你可以使用 -n 选项，下面的命令显示了系统中 正在运行的前 20 个进程：$ sudo $GOBIN/cpustat -n 20 你也可以像下面这样使用 -cpuprofile 选项将 CPU 信息写到文件，然后用 cat 命令查看文件：$ sudo $GOBIN/cpustat -cpuprofile cpuprof.txt $ cat cpuprof.txt 要显示帮助信息，像下面这样使用 -h 选项：$ sudo $GOBIN/cpustat -h 可以从 cpustat 的 Github 仓库：https://github.com/uber-common/cpustat 查阅其它资料。就是这些！在这篇文章中，我们向你展示了如何安装和使用 cpustat，Linux 下的一个有用的系统性能测量工具。通过下面的评论框和我们分享你的想法吧。作者简介：Aaron Kili 是一个 Linux 和 F.O.S.S（Free and Open-Source Software） 爱好者，一个 Linux 系统管理员、web 开发员，现在也是 TecMint 的内容创建者，他喜欢和电脑一起工作，他相信知识共享。via: http://www.tecmint.com/cpustat-monitors-cpu-utilization-by-processes-in-linux/作者：Aaron Kili 译者：ictlyh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
pyDash 是一个轻量且基于 web 的 Linux 性能监测工具，它是用 Python 和 Django 加上 Chart.js 来写的。经测试，在下面这些主流 Linux 发行版上可运行：CentOS、Fedora、Ubuntu、Debian、Raspbian 以及 Pidora 。你可以使用这个工具来监视你的 Linux 个人电脑/服务器资源，比如 CPU、内存、网络统计，包括在线用户的进程以及更多。仪表盘完全由主要的 Python 发行版本所提供的 Python 库开发，因此它的依赖关系很少，你不需要安装许多包或库来运行它。在这篇文章中，我将展示如何安装 pyDash 来监测 Linux 服务器性能。如何在 Linux 系统下安装 pyDash1、首先，像下面这样安装需要的软件包 git 和 Python pip：-------------- 在 Debian/Ubuntu 上 -------------- $ sudo apt-get install git python-pip -------------- 在 CentOS/RHEL 上 -------------- # yum install epel-release # yum install git python-pip -------------- 在 Fedora 22+ 上 -------------- # dnf install git python-pip 2、如果安装好了 git 和 Python pip，那么接下来，像下面这样安装 virtualenv，它有助于处理针对 Python 项目的依赖关系：# pip install virtualenv 或 $ sudo pip install virtualenv 3、现在，像下面这样使用 git 命令，把 pyDash 仓库克隆到 home 目录中：# git clone https://github.com/k3oni/pydash.git # cd pydash 4、下一步，使用下面的 virtualenv 命令为项目创建一个叫做 pydashtest 虚拟环境：$ virtualenv pydashtest #give a name for your virtual environment like pydashtest 创建虚拟环境重要：请注意，上面的屏幕截图中，虚拟环境的 bin 目录被高亮显示，你的可能和这不一样，取决于你把 pyDash 目录克隆到什么位置。5、创建好虚拟环境（pydashtest）以后，你需要在使用前像下面这样激活它：$ source /home/aaronkilik/pydash/pydashtest/bin/activate 激活虚拟环境从上面的屏幕截图中，你可以注意到，提示字符串 1（PS1）已经发生改变，这表明虚拟环境已经被激活，而且可以开始使用。6、现在，安装 pydash 项目 requirements；如何你好奇的话，可以使用 cat 命令查看 requirements.txt 的内容，然后像下面所示那样进行安装：$ cat requirements.txt $ pip install -r requirements.txt 7、现在，进入 pydash 目录，里面包含一个名为 settings.py 的文件，也可直接运行下面的命令打开这个文件，然后把 SECRET_KEY 改为一个特定值：$ vi pydash/settings.py 设置密匙保存文件然后退出。8、之后，运行下面的命令来创建一个项目数据库和安装 Django 的身份验证系统，并创建一个项目的超级用户：$ python manage.py syncdb 根据你的情况回答下面的问题：Would you like to create one now? (yes/no): yes Username (leave blank to use 'root'): admin Email address: aaronkilik@gmail.com Password: ########### Password (again): ############ 创建项目数据库9、这个时候，一切都设置好了，然后，运行下面的命令来启用 Django 开发服务器：$ python manage.py runserver 10、接下来，打开你的 web 浏览器，输入网址：http://127.0.0.1:8000/ 进入 web 控制台登录界面，输入你在第 8 步中创建数据库和安装 Django 身份验证系统时创建的超级用户名和密码，然后点击登录。pyDash 登录界面11、登录到 pydash 主页面以后，你将会可以看到监测系统的基本信息，包括 CPU、内存和硬盘使用量以及系统平均负载。向下滚动便可查看更多部分的信息。pydash 服务器性能概述12、下一个屏幕截图显示的是一段 pydash 的跟踪界面，包括 IP 地址、互联网流量、硬盘读/写、在线用户以及 netstats 。pyDash 网络概述13、下一个 pydash 主页面的截图显示了一部分系统中被监视的活跃进程。pyDash 监视活跃 Linux 进程如果想了解更多信息，请在 GitHub 上查看 pydash：https://github.com/k3oni/pydash这就是全部内容了。在这篇文章中，我们展示了在 Linux 中如何安装 pyDash 并测试它的主要特性。如果你有什么想法，可以通过下面的反馈部分联系我们；如果你知道任何有用或类似的工具，也可以在评论中告知我们。作者简介：我叫 Ravi Saive，是 TecMint 的原创作者，是一个喜欢在网上分享技巧和知识的计算机极客和 Linux Guru。我的大多数服务器都运行在 Linux 开源平台上。请关注我：Twitter、Facebook 以及 Google+ 。via: http://www.tecmint.com/pydash-a-web-based-linux-performance-monitoring-tool/作者：Ravi Saive 译者：ucasFL 校对：jasminepeng本文由 LCTT 原创编译，Linux中国 荣誉推出
为牺牲性能追求生产率而呐喊让我从关于 Python 中的 asyncio 这个标准库的讨论中休息一会，谈谈我最近正在思考的一些东西：Python 的速度。对不了解我的人说明一下，我是一个 Python 的粉丝，而且我在我能想到的所有地方都积极地使用 Python。人们对 Python 最大的抱怨之一就是它的速度比较慢，有些人甚至拒绝尝试使用 Python，因为它比其他语言速度慢。这里说说为什么我认为应该尝试使用 Python，尽管它是有点慢。速度不再重要过去的情形是，程序需要花费很长的时间来运行，CPU 比较贵，内存也很贵。程序的运行时间是一个很重要的指标。计算机非常的昂贵，计算机运行所需要的电也是相当贵的。对这些资源进行优化是因为一个永恒的商业法则：优化你最贵的资源。在过去，最贵的资源是计算机的运行时间。这就是导致计算机科学致力于研究不同算法的效率的原因。然而，这已经不再是正确的，因为现在硅芯片很便宜，确实很便宜。运行时间不再是你最贵的资源。公司最贵的资源现在是它的员工时间。或者换句话说，就是你。把事情做完比把它变快更加重要。实际上，这是相当的重要，我将把它再次放在这里，仿佛它是一个引文一样（给那些只是粗略浏览的人）:把事情做完比快速地做事更加重要。你可能会说：“我的公司在意速度，我开发一个 web 应用程序，那么所有的响应时间必须少于 x 毫秒。”或者，“我们失去了客户，因为他们认为我们的 app 运行太慢了。”我并不是想说速度一点也不重要，我只是想说速度不再是最重要的东西；它不再是你最贵的资源。速度是唯一重要的东西当你在编程的背景下说 速度 时，你通常是说性能，也就是 CPU 周期。当你的 CEO 在编程的背景下说 速度 时，他指的是业务速度，最重要的指标是产品上市的时间。基本上，你的产品/web 程序是多么的快并不重要。它是用什么语言写的也不重要。甚至它需要花费多少钱也不重要。在一天结束时，让你的公司存活下来或者死去的唯一事物就是产品上市时间。我不只是说创业公司的想法 -- 你开始赚钱需要花费多久，更多的是“从想法到客户手中”的时间期限。企业能够存活下来的唯一方法就是比你的竞争对手更快地创新。如果在你的产品上市之前，你的竞争对手已经提前上市了，那么你想出了多少好的主意也将不再重要。你必须第一个上市，或者至少能跟上。一但你放慢了脚步，你就输了。企业能够存活下来的唯一方法就是比你的竞争对手更快地创新。一个微服务的案例像 Amazon、Google 和 Netflix 这样的公司明白快速前进的重要性。他们创建了一个业务系统，可以使用这个系统迅速地前进和快速的创新。微服务是针对他们的问题的解决方案。这篇文章不谈你是否应该使用微服务，但是至少要理解为什么 Amazon 和 Google 认为他们应该使用微服务。微服务本来就很慢。微服务的主要概念是用网络调用来打破边界。这意味着你正在把使用的函数调用（几个 cpu 周期）转变为一个网络调用。没有什么比这更影响性能了。和 CPU 相比较，网络调用真的很慢。但是这些大公司仍然选择使用微服务。我所知道的架构里面没有比微服务还要慢的了。微服务最大的弊端就是它的性能，但是最大的长处就是上市的时间。通过在较小的项目和代码库上建立团队，一个公司能够以更快的速度进行迭代和创新。这恰恰表明了，非常大的公司也很在意上市时间，而不仅仅只是只有创业公司。CPU 不是你的瓶颈如果你在写一个网络应用程序，如 web 服务器，很有可能的情况会是，CPU 时间并不是你的程序的瓶颈。当你的 web 服务器处理一个请求时，可能会进行几次网络调用，例如到数据库，或者像 Redis 这样的缓存服务器。虽然这些服务本身可能比较快速，但是对它们的网络调用却很慢。这里有一篇很好的关于特定操作的速度差异的博客文章。在这篇文章里，作者把 CPU 周期时间缩放到更容易理解的人类时间。如果一个单独的 CPU 周期等同于 1 秒，那么一个从 California 到 New York 的网络调用将相当于 4 年。那就说明了网络调用是多少的慢。按一些粗略估计，我们可以假设在同一数据中心内的普通网络调用大约需要 3 毫秒。这相当于我们“人类比例” 3 个月。现在假设你的程序是高 CPU 密集型，这需要 100000 个 CPU 周期来对单一调用进行响应。这相当于刚刚超过 1 天。现在让我们假设你使用的是一种要慢 5 倍的语言，这将需要大约 5 天。很好，将那与我们 3 个月的网络调用时间相比，4 天的差异就显得并不是很重要了。如果有人为了一个包裹不得不至少等待 3 个月，我不认为额外的 4 天对他们来说真的很重要。上面所说的终极意思是，尽管 Python 速度慢，但是这并不重要。语言的速度（或者 CPU 时间）几乎从来不是问题。实际上谷歌曾经就这一概念做过一个研究，并且他们就此发表过一篇论文。那篇论文论述了设计高吞吐量的系统。在结论里，他们说到：在高吞吐量的环境中使用解释性语言似乎是矛盾的，但是我们已经发现 CPU 时间几乎不是限制因素；语言的表达性是指，大多数程序是源程序，同时它们的大多数时间花费在 I/O 读写和本机的运行时代码上。而且，解释性语言无论是在语言层面的轻松实验还是在允许我们在很多机器上探索分布计算的方法都是很有帮助的，再次强调：CPU 时间几乎不是限制因素。如果 CPU 时间是一个问题怎么办？你可能会说，“前面说的情况真是太好了，但是我们确实有过一些问题，这些问题中 CPU 成为了我们的瓶颈，并造成了我们的 web 应用的速度十分缓慢”，或者“在服务器上 X 语言比 Y 语言需要更少的硬件资源来运行。”这些都可能是对的。关于 web 服务器有这样的美妙的事情：你可以几乎无限地负载均衡它们。换句话说，可以在 web 服务器上投入更多的硬件。当然，Python 可能会比其他语言要求更好的硬件资源，比如 c 语言。只是把硬件投入在 CPU 问题上。相比于你的时间，硬件就显得非常的便宜了。如果你在一年内节省了两周的生产力时间，那将远远多于所增加的硬件开销的回报。那么，Python 更快一些吗？这一篇文章里面，我一直在谈论最重要的是开发时间。所以问题依然存在：当就开发时间而言，Python 要比其他语言更快吗？按常规惯例来看，我、google 还有其他几个人可以告诉你 Python 是多么的高效。它为你抽象出很多东西，帮助你关注那些你真正应该编写代码的地方，而不会被困在琐碎事情的杂草里，比如你是否应该使用一个向量或者一个数组。但你可能不喜欢只是听别人说的这些话，所以让我们来看一些更多的经验数据。在大多数情况下，关于 python 是否是更高效语言的争论可以归结为脚本语言（或动态语言）与静态类型语言两者的争论。我认为人们普遍接受的是静态类型语言的生产力较低，但是，这有一篇优秀的论文解释了为什么不是这样。就 Python 而言，这里有一项研究，它调查了不同语言编写字符串处理的代码所需要花费的时间，供参考。在上述研究中，Python 的效率比 Java 高出 2 倍。有一些其他研究也显示相似的东西。 Rosetta Code 对编程语言的差异进行了深入的研究。在论文中，他们把 python 与其他脚本语言/解释性语言相比较，得出结论：Python 更简洁，即使与函数式语言相比较（平均要短 1.2 到 1.6 倍）普遍的趋势似乎是 Python 中的代码行总是更少。代码行听起来可能像一个可怕的指标，但是包括上面已经提到的两项研究在内的多项研究表明，每种语言中每行代码所需要花费的时间大约是一样的。因此，限制代码行数就可以提高生产效率。甚至 codinghorror（一名 C# 程序员）本人写了一篇关于 Python 是如何更有效率的文章。我认为说 Python 比其他的很多语言更加的有效率是公正的。这主要是由于 Python 有大量的自带以及第三方库。这里是一篇讨论 Python 和其他语言间的差异的简单的文章。如果你不知道为何 Python 是如此的小巧和高效，我邀请你借此机会学习一点 python，自己多实践。这儿是你的第一个程序：import __hello__ 但是如果速度真的重要呢？上述论点的语气可能会让人觉得优化与速度一点也不重要。但事实是，很多时候运行时性能真的很重要。一个例子是，你有一个 web 应用程序，其中有一个特定的端点需要用很长的时间来响应。你知道这个程序需要多快，并且知道程序需要改进多少。在我们的例子中，发生了两件事：我们注意到有一个端点执行缓慢。我们承认它是缓慢，因为我们有一个可以衡量是否足够快的标准，而它没达到那个标准。我们不必在应用程序中微调优化所有内容，只需要让其中每一个都“足够快”。如果一个端点花费了几秒钟来响应，你的用户可能会注意到，但是，他们并不会注意到你将响应时间由 35 毫秒降低到 25 毫秒。“足够好”就是你需要做到的所有事情。免责声明: 我应该说有一些应用程序，如实时投标程序，确实需要细微优化，每一毫秒都相当重要。但那只是例外，而不是规则。为了明白如何对端点进行优化，你的第一步将是配置代码，并尝试找出瓶颈在哪。毕竟： 任何除了瓶颈之外的改进都是错觉。 Any improvements made anywhere besides the bottleneck are an illusion. -- Gene Kim如果你的优化没有触及到瓶颈，你只是浪费你的时间，并没有解决实际问题。在你优化瓶颈之前，你不会得到任何重要的改进。如果你在不知道瓶颈是什么前就尝试优化，那么你最终只会在部分代码中玩耍。在测量和确定瓶颈之前优化代码被称为“过早优化”。人们常提及 Donald Knuth 说的话，但他声称这句话实际上是他从别人那里听来的： 过早优化是万恶之源 Premature optimization is the root of all evil 。在谈到维护代码库时，来自 Donald Knuth 的更完整的引文是：在 97% 的时间里，我们应该忘记微不足道的效率：过早的优化是万恶之源。然而在关 键的 3%，我们不应该错过优化的机会。 —— Donald Knuth换句话说，他所说的是，在大多数时间你应该忘记对你的代码进行优化。它几乎总是足够好。在不是足够好的情况下，我们通常只需要触及 3% 的代码路径。比如因为你使用了 if 语句而不是函数，你的端点快了几纳秒，但这并不会使你赢得任何奖项。过早的优化包括调用某些更快的函数，或者甚至使用特定的数据结构，因为它通常更快。计算机科学认为，如果一个方法或者算法与另一个具有相同的渐近增长（或称为 Big-O），那么它们是等价的，即使在实践中要慢两倍。计算机是如此之快，算法随着数据/使用增加而造成的计算增长远远超过实际速度本身。换句话说，如果你有两个 O(log n) 的函数，但是一个要慢两倍，这实际上并不重要。随着数据规模的增大，它们都以同样的速度“慢下来”。这就是过早优化是万恶之源的原因；它浪费了我们的时间，几乎从来没有真正有助于我们的性能改进。就 Big-O 而言，你可以认为对你的程序而言，所有的语言都是 O(n)，其中 n 是代码或者指令的行数。对于同样的指令，它们以同样的速率增长。对于渐进增长，一种语言的速度快慢并不重要，所有语言都是相同的。在这个逻辑下，你可以说，为你的应用程序选择一种语言仅仅是因为它的“快速”是过早优化的最终形式。你选择某些预期快速的东西，却没有测量，也不理解瓶颈将在哪里。为您的应用选择语言只是因为它的“快速”，是过早优化的最终形式。优化 Python我最喜欢 Python 的一点是，它可以让你一次优化一点点代码。假设你有一个 Python 的方法，你发现它是你的瓶颈。你对它优化过几次，可能遵循这里和那里的一些指导，现在，你很肯定 Python 本身就是你的瓶颈。Python 有调用 C 代码的能力，这意味着，你可以用 C 重写这个方法来减少性能问题。你可以一次重写一个这样的方法。这个过程允许你用任何可以编译为 C 兼容汇编程序的语言，编写良好优化后的瓶颈方法。这让你能够在大多数时间使用 Python 编写，只在必要的时候都才用较低级的语言来写代码。有一种叫做 Cython 的编程语言，它是 Python 的超集。它几乎是 Python 和 C 的合并，是一种渐进类型的语言。任何 Python 代码都是有效的 Cython 代码，Cython 代码可以编译成 C 代码。使用 Cython，你可以编写一个模块或者一个方法，并逐渐进步到越来越多的 C 类型和性能。你可以将 C 类型和 Python 的鸭子类型混在一起。使用 Cython，你可以获得混合后的完美组合，只在瓶颈处进行优化，同时在其他所有地方不失去 Python 的美丽。星战前夜的一幅截图：这是用 Python 编写的 space MMO 游戏。当您最终遇到 Python 的性能问题阻碍时，你不需要把你的整个代码库用另一种不同的语言来编写。你只需要用 Cython 重写几个函数，几乎就能得到你所需要的性能。这就是星战前夜采取的策略。这是一个大型多玩家的电脑游戏，在整个架构中使用 Python 和 Cython。它们通过优化 C/Cython 中的瓶颈来实现游戏级别的性能。如果这个策略对他们有用，那么它应该对任何人都有帮助。或者，还有其他方法来优化你的 Python。例如，PyPy 是一个 Python 的 JIT 实现，它通过使用 PyPy 替掉 CPython（这是 Python 的默认实现），为长时间运行的应用程序提供重要的运行时改进（如 web 服务器）。让我们回顾一下要点：优化你最贵的资源。那就是你，而不是计算机。选择一种语言/框架/架构来帮助你快速开发（比如 Python）。不要仅仅因为某些技术的快而选择它们。当你遇到性能问题时，请找到瓶颈所在。你的瓶颈很可能不是 CPU 或者 Python 本身。如果 Python 成为你的瓶颈（你已经优化过你的算法），那么可以转向热门的 Cython 或者 C。尽情享受可以快速做完事情的乐趣。我希望你喜欢阅读这篇文章，就像我喜欢写这篇文章一样。如果你想说谢谢，请为我点下赞。另外，如果某个时候你想和我讨论 Python，你可以在 twitter 上艾特我（@nhumrich），或者你可以在 Python slack channel 找到我。作者简介：Nick Humrich -- 坚持采用持续交付的方法，并为之写了很多工具。同是还是一名 Python 黑客与技术狂热者，目前是一名 DevOps 工程师。（题图：Pixabay，CC0）via: https://medium.com/hacker-daily/yes-python-is-slow-and-i-dont-care-13763980b5a1作者：Nick Humrich 译者：zhousiyu325 校对：jasminepeng本文由 LCTT 原创编译，Linux中国 荣誉推出
最近几年中，面向数据的设计已经受到了很多的关注 —— 一种强调内存中数据布局的编程风格，包括如何访问以及将会引发多少的 cache 缺失。由于在内存读取操作中缺失所占的数量级要大于命中的数量级，所以缺失的数量通常是优化的关键标准。这不仅仅关乎那些对性能有要求的 code-data 结构设计的软件，由于缺乏对内存效益的重视而成为软件运行缓慢、膨胀的一个很大因素。高效缓存数据结构的中心原则是将事情变得平滑和线性。比如，在大部分情况下，存储一个序列元素更倾向于使用普通数组而不是链表 —— 每一次通过指针来查找数据都会为 cache 缺失增加一份风险；而普通数组则可以预先获取，并使得内存系统以最大的效率运行如果你知道一点内存层级如何运作的知识，下面的内容会是想当然的结果——但是有时候即便它们相当明显，测试一下任不失为一个好主意。几年前 Baptiste Wicht 测试过了 std::vector vs std::list vs std::deque，（后者通常使用分块数组来实现，比如：一个数组的数组）。结果大部分会和你预期的保持一致，但是会存在一些违反直觉的东西。作为实例：在序列链表的中间位置做插入或者移除操作被认为会比数组快，但如果该元素是一个 POD 类型，并且不大于 64 字节或者在 64 字节左右（在一个 cache 流水线内），通过对要操作的元素周围的数组元素进行移位操作要比从头遍历链表来的快。这是由于在遍历链表以及通过指针插入/删除元素的时候可能会导致不少的 cache 缺失，相对而言，数组移位则很少会发生。（对于更大的元素，非 POD 类型，或者你已经有了指向链表元素的指针，此时和预期的一样，链表胜出）多亏了类似 Baptiste 这样的数据，我们知道了内存布局如何影响序列容器。但是关联容器，比如 hash 表会怎么样呢？已经有了些权威推荐：Chandler Carruth 推荐的带局部探测的开放寻址（此时，我们没必要追踪指针），以及Mike Acton 推荐的在内存中将 value 和 key 隔离（这种情况下，我们可以在每一个 cache 流水线中得到更多的 key）， 这可以在我们必须查找多个 key 时提高局部性能。这些想法很有意义，但再一次的说明：测试永远是好习惯，但由于我找不到任何数据，所以只好自己收集了。测试我测试了四个不同的 quick-and-dirty 哈希表实现，另外还包括 std::unordered_map 。这五个哈希表都使用了同一个哈希函数 —— Bob Jenkins 的 SpookyHash（64 位哈希值）。（由于哈希函数在这里不是重点，所以我没有测试不同的哈希函数；我同样也没有检测我的分析中的总内存消耗。）实现会通过简短的代码在测试结果表中标注出来。UM： std::unordered_map 。在 VS2012 和 libstdc++-v3 （libstdc++-v3: gcc 和 clang 都会用到这东西）中，UM 是以链表的形式实现，所有的元素都在链表中，bucket 数组中存储了链表的迭代器。VS2012 中，则是一个双链表，每一个 bucket 存储了起始迭代器和结束迭代器；libstdc++ 中，是一个单链表，每一个 bucket 只存储了一个起始迭代器。这两种情况里，链表节点是独立申请和释放的。最大负载因子是 1 。Ch：分离的、链状 bucket 指向一个元素节点的单链表。为了避免分开申请每一个节点，元素节点存储在普通数组池中。未使用的节点保存在一个空闲链表中。最大负载因子是 1。OL：开地址线性探测 —— 每一个 bucket 存储一个 62 bit 的 hash 值，一个 2 bit 的状态值（包括 empty，filled，removed 三个状态），key 和 value 。最大负载因子是 2/3。DO1：“data-oriented 1” —— 和 OL 相似，但是哈希值、状态值和 key、values 分离在两个隔离的平滑数组中。DO2：“data-oriented 2” —— 与 OL 类似，但是哈希/状态，keys 和 values 被分离在 3 个相隔离的平滑数组中。在我的所有实现中，包括 VS2012 的 UM 实现，默认使用尺寸为 2 的 n 次方。如果超出了最大负载因子，则扩展两倍。在 libstdc++ 中，UM 默认尺寸是一个素数。如果超出了最大负载因子，则扩展为下一个素数大小。但是我不认为这些细节对性能很重要。素数是一种对低 bit 位上没有足够熵的低劣 hash 函数的挽救手段，但是我们正在用的是一个很好的 hash 函数。OL，DO1 和 DO2 的实现将共同的被称为 OA（open addressing）——稍后我们将发现它们在性能特性上非常相似。在每一个实现中，单元数从 100 K 到 1 M，有效负载（比如：总的 key + value 大小）从 8 到 4 k 字节我为几个不同的操作记了时间。 keys 和 values 永远是 POD 类型，keys 永远是 8 个字节（除了 8 字节的有效负载，此时 key 和 value 都是 4 字节）因为我的目的是为了测试内存影响而不是哈希函数性能，所以我将 key 放在连续的尺寸空间中。每一个测试都会重复 5 遍，然后记录最小的耗时。测试的操作在这里：Fill：将一个随机的 key 序列插入到表中（key 在序列中是唯一的）。Presized fill：和 Fill 相似，但是在插入之间我们先为所有的 key 保留足够的内存空间，以防止在 fill 过程中 rehash 或者重申请。Lookup：执行 100 k 次随机 key 查找，所有的 key 都在 table 中。Failed lookup: 执行 100 k 次随机 key 查找，所有的 key 都不在 table 中。Remove：从 table 中移除随机选择的半数元素。Destruct：销毁 table 并释放内存。你可以在这里下载我的测试代码。这些代码只能在 64 机器上编译（包括Windows和Linux）。在 main() 函数顶部附近有一些开关，你可把它们打开或者关掉——如果全开，可能会需要一两个小时才能结束运行。我收集的结果也放在了那个打包文件里的 Excel 表中。（注意： Windows 和 Linux 在不同的 CPU 上跑的，所以时间不具备可比较性）代码也跑了一些单元测试，用来验证所有的 hash 表实现都能运行正确。我还顺带尝试了附加的两个实现：Ch 中第一个节点存放在 bucket 中而不是 pool 里，二次探测的开放寻址。 这两个都不足以好到可以放在最终的数据里，但是它们的代码仍放在了打包文件里面。结果这里有成吨的数据!! 这一节我将详细的讨论一下结果，但是如果你对此不感兴趣，可以直接跳到下一节的总结。Windows这是所有的测试的图表结果，使用 Visual Studio 2012 编译，运行于 Windows 8.1 和 Core i7-4710HQ 机器上。从左至右是不同的有效负载大小，从上往下是不同的操作（注意：不是所有的Y轴都是相同的比例！）我将为每一个操作总结一下主要趋向。Fill：在我的 hash 表中，Ch 稍比任何的 OA 变种要好。随着哈希表大小和有效负载的加大，差距也随之变大。我猜测这是由于 Ch 只需要从一个空闲链表中拉取一个元素，然后把它放在 bucket 前面，而 OA 不得不搜索一部分 bucket 来找到一个空位置。所有的 OA 变种的性能表现基本都很相似，当然 DO1 稍微有点优势。在小负载的情况，UM 几乎是所有 hash 表中表现最差的 —— 因为 UM 为每一次的插入申请（内存）付出了沉重的代价。但是在 128 字节的时候，这些 hash 表基本相当，大负载的时候 UM 还赢了点。因为，我所有的实现都需要重新调整元素池的大小，并需要移动大量的元素到新池里面，这一点我几乎无能为力；而 UM 一旦为元素申请了内存后便不需要移动了。注意大负载中图表上夸张的跳步！这更确认了重新调整大小带来的问题。相反，UM 只是线性上升 —— 只需要重新调整 bucket 数组大小。由于没有太多隆起的地方，所以相对有效率。Presized fill：大致和 Fill 相似，但是图示结果更加的线性光滑，没有太大的跳步（因为不需要 rehash ），所有的实现差距在这一测试中要缩小了些。大负载时 UM 依然稍快于 Ch，问题还是在于重新调整大小上。Ch 仍是稳步少快于 OA 变种，但是 DO1 比其它的 OA 稍有优势。Lookup：所有的实现都相当的集中。除了最小负载时，DO1 和 OL 稍快，其余情况下 UM 和 DO2 都跑在了前面。（LCTT 译注： 你确定？）真的，我无法描述 UM 在这一步做的多么好。尽管需要遍历链表，但是 UM 还是坚守了面向数据的本性。顺带一提，查找时间和 hash 表的大小有着很弱的关联，这真的很有意思。 哈希表查找时间期望上是一个常量时间，所以在的渐进视图中，性能不应该依赖于表的大小。但是那是在忽视了 cache 影响的情况下！作为具体的例子，当我们在具有 10 k 条目的表中做 100 k 次查找时，速度会便变快，因为在第一次 10 k - 20 k 次查找后，大部分的表会处在 L3 中。Failed lookup：相对于成功查找，这里就有点更分散一些。DO1 和 DO2 跑在了前面，但 UM 并没有落下，OL 则是捉襟见肘啊。我猜测，这可能是因为 OL 整体上具有更长的搜索路径，尤其是在失败查询时；内存中，hash 值在 key 和 value 之飘来荡去的找不着出路，我也很受伤啊。DO1 和 DO2 具有相同的搜索长度，但是它们将所有的 hash 值打包在内存中，这使得问题有所缓解。Remove：DO2 很显然是赢家，但 DO1 也未落下。Ch 则落后，UM 则是差的不是一丁半点（主要是因为每次移除都要释放内存）；差距随着负载的增加而拉大。移除操作是唯一不需要接触数据的操作，只需要 hash 值和 key 的帮助，这也是为什么 DO1 和 DO2 在移除操作中的表现大相径庭，而其它测试中却保持一致。（如果你的值不是 POD 类型的，并需要析构，这种差异应该是会消失的。）Destruct：Ch 除了最小负载，其它的情况都是最快的（最小负载时，约等于 OA 变种）。所有的 OA 变种基本都是相等的。注意，在我的 hash 表中所做的所有析构操作都是释放少量的内存 buffer 。但是 在Windows中，释放内存的消耗和大小成比例关系。（而且，这是一个很显著的开支 —— 申请 ～1 GB 的内存需要 ～100 ms 的时间去释放！）UM 在析构时是最慢的一个（小负载时，慢的程度可以用数量级来衡量），大负载时依旧是稍慢些。对于 UM 来讲，释放每一个元素而不是释放一组数组真的是一个硬伤。Linux我还在装有 Linux Mint 17.1 的 Core i5-4570S 机器上使用 gcc 4.8 和 clang 3.5 来运行了测试。gcc 和 clang 的结果很相像，因此我只展示了 gcc 的；完整的结果集合包含在了代码下载打包文件中，链接在上面。大部分结果和 Windows 很相似，因此我只高亮了一些有趣的不同点。Lookup：这里 DO1 跑在前头，而在 Windows 中 DO2 更快些。（LCTT 译注： 这里原文写错了吧？）同样，UM 和 Ch 落后于其它所有的实现——过多的指针追踪，然而 OA 只需要在内存中线性的移动即可。至于 Windows 和 Linux 结果为何不同，则不是很清楚。UM 同样比 Ch 慢了不少，特别是大负载时，这很奇怪；我期望的是它们可以基本相同。Failed lookup：UM 再一次落后于其它实现，甚至比 OL 还要慢。我再一次无法理解为何 UM 比 Ch 慢这么多，Linux 和 Windows 的结果为何有着如此大的差距。Destruct：在我的实现中，小负载的时候，析构的消耗太少了，以至于无法测量；在大负载中，线性增加的比例和创建的虚拟内存页数量相关，而不是申请到的数量？同样，要比 Windows 中的析构快上几个数量级。但是并不是所有的都和 hash 表有关；我们在这里可以看出不同系统和运行时内存系统的表现。貌似，Linux 释放大内存块是要比 Windows 快上不少（或者 Linux 很好的隐藏了开支，或许将释放工作推迟到了进程退出，又或者将工作推给了其它线程或者进程）。UM 由于要释放每一个元素，所以在所有的负载中都比其它慢上几个数量级。事实上，我将图片做了剪裁，因为 UM 太慢了，以至于破坏了 Y 轴的比例。总结好，当我们凝视各种情况下的数据和矛盾的结果时，我们可以得出什么结果呢？我想直接了当的告诉你这些 hash 表变种中有一个打败了其它所有的 hash 表，但是这显然不那么简单。不过我们仍然可以学到一些东西。首先，在大多数情况下我们“很容易”做的比 std::unordered_map 还要好。我为这些测试所写的所有实现（它们并不复杂；我只花了一两个小时就写完了）要么是符合 unordered_map 要么是在其基础上做的提高，除了大负载（超过128字节）中的插入性能， unordered_map 为每一个节点独立申请存储占了优势。（尽管我没有测试，我同样期望 unordered_map 能在非 POD 类型的负载上取得胜利。）具有指导意义的是，如果你非常关心性能，不要假设你的标准库中的数据结构是高度优化的。它们可能只是在 C++ 标准的一致性上做了优化，但不是性能。:P其次，如果不管在小负载还是超负载中，若都只用 DO1 （开放寻址，线性探测，hashes/states 和 key/vaules分别处在隔离的普通数组中），那可能不会有啥好表现。这不是最快的插入，但也不坏（还比 unordered_map 快），并且在查找，移除，析构中也很快。你所知道的 —— “面向数据设计”完成了！注意，我的为这些哈希表做的测试代码远未能用于生产环境——它们只支持 POD 类型，没有拷贝构造函数以及类似的东西，也未检测重复的 key，等等。我将可能尽快的构建一些实际的 hash 表，用于我的实用库中。为了覆盖基础部分，我想我将有两个变种：一个基于 DO1，用于小的，移动时不需要太大开支的负载；另一个用于链接并且避免重新申请和移动元素（就像 unordered_map ),用于大负载或者移动起来需要大开支的负载情况。这应该会给我带来最好的两个世界。与此同时，我希望你们会有所启迪。最后记住，如果 Chandler Carruth 和 Mike Acton 在数据结构上给你提出些建议，你一定要听。作者简介：我是一名图形程序员，目前在西雅图做自由职业者。之前我在 NVIDIA 的 DevTech 软件团队中工作，并在美少女特工队工作室中为 PS3 和 PS4 的 Infamous 系列游戏开发渲染技术。自 2002 年起，我对图形非常感兴趣，并且已经完成了一系列的工作，包括：雾、大气雾霾、体积照明、水、视觉效果、粒子系统、皮肤和头发阴影、后处理、镜面模型、线性空间渲染、和 GPU 性能测量和优化。你可以在我的博客了解更多和我有关的事，除了图形，我还对理论物理和程序设计感兴趣。你可以在 nathaniel.reed@gmail.com 或者在 Twitter（@Reedbeta)/Google+ 上关注我。我也会经常在 StackExchange 上回答计算机图形的问题。via: http://reedbeta.com/blog/data-oriented-hash-table/作者：Nathan Reed 译者：sanfusu 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出