探索传统 JavaScript 基准测试

Summary: 现在看看另一方面，所有的静态性能测试——无论是微基准micro-benchmark还是大型应用的宏基准macro-benchmark，都注定要随着时间的推移变成噩梦！

![](/data/attachment/album/201709/29/153923vd38iudidk83zmt0.jpg)

可以很公平地说，[JavaScript](https://en.wikipedia.org/wiki/JavaScript) 是当下软件工程中*最重要的技术*。对于那些深入接触过编程语言、编译器和虚拟机的人来说，这仍然有点令人惊讶，因为在语言设计者们看来，JavaScript 不是十分优雅；在编译器工程师们看来，它没有多少可优化的地方；甚至还没有一个伟大的标准库。这取决于你和谁吐槽，JavaScript 的缺点你花上数周都枚举不完，而你总会找到一些你从所未知的奇怪的东西。尽管这看起来明显困难重重，不过 JavaScript 还是成为了当今 web 的核心，并且还（通过 [Node.js](https://nodejs.org/)）成为服务器端和云端的主导技术，甚至还开辟了进军物联网领域的道路。

那么问题来了，为什么 JavaScript 如此受欢迎？或者说如此成功？我知道没有一个很好的答案。如今我们有许多使用 JavaScript 的好理由，或许最重要的是围绕其构建的庞大的生态系统，以及现今大量可用的资源。但所有这一切实际上是发展到一定程度的后果。为什么 JavaScript 变得流行起来了？嗯，你或许会说，这是 web 多年来的通用语了。但是在很长一段时间里，人们极其讨厌 JavaScript。回顾过去，似乎第一波 JavaScript 浪潮爆发在上个年代的后半段。那个时候 JavaScript 引擎加速了各种不同的任务的执行，很自然的，这可能让很多人对 JavaScript 刮目相看。

回到过去那些日子，这些加速使用了现在所谓的传统 JavaScript 基准进行测试——从苹果的 [SunSpider 基准](https://webkit.org/perf/sunspider/sunspider.html)（JavaScript 微基准之母）到 Mozilla 的 [Kraken 基准](http://krakenbenchmark.mozilla.org/) 和谷歌的 V8 基准。后来，V8 基准被 [Octane 基准](https://developers.google.com/octane) 取代，而苹果发布了新的 [JetStream 基准](http://browserbench.org/JetStream)。这些传统的 JavaScript 基准测试驱动了无数人的努力，使 JavaScript 的性能达到了本世纪初没人能预料到的水平。据报道其性能加速达到了 1000 倍，一夜之间在网站使用 `<script>` 标签不再是与魔鬼共舞，做客户端不再仅仅是可能的了，甚至是被鼓励的。

[![性能测试，JS 基准的简史](/data/attachment/album/201709/29/154014gdq0nhklyornkyar.png)](https://www.youtube.com/watch?v=PvZdTZ1Nl5o)

（来源： [Advanced JS performance with V8 and Web Assembly](https://www.youtube.com/watch?v=PvZdTZ1Nl5o)， Chrome Developer Summit 2016, @s3ththompson。）

现在是 2016 年，所有（相关的）JavaScript 引擎的性能都达到了一个令人难以置信的水平，web 应用像原生应用一样快（或者能够像原生应用一样快）。引擎配有复杂的优化编译器，通过收集之前的关于类型/形状的反馈来推测某些操作（例如属性访问、二进制操作、比较、调用等），生成高度优化的机器代码的短序列。大多数优化是由 SunSpider 或 Kraken 等微基准以及 Octane 和 JetStream 等静态测试套件驱动的。由于有像 [asm.js](http://asmjs.org/) 和 [Emscripten](https://github.com/kripken/emscripten) 这样的 JavaScript 技术，我们甚至可以将大型 C++ 应用程序编译成 JavaScript，并在你的浏览器上运行，而无需下载或安装任何东西。例如，现在你可以在 web 上玩 [AngryBots](http://beta.unity3d.com/jonas/AngryBots)，无需沙盒，而过去的 web 游戏需要安装一堆诸如 Adobe Flash 或 Chrome PNaCl 的特殊插件。

这些成就绝大多数都要归功于这些微基准和静态性能测试套件的出现，以及与这些传统的 JavaScript 基准间的竞争的结果。你可以对 SunSpider 表示不满，但很显然，没有 SunSpider，JavaScript 的性能可能达不到今天的高度。好吧，赞美到此为止。现在看看另一方面，所有的静态性能测试——无论是<ruby> 微基准 <rt>  micro-benchmark </rt></ruby>还是大型应用的<ruby> 宏基准 <rt>  macro-benchmark </rt></ruby>，都注定要随着时间的推移变成噩梦！为什么？因为在开始摆弄它之前，基准只能教你这么多。一旦达到某个阔值以上（或以下），那么有益于特定基准的优化的一般适用性将呈指数级下降。例如，我们将 Octane 作为现实世界中 web 应用性能的代表，并且在相当长的一段时间里，它可能做得很不错，但是现在，Octane 与现实场景中的时间分布是截然不同的，因此即使眼下再优化 Octane 乃至超越自身，可能在现实世界中还是得不到任何显著的改进（无论是通用 web 还是 Node.js 的工作负载）。

[![基准与现实世界的时间分布对比](/data/attachment/album/201709/29/154019vof28up8e33y3989.png)](https://youtu.be/xCx4uC7mn6Y)

（来源：[Real-World JavaScript Performance](https://youtu.be/xCx4uC7mn6Y)，BlinkOn 6 conference，@tverwaes）

由于传统 JavaScript 基准（包括最新版的 JetStream 和 Octane）可能已经背离其有用性变得越来越远，我们开始在 2016 年初寻找新的方法来测量现实场景的性能，为 V8 和 Chrome 添加了大量新的性能追踪钩子。我们还特意添加一些机制来查看我们在浏览 web 时的时间究竟开销在哪里，例如，是脚本执行、垃圾回收、编译，还是什么地方？而这些调查的结果非常有趣和令人惊讶。从上面的幻灯片可以看出，运行 Octane 花费了 70% 以上的时间去执行 JavaScript 和垃圾回收，而浏览 web 的时候，通常执行 JavaScript 花费的时间不到 30%，垃圾回收占用的时间永远不会超过 5%。在 Octane 中并没有体现出它花费了大量时间来解析和编译。因此，将更多的时间用在优化 JavaScript 执行上将提高你的 Octane 跑分，但不会对加载 [youtube.com](http://youtube.com/) 有任何积极的影响。事实上，花费更多的时间来优化 JavaScript 执行甚至可能有损你现实场景的性能，因为编译器需要更多的时间，或者你需要跟踪更多的反馈，最终在编译、垃圾回收和<ruby> 运行时桶 <rt>  Runtime bucket </rt></ruby>等方面开销了更多的时间。

[![测速表](/data/attachment/album/201709/29/154025mze3xkrmx4n34njb.png)](http://browserbench.org/Speedometer)

还有另外一组基准测试用于测量浏览器整体性能（包括 JavaScript 和 DOM 性能），最新推出的是 [Speedometer 基准](http://browserbench.org/Speedometer)。该基准试图通过运行一个用不同的主流 web 框架实现的简单的 [TodoMVC](http://todomvc.com/) 应用（现在看来有点过时了，不过新版本正在研发中）以捕获更真实的现实场景的性能。上述幻灯片中的各种测试 （Angular、Ember、React、Vanilla、Flight 和 Backbone）挨着放在 Octane 之后，你可以看到，此时此刻这些测试似乎更好地代表了现实世界的性能指标。但是请注意，这些数据收集在本文撰写将近 6 个月以前，而且我们优化了更多的现实场景模式（例如我们正在重构垃圾回收系统以显著地降低开销，并且 [解析器也正在重新设计](https://twitter.com/bmeurer/status/806927160300556288)）。还要注意的是，虽然这看起来像是只和浏览器相关，但我们有非常强有力的证据表明传统的峰值性能基准也不能很好的代表现实场景中 Node.js 应用性能。

[![Speedometer 和 Octane 对比](/data/attachment/album/201709/29/154028bsb5qq9sn1zwwsmk.png)](https://youtu.be/xCx4uC7mn6Y)

（来源： [Real-World JavaScript Performance](https://youtu.be/xCx4uC7mn6Y)， BlinkOn 6 conference, @tverwaes.）

所有这一切可能已经路人皆知了，因此我将用本文剩下的部分强调一些具体案例，它们对关于我为什么认为这不仅有用，而且必须停止关注某一阈值的静态峰值性能基准测试对于 JavaScript 社区的健康是很关键的。让我通过一些例子说明 JavaScript 引擎怎样来玩弄基准的。

### 臭名昭著的 SunSpider 案例

一篇关于传统 JavaScript 基准测试的博客如果没有指出 SunSpider 那个明显的问题是不完整的。让我们从性能测试的最佳实践开始，它在现实场景中不是很适用：bitops-bitwise-and.js [性能测试](https://github.com/WebKit/webkit/blob/master/PerformanceTests/SunSpider/tests/sunspider-1.0.2/bitops-bitwise-and.js)。

[![bitops-bitwise-and.js](/data/attachment/album/201709/29/154036intchn8tmaa21nbu.png)](https://github.com/WebKit/webkit/blob/master/PerformanceTests/SunSpider/tests/sunspider-1.0.2/bitops-bitwise-and.js)

有一些算法需要进行快速的 AND 位运算，特别是从 `C/C++` 转译成 JavaScript 的地方，所以快速执行该操作确实有点意义。然而，现实场景中的网页可能不关心引擎在循环中执行 AND 位运算是否比另一个引擎快两倍。但是再盯着这段代码几秒钟后，你可能会注意到在第一次循环迭代之后 `bitwiseAndValue` 将变成 `0`，并且在接下来的 599999 次迭代中将保持为 `0`。所以一旦你让此获得了好的性能，比如在差不多的硬件上所有测试均低于 5ms，在经过尝试之后你会意识到，只有循环的第一次是必要的，而剩余的迭代只是在浪费时间（例如 [loop peeling](https://en.wikipedia.org/wiki/Loop_splitting) 后面的死代码），那你现在就可以开始玩弄这个基准测试了。这需要 JavaScript 中的一些机制来执行这种转换，即你需要检查 `bitwiseAndValue` 是全局对象的常规属性还是在执行脚本之前不存在，全局对象或者它的原型上必须没有拦截器。但如果你真的想要赢得这个基准测试，并且你愿意全力以赴，那么你可以在不到 1ms 的时间内完成这个测试。然而，这种优化将局限于这种特殊情况，并且测试的轻微修改可能不再触发它。

好吧，那么 [bitops-bitwise-and.js](https://github.com/WebKit/webkit/blob/master/PerformanceTests/SunSpider/tests/sunspider-1.0.2/bitops-bitwise-and.js) 测试彻底肯定是微基准最失败的案例。让我们继续转移到 SunSpider 中更逼真的场景——[string-tagcloud.js](https://github.com/WebKit/webkit/blob/master/PerformanceTests/SunSpider/tests/sunspider-1.0.2/string-tagcloud.js) 测试，它基本上是运行一个较早版本的 `json.js polyfill`。该测试可以说看起来比位运算测试更合理，但是花点时间查看基准的配置之后立刻会发现：大量的时间浪费在一条 `eval` 表达式（高达 20% 的总执行时间被用于解析和编译，再加上实际执行编译后代码的 10% 的时间）。

[![string-tagcloud.js](/data/attachment/album/201709/29/154046efgv9pgi4g6wv99z.png)](https://github.com/WebKit/webkit/blob/master/PerformanceTests/SunSpider/tests/sunspider-1.0.2/string-tagcloud.js#L199)

仔细看看，这个 `eval` 只执行了一次，并传递一个 JSON 格式的字符串，它包含一个由 2501 个含有 `tag` 和 `popularity` 属性的对象组成的数组：

```
([
  {
    "tag": "titillation",
    "popularity": 4294967296
  },
  {
    "tag": "foamless",
    "popularity": 1257718401
  },
  {
    "tag": "snarler",
    "popularity": 613166183
  },
  {
    "tag": "multangularness",
    "popularity": 368304452任何
  },
  {
    "tag": "Fesapo unventurous",
    "popularity": 248026512
  },
  {
    "tag": "esthesioblast",
    "popularity": 179556755
  },
  {
    "tag": "echeneidoid",
    "popularity": 136641578
  },
  {
    "tag": "embryoctony",
    "popularity": 107852576
  },
  ...
])

```

显然，解析这些对象字面量，为其生成本地代码，然后执行该代码的成本很高。将输入的字符串解析为 JSON 并生成适当的对象图的开销将更加低廉。所以，加快这个基准测试的一个小把戏就是模拟 `eval`，并尝试总是将数据首先作为 JSON 解析，如果以 JSON 方式读取失败，才回退进行真实的解析、编译、执行（尽管需要一些额外的黑魔法来跳过括号）。早在 2007 年，这甚至不算是一个坏点子，因为没有 [JSON.parse](https://tc39.github.io/ecma262/#sec-json.parse)，不过在 2017 年这只是 JavaScript 引擎的技术债，可能会让 `eval` 的合法使用遥遥无期。

```
--- string-tagcloud.js.ORIG     2016-12-14 09:00:52.869887104 +0100
+++ string-tagcloud.js  2016-12-14 09:01:01.033944051 +0100
@@ -198,7 +198,7 @@
                     replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(:?[eE][+\-]?\d+)?/g, ']').
                     replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

-                j = eval('(' + this + ')');
+                j = JSON.parse(this);

                 return typeof filter === 'function' ? walk('', j) : j;
             }

```

事实上，将基准测试更新到现代 JavaScript 会立刻会性能暴增，正如今天的 `V8 LKGR` 从 36ms 降到了 26ms，性能足足提升了 30%！

```
$ node string-tagcloud.js.ORIG
Time (string-tagcloud): 36 ms.
$ node string-tagcloud.js
Time (string-tagcloud): 26 ms.
$ node -v
v8.0.0-pre
$

```

这是静态基准和性能测试套件常见的一个问题。今天，没有人会正儿八经地用 `eval` 解析 `JSON` 数据（不仅是因为性能问题，还出于严重的安全性考虑），而是坚持为最近五年写的代码使用 [JSON.parse](https://tc39.github.io/ecma262/#sec-json.parse)。事实上，使用 `eval` 解析 JSON 可能会被视作产品级代码的的一个漏洞！所以引擎作者致力于新代码的性能所作的努力并没有反映在这个古老的基准中，相反地，而是使得 `eval` 不必要地~~更智能~~复杂化，从而赢得 `string-tagcloud.js` 测试。

好吧，让我们看看另一个例子：[3d-cube.js](https://github.com/WebKit/webkit/blob/master/PerformanceTests/SunSpider/tests/sunspider-1.0.2/3d-cube.js)。这个基准测试做了很多矩阵运算，即便是最聪明的编译器对此也无可奈何，只能说执行而已。基本上，该基准测试花了大量的时间执行 `Loop` 函数及其调用的函数。

[![3d-cube.js](/data/attachment/album/201709/29/154056qir2trmbgmr7m9cb.png)](https://github.com/WebKit/webkit/blob/master/PerformanceTests/SunSpider/tests/sunspider-1.0.2/3d-cube.js#L239)

一个有趣的发现是：`RotateX`、`RotateY` 和 `RotateZ` 函数总是调用相同的常量参数 `Phi`。

[![3d-cube.js](/data/attachment/album/201709/29/154100u0vikqi0jm15o2mm.png)](https://github.com/WebKit/webkit/blob/master/PerformanceTests/SunSpider/tests/sunspider-1.0.2/3d-cube.js#L151)

这意味着我们基本上总是为 [Math.sin](https://tc39.github.io/ecma262/#sec-math.sin) 和 [Math.cos](https://tc39.github.io/ecma262/#sec-math.cos) 计算相同的值，每次执行都要计算 204 次。只有 3 个不同的输入值：

* 0.017453292519943295
* 0.05235987755982989
* 0.08726646259971647

显然，你可以在这里做的一件事情就是通过缓存以前的计算值来避免重复计算相同的正弦值和余弦值。事实上，这是 V8 以前的做法，而其它引擎例如 `SpiderMonkey` 目前仍然在这样做。我们从 V8 中删除了所谓的<ruby> 超载缓存 <rt>  transcendental cache </rt></ruby>，因为缓存的开销在实际的工作负载中是不可忽视的，你不可能总是在一行代码中计算相同的值，这在其它地方倒不稀奇。当我们在 2013 和 2014 年移除这个特定的基准优化时，我们对 SunSpider 基准产生了强烈的冲击，但我们完全相信，为基准而优化并没有任何意义，并同时以这种方式批判了现实场景中的使用案例。

[![3d-cube 基准](/data/attachment/album/201709/29/154101tkcmjj4n5pk5twuh.png)](https://arewefastyet.com/#machine=12&view=single&suite=ss&subtest=cube&start=1343350217&end=1415382608)

（来源：[arewefastyet.com](https://arewefastyet.com/#machine=12&view=single&suite=ss&subtest=cube&start=1343350217&end=1415382608)）

显然，处理恒定正弦/余弦输入的更好的方法是一个内联的启发式算法，它试图平衡内联因素与其它不同的因素，例如在调用位置优先选择内联，其中<ruby> 常量叠算 <rt>  constant folding </rt></ruby>可以是有益的，例如在 `RotateX`、`RotateY` 和 `RotateZ` 调用位置的案例中。但是出于各种原因，这对于 `Crankshaft` 编译器并不可行。使用 `Ignition` 和 `TurboFan` 倒是一个明智的选择，我们已经在开发更好的[内联启发式算法](https://docs.google.com/document/d/1VoYBhpDhJC4VlqMXCKvae-8IGuheBGxy32EOgC2LnT8)。

#### 垃圾回收（GC）是有害的

除了这些非常具体的测试问题，SunSpider 基准测试还有一个根本性的问题：总体执行时间。目前 V8 在适当的英特尔硬件上运行整个基准测试大概只需要 200ms（使用默认配置）。<ruby> 次垃圾回收 <rt>  minor GC </rt></ruby>在 1ms 到 25ms 之间（取决于新空间中的存活对象和旧空间的碎片），而<ruby> 主垃圾回收 <rt>  major GC </rt></ruby>暂停的话可以轻松减掉 30ms（甚至不考虑增量标记的开销），这超过了 SunSpider 套件总体执行时间的 10%！因此，任何不想因垃圾回收循环而造成减速 10-20% 的引擎，必须用某种方式确保它在运行 SunSpider 时不会触发垃圾回收。

[![driver-TEMPLATE.html](/data/attachment/album/201709/29/154103b0c9kbb92krz5b2o.png)](https://github.com/WebKit/webkit/blob/master/PerformanceTests/SunSpider/resources/driver-TEMPLATE.html#L70)

就实现而言，有不同的方案，不过就我所知，没有一个在现实场景中产生了任何积极的影响。V8 使用了一个相当简单的技巧：由于每个 SunSpider 套件都运行在一个新的 `<iframe>` 中，这对应于 V8 中一个新的本地上下文，我们只需检测快速的 `<iframe>` 创建和处理（所有的 SunSpider 测试每个花费的时间小于 50ms），在这种情况下，在处理和创建之间执行垃圾回收，以确保我们在实际运行测试的时候不会触发垃圾回收。这个技巧运行的很好，在 99.9% 的案例中没有与实际用途冲突；除了时不时的你可能会受到打击，不管出于什么原因，如果你做的事情让你看起来像是 V8 的 SunSpider 测试驱动程序，你就可能被强制的垃圾回收打击到，这有可能对你的应用导致负面影响。所以谨记一点：**不要让你的应用看起来像 SunSpider！**

我可以继续展示更多 SunSpider 示例，但我不认为这非常有用。到目前为止，应该清楚的是，为刷新 SunSpider 评分而做的进一步优化在现实场景中没有带来任何好处。事实上，世界可能会因为没有 SunSpider 而更美好，因为引擎可以放弃只是用于 SunSpider 的奇淫技巧，或者甚至可以伤害到现实中的用例。不幸的是，SunSpider 仍然被（科技）媒体大量地用来比较他们眼中的浏览器性能，或者甚至用来比较手机！所以手机制造商和安卓制造商对于让 SunSpider（以及其它现在毫无意义的基准 FWIW） 上的 Chrome 看起来比较体面自然有一定的兴趣。手机制造商通过销售手机来赚钱，所以获得良好的评价对于电话部门甚至整间公司的成功至关重要。其中一些部门甚至在其手机中配置在 SunSpider 中得分较高的旧版 V8，将他们的用户置于各种未修复的安全漏洞之下（在新版中早已被修复），而让用户被最新版本的 V8 带来的任何现实场景的性能优势拒之门外！

[![Galaxy S7 和 S7 Edge 的评价：三星的高光表现](/data/attachment/album/201709/29/154105vfq1t69jw99kyw0s.png)](https://www.engadget.com/2016/03/08/galaxy-s7-and-s7-edge-review/)

（来源：[www.engadget.com](https://www.engadget.com/2016/03/08/galaxy-s7-and-s7-edge-review/)）

作为 JavaScript 社区的一员，如果我们真的想认真对待 JavaScript 领域的现实场景的性能，我们需要让各大技术媒体停止使用传统的 JavaScript 基准来比较浏览器或手机。能够在每个浏览器中运行一个基准测试，并比较它的得分自然是好的，但是请使用一个与当今世界相关的基准，例如真实的 web 页面；如果你觉得需要通过浏览器基准来比较两部手机，请至少考虑使用 [Speedometer](http://browserbench.org/Speedometer)。

#### 轻松一刻

![](/data/attachment/album/201709/29/154106zotvf2stz0hagrs6.jpg)

我一直很喜欢这个 [Myles Borins](https://twitter.com/thealphanerd) 谈话，所以我不得不无耻地向他偷师。现在我们从 SunSpider 的谴责中回过头来，让我们继续检查其它经典基准。

### 不是那么显眼的 Kraken 案例

Kraken 基准是 [Mozilla 于 2010 年 9 月 发布的](https://blog.mozilla.org/blog/2010/09/14/release-the-kraken-2)，据说它包含了现实场景应用的片段/内核，并且与 SunSpider 相比少了一个微基准。我不想在 Kraken 上花太多口舌，因为我认为它不像 SunSpider 和 Octane 一样对 JavaScript 性能有着深远的影响，所以我将强调一个特别的案例——[audio-oscillator.js](https://github.com/h4writer/arewefastyet/blob/master/benchmarks/kraken/tests/kraken-1.1/audio-oscillator.js) 测试。

[![audio-oscillator.js](/data/attachment/album/201709/29/154108bjzxutdtt0uttwnj.png)](https://github.com/h4writer/arewefastyet/blob/master/benchmarks/kraken/tests/kraken-1.1/audio-oscillator.js)

正如你所见，测试调用了 `calcOsc` 函数 500 次。`calcOsc` 首先在全局的 `sine` `Oscillator` 上调用 `generate`，然后创建一个新的 `Oscillator`，调用它的 `generate` 方法并将其添加到全局的 `sine` `Oscillator` 里。没有详细说明测试为什么是这样做的，让我们看看 `Oscillator` 原型上的 `generate` 方法。

[![audio-oscillator-data.js](/data/attachment/album/201709/29/154110t51zu1tuvmiuh4zx.png)](https://github.com/h4writer/arewefastyet/blob/master/benchmarks/kraken/tests/kraken-1.1/audio-oscillator-data.js#L687)

让我们看看代码，你也许会觉得这里主要是循环中的数组访问或者乘法或者 [Math.round](https://tc39.github.io/ecma262/#sec-math.round) 调用，但令人惊讶的是 `offset % this.waveTableLength` 表达式完全支配了 `Oscillator.prototype.generate` 的运行。在任何的英特尔机器上的分析器中运行此基准测试显示，超过 20% 的时间占用都属于我们为模数生成的 `idiv` 指令。然而一个有趣的发现是，`Oscillator` 实例的 `waveTableLength` 字段总是包含相同的值——2048，因为它在 `Oscillator` 构造器中只分配一次。

[![audio-oscillator-data.js](/data/attachment/album/201709/29/154115u1v1wyfaiiqa3141.png)](https://github.com/h4writer/arewefastyet/blob/master/benchmarks/kraken/tests/kraken-1.1/audio-oscillator-data.js#L566)

如果我们知道整数模数运算的右边是 2 的幂，我们显然可以生成[更好的代码](https://graphics.stanford.edu/%7Eseander/bithacks.html#ModulusDivisionEasy)，完全避免了英特尔上的 `idiv` 指令。所以我们需要获取一种信息使 `this.waveTableLength` 从 `Oscillator` 构造器到 `Oscillator.prototype.generate` 中的模运算都是 2048。一个显而易见的方法是尝试依赖于将所有内容内嵌到 `calcOsc` 函数，并让 `load/store` 消除为我们进行的常量传播，但这对于在 `calcOsc` 函数之外分配的 `sine` `oscillator` 无效。

因此，我们所做的就是添加支持跟踪某些常数值作为模运算符的右侧反馈。这在 V8 中是有意义的，因为我们为诸如 `+`、`*` 和 `%` 的二进制操作跟踪类型反馈，这意味着操作者跟踪输入的类型和产生的输出类型（参见最近的圆桌讨论中关于[动态语言的快速运算](https://docs.google.com/presentation/d/1wZVIqJMODGFYggueQySdiA3tUYuHNMcyp_PndgXsO1Y)的幻灯片）。当然，用 `fullcodegen` 和 `Crankshaft` 挂接起来也是相当容易的，`MOD` 的 `BinaryOpIC` 也可以跟踪右边已知的 2 的冥。

```
$ ~/Projects/v8/out/Release/d8 --trace-ic audio-oscillator.js
[...SNIP...]
[BinaryOpIC(MOD:None*None->None) => (MOD:Smi*2048->Smi) @ ~Oscillator.generate+598 at audio-oscillator.js:697]
[...SNIP...]
$

```

事实上，以默认配置运行的 V8 （带有 Crankshaft 和 fullcodegen）表明 `BinaryOpIC` 正在为模数的右侧拾取适当的恒定反馈，并正确跟踪左侧始终是一个小整数（以 V8 的话叫做 `Smi`），我们也总是产生一个小整数结果。 使用 `--print-opt-code -code-comments` 查看生成的代码，很快就显示出，`Crankshaft` 利用反馈在 `Oscillator.prototype.generate` 中为整数模数生成一个有效的代码序列：

```
[...SNIP...]
                  ;;; <@80,#84> load-named-field
0x133a0bdacc4a   330  8b4343         movl rax,[rbx+0x43]
                  ;;; <@83,#86> compare-numeric-and-branch
0x133a0bdacc4d   333  3d00080000     cmp rax,0x800
0x133a0bdacc52   338  0f85ff000000   jnz 599  (0x133a0bdacd57)
[...SNIP...]
                  ;;; <@90,#94> mod-by-power-of-2-i
0x133a0bdacc5b   347  4585db         testl r11,r11
0x133a0bdacc5e   350  790f           jns 367  (0x133a0bdacc6f)
0x133a0bdacc60   352  41f7db         negl r11
0x133a0bdacc63   355  4181e3ff070000 andl r11,0x7ff
0x133a0bdacc6a   362  41f7db         negl r11
0x133a0bdacc6d   365  eb07           jmp 374  (0x133a0bdacc76)
0x133a0bdacc6f   367  4181e3ff070000 andl r11,0x7ff
[...SNIP...]
                  ;;; <@127,#88> deoptimize
0x133a0bdacd57   599  e81273cdff     call 0x133a0ba8406e
[...SNIP...]

```

所以你看到我们加载 `this.waveTableLength`（`rbx` 持有 `this` 的引用）的值，检查它仍然是 2048（十六进制的 0x800），如果是这样，就只用适当的掩码 0x7ff（`r11` 包含循环感应变量 `i` 的值）执行一个位操作 AND ，而不是使用 `idiv` 指令（注意保留左侧的符号）。

#### 过度特定的问题

所以这个技巧酷毙了，但正如许多基准关注的技巧都有一个主要的缺点：太过于特定了！一旦右侧发生变化，所有优化过的代码就失去了优化（假设右手始终是不再处理的 2 的冥），任何进一步的优化尝试都必须再次使用 `idiv`，因为 `BinaryOpIC` 很可能以 `Smi * Smi -> Smi` 的形式报告反馈。例如，假设我们实例化另一个 `Oscillator`，在其上设置不同的 `waveTableLength`，并为 `Oscillator` 调用 `generate`，那么即使我们实际上感兴趣的 `Oscillator` 不受影响，我们也会损失 20% 的性能（例如，引擎在这里实行非局部惩罚）。

```
--- audio-oscillator.js.ORIG    2016-12-15 22:01:43.897033156 +0100
+++ audio-oscillator.js 2016-12-15 22:02:26.397326067 +0100
@@ -1931,6 +1931,10 @@
 var frequency = 344.53;
 var sine = new Oscillator(Oscillator.Sine, frequency, 1, bufferSize, sampleRate);

+var unused = new Oscillator(Oscillator.Sine, frequency, 1, bufferSize, sampleRate);
+unused.waveTableLength = 1024;
+unused.generate();
+
 var calcOsc = function() {
   sine.generate();

```

将原始的 `audio-oscillator.js` 执行时间与包含额外未使用的 `Oscillator` 实例与修改的 `waveTableLength` 的版本进行比较，显示的是预期的结果：

```
$ ~/Projects/v8/out/Release/d8 audio-oscillator.js.ORIG
Time (audio-oscillator-once): 64 ms.
$ ~/Projects/v8/out/Release/d8 audio-oscillator.js
Time (audio-oscillator-once): 81 ms.
$

```

这是一个非常可怕的性能悬崖的例子：假设开发人员编写代码库，并使用某些样本输入值进行仔细的调整和优化，性能是体面的。现在，用户读过了性能说明开始使用该库，但不知何故从性能悬崖下降，因为她/他正在以一种稍微不同的方式使用库，即特定的 `BinaryOpIC` 的某种污染方式的类型反馈，并且遭受 20% 的减速（与该库作者的测量相比），该库的作者和用户都无法解释，这似乎是随机的。

现在这种情况在 JavaScript 领域并不少见，不幸的是，这些悬崖中有几个是不可避免的，因为它们是由于 JavaScript 的性能是基于乐观的假设和猜测。我们已经花了 **大量** 时间和精力来试图找到避免这些性能悬崖的方法，而仍提供了（几乎）相同的性能。事实证明，尽可能避免 `idiv` 是很有意义的，即使你不一定知道右边总是一个 2 的幂（通过动态反馈），所以为什么 `TurboFan` 的做法有异于 `Crankshaft` 的做法，因为它总是在运行时检查输入是否是 2 的幂，所以一般情况下，对于有符整数模数，优化右手侧的（未知的） 2 的冥看起来像这样（伪代码）：

```
if 0 < rhs then
  msk = rhs - 1
  if rhs & msk != 0 then
    lhs % rhs
  else
    if lhs < 0 then
      -(-lhs & msk)
    else
      lhs & msk
else
  if rhs < -1 then
    lhs % rhs
  else
    zero

```

这产生更加一致和可预测的性能（使用 `TurboFan`）：

```
$ ~/Projects/v8/out/Release/d8 --turbo audio-oscillator.js.ORIG
Time (audio-oscillator-once): 69 ms.
$ ~/Projects/v8/out/Release/d8 --turbo audio-oscillator.js
Time (audio-oscillator-once): 69 ms.
$

```

基准和过度特定化的问题在于基准可以给你提示可以看看哪里以及该怎么做，但它不告诉你应该做到什么程度，不能保护合理优化。例如，所有 JavaScript 引擎都使用基准来防止性能回退，但是运行 Kraken 不能保护我们在 `TurboFan` 中使用的常规方法，即我们可以将 `TurboFan` 中的模优化降级到过度特定的版本的 `Crankshaft`，而基准不会告诉我们性能回退的事实，因为从基准的角度来看这很好！现在你可以扩展基准，也许以上面我们相同的方式，并试图用基准覆盖一切，这是引擎实现者在一定程度上做的事情，但这种方法不能任意缩放。即使基准测试方便，易于用来沟通和竞争，以常识所见你还是需要留下空间，否则过度特定化将支配一切，你会有一个真正的、非常好的可接受的性能，以及巨大的性能悬崖线。

Kraken 测试还有许多其它的问题，不过现在让我们继续讨论过去五年中最有影响力的 JavaScript 基准测试—— Octane 测试。

### 深入接触 Octane

[Octane](https://developers.google.com/octane) 基准是 V8 基准的继承者，最初由[谷歌于 2012 年中期发布](https://blog.chromium.org/2012/08/octane-javascript-benchmark-suite-for.html)，目前的版本 Octane 2.0 [于 2013 年年底发布](https://blog.chromium.org/2013/11/announcing-octane-20.html)。这个版本包含 15 个独立测试，其中对于 `Splay` 和 `Mandreel`，我们用来测试吞吐量和延迟。这些测试范围从 [微软 TypeScript 编译器](http://www.typescriptlang.org/) 编译自身到 `zlib` 测试测量原生的 [asm.js](http://asmjs.org/) 性能，再到 `RegExp` 引擎的性能测试、光线追踪器、2D 物理引擎等。有关各个基准测试项的详细概述，请参阅[说明书](https://developers.google.com/octane/benchmark)。所有这些测试项目都经过仔细的筛选，以反映 JavaScript 性能的方方面面，我们认为这在 2012 年非常重要，或许预计在不久的将来会变得更加重要。

在很大程度上 Octane 在实现其将 JavaScript 性能提高到更高水平的目标方面无比的成功，它在 2012 年和 2013 年引导了良性的竞争，Octane 创造了巨大的业绩和成就。但是现在将近 2017 年了，世界看起来与 2012 年真的迥然不同了。除了通常和经常被引用的批评，Octane 中的大多数项目基本上已经过时（例如，老版本的 `TypeScript`，`zlib` 通过老版本的 [Emscripten](https://github.com/kripken/emscripten) 编译而成，`Mandreel` 甚至不再可用等等），某种更重要的方式影响了 Octane 的用途：

我们看到大型 web 框架赢得了 web 种族之争，尤其是像 [Ember](http://emberjs.com/) 和 [AngularJS](https://angularjs.org/) 这样的重型框架，它们使用了 JavaScript 执行模式，不过根本没有被 Octane 所反映，并且经常受到（我们）Octane 具体优化的损害。我们还看到 JavaScript 在服务器和工具前端获胜，这意味着有大规模的 JavaScript 应用现在通常运行上数星期，如果不是运行上数年都不会被 Octane 捕获。正如开篇所述，我们有硬数据表明 Octane 的执行和内存配置文件与我们每天在 web 上看到的截然不同。

让我们来看看今天一些玩弄 Octane 基准的具体例子，其中优化不再反映在现实场景。请注意，即使这可能听起来有点负面回顾，它绝对不意味着这样！正如我已经说过好几遍，Octane 是 JavaScript 性能故事中的重要一章，它发挥了至关重要的作用。在过去由 Octane 驱动的 JavaScript 引擎中的所有优化都是善意地添加的，因为 Octane 是现实场景性能的好代理！每个年代都有它的基准，而对于每一个基准都有一段时间你必须要放手！

话虽如此，让我们在路上看这个节目，首先看看 `Box2D` 测试，它是基于 [Box2DWeb](https://github.com/hecht-software/box2dweb) （一个最初由 Erin Catto 编写的移植到 JavaScript 的流行的 2D 物理引擎）的。总的来说，很多浮点数学驱动了很多 JavaScript 引擎下很好的优化，但是，事实证明它包含一个可以肆意玩弄基准的漏洞（怪我，我发现了漏洞，并添加在这种情况下的漏洞）。在基准中有一个函数 `D.prototype.UpdatePairs`，看起来像这样：

```
D.prototype.UpdatePairs = function(b) {
    var e = this;
    var f = e.m_pairCount = 0,
        m;
    for (f = 0; f < e.m_moveBuffer.length; ++f) {
        m = e.m_moveBuffer[f];
        var r = e.m_tree.GetFatAABB(m);
        e.m_tree.Query(function(t) {
                if (t == m) return true;
                if (e.m_pairCount == e.m_pairBuffer.length) e.m_pairBuffer[e.m_pairCount] = new O;
                var x = e.m_pairBuffer[e.m_pairCount];
                x.proxyA = t < m ? t : m;
                x.proxyB = t >= m ? t : m;
                ++e.m_pairCount;
                return true
            },
            r)
    }
    for (f = e.m_moveBuffer.length = 0; f < e.m_pairCount;) {
        r = e.m_pairBuffer[f];
        var s = e.m_tree.GetUserData(r.proxyA),
            v = e.m_tree.GetUserData(r.proxyB);
        b(s, v);
        for (++f; f < e.m_pairCount;) {
            s = e.m_pairBuffer[f];
            if (s.proxyA != r.proxyA || s.proxyB != r.proxyB) break;
            ++f
        }
    }
};

```

一些分析显示，在第一个循环中传递给 `e.m_tree.Query` 的无辜的内部函数花费了大量的时间：

```
function(t) {
    if (t == m) return true;
    if (e.m_pairCount == e.m_pairBuffer.length) e.m_pairBuffer[e.m_pairCount] = new O;
    var x = e.m_pairBuffer[e.m_pairCount];
    x.proxyA = t < m ? t : m;
    x.proxyB = t >= m ? t : m;
    ++e.m_pairCount;
    return true
}

```

更准确地说，时间并不是开销在这个函数本身，而是由此触发的操作和内置库函数。结果，我们花费了基准调用的总体执行时间的 4-7% 在 [Compare` 运行时函数](https://github.com/v8/v8/blob/5124589642ba12228dcd66a8cb8c84c986a13f35/src/runtime/runtime-object.cc#L884)上，它实现了[抽象关系](https://tc39.github.io/ecma262/#sec-abstract-relational-comparison)比较的一般情况。

![Box2D 比较分析](/data/attachment/album/201709/29/154121zhsoarsa98osofm8.png)

几乎所有对运行时函数的调用都来自 [CompareICStub](https://github.com/v8/v8/blob/5124589642ba12228dcd66a8cb8c84c986a13f35/src/x64/code-stubs-x64.cc#L2495)，它用于内部函数中的两个关系比较：

```
x.proxyA = t < m ? t : m;
x.proxyB = t >= m ? t : m;

```

所以这两行无辜的代码要负起 99% 的时间开销的责任！这怎么来的？好吧，与 JavaScript 中的许多东西一样，[抽象关系比较](https://tc39.github.io/ecma262/#sec-abstract-relational-comparison) 的直观用法不一定是正确的。在这个函数中，`t` 和 `m` 都是 `L` 的实例，它是这个应用的一个中心类，但不会覆盖 `Symbol.toPrimitive`、`“toString”`、`“valueOf”` 或 `Symbol.toStringTag` 属性，它们与抽象关系比较相关。所以如果你写 `t < m` 会发生什么呢？

1. 调用 [ToPrimitive](https://tc39.github.io/ecma262/#sec-toprimitive)(`t`, `hint Number`)。
2. 运行 [OrdinaryToPrimitive](https://tc39.github.io/ecma262/#sec-ordinarytoprimitive)(`t`, `"number"`)，因为这里没有 `Symbol.toPrimitive`。
3. 执行 `t.valueOf()`，这会获得 `t` 自身的值，因为它调用了默认的 [Object.prototype.valueOf](https://tc39.github.io/ecma262/#sec-object.prototype.valueof)。
4. 接着执行 `t.toString()`，这会生成 `"[object Object]"`，因为调用了默认的 [Object.prototype.toString](https://tc39.github.io/ecma262/#sec-object.prototype.toString)，并且没有找到 `L` 的 [Symbol.toStringTag](https://tc39.github.io/ecma262/#sec-symbol.tostringtag)。
5. 调用 [ToPrimitive](https://tc39.github.io/ecma262/#sec-toprimitive)(`m`, `hint Number`)。
6. 运行 [OrdinaryToPrimitive](https://tc39.github.io/ecma262/#sec-ordinarytoprimitive)(`m`, `"number"`)，因为这里没有 `Symbol.toPrimitive`。
7. 执行 `m.valueOf()`，这会获得 `m` 自身的值，因为它调用了默认的 [Object.prototype.valueOf](https://tc39.github.io/ecma262/#sec-object.prototype.valueof)。
8. 接着执行 `m.toString()`，这会生成 `"[object Object]"`，因为调用了默认的 [Object.prototype.toString](https://tc39.github.io/ecma262/#sec-object.prototype.toString)，并且没有找到 `L` 的 [Symbol.toStringTag](https://tc39.github.io/ecma262/#sec-symbol.tostringtag)。
9. 执行比较 `"[object Object]" < "[object Object]"`，结果是 `false`。

至于 `t >= m` 亦复如是，它总会输出 `true`。所以这里是一个漏洞——使用抽象关系比较这种方法没有意义。而利用它的方法是使编译器常数折叠，即给基准打补丁：

```
--- octane-box2d.js.ORIG        2016-12-16 07:28:58.442977631 +0100
+++ octane-box2d.js     2016-12-16 07:29:05.615028272 +0100
@@ -2021,8 +2021,8 @@
                     if (t == m) return true;
                     if (e.m_pairCount == e.m_pairBuffer.length) e.m_pairBuffer[e.m_pairCount] = new O;
                     var x = e.m_pairBuffer[e.m_pairCount];
-                    x.proxyA = t < m ? t : m;
-                    x.proxyB = t >= m ? t : m;
+                    x.proxyA = m;
+                    x.proxyB = t;
                     ++e.m_pairCount;
                     return true
                 },

```

因为这样做会跳过比较以达到 13% 的惊人的性能提升，并且所有的属性查找和内置函数的调用都会被它触发。

```
$ ~/Projects/v8/out/Release/d8 octane-box2d.js.ORIG
Score (Box2D): 48063
$ ~/Projects/v8/out/Release/d8 octane-box2d.js
Score (Box2D): 55359
$

```

那么我们是怎么做呢？事实证明，我们已经有一种用于跟踪比较对象的形状的机制，比较发生于 `CompareIC`，即所谓的已知接收器映射跟踪（其中的映射是 V8 的对象形状+原型），不过这是有限的抽象和严格相等比较。但是我可以很容易地扩展跟踪，并且收集反馈进行抽象的关系比较：

```
$ ~/Projects/v8/out/Release/d8 --trace-ic octane-box2d.js
[...SNIP...]
[CompareIC in ~+557 at octane-box2d.js:2024 ((UNINITIALIZED+UNINITIALIZED=UNINITIALIZED)->(RECEIVER+RECEIVER=KNOWN_RECEIVER))#LT @ 0x1d5a860493a1]
[CompareIC in ~+649 at octane-box2d.js:2025 ((UNINITIALIZED+UNINITIALIZED=UNINITIALIZED)->(RECEIVER+RECEIVER=KNOWN_RECEIVER))#GTE @ 0x1d5a860496e1]
[...SNIP...]
$

```

这里基准代码中使用的 `CompareIC` 告诉我们，对于我们正在查看的函数中的 `LT`（小于）和 `GTE`（大于或等于）比较，到目前为止这只能看到 `RECEIVERs`（接收器，V8 的 JavaScript 对象），并且所有这些接收器具有相同的映射 `0x1d5a860493a1`，其对应于 `L` 实例的映射。因此，在优化的代码中，只要我们知道比较的两侧映射的结果都为 `0x1d5a860493a1`，并且没人混淆 `L` 的原型链（即 `Symbol.toPrimitive`、`"valueOf"` 和 `"toString"` 这些方法都是默认的，并且没人赋予过 `Symbol.toStringTag` 的访问权限），我们可以将这些操作分别常量折叠为 `false` 和 `true`。剩下的故事都是关于 `Crankshaft` 的黑魔法，有很多一部分都是由于初始化的时候忘记正确地检查 `Symbol.toStringTag` 属性：

[![Hydrogen 黑魔法](/data/attachment/album/201709/29/154140mdvzczdscadzyvam.png)](https://codereview.chromium.org/1355113002)

最后，性能在这个特定的基准上有了质的飞跃：

![Box2D 加速](/data/attachment/album/201709/29/154210svz6kfq001ss60s6.png)

我要声明一下，当时我并不相信这个特定的行为总是指向源代码中的漏洞，所以我甚至期望外部代码经常会遇到这种情况，同时也因为我假设 JavaScript 开发人员不会总是关心这些种类的潜在错误。但是，我大错特错了，在此我马上悔改！我不得不承认，这个特殊的优化纯粹是一个基准测试的东西，并不会有助于任何真实代码（除非代码是为了从这个优化中获益而写，不过以后你可以在代码中直接写入 `true` 或 `false`，而不用再总是使用常量关系比较）。你可能想知道我们为什么在打补丁后又马上回滚了一下。这是我们整个团队投入到 `ES2015` 实施的非常时期，这才是真正的恶魔之舞，我们需要在没有严格的回归测试的情况下将所有新特性（`ES2015` 就是个怪兽）纳入传统基准。

关于 `Box2D` 点到为止了，让我们看看 `Mandreel` 基准。`Mandreel` 是一个用来将 `C/C++` 代码编译成 JavaScript 的编译器，它并没有用上新一代的 [Emscripten](https://github.com/kripken/emscripten) 编译器所使用，并且已经被弃用（或多或少已经从互联网消失了）大约三年的 JavaScript 子集 [asm.js](http://asmjs.org/)。然而，Octane 仍然有一个通过 [Mandreel](http://www.mandreel.com/) 编译的[子弹物理引擎](http://bulletphysics.org/wordpress/)。`MandreelLatency` 测试十分有趣，它测试 `Mandreel` 基准与频繁的时间测量检测点。有一种说法是，由于 `Mandreel` 强制使用虚拟机编译器，此测试提供了由编译器引入的延迟的指示，并且测量检测点之间的长时间停顿降低了最终得分。这听起来似乎合情合理，确实有一定的意义。然而，像往常一样，供应商找到了在这个基准上作弊的方法。

[![Mozilla 1162272 漏洞](/data/attachment/album/201709/29/154227sjmzbulcchaqzclm.png)](https://bugzilla.mozilla.org/show_bug.cgi?id=1162272)

`Mandreel` 自带一个重型初始化函数 `global_init`，光是解析这个函数并为其生成基线代码就花费了不可思议的时间。因为引擎通常在脚本中多次解析各种函数，一个所谓的预解析步骤用来发现脚本内的函数。然后作为函数第一次被调用完整的解析步骤以生成基线代码（或者说字节码）。这在 V8 中被称为[懒解析](https://docs.google.com/presentation/d/1214p4CFjsF-NY4z9in0GEcJtjbyVQgU0A-UqEvovzCs)。V8 有一些启发式检测函数，当预解析浪费时间的时候可以立刻调用，不过对于 `Mandreel` 基准的 `global_init` 函数就不太清楚了，于是我们将经历这个大家伙“预解析+解析+编译”的长时间停顿。所以我们[添加了一个额外的启发式函数](https://codereview.chromium.org/1102523003)以避免 `global_init` 函数的预解析。

[![MandreelLatency 基准](/data/attachment/album/201709/29/154232rw4csncwcwec4ae4.png)](https://arewefastyet.com/#machine=29&view=single&suite=octane&subtest=MandreelLatency&start=1415924086&end=1446461709)

由此可见，在检测 `global_init` 和避免昂贵的预解析步骤我们几乎提升了 2 倍。我们不太确定这是否会对真实用例产生负面影响，不过保证你在预解析大函数的时候将会受益匪浅（因为它们不会立即执行）。

让我们来看看另一个稍有争议的基准测试：[splay.js](https://github.com/chromium/octane/blob/master/splay.js) 测试，一个用于处理<ruby> 伸展树 <rt>  splay tree </rt></ruby>（二叉查找树的一种）和练习自动内存管理子系统（也被称为垃圾回收器）的数据操作基准。它自带一个延迟测试，这会引导 `Splay` 代码通过频繁的测量检测点，检测点之间的长时间停顿表明垃圾回收器的延迟很高。此测试测量延迟暂停的频率，将它们分类到桶中，并以较低的分数惩罚频繁的长暂停。这听起来很棒！没有 GC 停顿，没有垃圾。纸上谈兵到此为止。让我们看看这个基准，以下是整个伸展树业务的核心：

[![splay.js](/data/attachment/album/201709/29/154235yqv8ur8u3u8wa00q.png)](https://github.com/chromium/octane/blob/master/splay.js#L85)

这是伸展树结构的核心构造，尽管你可能想看完整的基准，不过这基本上是 `SplayLatency` 得分的重要来源。怎么回事？实际上，该基准测试是建立巨大的伸展树，尽可能保留所有节点，从而还原它原本的空间。使用像 V8 这样的代数垃圾回收器，如果程序违反了[代数假设](http://www.memorymanagement.org/glossary/g.html)，会导致极端的时间停顿，从本质上看，将所有东西从新空间撤回到旧空间的开销是非常昂贵的。在旧配置中运行 V8 可以清楚地展示这个问题：

```
$ out/Release/d8 --trace-gc --noallocation_site_pretenuring octane-splay.js
[20872:0x7f26f24c70d0]       10 ms: Scavenge 2.7 (6.0) -> 2.7 (7.0) MB, 1.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       12 ms: Scavenge 2.7 (7.0) -> 2.7 (8.0) MB, 1.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       14 ms: Scavenge 3.7 (8.0) -> 3.6 (10.0) MB, 0.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       18 ms: Scavenge 4.8 (10.5) -> 4.7 (11.0) MB, 2.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       22 ms: Scavenge 5.7 (11.0) -> 5.6 (16.0) MB, 2.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       28 ms: Scavenge 8.7 (16.0) -> 8.6 (17.0) MB, 4.3 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       35 ms: Scavenge 9.6 (17.0) -> 9.6 (28.0) MB, 6.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       49 ms: Scavenge 16.6 (28.5) -> 16.4 (29.0) MB, 8.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       65 ms: Scavenge 17.5 (29.0) -> 17.5 (52.0) MB, 15.3 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]       93 ms: Scavenge 32.3 (52.5) -> 32.0 (53.5) MB, 17.6 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      126 ms: Scavenge 33.4 (53.5) -> 33.3 (68.0) MB, 31.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      151 ms: Scavenge 47.9 (68.0) -> 47.6 (69.5) MB, 15.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      183 ms: Scavenge 49.2 (69.5) -> 49.2 (84.0) MB, 30.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      210 ms: Scavenge 63.5 (84.0) -> 62.4 (85.0) MB, 14.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      241 ms: Scavenge 64.7 (85.0) -> 64.6 (99.0) MB, 28.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      268 ms: Scavenge 78.2 (99.0) -> 77.6 (101.0) MB, 16.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      298 ms: Scavenge 80.4 (101.0) -> 80.3 (114.5) MB, 28.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      324 ms: Scavenge 93.5 (114.5) -> 92.9 (117.0) MB, 16.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      354 ms: Scavenge 96.2 (117.0) -> 96.0 (130.0) MB, 27.6 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      383 ms: Scavenge 108.8 (130.0) -> 108.2 (133.0) MB, 16.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      413 ms: Scavenge 111.9 (133.0) -> 111.7 (145.5) MB, 27.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      440 ms: Scavenge 124.1 (145.5) -> 123.5 (149.0) MB, 17.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      473 ms: Scavenge 127.6 (149.0) -> 127.4 (161.0) MB, 29.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      502 ms: Scavenge 139.4 (161.0) -> 138.8 (165.0) MB, 18.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      534 ms: Scavenge 143.3 (165.0) -> 143.1 (176.5) MB, 28.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      561 ms: Scavenge 154.7 (176.5) -> 154.2 (181.0) MB, 19.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      594 ms: Scavenge 158.9 (181.0) -> 158.7 (192.0) MB, 29.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      622 ms: Scavenge 170.0 (192.5) -> 169.5 (197.0) MB, 19.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      655 ms: Scavenge 174.6 (197.0) -> 174.3 (208.0) MB, 28.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      683 ms: Scavenge 185.4 (208.0) -> 184.9 (212.5) MB, 19.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      715 ms: Scavenge 190.2 (213.0) -> 190.0 (223.5) MB, 27.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      743 ms: Scavenge 200.7 (223.5) -> 200.3 (228.5) MB, 19.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      774 ms: Scavenge 205.8 (228.5) -> 205.6 (239.0) MB, 27.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      802 ms: Scavenge 216.1 (239.0) -> 215.7 (244.5) MB, 19.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      833 ms: Scavenge 221.4 (244.5) -> 221.2 (254.5) MB, 26.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      861 ms: Scavenge 231.5 (255.0) -> 231.1 (260.5) MB, 19.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      892 ms: Scavenge 237.0 (260.5) -> 236.7 (270.5) MB, 26.3 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      920 ms: Scavenge 246.9 (270.5) -> 246.5 (276.0) MB, 20.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      951 ms: Scavenge 252.6 (276.0) -> 252.3 (286.0) MB, 25.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]      979 ms: Scavenge 262.3 (286.0) -> 261.9 (292.0) MB, 20.3 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1014 ms: Scavenge 268.2 (292.0) -> 267.9 (301.5) MB, 29.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1046 ms: Scavenge 277.7 (302.0) -> 277.3 (308.0) MB, 22.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1077 ms: Scavenge 283.8 (308.0) -> 283.5 (317.5) MB, 25.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1105 ms: Scavenge 293.1 (317.5) -> 292.7 (323.5) MB, 20.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1135 ms: Scavenge 299.3 (323.5) -> 299.0 (333.0) MB, 24.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1164 ms: Scavenge 308.6 (333.0) -> 308.1 (339.5) MB, 20.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1194 ms: Scavenge 314.9 (339.5) -> 314.6 (349.0) MB, 25.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1222 ms: Scavenge 324.0 (349.0) -> 323.6 (355.5) MB, 21.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1253 ms: Scavenge 330.4 (355.5) -> 330.1 (364.5) MB, 25.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1282 ms: Scavenge 339.4 (364.5) -> 339.0 (371.0) MB, 22.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1315 ms: Scavenge 346.0 (371.0) -> 345.6 (380.0) MB, 25.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1413 ms: Mark-sweep 349.9 (380.0) -> 54.2 (305.0) MB, 5.8 / 0.0 ms  (+ 87.5 ms in 73 steps since start of marking, biggest step 8.2 ms, walltime since start of marking 131 ms) finalize incremental marking via stack guard GC in old space requested
[20872:0x7f26f24c70d0]     1457 ms: Scavenge 65.8 (305.0) -> 65.1 (305.0) MB, 31.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1489 ms: Scavenge 69.9 (305.0) -> 69.7 (305.0) MB, 27.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1523 ms: Scavenge 80.9 (305.0) -> 80.4 (305.0) MB, 22.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1553 ms: Scavenge 85.5 (305.0) -> 85.3 (305.0) MB, 24.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1581 ms: Scavenge 96.3 (305.0) -> 95.7 (305.0) MB, 18.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1616 ms: Scavenge 101.1 (305.0) -> 100.9 (305.0) MB, 29.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1648 ms: Scavenge 111.6 (305.0) -> 111.1 (305.0) MB, 22.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1678 ms: Scavenge 116.7 (305.0) -> 116.5 (305.0) MB, 25.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1709 ms: Scavenge 127.0 (305.0) -> 126.5 (305.0) MB, 20.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1738 ms: Scavenge 132.3 (305.0) -> 132.1 (305.0) MB, 23.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1767 ms: Scavenge 142.4 (305.0) -> 141.9 (305.0) MB, 19.6 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1796 ms: Scavenge 147.9 (305.0) -> 147.7 (305.0) MB, 23.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1825 ms: Scavenge 157.8 (305.0) -> 157.3 (305.0) MB, 19.9 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1853 ms: Scavenge 163.5 (305.0) -> 163.2 (305.0) MB, 22.2 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1881 ms: Scavenge 173.2 (305.0) -> 172.7 (305.0) MB, 19.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1910 ms: Scavenge 179.1 (305.0) -> 178.8 (305.0) MB, 23.0 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1944 ms: Scavenge 188.6 (305.0) -> 188.1 (305.0) MB, 25.1 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     1979 ms: Scavenge 194.7 (305.0) -> 194.4 (305.0) MB, 28.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2011 ms: Scavenge 204.0 (305.0) -> 203.6 (305.0) MB, 23.4 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2041 ms: Scavenge 210.2 (305.0) -> 209.9 (305.0) MB, 23.8 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2074 ms: Scavenge 219.4 (305.0) -> 219.0 (305.0) MB, 24.5 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2105 ms: Scavenge 225.8 (305.0) -> 225.4 (305.0) MB, 24.7 / 0.0 ms  allocation failure
[20872:0x7f26f24c70d0]     2138 ms: Scavenge 234.8 (305.0) -> 234.4 (305.0) MB, 23.1 / 0.0 ms  allocation failure
[...SNIP...]
$

```

因此这里关键的发现是直接在旧空间中分配伸展树节点可基本避免在周围复制对象的所有开销，并且将次要 GC 周期的数量减少到最小（从而减少 GC 引起的停顿时间）。我们想出了一种称为<ruby> <a href="https://research.google.com/pubs/pub43823.html">  分配场所预占 </a> <rt>  allocation site pretenuring </rt></ruby>的机制，当运行到基线代码时，将尝试动态收集分配场所的反馈，以决定在此分配的对象的确切部分是否存在，如果是，则优化代码以直接在旧空间分配对象——即预占对象。

```
$ out/Release/d8 --trace-gc octane-splay.js
[20885:0x7ff4d7c220a0]        8 ms: Scavenge 2.7 (6.0) -> 2.6 (7.0) MB, 1.2 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       10 ms: Scavenge 2.7 (7.0) -> 2.7 (8.0) MB, 1.6 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       11 ms: Scavenge 3.6 (8.0) -> 3.6 (10.0) MB, 0.9 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       17 ms: Scavenge 4.8 (10.5) -> 4.7 (11.0) MB, 2.9 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       20 ms: Scavenge 5.6 (11.0) -> 5.6 (16.0) MB, 2.8 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       26 ms: Scavenge 8.7 (16.0) -> 8.6 (17.0) MB, 4.5 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       34 ms: Scavenge 9.6 (17.0) -> 9.5 (28.0) MB, 6.8 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       48 ms: Scavenge 16.6 (28.5) -> 16.4 (29.0) MB, 8.6 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       64 ms: Scavenge 17.5 (29.0) -> 17.5 (52.0) MB, 15.2 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]       96 ms: Scavenge 32.3 (52.5) -> 32.0 (53.5) MB, 19.6 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]      153 ms: Scavenge 61.3 (81.5) -> 57.4 (93.5) MB, 27.9 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]      432 ms: Scavenge 339.3 (364.5) -> 326.6 (364.5) MB, 12.7 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]      666 ms: Scavenge 563.7 (592.5) -> 553.3 (595.5) MB, 20.5 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]      825 ms: Mark-sweep 603.9 (644.0) -> 96.0 (528.0) MB, 4.0 / 0.0 ms  (+ 92.5 ms in 51 steps since start of marking, biggest step 4.6 ms, walltime since start of marking 160 ms) finalize incremental marking via stack guard GC in old space requested
[20885:0x7ff4d7c220a0]     1068 ms: Scavenge 374.8 (528.0) -> 362.6 (528.0) MB, 19.1 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]     1304 ms: Mark-sweep 460.1 (528.0) -> 102.5 (444.5) MB, 10.3 / 0.0 ms  (+ 117.1 ms in 59 steps since start of marking, biggest step 7.3 ms, walltime since start of marking 200 ms) finalize incremental marking via stack guard GC in old space requested
[20885:0x7ff4d7c220a0]     1587 ms: Scavenge 374.2 (444.5) -> 361.6 (444.5) MB, 13.6 / 0.0 ms  allocation failure
[20885:0x7ff4d7c220a0]     1828 ms: Mark-sweep 485.2 (520.0) -> 101.5 (519.5) MB, 3.4 / 0.0 ms  (+ 102.8 ms in 58 steps since start of marking, biggest step 4.5 ms, walltime since start of marking 183 ms) finalize incremental marking via stack guard GC in old space requested
[20885:0x7ff4d7c220a0]     2028 ms: Scavenge 371.4 (519.5) -> 358.5 (519.5) MB, 12.1 / 0.0 ms  allocation failure
[...SNIP...]
$

```

事实上，这完全解决了 `SplayLatency` 基准的问题，并提高我们的得分至超过 250%！

[![SplayLatency 基准](/data/attachment/album/201709/29/154237maauq4haag2t34qk.png)](https://arewefastyet.com/#machine=12&view=single&suite=octane&subtest=SplayLatency&start=1384889558&end=1415405874)

正如 [SIGPLAN 论文](https://research.google.com/pubs/pub43823.html) 中所提及的，我们有充分的理由相信，分配场所预占机制可能真的赢得了真实世界应用的欢心，并真正期待看到改进和扩展后的机制，那时将不仅仅是对象和数组字面量。但是不久后我们意识到[分配场所预占机制对真实世界应用产生了相当严重的负面影响](https://bugs.chromium.org/p/v8/issues/detail?id=3665)。我们实际上听到很多负面报道，包括 `Ember.js` 开发者和用户的唇枪舌战，虽然不仅是因为分配场所预占机制，不过它是事故的罪魁祸首。

分配场所预占机制的基本问题数之不尽，这在今天的应用中非常常见（主要是由于框架，同时还有其它原因），假设你的对象工厂最初是用于创建构成你的对象模型和视图的长周期对象的，它将你的工厂方法中的分配场所转换为永久状态，并且从工厂分配的所有内容都立即转到旧空间。现在初始设置完成后，你的应用开始工作，作为其中的一部分，从工厂分配临时对象会污染旧空间，最终导致开销昂贵的垃圾回收周期以及其它负面的副作用，例如过早触发增量标记。

我们开始重新考虑基准驱动的工作，并开始寻找现实场景驱动的替代方案，这导致了 [Orinoco](http://v8project.blogspot.de/2016/04/jank-busters-part-two-orinoco.html) 的诞生，它的目标是逐步改进垃圾回收器；这个努力的一部分是一个称为“<ruby> 统一堆 <rt>  unified heap </rt></ruby>”的项目，如果页面中所有内容基本都存在，它将尝试避免复制对象。也就是说站在更高的层面看：如果新空间充满活动对象，只需将所有新空间页面标记为属于旧空间，然后从空白页面创建一个新空间。这可能不会在 `SplayLatency` 基准测试中得到相同的分数，但是这对于真实用例更友好，它可以自动适配具体的用例。我们还考虑<ruby> 并发标记 <rt>  concurrent marking </rt></ruby>，将标记工作卸载到单独的线程，从而进一步减少增量标记对延迟和吞吐量的负面影响。

#### 轻松一刻

![](/data/attachment/album/201709/29/154437hiyrrrqmqaq9q6kq.jpeg)

喘口气。

好吧，我想这足以强调我的观点了。我可以继续指出更多的例子，其中 Octane 驱动的改进后来变成了一个坏主意，也许改天我会接着写下去。但是今天就到此为止了吧。

### 结论

我希望现在应该清楚为什么基准测试通常是一个好主意，但是只对某个特定的级别有用，一旦你跨越了<ruby> 有用竞争 <rt>  useful competition </rt></ruby>的界限，你就会开始浪费你们工程师的时间，甚至开始损害到你的真实世界的性能！如果我们认真考虑 web 的性能，我们需要根据真实世界的性能来测评浏览器，而不是它们玩弄一个四年前的基准的能力。我们需要开始教育（技术）媒体，可能这没用，但至少请忽略他们。

[![2016 年 10 月浏览器基准之战: Chrome、Firefox 和 Edge 的决战](/data/attachment/album/201709/29/154245agzftkgmks1lfl8q.png)](http://venturebeat.com/2016/10/25/browser-benchmark-battle-october-2016-chrome-vs-firefox-vs-edge/3/)

没人害怕竞争，但是玩弄可能已经坏掉的基准不像是在合理使用工程时间。我们可以尽更大的努力，并把 JavaScript 提高到更高的水平。让我们开展有意义的性能测试，以便为最终用户和开发者带来有意思的领域竞争。此外，让我们再对运行在 Node.js（ V8 或 `ChakraCore`）中的服务器端和工具端代码做一些有意义的改进！

![](/data/attachment/album/201709/29/154246gl9bifz0leieviii.jpg)

结束语：不要用传统的 JavaScript 基准来比较手机。这是真正最没用的事情，因为 JavaScript 的性能通常取决于软件，而不一定是硬件，并且 Chrome 每 6 周发布一个新版本，所以你在三月份的测试结果到了四月份就已经毫不相关了。如果为手机中的浏览器做个排名不可避免，那么至少请使用一个现代健全的浏览器基准来测试，至少这个基准要知道人们会用浏览器来干什么，比如 [Speedometer 基准](http://browserbench.org/Speedometer)。

感谢你花时间阅读！

---

作者简介：

我是 Benedikt Meurer，住在 Ottobrunn（德国巴伐利亚州慕尼黑东南部的一个市镇）的一名软件工程师。我于 2007 年在锡根大学获得应用计算机科学与电气工程的文凭，打那以后的 5 年里我在编译器和软件分析领域担任研究员（2007 至 2008 年间还研究过微系统设计）。2013 年我加入了谷歌的慕尼黑办公室，我的工作目标主要是 V8 JavaScript 引擎，目前是 JavaScript 执行性能优化团队的一名技术领导。

---

via: <http://benediktmeurer.de/2016/12/16/the-truth-about-traditional-javascript-benchmarks>

作者：[Benedikt Meurer](http://benediktmeurer.de/) 译者：[OneNewLife](https://github.com/OneNewLife) 校对：[OneNewLife](https://github.com/OneNewLife), [wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


从 Node 到 Go：一个粗略的比较

看不出来有什么意义的比较，Go的起源就是为解决服务器并发，借鉴了C/C++和JAVA的特性，天生的优势，不过话说回来，用GO怎么写GUI的问题还没解决

![](/data/attachment/album/201709/30/210213z2e6jblskrsueuzr.jpg)

在 XO 公司，我们最初使用 Node 和 Ruby 构建相互连接的服务系统。我们享受 Node 带来的明显性能优势，以及可以访问已有的大型软件包仓库。我们也可以轻松地在公司内部发布并复用已有的插件和模块。极大地提高了开发效率，使得我们可以快速编写出可拓展的和可靠的应用。而且，庞大的 Node 社区使我们的工程师向开源软件贡献更加容易（比如 [BunnyBus](https://medium.com/xo-tech/bunnybus-building-a-data-transit-system-b9647f6283e5#.l64fdvfys) 和 [Felicity](https://medium.com/xo-tech/introducing-felicity-7b6d0b734ce#.hmloiiyx8)）。

虽然我在大学时期和刚刚工作的一些时间在使用更严谨的编译语言，比如 C++ 和 C#，而后来我开始使用 JavaScript。我很喜欢它的自由和灵活，但是我最近开始怀念静态和结构化的语言，因为当时有一个同事让我对 Go 语言产生了兴趣。

我从写 JavaScript 到写 Go，我发现两种语言有很多相似之处。两者学习起来都很快并且易于上手，都具有充满表现力的语法，并且在开发者社区中都有很多工作机会。没有完美的编程语言，所以你应该总是选择一个适合手头项目的语言。在这篇文章中，我将要说明这两种语言深层次上的关键区别，希望能鼓励没有用过 Go 语言的用户~~可以~~有机会使用 Go 。

### 大体上的差异

在深入细节之前，我们应该先了解一下两种语言之间的重要区别。

Go，或称 Golang，是 Google 在 2007 年创建的自由开源编程语言。它以快速和简单为设计目标。Go 被直接编译成机器码，这就是它速度的来源。使用编译语言调试是相当容易的，因为你可以在早期捕获大量错误。Go 也是一种强类型的语言，它有助于数据完整，并可以在编译时查找类型错误。

另一方面，JavaScript 是一种弱类型语言。除了忽略验证数据的类型和真值判断陷阱所带来的额外负担之外，使用弱类型语言也有自己的好处。比起使用<ruby> 接口 <rt>  interfaces </rt></ruby>和<ruby> 范型 <rt>  generics </rt></ruby>，<ruby> 柯里化 <rt>  currying </rt></ruby>和<ruby> 可变的形参个数 <rt>  flexible arity </rt></ruby>让函数变得更加灵活。JavaScript 在运行时进行解释，这可能导致错误处理和调试的问题。Node 是一款基于 Google V8 虚拟机的 JavaScript 运行库，这使它成为一个轻量和快速的 Web 开发平台。

### 语法

作为原来的 JavaScript 开发者，Go 简单和直观的语法很吸引我。由于两种语言的语法可以说都是从 C 语言演变而来的，所以它们的语法有很多相同之处。Go 被普遍认为是一种“容易学习的语言”。那是因为它的对开发者友好的工具、精简的语法和固守惯例（LCTT 译注：惯例优先）。

Go 包含大量有助于简化开发的内置特性。你可以用标准 Go 构建工具把你的程序用 `go build` 命令编译成二进制可执行文件。使用内置的测试套件进行测试只需要运行 `go test` 即可。 诸如原生支持的并发等特性甚至在语言层面上提供。

[Google 的 Go 开发者](https://golang.org/doc/faq)认为，现在的编程太复杂了，太多的“记账一样，重复劳动和文书工作”。这就是为什么 Go 的语法被设计得如此简单和干净，以减少混乱、提高效率和增强可读性。它还鼓励开发人员编写明确的、易于理解的代码。Go 只有 [25 个保留关键字](https://golang.org/ref/spec#Keywords)和一种循环（`for` 循环），而不像 JavaScript 有  [大约 84 个关键字](https://www.w3schools.com/js/js_reserved.asp)（包括保留关键字字、对象、属性和方法）。

为了说明语法的一些差异和相似之处，我们来看几个例子：

* 标点符号： Go 去除了所有多余的符号以提高效率和可读性。尽管 JavaScript 中需要符号的地方也不多（参见： [Lisp](https://en.wikipedia.org/wiki/Lisp_%28programming_language%29)），而且经常是可选的，但我更加喜欢 Go 的简单。

```
// JavaScript 的逗号和分号
for (var i = 0; i < 10; i++) {
console.log(i);
}

```

*JavaScript 中的标点*

```
// Go 使用最少数量标点
for i := 0; i < 10; i++ {
fmt.Println(i)
}

```

*Go 中的标点*
* 赋值：由于 Go 是强类型语言，所以你在初始化变量时可以使用 `:=` 操作符来进行类型推断，以避免[重复声明](https://golang.org/doc/faq#principles)，而 JavaScript 则在运行时声明类型。

```
// JavaScript 赋值
var foo = "bar";

```

*JavaScript 中的赋值*

```
// Go 的赋值
var foo string //不使用类型推导
foo = "bar"
foo := "bar" //使用类型推导

```

*Go 的赋值*
* 导出：在 JavaScript 中，你必须从某个模块中显式地导出。 在 Go 中，任何大写的函数将被默认导出。

```
const Bar = () => {};
module.exports = {
Bar
}

```

*JavaScript 中的导出*

```
// Go 中的导出
package foo // 定义包名
func Bar (s string) string {
// Bar 将被导出
}

```

*Go 中的导出*
* 导入：在 JavaScript 中 `required` 库是导入依赖项和模块所必需的，而 Go 则利用原生的 `import` 关键字通过包的路径导入模块。另一个区别是，与 Node 的中央 NPM 存储库不同，Go 使用 URL 作为路径来导入非标准库的包，这是为了从包的源码仓库直接克隆依赖。

```
// Javascript 的导入
var foo = require('foo');
foo.bar();

```

*JavaScript 的导入*

```
// Go 的导入
import (
"fmt" // Go 的标准库部分
"github.com/foo/foo" // 直接从仓库导入
)
foo.Bar()

```

*Go 的导入*
* 返回值：通过 Go 的多值返回特性可以优雅地传递和处理返回值和错误，并且通过传递引用减少了不正确的值传递。在 JavaScript 中需要通过一个对象或者数组来返回多个值。

```
// Javascript - 返回多值
function foo() {
return {a: 1, b: 2};
}
const { a, b } = foo();

```

*JavaScript 的返回*

```
// Go - 返回多值
func foo() (int, int) {
return 1, 2
}
a, b := foo()

```

*Go 的返回*
* 错误处理：Go 推荐在错误出现的地方捕获它们，而不是像 Node 一样在回调中让错误冒泡。

```
// Node 的错误处理
foo('bar', function(err, data) {
// 处理错误
}

```

*JavaScript 的错误处理*

```
//Go 的错误处理
foo, err := bar()
if err != nil {
// 用 defer、 panic、 recover 或 log.fatal 等等处理错误.
}

```

*Go 的错误处理*
* 可变参数函数：Go 和 JavaScript 的函数都支持传入不定数量的参数。

```
function foo (...args) {
console.log(args.length);
}
foo(); // 0
foo(1, 2, 3); // 3

```

*JavaScript 中的可变参数函数*

```
func foo (args ...int) {
fmt.Println(len(args))
}
func main() {
foo() // 0
foo(1,2,3) // 3
}

```

*Go 中的可变参数函数*

### 社区

当比较 Go 和 Node 提供的编程范式哪种更方便时，两边都有不同的拥护者。Node 在软件包数量和社区的大小上完全胜过了 Go。Node 包管理器（NPM），是世界上最大的软件仓库，拥有[超过 410,000 个软件包，每天以 555 个新软件包的惊人速度增长](http://www.modulecounts.com/)。这个数字可能看起来令人吃惊（确实是），但是需要注意的是，这些包许多是重复的，且质量不足以用在生产环境。 相比之下，Go 大约有 13 万个包。

![](/data/attachment/album/201709/30/210334hgodtlx7qeucdets.png)

*Node 和 Go 包的数量*

尽管 Node 和 Go 岁数相仿，但 JavaScript 使用更加广泛，并拥有巨大的开发者和开源社区。因为 Node 是为所有人开发的，并在开始的时候就带有一个强壮的包管理器，而 Go 是特地为 Google 开发的。下面的[Spectrum 排行榜](http://spectrum.ieee.org/static/interactive-the-top-programming-languages-2016)显示了当前流行的的顶尖 Web 开发语言。

![](/data/attachment/album/201709/30/210337xnfkc0jgzfr22x10.png)

*Web 开发语言排行榜前 7 名*

JavaScript 的受欢迎程度近年来似乎保持相对稳定，而 [Go 一直在保持上升趋势](http://www.tiobe.com/tiobe-index/)。

![](/data/attachment/album/201709/30/210338ewom782b98my9bn6.png)

*编程语言趋势*

### 性能

如果你的主要关注点是速度呢？当今似乎人们比以前更重视性能的优化。用户不喜欢等待信息。 事实上，如果网页的加载时间超过 3 秒，[40％ 的用户会放弃访问您的网站](https://hostingfacts.com/internet-facts-stats-2016/)。

因为它的非阻塞异步 I/O，Node 经常被认为是高性能的语言。另外，正如我之前提到的，Node 运行在针对动态语言进行了优化的 Google V8 引擎上。而 Go 的设计也考虑到速度。[Google 的开发者们](https://golang.org/doc/faq)通过建立了一个“充满表现力而轻量级的类型系统；并发和垃圾回收机制；强制地指定依赖版本等等”，达成了这一目标。

我运行了一些测试来比较 Node 和 Go 之间的性能。这些测试注重于语言提供的初级能力。如果我准备测试例如 HTTP 请求或者 CPU 密集型运算，我会使用 Go 语言级别的并发工具（goroutines/channels）。但是我更注重于各个语言提供的基本特性（参见 [三种并发方法](https://medium.com/xo-tech/concurrency-in-three-flavors-51ed709876fb#.khvqrttxa) 了解关于 goroutines 和 channels 的更多知识）。

我在基准测试中也加入了 Python，所以无论如何我们对 Node 和 Go 的结果都很满意。

#### 循环/算术

迭代十亿项并把它们相加：

```
var r = 0;
for (var c = 0; c < 1000000000; c++) {
    r += c;
}

```

*Node*

```
package main
func main() {
    var r int
    for c := 0; c < 1000000000; c++ {
        r += c
    }
}

```

*Go*

```
sum(xrange(1000000000))

```

*Python*

![](/data/attachment/album/201709/30/210341c4fwxvoffmk1a2et.png)

*结果*

这里的输家无疑是 Python，花了超过 7 秒的 CPU 时间。而 Node 和 Go 都相当高效，分别用了 900 ms 和 408 ms。

*修正：由于一些评论表明 Python 的性能还可以提高。我更新了结果来反映这些变化。同时，使用 PyPy 大大地提高了性能。当使用 Python 3.6.1 和 PyPy 3.5.7 运行时，性能提升到 1.234 秒，但仍然不及 Go 和 Node 。*

#### I/O

遍历一百万个数字并将其写入一个文件。

```
var fs = require('fs');
var wstream = fs.createWriteStream('node');

for (var c = 0; c < 1000000; ++c) {
  wstream.write(c.toString());
}
wstream.end();

```

*Node*

```
package main

import (
    "bufio"
    "os"
    "strconv"
)

func main() {
    file, _ := os.Create("go")
    b := bufio.NewWriter(file)
    for c := 0; c < 1000000; c++ {
        num := strconv.Itoa(c)
        b.WriteString(num)
    }
    file.Close()
}

```

*Go*

```
with open("python", "a") as text_file:
    for i in range(1000000):
        text_file.write(str(i))

```

*Python*

![](/data/attachment/album/201709/30/210344vlr73w3s6kyllh3r.png)

*结果*

Python 以 7.82 秒再次排名第三。 这次测试中，Node 和 Go 之间的差距很大，Node 花费大约 1.172 秒，Go 花费了 213 毫秒。真正令人印象深刻的是，Go 大部分的处理时间花费在编译上。如果我们将代码编译，以二进制运行，这个 I/O 测试仅花费 78 毫秒——要比 Node 快 15 倍。

*修正：修改了 Go 代码以实现缓存 I/O。*

#### 冒泡排序

将含有十个元素的数组排序一千万次。

```
function bubbleSort(input) {
    var n = input.length;
    var swapped = true;
    while (swapped) {
        swapped = false;
        for (var i = 0; i < n; i++) {
            if (input[i - 1] > input [i]) {
                [input[i], input[i - 1]] = [input[i - 1], input[i]];
                swapped = true;
            }
        }
    }
}

for (var c = 0; c < 1000000; c++) {
    const toBeSorted = [1, 3, 2, 4, 8, 6, 7, 2, 3, 0];
    bubbleSort(toBeSorted);
}

```

*Node*

```
package main

var toBeSorted [10]int = [10]int{1, 3, 2, 4, 8, 6, 7, 2, 3, 0}

func bubbleSort(input [10]int) {
    n := len(input)
    swapped := true
    for swapped {
        swapped = false
        for i := 1; i < n; i++ {
            if input[i-1] > input[i] {
                input[i], input[i-1] = input[i-1], input[i]
                swapped = true
            }
        }
    }
}

func main() {
    for c := 0; c < 1000000; c++ {
        bubbleSort(toBeSorted)
    }
}

```

*Go*

```
def bubbleSort(input):
    length = len(input)
    swapped = True

    while swapped:
        swapped = False
        for i in range(1,length):
            if input[i - 1] > input[i]:
                input[i], input[i - 1] = input[i - 1], input[i]
                swapped = True

for i in range(1000000):
    toBeSorted = [1, 3, 2, 4, 8, 6, 7, 2, 3, 0]
    bubbleSort(toBeSorted)

```

*Python*

![](/data/attachment/album/201709/30/210348dhcqdd3mhdmywjmk.png)

*结果*

像刚才一样，Python 的表现是最差的，大约花费 15 秒完成了任务。 Go 完成任务的速度是 Node 的 16 倍。

#### 判决

Go 无疑是这三个测试中的赢家，而 Node 大部分表现都很出色。Python 也表现不错。要清楚，性能不是选择编程语言需要考虑的全部内容。如果您的应用不需要处理大量数据，那么 Node 和 Go 之间的性能差异可能是微不足道的。 有关性能的一些其他比较，请参阅以下内容：

* [Node Vs. Go](https://jaxbot.me/articles/node-vs-go-2014)
* [Multiple Language Performance Test](https://hashnode.com/post/comparison-nodejs-php-c-go-python-and-ruby-cio352ydg000ym253frmfnt70)
* [Benchmarks Game](https://benchmarksgame.alioth.debian.org/u64q/compare.php?lang=go&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lang2=node)

### 结论

这个帖子不是为了证明一种语言比另一种语言更好。由于各种原因，每种编程语言都在软件开发社区中占有一席之地。 我的意图是强调 Go 和 Node 之间的差异，并且促进展示一种新的 Web 开发语言。 在为一个项目选择语言时，有各种因素需要考虑，比如开发人员的熟悉程度、花费和实用性。 我鼓励在决定哪种语言适合您时进行一次彻底的底层分析。

正如我们所看到的，Go 有如下的优点：接近底层语言的性能，简单的语法和相对简单的学习曲线使它成为构建可拓展和安全的 Web 应用的理想选择。随着 Go 的使用率和社区活动的快速增长，它将会成为现代网络开发中的重要角色。话虽如此，我相信如果 Node 被正确地实现，它正在向正确的方向努力，仍然是一种强大而有用的语言。它具有大量的追随者和活跃的社区，使其成为一个简单的平台，可以让 Web 应用在任何时候启动和运行。

### 资料

如果你对学习 Go 语言感兴趣，可以参阅下面的资源：

* [Golang 网站](https://golang.org/doc/#learning)
* [Golang Wiki](https://github.com/golang/go/wiki/Learn)
* [Golang Subreddit](https://www.reddit.com/r/golang/)

---

via: <https://medium.com/xo-tech/from-node-to-go-a-high-level-comparison-56c8b717324a#.byltlz535>

作者：[John Stamatakos](https://medium.com/@johnstamatakos?source=post_header_lockup) 译者：[trnhoe](https://github.com/trnhoe) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


2700 万美金：阿里巴巴领投 MariaDB

![](/data/attachment/album/201710/01/232254qf58x5ufed4uz407.jpg)

阿里巴巴在 2017 年一直努力推进其云计算业务，而现在它将首次对该领域的一家西方初创公司做出重大投资。

据知情人士透露，这家中国的电子商务巨头同意对 MariaDB 领投 2290 万欧元（2700 万美元）。MariaDB 是一家欧洲公司，其开发维护的 mariadb 数据库是最受欢迎的开源数据库之一。该交易仍未完成，但在本周 MariaDB 股东批准之后有望很快完成。

阿里巴巴和 MariaDB 均未对此置评。

据悉，阿里巴巴本次将投资 2000 万欧元，而剩余部分将由 MariaDB 现有股东追加投资。该交易将使 MariaDB 的估值达到 3 亿欧元（约合 3.54 亿美元），而阿里巴巴的云业务部门的首席工程师余锋将加入该公司董事会。

今年五月份 MariaDB 从欧洲投资银行[募集到的 2500 万欧元（当时价值 2700 万美元）](https://techcrunch.com/2017/05/08/open-source-database-developer-mariadb-picks-up-27m-from-the-eib/)，当时估值越 2 亿至 2.5 亿美元，此次估值较之前次大为提高。据悉阿里巴巴非常希望完成该笔投资，因此几乎允许 MariaDB 对其出让的股份自行开价。

MariaDB 最常用作流行的数据库 MySQL 的替代品，二者都是开源产品，但有些人对 MySQL 心存疑虑，因为它现在属于 Oracle 公司。

### 关于阿里巴巴

阿里巴巴的云计算业务是其增长最快的业务单元，在去年其达到了三位数的营收增长。

[我们今年早些时候的文章提到过](https://techcrunch.com/2017/02/27/alibaba-aliyun-cloud-computing/)，它现在正在努力与该行业最大的竞争对手，如 AWS、微软 Azure 和谷歌云展开竞争。虽然这还有一些路要走，但迹象表明还是有希望的。阿里云业务在最近一个财年的营收达到了 10 亿美元，前一个季度超过了 100 万个用户。该业务最近几个季度将可能达到收支平衡，虽然其目前只占了阿里巴巴全部业务营收的不到 5%。

云是阿里巴巴正在重点发展领域之一，因为它看起来将是新的收入来源之一，这可以让它摆脱对其核心的中国电子商务业务的依赖——[尽管它目前仍然带来了大量利润](https://techcrunch.com/2017/08/17/alibaba-profit-doubles-to-2-1b/)。

该公司也在其它领域展开了大量投资，比如对印度的独角兽公司 [Paytm](https://techcrunch.com/2017/03/06/alibaba-paytm-amazon-india/) 和东南亚的 [Lazada](https://techcrunch.com/2017/06/28/alibaba-ups-its-stake-in-southeast-asias-lazada-with-1-billion-investment/) 和 [Tokopedia](https://techcrunch.com/2017/08/17/alibaba-tokopedia/) 的投资，所以对更大范围的云和基础架构领域进行投资、以推进其战略也并不为奇。

对中国同领域的公司，如[云存储厂商七牛](http://www.avcj.com/avcj/news/3006331/yunfeng-alibaba-invest-usd152m-in-chinese-cloud-storage-firm)和[大数据公司数梦工场](http://technode.com/2017/06/12/chinas-cloud-industry-moving-to-new-era-with-emergence-of-unicorns/)进行投资，以及[加入微软的开源社区](https://venturebeat.com/2017/04/03/alibaba-cloud-adopts-microsofts-open-source-networking-software/)，甚至[在中国直接和 MariaDB 进行合作](http://diginomica.com/2017/04/12/alibaba-on-open-source-and-cloud-business-in-china-live-from-the-mariadb-user-conference/)，这些都使阿里巴巴在海外市场备受关注。

### 关于 MariaDB

以前名为 SkySQL 的 MariaDB 由于其在该行业的地位而备受阿里巴巴关注。它在瑞典和美国设有办事处，其声称已经服务了全球 1200 万数据库用户，这其中包括一些著名公司，如 Booking.com、HP、 Virgin Mobile 和维基。其解决方案用在私有云、公有云和混合云部署中，它是包括 Red Hat、Ubuntu 和 SUSE 在内的大量 Linux 发行版的默认数据库软件，这会为其进一步增加 6000 万用户。

SkySQL 最初创立于 2009 年，并于 2013 年与 Monty Program Ab 合并，后者是 Michael ‘Monty’ Widenius 在 2008 年将其公司 MySQL 以 10 亿美金卖给了太阳微系统公司（现在属于 Oracle）后创立的。由于担心 Oracle 对 MySQL 的一些做法，MariaDB 数据库后来从 MySQL 数据库中分支而来。


编排工具充分发挥了 Linux 容器技术优势

>
> 一旦公司越过了“让我们看看这些容器如何工作”的阶段，他们最终会在许多不同的地方运行容器
>
>
>

![Man conducting orchestra](/data/attachment/album/201710/02/150503qcqfkhaaubkyu9q4.jpg)

需要快速、高效地交付程序的公司 —— 而今天，哪些公司不需要这样做？—— 是那些正在转向 Linux 容器的公司。他们还发现，一旦公司越过了“让我们看看这些容器如何工作”的阶段，他们最终会在许多不同的地方运行容器。

Linux 容器技术不是新技术，但它随着最初由 Docker 发明的创新性打包格式（现在的 [OCI](https://github.com/opencontainers/image-spec) 格式）以及新应用对持续开发和部署的需求开始变得流行。在 Red Hat 的 2016 年 5 月的 Forrester 研究中，有 48％ 的受访者表示已经在开发中使用容器，今年的数字预计将达到 53％。只有五分之一的受访者表示，他们在 2017 年不会在开发过程中利用容器。

像乐高积木一样，容器镜像可以轻松地重用代码和服务。每个容器镜像就像一个单独的、旨在做好一部分工作的乐高积木。它可能是数据库、数据存储、甚至预订服务或分析服务。通过单独包装每个组件，从而可以在不同的应用中使用。但是，如果没有某种程序定义（即<ruby> 指令手册 <rt>  instruction booklet </rt></ruby>），则难以在不同环境中创建完整应用程序的副本。那就是容器编排的来由。

![life container megabricks](/data/attachment/album/201710/02/150505qdglpt7uz8i84879.jpg)

容器编排提供了像乐高系统这样的基础设施 —— 开发人员可以提供如何构建应用程序的简单说明。编排引擎将知道如何运行它。这使得可以轻松创建同一应用程序的多个副本，跨越开发人员电脑、CI/CD 系统，甚至生产数据中心和云提供商环境。

Linux 容器镜像允许公司在整个运行时环境（操作系统部件）中打包和隔离应用程序的构建块。在此基础上，通过容器编排，可以很容易地定义并运行所有的块，并一起构成完整的应用程序。一旦定义了完整的应用程序，它们就可以在不同的环境（开发、测试、生产等）之间移动，而不会破坏它们，且不改变它们的行为。

### 仔细调查容器

很明显，容器是有意义的，越来越多的公司像“对轮胎踹两脚”一样去研究容器。一开始，可能是一个开发人员使用一个容器工作，或是一组开发人员在使用多个容器。在后一种情况下，开发人员可能会随手编写一些代码来处理在容器部署超出单个实例之后快速出现的复杂性。

这一切都很好，毕竟他们是开发人员 —— 他们已经做到了。但即使在开发人员世界也会变得混乱，而且随手代码模式也没法跟着容器进入 QA 和生产环境下。

编排工具基本上做了两件事。首先，它们帮助开发人员定义他们的应用程序的表现 —— 一组用来构建应用程序实例的服务 —— 数据库、数据存储、Web 服务等。编排器帮助标准化应用程序的所有部分，在一起运行并彼此通信，我将这称之为标准化程序定义。其次，它们管理一个计算资源集群中启动、停止、升级和运行多个容器的过程，这在运行任何给定应用程序的多个副本时特别有用，例如持续集成 （CI） 和连续交付 （CD）。

想像一个公寓楼。居住在那里的每个人都有相同的街道地址，但每个人都有一个数字或字母或两者的组合，专门用来识别他或她。这是必要的，就像将正确的邮件和包裹交付给合适的租户一样。

同样，在容器中，只要你有两个容器或两个要运行这些容器的主机，你必须跟踪开发人员测试数据库连接或用户连接到正在运行的服务的位置。容器编排工具实质上有助于管理跨多个主机的容器的后勤。它们将生命周期管理功能扩展到由多个容器组成的完整应用程序，部署在一组机器上，从而允许用户将整个集群视为单个部署目标。

这真的很简单，又很复杂。编排工具提供了许多功能，从配置容器到识别和重新调度故障容器，将容器暴露给集群外的系统和服务，根据需要添加和删除容器等等。

虽然容器技术已经存在了一段时间，但容器编排工具只出现了几年。编排工具是 Google 从内部的高性能计算（HPC）和应用程序管理中吸取的经验教训开发的。在本质上，其要解决的就是在一堆服务器上运行一堆东西（批处理作业、服务等）。从那时起，编排工具已经进化到可以使公司能够战略性地利用容器。

一旦你的公司确定需要容器编排，下一步就是确定哪个平台对于业务是最有意义的。在评估容器编排时，请仔细查看（尤其）：

* 应用程序定义语言
* 现有能力集
* 添加新功能的速度
* 开源还是专有
* 社区健康度（成员的积极性/高效，成员提交的质量/数量，贡献者的个人和公司的多样性）
* 强化努力
* 参考架构
* 认证
* 产品化过程

有三个主要的容器编排平台，它们似乎领先于其他，每个都有自己的历史。

1. **Docker Swarm：** Swarm 是容器典范 Docker 的附件。Swarm 允许用户建立并管理 Docker 节点的集群为单个虚拟系统。Swarm 似乎正在成为一个单一供应商的项目。
2. **Mesos：** Mesos 是从 Apache 和高性能计算中成长起来的，因此是一个优秀的调度员。Mesos 的技术也非常先进，虽然与其他相比似乎没有发展速度或投资优势。
3. **Kubernetes：** 由 Google 开发，由其内部编排工具 Borg 经验而来，Kubernetes 被广泛使用，并拥有强大的社区。其实这是 GitHub 上排名第一的项目。Mesos 目前可能比 Kubernetes 略有技术优势，但是 Kubernetes 是一个快速发展的项目，这也是为了长期技术上的收益而进行的架构投资。在不久的将来，在技术能力上应该能赶超 Mesos。

### 编排的未来

展望未来，企业们可以期待看到编排工具在应用程序和服务为中心的方向上发展。因为在现实中，如今快速应用程序开发实际上是在快速地利用服务、代码和数据的组合。无论这些服务是开源的，还是由内部团队部署的抑或从云提供商处购买的，未来将会是两者的混合。由于今天的编排器也在处理应用程序定义方面的挑战，所以期望看到它们越来越多地应对外部服务的整合。

此时此刻，想要充分利用容器的公司必须利用容器编排。

（题图：Thinkstock）

---

via: <https://www.infoworld.com/article/3205304/containers/orchestration-tools-enable-companies-to-fully-exploit-linux-container-technology.html>

作者：[Scott McCarty](https://www.infoworld.com/author/Scott-McCarty/) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


LFCS 基础：sed 命令

Summary: “sed” 命令是一个可以将文件作为流进行编辑的编辑器。流式传输文件的方法是从另一个命令使用管道（> 或 |）传递，或将其直接加载到 “sed” 中。

![](/data/attachment/album/201710/02/155653hvvvvdgfgzfv7isg.jpg)

Linux 基金会认证系统管理员（LFCS）的另一个有用的命令是 “sed”，最初表示<ruby> “流式编辑器” <rp>  （ </rp> <rt>  Streaming EDitor </rt> <rp>  ） </rp></ruby>。

“sed” 命令是一个可以将文件作为流进行编辑的编辑器。流式传输文件的方法是从另一个命令使用管道（`>` 或 `|`）传递，或将其直接加载到 “sed” 中。

该命令的工作方式与其他编辑器相同，只是文件不显示，也不允许可视化编辑。命令被传递给 “sed” 来操纵流。

用 “sed” 可以做五件基本的事。当然，“sed” 如此强大，还有其他高级的功能，但你只需要集中精力在五件基本的事上。五种功能类型如下：

1. 搜索
2. 替换
3. 删除
4. 添加
5. 改变/变换

在深入命令参数之前，我们需要看看基本的语法。

### 语法

“sed” 命令的语法是：

```
sed [选项] 命令 [要编辑的文件]

```

本文将在适当的部分中介绍这些“选项”。“命令”可以是正则表达式的搜索和替换模式。请继续阅读了解 “sed” 如何工作的并学习基本命令。正如我之前提到的，“sed” 是一个非常强大的工具，有更多的选项可用，我将在本文中介绍。

### 示例文件

如果你打开一个终端，那你可以创建一个用于 “sed” 示例的文件。执行以下命令：

```
cd ~
grep --help >grephelp.txt

```

你现在应该在 HOME 文件夹中有一个名为 `grephelp.txt` 的文件。该文件的内容是 `grep` 命令的帮助说明。

### 搜索

搜索特定字符串是编辑器的常见功能，在 “sed” 中执行搜索也不例外。

执行搜索以在文件中查找字符串。我们来看一下基本的搜索。

如果我们想在示例文件搜索 `PATTERN` 这个词，我们将使用如下命令：

```
sed -n 's/PATTERN/PATTERN/p' grephelp.txt

```

**注意：** 如果剪切粘贴命令，请确保将单引号替换为键盘上的标准单引号。

参数 `-n` 用于抑制每行的自动打印（除了用 `p` 命令指定的行）。默认情况下，流入 “sed” 的每一行将被打印到标准输出（stdout）。如果你不使用 “-n” 选项运行上述命令，你将看到原始文件的每一行以及匹配的行。

要搜索的文件名是我们在“示例文件”部分中创建的 “grephelp.txt”。

剩下的部分是 `'s/PATTERN/PATTERN/p'` 。这一段基本分为四个部分。第一部分的 `s` 指定执行替换，或搜索并替换。

剩下的第二部分和第三部分是模式。第一个是要搜索的模式，最后一个是替换流中匹配字符串的模式。此例中，我们找到字符串 `PATTERN`，并用 `PATTERN` 替换。通过查找和替换相同的字符串，我们完全不会更改文件，甚至在屏幕上也一样。

最后一个命令是 `p`。 它指定在替换后打印新行。当然，因为替换的是相同的字符串，所以没有改变。由于我们使用 `-n` 参数抑制打印行，所以更改的行将使用 `p` 命令打印。

这个完整的命令允许我们执行搜索并查看匹配的结果。

### 替换

当搜索特定字符串时，你可能希望用匹配的字符串替换新字符串。用另一个字符串替换是很常见的操作。

我们可以使用以下命令执行相同的搜索：

```
sed -n 's/PATTERN/Pattern/p' grephelp.txt

```

在这时，字符串 “PATTERN” 变为 “Pattern” 并显示。如果你使用命令 `cat grephelp.txt` 查看文件，你会看到该文件没有更改。该更改仅对屏幕上的输出进行。你可以使用以下命令将输出通过管道传输到另一个文件：

```
sed 's/PATTERN/Pattern/' grephelp.txt > grephelp1.txt

```

现在将存在一个名为 `grephelp1.txt` 的新文件，其中保存了更改的文件。如果 `p` 作为第四个选项留下，那么有个问题是被替换的字符串的每一行将在文件中重复两次。我们也可以删除 “-n” 参数以允许所有的行打印。

使用相同字符串替换字符串的另一种方法是使用 `&` 符号来表示搜索字符串。例如，命令 `s/PATTERN/&/p` 效果是一样的。我们可以添加字符串，例如添加 `S`，可以使用命令 `s/PATTERN/&S/p`。

如果我们希望在每一行中只替换某种模式呢？可以指定要替换的匹配项的特定出现。当然，每一行的替换都是一个特定的编号。例如，示例文件上有很多破折号。一些行至少有两条破折号，所以我们可以用另一个字符代替每一行的第二个破折号。每行用星号 `*` 替换第二个破折号 `-` 的命令将是：

```
sed 's/-/*/2' grephelp.txt

```

在这里，我们用最初的 `s` 来执行替换。字符 `-` 被替换为 `*`。`2` 表示我们想要替换每行上的第二个 `-`（如果存在）。如果我们忽略了命令 `2`，则替换第一次出现的破折号。只有第一个破折号而不是每行的破折号都被替换。

如果要搜索并替换带有星号的行上的所有破折号，请使用 `g` 命令：

```
sed 's/-/*/g' grephelp.txt

```

命令也可以组合。假设你想要替换从第二次开始出现的破折号，命令将是：

```
sed 's/-/*/2g' grephelp.txt

```

现在从第二个开始出现的破折号将被星号取代。

### 删除

搜索过程中有很多时候你可能想要完全删除搜索字符串。

例如，如果要从文件中删除所有破折号，你可以使用以下命令：

```
sed 's/-//g' grephelp.txt

```

替换字符串为空白，因此匹配的字符串将被删除。

### 添加

当找到匹配时，你可以添加一行特定的文本，来使这行在浏览或打印中突出。

如果要在匹配后插入新行，那么使用 `a` 命令，后面跟上新行的字符串。还包括要匹配的字符串。例如，我们可以找到一个 `--`，并在匹配的行之后添加一行。新行的字符串将是 `double dash before this line`。

```
sed '/--/ a "double dash before this line"' grephelp.txt

```

如果要在包含匹配字符串的行之前加上这行，请使用 `i` 命令，如下所示：

```
sed '/--/ i "double dash after this line"' grephelp.txt

```

### 改变/变换

如果需要改变/变换一行，则可以使用命令 `c`。

假设我们有个有一些私人信息的文档，我们需要更改包含特定字符串的行。`c` 命令将改变整行，而不仅仅是搜索字符串。

假设我们想要阻止示例文件中包含单词 `PATTERN` 的每一行。更改的行将显示为 `This line is Top Secret`。命令是：

```
sed '/PATTERN/ c This line is Top Secret' grephelp.txt

```

可以进行更改特定字母的大小写的转换。例如，我们可以使用命令 `y` 将所有小写 `a` 更改为大写 `A`，如下所示：

```
sed 'y/a/A/' grephelp.txt

```

可以指定多个字母，如 `abdg`，如下命令所示：

```
sed 'y/abdg/ABDG/' grephelp.txt

```

确保第二组字母与第一组字母的顺序相同，否则会被替换和转换。例如，字符串 `y/a/D/` 将用大写 `D` 替换所有小写的 `a`。

### 就地更改

如果你确实要更改所使用的文件，请使用 `-i` 选项。

例如，要将 `PATTERN` 改为 `Pattern`，并对文件进行更改，则命令为：

```
sed -i 's/PATTERN/Pattern/' grephelp.txt

```

现在文件 `grephelp.txt` 将被更改。`-i` 选项可以与上述任何命令一起使用来更改原始文件的内容。

练习这些命令，并确保你理解它们。“sed” 命令非常强大。

（题图：Pixabay，CC0）

---

via: <https://www.linux.org/threads/lfcs-sed-command.4561/>

作者：[Jarret B](https://www.linux.org/threads/lfcs-sed-command.4561/) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


GitHub 的 MySQL 基础架构自动化测试

![](/data/attachment/album/201710/03/120948wz4d0xvwq4w0vwxi.jpg)

我们 MySQL 数据库基础架构是 Github 关键组件。 MySQL 提供 Github.com、 GitHub 的 API 和验证等等的服务。每一次的 `git` 请求都以某种方式触及 MySQL。我们的任务是保持数据的可用性，并保持其完整性。即使我们 MySQL 集群是按流量分配的，但是我们还是需要执行深度清理、即时更新、在线<ruby> 模式 <rt>  schema </rt></ruby>迁移、集群拓扑重构、<ruby> 连接池化 <rt>  pooling </rt></ruby>和负载平衡等任务。 我们建有基础架构来自动化测试这些操作，在这篇文章中，我们将分享几个例子，来说明我们是如何通过持续测试打造我们的基础架构的。这是让我们一梦到天亮的根本保障。

### 备份

没有比备份数据更重要的了，如果您没有备份数据库，在它出事前这可能并不是什么问题。Percona 的 [Xtrabackup](https://www.percona.com/software/mysql-database/percona-xtrabackup) 是我们一直用来完整备份 MySQL 数据库的工具。如果有专门需要备份的数据，我们就会备份到另一个专门备份数据的服务器上。

除了完整的二进制备份外，我们每天还会多次运行逻辑备份。这些备份数据可以让我们的工程师获取到最新的数据副本。有时候，他们希望从表中获取一整套数据，以便他们可以在一个生产级规模的表上测试索引的修改，或查看特定时间以来的数据。Hubot 可以让我们恢复备份的表，并且当表准备好使用时会通知我们。

**tomkrouper**

```
.mysql backup-list locations

```

**Hubot**

```
+-----------+------------+---------------+---------------------+---------------------+----------------------------------------------+
| Backup ID | Table Name | Donor Host    | Backup Start        | Backup End          | File Name                                    |
+-----------+------------+---------------+---------------------+---------------------+----------------------------------------------+
|   1699494 | locations  | db-mysql-0903 | 2017-07-01 22:09:17 | 2017-07-01 22:09:17 | backup-mycluster-locations-1498593122.sql.gz |
|   1699133 | locations  | db-mysql-0903 | 2017-07-01 16:11:37 | 2017-07-01 16:11:39 | backup-mycluster-locations-1498571521.sql.gz |
|   1698772 | locations  | db-mysql-0903 | 2017-07-01 10:09:21 | 2017-07-01 10:09:22 | backup-mycluster-locations-1498549921.sql.gz |
|   1698411 | locations  | db-mysql-0903 | 2017-07-01 04:12:32 | 2017-07-01 04:12:32 | backup-mycluster-locations-1498528321.sql.gz |
|   1698050 | locations  | db-mysql-0903 | 2017-06-30 22:18:23 | 2017-06-30 22:18:23 | backup-mycluster-locations-1498506721.sql.gz |
| ...
|   1262253 | locations  | db-mysql-0088 | 2016-08-01 01:58:51 | 2016-08-01 01:58:54 | backup-mycluster-locations-1470034801.sql.gz |
|   1064984 | locations  | db-mysql-0088 | 2016-04-04 13:07:40 | 2016-04-04 13:07:43 | backup-mycluster-locations-1459494001.sql.gz |
+-----------+------------+---------------+---------------------+---------------------+----------------------------------------------+

```

**tomkrouper**

```
.mysql restore 1699133

```

**Hubot**

```
A restore job has been created for the backup job 1699133. You will be notified in #database-ops when the restore is complete.

```

**Hubot**

```
@tomkrouper: the locations table has been restored as locations_2017_07_01_16_11 in the restores database on db-mysql-0482

```

数据被加载到非生产环境的数据库，该数据库可供请求该次恢复的工程师访问。

我们保留数据的“备份”的最后一个方法是使用<ruby> <a href="https://dev.mysql.com/doc/refman/5.6/en/replication-delayed.html">  延迟副本 </a> <rt>  delayed replica </rt></ruby>。这与其说是备份，不如说是保护。对于每个生产集群，我们有一个延迟 4 个小时复制的主机。如果运行了一个不该运行的请求，我们可以在 chatops 中运行 `mysql panic` 。这将导致我们所有的延迟副本立即停止复制。这也将给值班 DBA 发送消息。从而我们可以使用延迟副本来验证是否有问题，并快速前进到二进制日志的错误发生之前的位置。然后，我们可以将此数据恢复到主服务器，从而恢复数据到该时间点。

备份固然好，但如果发生了一些未知或未捕获的错误破坏它们，它们就没有价值了。让脚本恢复备份的好处是它允许我们通过 cron 自动执行备份验证。我们为每个集群设置了一个专用的主机，用于运行最新备份的恢复。这样可以确保备份运行正常，并且我们能够从备份中检索数据。

根据数据集大小，我们每天运行几次恢复。恢复的服务器被加入到复制工作流，并通过复制保持数据更新。这测试不仅让我们得到了可恢复的备份，而且也让我们得以正确地确定备份的时间点，并且可以从该时间点进一步应用更改。如果恢复过程中出现问题，我们会收到通知。

我们还追踪恢复所需的时间，所以我们知道在紧急情况下建立新的副本或还原需要多长时间。

以下是由 Hubot 在我们的机器人聊天室中输出的自动恢复过程。

**Hubot**

```
gh-mysql-backup-restore: db-mysql-0752: restore_log.id = 4447
gh-mysql-backup-restore: db-mysql-0752: Determining backup to restore for cluster 'prodcluster'.
gh-mysql-backup-restore: db-mysql-0752: Enabling maintenance mode
gh-mysql-backup-restore: db-mysql-0752: Setting orchestrator downtime
gh-mysql-backup-restore: db-mysql-0752: Disabling Puppet
gh-mysql-backup-restore: db-mysql-0752: Stopping MySQL
gh-mysql-backup-restore: db-mysql-0752: Removing MySQL files
gh-mysql-backup-restore: db-mysql-0752: Running gh-xtrabackup-restore
gh-mysql-backup-restore: db-mysql-0752: Restore file: xtrabackup-notify-2017-07-02_0000.xbstream
gh-mysql-backup-restore: db-mysql-0752: Running gh-xtrabackup-prepare
gh-mysql-backup-restore: db-mysql-0752: Starting MySQL
gh-mysql-backup-restore: db-mysql-0752: Update file ownership
gh-mysql-backup-restore: db-mysql-0752: Upgrade MySQL
gh-mysql-backup-restore: db-mysql-0752: Stopping MySQL
gh-mysql-backup-restore: db-mysql-0752: Starting MySQL
gh-mysql-backup-restore: db-mysql-0752: Backup Host: db-mysql-0034
gh-mysql-backup-restore: db-mysql-0752: Setting up replication
gh-mysql-backup-restore: db-mysql-0752: Starting replication
gh-mysql-backup-restore: db-mysql-0752: Replication catch-up
gh-mysql-backup-restore: db-mysql-0752: Restore complete (replication running)
gh-mysql-backup-restore: db-mysql-0752: Enabling Puppet
gh-mysql-backup-restore: db-mysql-0752: Disabling maintenance mode
gh-mysql-backup-restore: db-mysql-0752: Setting orchestrator downtime
gh-mysql-backup-restore: db-mysql-0752: Restore process complete.

```

还有一件我们可以使用备份做的事情是向一组现有的 MySQL 服务器添加一个新的副本。我们初始化一个新的服务器，一旦我们得知它准备就绪，我们就可以开始恢复该特定集群的最新备份。我们有一个脚本，可以运行所有需要手动执行的恢复命令。我们的自动恢复系统基本上使用的是相同的脚本。这简化了系统构建过程，并允许我们在一个启动运行的主机上使用几个对话命令（chatops）来替代一堆手动执行命令。下面显示的是在该对话（chatops）中手动执行的恢复：

**jessbreckenridge**

```
.mysql backup-restore -H db-mysql-0007 -o -r magic_word=daily_rotating_word

```

**Hubot**

```
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Determining backup to restore for cluster 'mycluster'.
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: restore_log.id = 4449
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Enabling maintenance mode
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Setting orchestrator downtime
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Disabling Puppet
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Stopping MySQL
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Removing MySQL files
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Running gh-xtrabackup-restore
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Restore file: xtrabackup-mycluster-2017-07-02_0015.xbstream
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Running gh-xtrabackup-prepare
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Update file ownership
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Starting MySQL
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Upgrade MySQL
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Stopping MySQL
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Starting MySQL
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Setting up replication
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Starting replication
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Backup Host: db-mysql-0201
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Replication catch-up
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Replication behind by 4589 seconds, waiting 1800 seconds before next check.
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Restore complete (replication running)
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Enabling puppet
@jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Disabling maintenance mode

```

### 故障转移

[我们使用协调器](http://githubengineering.com/orchestrator-github/) 来为<ruby> 主服务器 <rt>  master </rt></ruby>和<ruby> 中间服务器 <rt>  intermediate master </rt></ruby>执行自动化故障切换。我们期望<ruby> 协调器 <rt>  orchestrator </rt></ruby>能够正确检测主服务器故障，指定一个副本进行晋升，在所指定的副本下修复拓扑，完成晋升。我们预期 VIP（虚拟 IP）、连接池可以相应地进行变化、客户端进行重连、puppet 在晋升后的主服务器上运行基本组件等等。故障转移是一项复杂的任务，涉及到我们基础架构的许多方面。

为了建立对我们的故障转移的信赖，我们建立了一个*类生产环境*的测试集群，并且我们不断地崩溃它来观察故障转移情况。

这个*类生产环境*的测试集群是一套复制环境，与我们的生产集群的各个方面都相同：硬件类型、操作系统、MySQL 版本、网络环境、VIP、puppet 配置、[haproxy 设置](https://githubengineering.com/context-aware-mysql-pools-via-haproxy/) 等。与生产集群唯一不同的是它不发送/接收生产流量。

我们在测试集群上模拟写入负载，同时避免复制滞后。写入负载不会太大，但是有一些有意地写入相同数据集的竞争请求。这在正常情况下并不是很有用，但是事实证明这在故障转移中是有用的，我们将会稍后简要描述它。

我们的测试集群有来自三个数据中心的典型的服务器。我们希望故障转移能够从同一个数据中心内晋升替代副本。我们希望在这样的限制下尽可能多地恢复副本。我们要求尽可能地实现这两者。协调器对拓扑结构没有<ruby> 先验假定 <rt>  prior assumption </rt></ruby>；它必须依据崩溃时的状态作出反应。

然而，我们有兴趣创建各种复杂而多变的故障恢复场景。我们的故障转移测试脚本为故障转移提供了基础：

* 它能够识别现有的主服务器
* 它能够重构拓扑结构，来代表主服务器下的所有的三个数据中心。不同的数据中心具有不同的网络延迟，并且预期会在不同的时间对主机崩溃做出反应。
* 能够选择崩溃方式。可以选择干掉主服务器（`kill -9`）或网络隔离（比较好的方式： `iptables -j REJECT` 或无响应的方式： `iptables -j DROP`）方式。

脚本通过选择的方法使主机崩溃，并等待协调器可靠地检测到崩溃然后执行故障转移。虽然我们期望检测和晋升在 30 秒钟内完成，但脚本会稍微放宽这一期望，并在查找故障转移结果之前休眠一段指定的时间。然后它将检查：

* 一个新的（不同的）主服务器是否到位
* 集群中有足够的副本
* 主服务器是可写的
* 对主服务器的写入在副本上可见
* 内部服务发现项已更新（如预期般识别到新的主服务器；移除旧的主服务器）
* 其他内部检查

这些测试可以证实故障转移是成功的，不仅是 MySQL 级别的，而是在更大的基础设施范围内成功的。VIP 被赋予；特定的服务已经启动；信息到达了应该去的地方。

该脚本进一步继续恢复那个失败的服务器：

* 从备份恢复它，从而隐含地测试了我们的备份/恢复过程
* 验证服务器配置是否符合预期（该服务器不再认为其是主服务器）
* 将其加入到复制集群，期望找到在主服务器上写入的数据

看一下以下可视化的计划的故障转移测试：从运行良好的群集，到在某些副本上发现问题，诊断主服务器（`7136`）是否死机，选择一个服务器（`a79d`）来晋升，重构该服务器下的拓扑，晋升它（故障切换成功），恢复失败的（原）主服务器并将其放回群集。

![automated master failover](/data/attachment/album/201710/03/121119p1z5175cs5s2vllp.gif)

#### 测试失败怎么样?

我们的测试脚本使用了一种“停止世界”的方法。任何故障切换组件中的单个故障都将导致整个测试失败，因此在有人解决该问题之前，无法进行任何进一步的自动化测试。我们会得到警报，并检查状态和日志进行处理。

脚本将各种情况下失败，如不可接受的检测或故障转移时间；备份/还原出现问题；失去太多服务器；在故障切换后的意外配置等等。

我们需要确保协调器正确地连接服务器。这是竞争性写入负载有用的地方：如果设置不正确，复制很容易中断。我们会得到 `DUPLICATE KEY` 或其他错误提示出错。

这是特别重要的，因此我们改进协调器并引入新的行为，以允许我们在安全的环境中测试这些变化。

#### 出现：混乱测试

上面所示的测试程序将捕获（并已经捕获）我们基础设施许多部分的问题。这些够了吗？

在生产环境中总是有其他的东西。有些特定测试方法不适用于我们的生产集群。它们不具有相同的流量和流量方式，也不具有完全相同的服务器集。故障类型可能有所不同。

我们正在为我们的生产集群设计混乱测试。 混乱测试将会在我们的生产中，但是按照预期的时间表和充分控制的方式来逐个破坏我们的部分生产环境。 混乱测试在恢复机制中引入更高层次的信赖，并影响（因此测试）我们的基础设施和应用程序的更大部分。

这是微妙的工作：当我们承认需要混乱测试时，我们也希望可以避免对我们的服务造成不必要的影响。不同的测试将在风险级别和影响方面有所不同，我们将努力确保我们的服务的可用性。

### 模式迁移

[我们使用 gh-ost](http://githubengineering.com/gh-ost-github-s-online-migration-tool-for-mysql/)来运行实时<ruby> 模式迁移 <rt>  schema migration </rt></ruby>。gh-ost 是稳定的，但也处于活跃开发中，重大新功能正在不断开发和计划中。

gh-ost 通过将数据复制到 ghost 表来迁移，将由二进制日志拦截的进一步更改应用到 ghost 表中，就如其正在写入原始表。然后它将 ghost 表交换代替原始表。迁移完成时，GitHub 继续使用由 gh-ost 生成和填充的表。

在这个时候，几乎所有的 GitHub 的 MySQL 数据都被 gh-ost 重新创建，其中大部分重新创建多次。我们必须高度信赖 gh-ost，让它一遍遍地操弄我们的数据，即使它还处于活跃开发中。下面是我们如何获得这种信赖的。

gh-ost 提供生产环境测试能力。它支持在副本上运行迁移，其方式与在主服务器上运行的方式大致相同： gh-ost 将连接到副本，并将其视为主服务器。它将采用与实际主机迁移相同的方式解析其二进制日志。但是，它将复制行并将二进制日志事件应用于副本，并避免对主服务器进行写入。

我们在生产环境中给 gh-ost 提供专用的副本。这些副本并不为生产环境提供服务。每个这样的副本将检索生产表的当前列表，并以随机顺序对其进行迭代。一个接一个地选择一个表并在该表上执行复制迁移。迁移实际上并不修改表结构，而是运行一个微不足道的 `ENGINE=InnoDB` 更改。该测试运行迁移时，如果表正在生产环境中使用，会复制实际的生产数据，并接受二进制日志之外的真实的生产环境流量。

这些迁移可以被审计。以下是我们如何从对话中（chatops）检查运行中的测试状态：

**ggunson**

```
.migration test-status

```

**Hubot**

```
# Migrating `prod`.`pull_requests`; Ghost table is `prod`.`_pull_requests_gho`
# Migrating ghost-db-mysql-0007:3306; inspecting ghost-db-mysql-0007:3306; executing on ghost-db-mysql-0007
# Migration started at Mon Jan 30 02:13:39 -0800 2017
# chunk-size: 2500; max-lag-millis: 1500ms; max-load: Threads_running=30; critical-load: Threads_running=1000; nice-ratio: 0.000000
# throttle-additional-flag-file: /tmp/gh-ost.throttle
# panic-flag-file: /tmp/ghost-test-panic.flag
# Serving on unix socket: /tmp/gh-ost.test.sock
Copy: 57992500/86684838 66.9%; Applied: 57708; Backlog: 1/100; Time: 3h28m38s(total), 3h28m36s(copy); streamer: mysql-bin.000576:142993938; State: migrating; ETA: 1h43m12s

```

当测试迁移完成表数据的复制时，它将停止复制并执行切换，使用 ghost 表替换原始表，然后交换回来。我们对实际替换数据并不感兴趣。相反，我们将留下原始的表和 ghost 表，它们应该是相同的。我们通过校验两个表的整个表数据来验证。

测试能以下列方式完成：

* *成功* ：一切顺利，校验和相同。我们期待看到这一结果。
* *失败* ：执行问题。这可能偶尔发生，因为迁移进程被杀死、复制问题等，并且通常与 gh-ost 自身无关。
* *校验失败* ：表数据不一致。对于被测试的分支，这个需要修复。对于正在进行的 master 分支测试，这意味着立即阻止生产迁移。我们不会遇到后者。

测试结果经过审核，发送到机器人聊天室，作为事件发送到我们的度量系统。下图中的每条垂直线代表成功的迁移测试:

![automated master failover](/data/attachment/album/201710/03/121122c1a8vl1aajbapsl1.png)

这些测试不断运行。如果发生故障，我们会收到通知。当然，我们可以随时访问机器人聊天室（chatops），了解发生了什么。

#### 测试新版本

我们不断改进 gh-ost。我们的开发流程基于 git 分支，然后我们通过[拉取请求（PR）](https://github.com/github/gh-ost/pulls)来提供合并。

提交的 gh-ost 拉取请求（PR）通过持续集成（CI）进行基本的编译和单元测试。一旦通过，该 PR 在技术上就有资格合并，但更好的是它[有资格通过 Heaven 进行部署](https://githubengineering.com/deploying-branches-to-github-com/)。作为我们基础架构中的敏感组件，在其进入 master 分支前，我们会小心部署分支进行密集测试。

**shlomi-noach**

```
.deploy gh-ost/fix-reappearing-throttled-reasons to prod/ghost-db-mysql-0007

```

**Hubot**

```
@shlomi-noach is deploying gh-ost/fix-reappearing-throttled-reasons (baee4f6) to production (ghost-db-mysql-0007).
@shlomi-noach's production deployment of gh-ost/fix-reappearing-throttled-reasons (baee4f6) is done! (2s)
@shlomi-noach, make sure you watch for exceptions in haystack

```

**jonahberquist**

```
.deploy gh-ost/interactive-command-question to prod/ghost-db-mysql-0012

```

**Hubot**

```
@jonahberquist is deploying gh-ost/interactive-command-question (be1ab17) to production (ghost-db-mysql-0012).
@jonahberquist's production deployment of gh-ost/interactive-command-question (be1ab17) is done! (2s)
@jonahberquist, make sure you watch for exceptions in haystack

```

**shlomi-noach**

```
.wcid gh-ost

```

**Hubot**

```
shlomi-noach testing fix-reappearing-throttled-reasons 41 seconds ago: ghost-db-mysql-0007
jonahberquist testing interactive-command-question 7 seconds ago: ghost-db-mysql-0012

Nobody is in the queue.

```

一些 PR 很小，不影响数据本身。对状态消息，交互式命令等的更改对 gh-ost 应用程序的影响较小。而其他的 PR 对迁移逻辑和操作会造成重大变化，我们将严格测试这些，通过我们的生产表车队运行这些，直到其满足了这些改变不会造成数据损坏威胁的程度。

### 总结

在整个测试过程中，我们建立对我们的系统的信赖。通过自动化这些测试，在生产环境中，我们得到了一切都按预期工作的反复确认。随着我们继续发展我们的基础设施，我们还通过调整测试来覆盖最新的变化。

产品总会有令你意想不到的未被测试覆盖的场景。我们对生产环境的测试越多，我们对应用程序的期望越多，基础设施的能力就越强。

---

via: <https://githubengineering.com/mysql-testing-automation-at-github/>

作者：[tomkrouper](https://github.com/tomkrouper)， [Shlomi Noach](https://github.com/shlomi-noach) 译者：[MonkeyDEcho](https://github.com/MonkeyDEcho) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


Adobe 软件的最佳 Linux 替代品

Summary: 你是一名正在寻找 Adobe 的替代品的 Linux 用户吗？那你不是一个人。

挺好，选择很多

你是一名正在寻找 Adobe 的替代品的 Linux 用户吗？那你不是一个人。

![](/data/attachment/album/201710/03/233322wdysvjddh6mmvvsl.jpg "Best Linux Adobe Alternatives You Need to Knows")

你是一名正在寻找 Adobe 的替代品的 Linux 用户吗？那你不是一个人。如果你是一个狂热的平面设计师，那么你可能很擅长避开昂贵的 Adobe 产品。不过，对于 Adobe 来说，Linux 用户通常是其支持最不利的。因此，Adobe 的替代品是必须的 —— 但是最好的选择是什么？

这最终要看具体的 Adobe 程序和你希望完成的事情。幸运的是，由于需求是所有发明之母，有人响应了这些号召。其结果是出现了一系列高效的 Adobe 替代品。

### Evince (Adobe Acrobat Reader)

![Evince_AdobeReaderSubstitute](/data/attachment/album/201710/03/233323pgns9vszn9vdfgov.jpg "Evince_AdobeReaderSubstitute")

就像 Adobe Acrobat Reader 一样，[Evince](https://wiki.gnome.org/Apps/Evince) 是一个“支持多种文档格式的文档查看器”。例如，用户可以使用 Evince 作为 PDF 查看器。它还支持各种漫画书格式（cbr、cbz、cb7 和 cbt）。你可以在 Evince 网站上找到[支持格式的完整列表](https://wiki.gnome.org/Apps/Evince/SupportedDocumentFormats)。

对于 Evince ，Linux 用户不用高估，也不用贬低，它就是个标准的查看器。你可以在需要时前往官方网站进行更新。

### Pixlr (Adobe Photoshop)

![Pixlr_Photoshopsubstitute](/data/attachment/album/201710/03/233326aqja4wzawvww483a.jpg "Pixlr_Photoshopsubstitute")

关于 [Pixlr](https://pixlr.com/) 的很棒的一点是这个 Adobe 替代品的各种工具可以在线获得。如果你有一个互联网连接，那么你就有一个强大的图像编辑工作区。

[Pixlr Editor](https://pixlr.com/editor/) 是 Photoshop 的一个功能强大的替代品，你可以使用图层和相关效果。它还有一些漂亮的绘图和颜色编辑工具。[Pixlr Express](https://pixlr.com/express/) 没有这么多功能，因为它主要用于图像增强、调整颜色和清晰度，并增加一些 Instagram 适用的效果！

你可以通过 Pixlr 完成的任务简直不可置信，而且完全免费的。

### Inkscape (Adobe Illustrator)

![Inkscape_Illustratorsubstitute](/data/attachment/album/201710/03/233330ts5sebls5h5wssd7.jpg "Inkscape_Illustratorsubstitute")

[Inkscape](https://inkscape.org/en/)是另一个值得推荐的免费 Adobe 替代品。它主要作为一个“专业的矢量图形编辑器”。除了 Illustrator，Inkscape 也与 Corel Draw、Freehand 和 Xara X 的功能差不多。

它的矢量设计工具可用于制作 logo 和“高可伸缩性”艺术品。Inkscape 包含绘图、形状和文本工具。图层工具允许你锁定、分组或隐藏单个图层。

### Pinegrow Web Editor (Adobe Dreamweaver)

![Pinegrow_Dreamweaversubstitute](/data/attachment/album/201710/03/233331azt7imxe044uxmzm.jpg "Pinegrow_Dreamweaversubstitute")

[Pinegrow Web Editor](http://pinegrow.com/) 是 Dreamweaver 在 Linux 上的绝佳替代品。该程序可让你在桌面上直接制作 HTML 网站。

不仅是使用代码创建（而且需要稍后预览），Pinegrow 可以提供详细的可视化编辑体验。你可以直接查看和测试你的 HTML 项目，实时了解链接是否正常工作，或者图片是否在它该在的地方。Pinegrow 还附带了 WordPress 主题构建器。

免费试用 30 天。如果你喜欢，你可以一次性支付 $49 购买。

### Scribus (Adobe InDesign)

![Scribus_InDesignsubstitute](/data/attachment/album/201710/03/233332t0p0mvatebbgtxka.jpg "Scribus_InDesignsubstitute")

[Scribus](https://www.scribus.net/) 可能是最接近 Adobe InDesign 的替代品。根据开发者的说法，你应该[认真考虑使用](https://www.scribus.net/why-on-earth-should-i-use-scribus-2/) Scribus，因为它是可靠和免费的。

实际上，Scribus 不仅仅是一个出色的桌面出版工具，也是一个很好的自出版工具。当你可以自己做高质量的杂志和书籍时，为什么要依靠昂贵的商业软件来创建？Scribus 目前允许设计师使用一个 200 色的调色板，下一个稳定版中[承诺将会加倍颜色数](https://www.scribus.net/because-color-matters/)。

### digiKam (Adobe Lightroom)

![digiKam_Lightroomsubstitute](/data/attachment/album/201710/03/233332oh2pohtkvzhnekkk.jpg "digiKam_Lightroomsubstitute")

[digiKam](http://digikam.org/) 也许是目前 Linux 用户最好的 Lightroom 替代品。功能包括导入照片、整理图片集、图像增强、创建幻灯片等功能。

它的时尚设计和先进的功能是真正用心之作。实际上，digiKam 背后的人是摄影师们。不仅如此，他们希望在 Linux 中完成在 Lightroom 能做的任何工作。

### Webflow (Adobe Muse)

![Webflow_Museubstitute](/data/attachment/album/201710/03/233332skzdoacppou3k8ak.jpg "Webflow_Museubstitute")

[Webflow](https://webflow.com/) 是另一个可以证明你无需下载软件而可以完成很多事的网站。这是一个非常方便的 Adobe Muse 替代品，Webflow 是创建高响应式网站设计的理想选择。

Webflow 的最好的一方面是你不需要自己编写代码。你只需拖放图像并写入文本。Webflow 为你做了所有杂事。你可以从头开始构建网站，也可以使用各种模板。虽然是免费的，但是其高级版本还提供了额外的功能，如能够轻松地导出 HTML 和 CSS 以在其他地方使用。

### Tupi (Adobe Animate)

![Tupi_Animatesubstitute](/data/attachment/album/201710/03/233333eyklpq00w00kyr12.jpg "Tupi_Animatesubstitute")

[Tupi](http://www.maefloresta.com/portal/) 是 Adobe Animate 的替代品，或者也可以用于那些[不太热衷于 Flash 的人](https://www.maketecheasier.com/sites-moving-away-flash/)。当然，Tupi 的作者说这并不是与 Flash 竞争。然而，其使用 HTML5 的能力使其成为了理想的替代品。

在 PC 或平板电脑上绘制 2D 动画。不确定如何开始？使用网站的 [YouTube 教程](https://www.youtube.com/user/maefloresta)了解如何制作剪贴画动画以及更多。

### Black Magic Fusion (Adobe After Effects)

![Fusion_AFterEffectssubstitute](/data/attachment/album/201710/03/233334f28jx8nnpjlxj2rb.jpg "Fusion_AFterEffectssubstitute")

[Black Magic Fusion](https://www.blackmagicdesign.com/) 注定是 Adobe After Effects 的替代者。这个视觉效果软件历经了大约 25 年的开发！Fusion 通常用于在好莱坞电影和电视节目中制造令人印象深刻的效果 —— 这靠的是其丰富而时尚的功能。

Fusion 通过使用节点，即那些“代表效果、过滤器和其他处理的小图标”工作。将这些节点连接在一起，创建一系列复杂的视觉效果。该程序包括许多功能，如图片修饰、对象跟踪和令人兴奋的 3D 效果。

你可以选择免费版或者 $995 的 Fusion Studio。为了帮助你决定，[你可以比较](https://www.blackmagicdesign.com/products/fusion/compare)免费和高级版的 Fusion 功能。

### 总结

如你所见，这些远不止于是 Adobe 替代品。由于开源的缘故，显著提升的替代品的不断地发布。我们很快就会看到一个仅为 Linux 用户创建的完整套件。在此之前，你可以随意选择这些替代品。

知道这里没有提到的其它有用的 Adobe 替代品吗？在下面的评论区分享软件建议。

---

via: <https://www.maketecheasier.com/adobe-alternatives-for-linux/>

作者：[Toni Matthews-El](https://www.maketecheasier.com/author/ttmatthe/) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出