分类 技术 下的文章

有时你需要知道的最重要的信息是什么,你当前的程序状态是如何到达那里的。有一个 backtrace 命令,它给你提供了程序当前的函数调用链。这篇文章将向你展示如何在 x86\_64 上实现堆栈展开以生成这样的回溯。

系列索引

这些链接将会随着其他帖子的发布而上线。

  1. 准备环境
  2. 断点
  3. 寄存器和内存
  4. ELF 和 DWARF
  5. 源码和信号
  6. 源码级逐步执行
  7. 源码级断点
  8. 堆栈展开
  9. 读取变量
  10. 之后步骤

用下面的程序作为例子:

void a() {
    //stopped here
}

void b() {
     a();
}

void c() {
     a();
}

int main() {
    b();
    c();
}

如果调试器停在 //stopped here' 这行,那么有两种方法可以达到:main->b->amain->c->a`。如果我们用 LLDB 设置一个断点,继续执行并请求一个回溯,那么我们将得到以下内容:

* frame #0: 0x00000000004004da a.out`a() + 4 at bt.cpp:3
  frame #1: 0x00000000004004e6 a.out`b() + 9 at bt.cpp:6
  frame #2: 0x00000000004004fe a.out`main + 9 at bt.cpp:14
  frame #3: 0x00007ffff7a2e830 libc.so.6`__libc_start_main + 240 at libc-start.c:291
  frame #4: 0x0000000000400409 a.out`_start + 41

这说明我们目前在函数 a 中,a 从函数 b 中跳转,bmain 中跳转等等。最后两个帧是编译器如何引导 main 函数的。

现在的问题是我们如何在 x86\_64 上实现。最稳健的方法是解析 ELF 文件的 .eh_frame 部分,并解决如何从那里展开堆栈,但这会很痛苦。你可以使用 libunwind 或类似的来做,但这很无聊。相反,我们假设编译器以某种方式设置了堆栈,我们将手动遍历它。为了做到这一点,我们首先需要了解堆栈的布局。

            High
        |   ...   |
        +---------+
     +24|  Arg 1  |
        +---------+
     +16|  Arg 2  |
        +---------+
     + 8| Return  |
        +---------+
EBP+--> |Saved EBP|
        +---------+
     - 8|  Var 1  |
        +---------+
ESP+--> |  Var 2  |
        +---------+
        |   ...   |
            Low

如你所见,最后一个堆栈帧的帧指针存储在当前堆栈帧的开始处,创建一个链接的指针列表。堆栈依据这个链表解开。我们可以通过查找 DWARF 信息中的返回地址来找出列表中下一帧的函数。一些编译器将忽略跟踪 EBP 的帧基址,因为这可以表示为 ESP 的偏移量,并可以释放一个额外的寄存器。即使启用了优化,传递 -fno-omit-frame-pointer 到 GCC 或 Clang 会强制它遵循我们依赖的约定。

我们将在 print_backtrace 函数中完成所有的工作:

void debugger::print_backtrace() {

首先要决定的是使用什么格式打印出帧信息。我用了一个 lambda 来推出这个方法:

    auto output_frame = [frame_number = 0] (auto&& func) mutable {
        std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func)
                  << ' ' << dwarf::at_name(func) << std::endl;
    };

打印输出的第一帧是当前正在执行的帧。我们可以通过查找 DWARF 中的当前程序计数器来获取此帧的信息:

    auto current_func = get_function_from_pc(get_pc());
    output_frame(current_func);

接下来我们需要获取当前函数的帧指针和返回地址。帧指针存储在 rbp 寄存器中,返回地址是从帧指针堆栈起的 8 字节。

    auto frame_pointer = get_register_value(m_pid, reg::rbp);
    auto return_address = read_memory(frame_pointer+8);

现在我们拥有了展开堆栈所需的所有信息。我只需要继续展开,直到调试器命中 main,但是当帧指针为 0x0 时,你也可以选择停止,这些是你在调用 main 函数之前调用的函数。我们将从每帧抓取帧指针和返回地址,并打印出信息。

    while (dwarf::at_name(current_func) != "main") {
        current_func = get_function_from_pc(return_address);
        output_frame(current_func);
        frame_pointer = read_memory(frame_pointer);
        return_address = read_memory(frame_pointer+8);
    }
}

就是这样!以下是整个函数:

void debugger::print_backtrace() {
    auto output_frame = [frame_number = 0] (auto&& func) mutable {
        std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func)
                  << ' ' << dwarf::at_name(func) << std::endl;
    };

    auto current_func = get_function_from_pc(get_pc());
    output_frame(current_func);

    auto frame_pointer = get_register_value(m_pid, reg::rbp);
    auto return_address = read_memory(frame_pointer+8);

    while (dwarf::at_name(current_func) != "main") {
        current_func = get_function_from_pc(return_address);
        output_frame(current_func);
        frame_pointer = read_memory(frame_pointer);
        return_address = read_memory(frame_pointer+8);
    }
}

添加命令

当然,我们必须向用户公开这个命令。

    else if(is_prefix(command, "backtrace")) {
        print_backtrace();
    }

测试

测试此功能的一个方法是通过编写一个测试程序与一堆互相调用的小函数。设置几个断点,跳到代码附近,并确保你的回溯是准确的。

我们已经从一个只能产生并附加到其他程序的程序走了很长的路。本系列的倒数第二篇文章将通过支持读写变量来完成调试器的实现。在此之前,你可以在这里找到这个帖子的代码。


via: https://blog.tartanllama.xyz/c++/2017/06/24/writing-a-linux-debugger-unwinding/

作者:Simon Brand 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

如果你看过译者以前翻译的 snappy 文章,不知有没有感觉相关主题都是浅尝辄止,讲得不够透彻,看得也不太过瘾?如果有的话,相信这篇详细讲解如何从零开始制作一个 snap 包的文章应该不会让你失望。

在这篇文章中,我们将看到如何为名为 timg 的实用程序制作对应的 snap 包。如果这是你第一次听说 snap 安装包,你可以先看看 如何创建你的第一个 snap 包

今天我们将学习以下有关使用 snapcraft 制作 snap 包的内容:

  • timg 源码中的 Makefile 文件是手工编写,我们需要修改一些 make 插件参数
  • 这个程序是用 C++ 语言写的,依赖几个额外的库文件。我们需要把相关的代码添加到 snap 包中。
  • 严格限制还是传统限制?我们将会讨论如何在它们之间进行选择。

首先,我们了解下 timg 有什么用?

背景

Linux 终端模拟器已经变得非常炫酷,并且还能显示颜色!

1.png-19.9kB

除了标准的颜色,大多数终端模拟器(如上图显示的 GNOME 终端)都支持真彩色(1600 万种颜色)。

图片.png-61.9kB

是的!终端模拟器已经支持真彩色了!从这个页面“ 多个终端和终端应用程序已经支持真彩色(1600 万种颜色)” 可以获取 AWK 代码自己进行测试。你可以看到在代码中使用了一些 转义序列 来指定 RGB 的值(256 * 256 * 256 ~= 1600 万种颜色)。

timg 是什么?

好了,言归正传,timg 有什么用?它能将输入的图片重新调整为终端窗口字符所能显示范围的大小(比如:80 x 25),然后在任何分辨率的终端窗口用彩色字符显示图像。

图片.png-37.3kB

这幅图用彩色块字符显示了 Ubuntu 的 logo,原图是一个 PNG 格式的文件。

图片.png-165kB

这是 @Doug8888 拍摄的花

如果你通过远程连接服务器来管理自己的业务,并想要查看图像文件,那么 timg 将会特别有用。

除了静态图片,timg 同样也可以显示 gif 动图。

那么让我们开始 snap 之旅吧!

熟悉 timg 的源码

timg 的源码可以在 https://github.com/hzeller/timg 找到。让我们试着手动编译它,以了解它有什么需求。

图片.png-128.4kB

Makefilesrc/ 子文件夹中而不是项目的根文件夹中。在 github 页面上,他们说需要安装两个开发包(GraphicsMagic++ 和 WebP),然后使用 make 就能生成可执行文件。在截图中可以看到我已经将它们安装好了(在我读完相关的 Readme.md 文件后)。

因此,在编写 snapcraft.yaml 文件时已经有了四条腹稿:

  1. Makefilesrc/ 子文件夹中而不是项目的根文件夹中。
  2. 这个程序编译时需要两个开发库。
  3. 为了让 timg 以 snap 包形式运行,我们需要将这两个库捆绑在 snap 包中(或者静态链接它们)。
  4. timg 是用 C++ 编写的,所以需要安装 g++。在编译之前,让我们通过 snapcraft.yaml 文件来检查 build-essential 元包是否已经安装。

从 snapcraft 开始

让我们新建一个名为 timg-snap/ 的文件夹,并在其中运行 snapcraft init 这条命令来创建 snapcraft.yaml 工作的框架。

ubuntu@snaps:~$ mkdir timg-snap
ubuntu@snaps:~$ cd timg-snap/
ubuntu@snaps:~/timg-snap$ snapcraft init
Created snap/snapcraft.yaml.
Edit the file to your liking or run `snapcraft` to get started
ubuntu@snaps:~/timg-snap$ cat snap/snapcraft.yaml 
name: my-snap-name # you probably want to 'snapcraft register <name>'
version: '0.1' # just for humans, typically '1.2+git' or '1.3.2'
summary: Single-line elevator pitch for your amazing snap # 79 char long summary
description: |
  This is my-snap's description. You have a paragraph or two to tell the most important story about your snap. Keep it under 100 words though, we live in tweetspace and your description wants to look good in the snap store.

grade: devel # must be 'stable' to release into candidate/stable channels
confinement: devmode # use 'strict' once you have the right plugs and slots

parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil

填充元数据

snapcraft.yaml 配置文件的上半部分是元数据。我们需要一个一个把它们填满,这算是比较容易的部分。元数据由以下字段组成:

  1. name (名字)—— snap 包的名字,它将公开在 Ubuntu 商店中。
  2. version (版本)—— snap 包的版本号。可以是源代码存储库中一个适当的分支或者标记,如果没有分支或标记的话,也可以是当前日期。
  3. summary (摘要)—— 不超过 80 个字符的简短描述。
  4. description (描述)—— 长一点的描述, 100 个字以下。
  5. grade (等级)—— stable (稳定)或者 devel (开发)。因为我们想要在 Ubuntu 商店的稳定通道中发布这个 snap 包,所以在 snap 包能正常工作后,就把它设置成 stable
  6. confinement (限制)—— 我们首先设置为 devmode (开发模式),这样系统将不会以任何方式限制 snap 包。一旦它在 devmode下能正常工作,我们再考虑选择 strict (严格)还是 classic (传统)限制。

我们将使用 timg 这个名字:

ubuntu@snaps:~/timg-snap$ snapcraft register timg
Registering timg.
You already own the name 'timg'.

是的,这个名字我已经注册了 :-)。

接下来,我们应该选择哪个版本的 timg?

图片.png-72.7kB

当在仓库中寻找分支或标记时,我们会发现有一个 v0.9.5 标签,其中有 2016 年 6 月 27 日最新提交的代码。

图片.png-71.4kB

然而主分支(master)中有两个看起来很重要的提交。因此我们使用主分支而不用 v0.9.5 标签的那个。我们使用今天的日期—— 20170226 做为版本号。

我们从仓库中搜集了摘要和描述。其中摘要的内容为 A terminal image viewer,描述的内容为 A viewer that uses 24-Bit color capabilities and unicode character blocks to display images in the terminal

最后,将 grade (等级)设置为 stable (稳定),将 confinement 限制设置为 devmode (开发模式)(一直到 snap 包真正起作用)。

这是更新后的 snapcraft.yaml,带有所有的元数据:

ubuntu@snaps:~/timg-snap$ cat snap/snapcraft.yaml 
name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks to display images in the terminal.

grade: stable 
confinement: devmode

parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil

弄清楚 parts: 是什么

现在我们需要将上面已经存在的 parts: 部分替换成真实的 parts:

timg-git-url.png-8kB

Git 仓库的 URL。

图片.png-28.7kB

存在 Makefile,因此我们需要 make 插件。

我们已经知道 git 仓库的 URL 链接,并且 timg 源码中已有了 Makefile 文件。至于 snapcraft make 插件 的 Makefile 命令,正如文档所言,这个插件总是会运行 make 后再运行 make install。为了确认 make 插件的用法,我查看了 snapcraft 可用插件列表

因此,我们将最初的配置:

parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil

修改为:

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    plugin: make

这是当前 snapcraft.yaml 文件的内容:

name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks 
  to display images in the terminal.

grade: stable 
confinement: devmode

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    plugin: make

让我们运行下 snapcraft prime 命令看看会发生什么:

ubuntu@snaps:~/timg-snap$ snapcraft prime
Preparing to pull timg 
Pulling timg 
Cloning into '/home/ubuntu/timg-snap/parts/timg/src'...
remote: Counting objects: 144, done.
remote: Total 144 (delta 0), reused 0 (delta 0), pack-reused 144
Receiving objects: 100% (144/144), 116.00 KiB | 0 bytes/s, done.
Resolving deltas: 100% (89/89), done.
Checking connectivity... done.
Preparing to build timg 
Building timg 
make -j4
make: *** No targets specified and no makefile found.  Stop.
Command '['/bin/sh', '/tmp/tmpem97fh9d', 'make', '-j4']' returned non-zero exit status 2
ubuntu@snaps:~/timg-snap$

我们可以看到 snapcraft 无法在源代码中找到 Makefile 文件,正如我们之前所暗示的,Makefile 位于 src/ 子文件夹中。那么,我们可以让 snapcraft 使用 src/ 文件夹中的 Makefile 文件吗?

每个 snapcraft 插件都有自己的选项,并且有一些通用选项是所有插件共享的。在本例中,我们希望研究那些与源代码相关的 snapcraft 选项。我们开始吧:

source-subdir:path

snapcraft 会 检出 checkout source 关键字所引用的仓库或者解压归档文件到 parts/<part-name>/src/ 中,但是它只会将特定的子目录复制到 parts/<part-name>/build/ 中。

我们已经有了适当的选项,下面更新下 parts

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make

然后再次运行 snapcraft prime

ubuntu@snaps:~/timg-snap$ snapcraft prime 
The 'pull' step of 'timg' is out of date:

The 'source-subdir' part property appears to have changed.

Please clean that part's 'pull' step in order to continue
ubuntu@snaps:~/timg-snap$ snapcraft clean
Cleaning up priming area
Cleaning up staging area
Cleaning up parts directory
ubuntu@snaps:~/timg-snap$ snapcraft prime 
Skipping pull timg (already ran)
Preparing to build timg 
Building timg 
make -j4
g++ `GraphicsMagick++-config --cppflags --cxxflags` -Wall -O3 -fPIC -c -o timg.o timg.cc
g++ -Wall -O3 -fPIC   -c -o terminal-canvas.o terminal-canvas.cc
/bin/sh: 1: GraphicsMagick++-config: not found
timg.cc:33:22: fatal error: Magick++.h: No such file or directory
compilation terminated.
Makefile:10: recipe for target 'timg.o' failed
make: *** [timg.o] Error 1
make: *** Waiting for unfinished jobs....
Command '['/bin/sh', '/tmp/tmpeeyxj5kw', 'make', '-j4']' returned non-zero exit status 2
ubuntu@snaps:~/timg-snap$

从错误信息我们可以得知 snapcraft 找不到 GraphicsMagick++ 这个开发库文件。根据 snapcraft 常见关键字 可知,我们需要在 snapcraft.yaml 中指定这个库文件,这样 snapcraft 才能安装它。

build-packages:[deb, deb, deb…]

列出构建 part 前需要在主机中安装的 Ubuntu 包。这些包通常不会进入最终的 snap 包中,除非它们含有 snap 包中二进制文件直接依赖的库文件(在这种情况下,可以通过 ldd 发现它们),或者在 stage-package 中显式地指定了它们。

让我们寻找下这个开发包的名字:

ubuntu@snaps:~/timg-snap$ apt-cache search graphicsmagick++ | grep dev
graphicsmagick-libmagick-dev-compat/xenial 1.3.23-1build1 all
libgraphicsmagick++1-dev/xenial 1.3.23-1build1 amd64
  format-independent image processing - C++ development files
libgraphicsmagick1-dev/xenial 1.3.23-1build1 amd64
  format-independent image processing - C development files
ubuntu@snaps:~/timg-snap$

可以看到包名为 libgraphicsmagick++1-dev,下面是更新后的 parts

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages: 
      - libgraphicsmagick++1-dev

再次运行 snapcraft

ubuntu@snaps:~/timg-snap$ snapcraft
Installing build dependencies: libgraphicsmagick++1-dev
[...]
The following NEW packages will be installed:
  libgraphicsmagick++-q16-12 libgraphicsmagick++1-dev libgraphicsmagick-q16-3
  libgraphicsmagick1-dev libwebp5
[...]
Building timg 
make -j4
g++ `GraphicsMagick++-config --cppflags --cxxflags` -Wall -O3 -fPIC -c -o timg.o timg.cc
g++ -Wall -O3 -fPIC   -c -o terminal-canvas.o terminal-canvas.cc
g++ -o timg timg.o terminal-canvas.o `GraphicsMagick++-config --ldflags --libs`
/usr/bin/ld: cannot find -lwebp
collect2: error: ld returned 1 exit status
Makefile:7: recipe for target 'timg' failed
make: *** [timg] Error 1
Command '['/bin/sh', '/tmp/tmptma45jzl', 'make', '-j4']' returned non-zero exit status 2
ubuntu@snaps:~/timg-snap$

虽然只指定了开发库 libgraphicsmagick+1-dev,但 Ubuntu 还安装了一些代码库,包括 libgraphicsmagick ++-q16-12,以及动态代码库 libwebp

这里仍然有一个错误,这个是因为缺少开发版本的 webp 库(一个静态库)。我们可以通过下面的命令找到它:

ubuntu@snaps:~/timg-snap$ apt-cache search libwebp | grep dev
libwebp-dev - Lossy compression of digital photographic images.
ubuntu@snaps:~/timg-snap$

上面安装的 libwebp5 包只提供了一个动态库(.so)。通过 libwebp-dev 包,我们可以得到相应的静态库(.a)。好了,让我们更新下 parts: 部分:

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages:
      - libgraphicsmagick++1-dev
      - libwebp-dev

下面是更新后的 snapcraft.yaml 文件的内容:

name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks 
  to display images in the terminal.

grade: stable 
confinement: devmode

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages: 
      - libgraphicsmagick++1-dev
      - libwebp-dev

让我们运行下 snapcraft prime

ubuntu@snaps:~/timg-snap$ snapcraft prime
Skipping pull timg (already ran)
Preparing to build timg 
Building timg 
make -j4
g++ `GraphicsMagick++-config --cppflags --cxxflags` -Wall -O3 -fPIC -c -o timg.o timg.cc
g++ -Wall -O3 -fPIC   -c -o terminal-canvas.o terminal-canvas.cc
g++ -o timg timg.o terminal-canvas.o `GraphicsMagick++-config --ldflags --libs`
make install DESTDIR=/home/ubuntu/timg-snap/parts/timg/install
install timg /usr/local/bin
install: cannot create regular file '/usr/local/bin/timg': Permission denied
Makefile:13: recipe for target 'install' failed
make: *** [install] Error 1
Command '['/bin/sh', '/tmp/tmptq_s1itc', 'make', 'install', 'DESTDIR=/home/ubuntu/timg-snap/parts/timg/install']' returned non-zero exit status 2
ubuntu@snaps:~/timg-snap$

我们遇到了一个新问题。由于 Makefile 文件是手工编写的,不符合 snapcraft make 插件 的参数设置,所以不能正确安装到 prime/ 文件夹中。Makefile 会尝试安装到 usr/local/bin 中。

我们需要告诉 snapcraft make 插件 不要运行 make install,而是找到 timg 可执行文件然后把它放到 prime/ 文件夹中。根据文档的描述:

- artifacts:
(列表)
将 make 生成的指定文件复制或者链接到 snap 包安装目录。如果使用,则 `make install` 这步操作将被忽略。

所以,我们需要将一些东西放到 artifacts: 中。但是具体是哪些东西?

ubuntu@snaps:~/timg-snap/parts/timg$ ls build/src/
Makefile            terminal-canvas.h  timg*     timg.o
terminal-canvas.cc  terminal-canvas.o  timg.cc
ubuntu@snaps:~/timg-snap/parts/timg$

build/ 子目录中,我们可以找到 make 的输出结果。由于我们设置了 source-subdir:src,所以 artifacts: 的基目录为 build/src。在这里我们可以找到可执行文件 timg,我们需要将它设置为 artifacts: 的一个参数。通过 artifacts:,我们可以把 make 输出的某些文件复制到 snap 包的安装目录(在 prime/ 中)。

下面是更新后 snapcraft.yaml 文件 parts: 部分的内容:

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages: 
      - libgraphicsmagick++1-dev
      - libwebp-dev
    artifacts: [timg]

让我们运行 snapcraft prime

ubuntu@snaps:~/timg-snap$ snapcraft prime
Preparing to pull timg 
Pulling timg 
Cloning into '/home/ubuntu/timg-snap/parts/timg/src'...
remote: Counting objects: 144, done.
remote: Total 144 (delta 0), reused 0 (delta 0), pack-reused 144
Receiving objects: 100% (144/144), 116.00 KiB | 207.00 KiB/s, done.
Resolving deltas: 100% (89/89), done.
Checking connectivity... done.
Preparing to build timg 
Building timg 
make -j4
g++ `GraphicsMagick++-config --cppflags --cxxflags` -Wall -O3 -fPIC -c -o timg.o timg.cc
g++ -Wall -O3 -fPIC   -c -o terminal-canvas.o terminal-canvas.cc
g++ -o timg timg.o terminal-canvas.o `GraphicsMagick++-config --ldflags --libs`
Staging timg 
Priming timg 
ubuntu@snaps:~/timg-snap$

我们还将继续迭代。

导出命令

到目前为止,snapcraft 生成了可执行文件,但没有导出给用户使用的命令。接下来我们需要通过 apps: 导出一个命令。

首先我们需要知道命令在 prime/ 的哪个子文件夹中:

ubuntu@snaps:~/timg-snap$ ls prime/
meta/  snap/  timg*  usr/
ubuntu@snaps:~/timg-snap$

它在 prime/ 子文件夹的根目录中。现在,我们已经准备好要在 snapcaft.yaml 中增加 apps: 的内容:

ubuntu@snaps:~/timg-snap$ cat snap/snapcraft.yaml 
name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks 
  to display images in the terminal.

grade: stable 
confinement: devmode

apps:
  timg: 
    command: timg

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages: 
      - libgraphicsmagick++1-dev
      - libwebp-dev
    artifacts: [timg]

让我们再次运行 snapcraft prime,然后测试下生成的 snap 包:

ubuntu@snaps:~/timg-snap$ snapcraft prime 
Skipping pull timg (already ran)
Skipping build timg (already ran)
Skipping stage timg (already ran)
Skipping prime timg (already ran)
ubuntu@snaps:~/timg-snap$ snap try --devmode prime/
timg 20170226 mounted from /home/ubuntu/timg-snap/prime
ubuntu@snaps:~/timg-snap$

图片.png-42.3kB

图片来源: https://www.flickr.com/photos/mustangjoe/6091603784/

我们可以通过 snap try --devmode prime/ 启用该 snap 包然后测试 timg 命令。这是一种高效的测试方法,可以避免生成 .snap 文件,并且无需安装和卸载它们,因为 snap try prime/ 直接使用了 prime/ 文件夹中的内容。

限制 snap

到目前为止,snap 包一直是在不受限制的开发模式下运行的。让我们看看如何限制它的运行:

ubuntu@snaps:~/timg-snap$ snap list
Name           Version   Rev   Developer  Notes
core           16-2      1337  canonical  -
timg           20170226  x1               devmode,try
ubuntu@snaps:~/timg-snap$ snap try --jailmode prime
timg 20170226 mounted from /home/ubuntu/timg-snap/prime
ubuntu@snaps:~/timg-snap$ snap list
Name           Version   Rev   Developer  Notes
core           16-2      1337  canonical  -
timg           20170226  x2               jailmode,try
ubuntu@snaps:~/timg-snap$ timg pexels-photo-149813.jpeg 
Trouble loading pexels-photo-149813.jpeg (Magick: Unable to open file (pexels-photo-149813.jpeg) reported by magick/blob.c:2828 (OpenBlob))
ubuntu@snaps:~/timg-snap$

通过这种方式,我们可以无需修改 snapcraft.yaml 文件就从开发模式(devmode)切换到限制模式(jailmode)(confinement: strict)。正如预期的那样,timg 无法读取图像,因为我们没有开放访问文件系统的权限。

现在,我们需要作出决定。使用限制模式,我们可以很容易授予某个命令访问用户 $HOME 目录中文件的权限,但是只能访问那里。如果图像文件位于其它地方,我们总是需要复制到 $HOME 目录并在 $HOME 的副本上运行 timg。如果我们觉得可行,那我们可以设置 snapcraf.yaml 为:

name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks 
  to display images in the terminal.

grade: stable 
confinement: strict

apps:
  timg: 
    command: timg
    plugs: [home]

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages: 
      - libgraphicsmagick++1-dev
      - libwebp-dev
    artifacts: [timg]

另一方面,如果希望 timg snap 包能访问整个文件系统,我们可以设置传统限制来实现。对应的 snapcraft.yaml 内容如下:

name: timg
version: '20170226'
summary: A terminal image viewer
description: |
  A viewer that uses 24-Bit color capabilities and unicode character blocks 
  to display images in the terminal.

grade: stable 
confinement: classic

apps:
  timg: 
    command: timg

parts:
  timg:
    source: https://github.com/hzeller/timg.git
    source-subdir: src
    plugin: make
    build-packages: 
      - libgraphicsmagick++1-dev
      - libwebp-dev
    artifacts: [timg]

接下来我们将选择严格(strict)约束选项。因此,图像应该只能放在 $HOME 中。

打包和测试

让我们打包这个 snap,也就是制作 .snap 文件,然后在新安装的 Ubuntu 系统上对它进行测试。

ubuntu@snaps:~/timg-snap$ snapcraft 
Skipping pull timg (already ran)
Skipping build timg (already ran)
Skipping stage timg (already ran)
Skipping prime timg (already ran)
Snapping 'timg' \                                                 
Snapped timg_20170226_amd64.snap
ubuntu@snaps:~/timg-snap$

我们如何在几秒钟内得到一个全新安装的 Ubuntu 系统来对 snap 包进行测试?

请查看 尝试在 Ubuntu 上使用 LXD 容器,并在你的系统上设置 LXD。然后回到这里,尝试运行下面的命令:

$ lxc launch ubuntu:x snaptesting
Creating snaptesting
Starting snaptesting
$ lxc file push timg_20170226_amd64.snap snaptesting/home/ubuntu/
$ lxc exec snaptesting -- sudo su - ubuntu
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ubuntu@snaptesting:~$ ls
timg_20170226_amd64.snap
ubuntu@snaptesting:~$ snap install timg_20170226_amd64.snap 
error: access denied (try with sudo)
ubuntu@snaptesting:~$ sudo snap install timg_20170226_amd64.snap
error: cannot find signatures with metadata for snap "timg_20170226_amd64.snap"
ubuntu@snaptesting:~$ sudo snap install timg_20170226_amd64.snap --dangerous
error: cannot perform the following tasks:
- Mount snap "core" (1337) ([start snap-core-1337.mount] failed with exit status 1: Job for snap-core-1337.mount failed. See "systemctl status snap-core-1337.mount" and "journalctl -xe" for details.
)
ubuntu@snaptesting:~$ sudo apt install squashfuse
[...]
Setting up squashfuse (0.1.100-0ubuntu1~ubuntu16.04.1) ...
ubuntu@snaptesting:~$ sudo snap install timg_20170226_amd64.snap --dangerous
timg 20170226 installed
ubuntu@snaptesting:~$ wget https://farm7.staticflickr.com/6187/6091603784_d6960c8be2_z_d.jpg
[...]
2017-02-26 22:12:18 (636 KB/s) - ‘6091603784_d6960c8be2_z_d.jpg’ saved [240886/240886]
ubuntu@snaptesting:~$ timg 6091603784_d6960c8be2_z_d.jpg 
[it worked!]
ubuntu@snaptesting:~$

我们启动了一个名为 snaptesting 的 LXD 容器,并将 .snap 文件复制进去。然后,通过普通用户连接到容器,并尝试安装 snap 包。最初,我们安装失败了,因为在无特权的 LXD 容器中安装 snap 包需要使用 sudo 。接着又失败了,因为 .snap 没有经过签名(我们需要使用 --dangerous 参数)。然而还是失败了,这次是因为我们需要安装 squashfuse 包(Ubuntu 16.04 镜像中没有预装)。最后,我们成功安装了snap,并设法查看了图像。

在一个全新安装的 Linux 系统中测试 snap 包是很重要的,因为这样才能确保 snap 包中包含所有必须的代码库。在这个例子中,我们使用了静态库并运行良好。

发布到 Ubuntu 商店

这是 发布 snap 包到 Ubuntu 商店的说明。 在之前的教程中,我们已经发布了一些 snap 包。对于 timg 来说,我们设置了严格限制和稳定等级。因此,我们会将它发布到稳定通道。

$ snapcraft push timg_20170226_amd64.snap 
Pushing 'timg_20170226_amd64.snap' to the store.
Uploading timg_20170226_amd64.snap [                                       ]   0%
Uploading timg_20170226_amd64.snap [=======================================] 100%
Ready to release!|                                                               
Revision 6 of 'timg' created.
$ snapcraft release timg 6 stable
Track    Arch    Series    Channel    Version    Revision
latest   amd64   16        stable     20170226   6
                           candidate  ^          ^
                           beta       0.9.5      5
                           edge       0.9.5      5
The 'stable' channel is now open.

我们把 .snap 包推送到 Ubuntu 商店后,得到了一个 Revision 6。然后,我们将 timg Revision 6 发布到了 Ubuntu 商店的稳定通道。

在候选通道中没有已发布的 snap 包,它继承的是稳定通道的包,所以显示 ^ 字符。

在之前的测试中,我将一些较老版本的 snap 包上传到了测试和边缘通道。这些旧版本使用了 timg 标签为 0.9.5 的源代码。

我们可以通过将稳定版本发布到测试和边缘通道来移除旧的 0.9.5 版本的包。

$ snapcraft release timg 6 beta
Track    Arch    Series    Channel    Version    Revision
latest   amd64   16        stable     20170226   6
                           candidate  ^          ^
                           beta       20170226   6
                           edge       0.9.5      5
$ snapcraft release timg 6 edge
Track    Arch    Series    Channel    Version    Revision
latest   amd64   16        stable     20170226   6
                           candidate  ^          ^
                           beta       20170226   6
                           edge       20170226   6

使用 timg

让我们不带参数运行 timg

ubuntu@snaptesting:~$ timg
Expected image filename.
usage: /snap/timg/x1/timg [options] <image> [<image>...]
Options:
    -g<w>x<h>  : Output pixel geometry. Default from terminal 80x48
    -s[<ms>]   : Scroll horizontally (optionally: delay ms (60)).
    -d<dx:dy>  : delta x and delta y when scrolling (default: 1:0).
    -w<seconds>: If multiple images given: Wait time between (default: 0.0).
    -t<seconds>: Only animation or scrolling: stop after this time.
    -c<num>    : Only Animation or scrolling: number of runs through a full cycle.
    -C         : Clear screen before showing image.
    -F         : Print filename before showing picture.
    -v         : Print version and exit.
If both -c and -t are given, whatever comes first stops.
If both -w and -t are given for some animation/scroll, -t takes precedence
ubuntu@snaptesting:~$

这里提到当前我们终端模拟器的缩放级别,即分辨率为:80 × 48。

让我们缩小一点,并最大化 GNOME 终端窗口。

-g<w>x<h>  : Output pixel geometry. Default from terminal 635x428

这是一个更好的解决方案,但我几乎看不到字符,因为他们太小了。让我们调用前面的命令再次显示这辆车。

图片.png-904.9kB

你所看到的是调整后的图像(1080p)。虽然它是用彩色文本字符显示的,但看起来依旧很棒。

接下来呢?timg 其实也可以播放 gif 动画哦!

$ wget https://m.popkey.co/9b7141/QbAV_f-maxage-0.gif -O JonahHillAmazed.gif$ timg JonahHillAmazed.gif

你可以试着安装 timg 来体验 gif 动画。要是不想自己动手,可以在 asciinema 上查看相关记录 (如果视频看上去起伏不定的,请重新运行它)。

谢谢阅读!


译者简介:

经常混迹于 snapcraft.io,对 Ubuntu Core、Snaps 和 Snapcraft 有着浓厚的兴趣,并致力于将这些还在快速发展的新技术通过翻译或原创的方式介绍到中文世界。有兴趣的小伙伴也可以关注译者个人公众号: Snapcraft


via:https://blog.simos.info/how-to-create-a-snap-for-timg-with-snapcraft-on-ubuntu/

作者:Mi blog lah! 译者:Snapcrafter 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

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

备份

没有比备份数据更重要的了,如果您没有备份数据库,在它出事前这可能并不是什么问题。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

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

我们保留数据的“备份”的最后一个方法是使用 延迟副本 delayed replica 。这与其说是备份,不如说是保护。对于每个生产集群,我们有一个延迟 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 

故障转移

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

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

这个类生产环境的测试集群是一套复制环境,与我们的生产集群的各个方面都相同:硬件类型、操作系统、MySQL 版本、网络环境、VIP、puppet 配置、haproxy 设置 等。与生产集群唯一不同的是它不发送/接收生产流量。

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

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

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

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

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

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

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

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

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

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

automated master failover

测试失败怎么样?

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

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

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

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

出现:混乱测试

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

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

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

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

模式迁移

我们使用 gh-ost来运行实时 模式迁移 schema migration 。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

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

测试新版本

我们不断改进 gh-ost。我们的开发流程基于 git 分支,然后我们通过拉取请求(PR)来提供合并。

提交的 gh-ost 拉取请求(PR)通过持续集成(CI)进行基本的编译和单元测试。一旦通过,该 PR 在技术上就有资格合并,但更好的是它有资格通过 Heaven 进行部署。作为我们基础架构中的敏感组件,在其进入 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/

作者:tomkrouperShlomi Noach 译者:MonkeyDEcho 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Linux 基金会认证系统管理员(LFCS)的另一个有用的命令是 “sed”,最初表示 “流式编辑器” Streaming EDitor

“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 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

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

Man conducting orchestra

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

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

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

life container megabricks

容器编排提供了像乐高系统这样的基础设施 —— 开发人员可以提供如何构建应用程序的简单说明。编排引擎将知道如何运行它。这使得可以轻松创建同一应用程序的多个副本,跨越开发人员电脑、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 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 XO 公司,我们最初使用 Node 和 Ruby 构建相互连接的服务系统。我们享受 Node 带来的明显性能优势,以及可以访问已有的大型软件包仓库。我们也可以轻松地在公司内部发布并复用已有的插件和模块。极大地提高了开发效率,使得我们可以快速编写出可拓展的和可靠的应用。而且,庞大的 Node 社区使我们的工程师向开源软件贡献更加容易(比如 BunnyBusFelicity)。

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

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

大体上的差异

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

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

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

语法

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

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

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

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

  • 标点符号: Go 去除了所有多余的符号以提高效率和可读性。尽管 JavaScript 中需要符号的地方也不多(参见: Lisp),而且经常是可选的,但我更加喜欢 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 是强类型语言,所以你在初始化变量时可以使用 := 操作符来进行类型推断,以避免重复声明,而 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 个新软件包的惊人速度增长。这个数字可能看起来令人吃惊(确实是),但是需要注意的是,这些包许多是重复的,且质量不足以用在生产环境。 相比之下,Go 大约有 13 万个包。

Node 和 Go 包的数量

尽管 Node 和 Go 岁数相仿,但 JavaScript 使用更加广泛,并拥有巨大的开发者和开源社区。因为 Node 是为所有人开发的,并在开始的时候就带有一个强壮的包管理器,而 Go 是特地为 Google 开发的。下面的Spectrum 排行榜显示了当前流行的的顶尖 Web 开发语言。

Web 开发语言排行榜前 7 名

JavaScript 的受欢迎程度近年来似乎保持相对稳定,而 Go 一直在保持上升趋势

编程语言趋势

性能

如果你的主要关注点是速度呢?当今似乎人们比以前更重视性能的优化。用户不喜欢等待信息。 事实上,如果网页的加载时间超过 3 秒,40% 的用户会放弃访问您的网站

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

我运行了一些测试来比较 Node 和 Go 之间的性能。这些测试注重于语言提供的初级能力。如果我准备测试例如 HTTP 请求或者 CPU 密集型运算,我会使用 Go 语言级别的并发工具(goroutines/channels)。但是我更注重于各个语言提供的基本特性(参见 三种并发方法 了解关于 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

结果

这里的输家无疑是 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

结果

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

结果

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

判决

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

结论

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

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

资料

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


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

作者:John Stamatakos 译者:trnhoe 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出