掉进兔子洞:一个清漆重启错误的故事 - 第 1 部分

幽灵努山卡在前 20 分钟里,他不停地敲击按钮,仿佛他的生命就在于此,他转向我,眼神中带着半狂野的表情,狡猾地一笑——“伙计,我想我明白了。”

“看这里,”他指着屏幕上的一个符号说道,“我敢打赌,我的红帽子,如果我们添加我刚刚发给你的内容,”他指着另一段代码,“错误将不再显示。”

有点困惑和疲惫,我更改了我们已经工作了一段时间的 sed 语句,保存文件,然后运行 systemctl varnish reload。 错误信息消失了...

“我与候选人交换的电子邮件,”我的同事继续说道,他的傻笑变成了充满喜悦的真诚微笑,“我突然意识到这完全是同一个问题!”

这一切是如何开始的

本文假设您了解 bash、awk、sed 和 systemd 的工作原理。 了解清漆是首选,但不是必需的。
片段中的时间戳已更改。
写与 幽灵努山卡.
本文是两周前发表的英文原文的翻译; 翻译 博伊科登.

另一个温暖的秋日早晨,阳光透过全景窗户照射进来,一杯现煮的含咖啡因的饮料远离键盘,耳机中的机械键盘沙沙作响,播放着人们最喜欢的交响乐,看板板上待办事项列表中的第一个条目俏皮地闪烁着致命的标题“调查 varnishreload sh: echo: 舞台中的 I/O 错误”(调查舞台中的“varnishreload sh : echo: I/O 错误”) 。 说到清漆,没有也不可能有任何错误,即使它们不会导致任何问题,就像本例一样。

对于不熟悉的人 清漆重新加载,这是一个简单的 shell 脚本,用于重新加载配置 - 也称为 VCL。

正如票据标题所示,错误发生在阶段中的一台服务器上,并且由于我确信清漆在阶段中的路由工作正常,因此我认为这将是一个小错误。 因此,这只是一条进入已经关闭的输出流的消息。 我为自己拿了一张票,完全相信自己会在 30 分钟内将其标记为准备就绪,拍拍自己的肩膀,清除板上的下一个垃圾,然后回到更重要的事情上。

以 200 公里/小时的速度撞墙

打开文件 varnishreload,在其中一台运行 Debian Stretch 的服务器上,我看到了一个不到 200 行的 shell 脚本。

运行该脚本,我没有看到任何直接从终端运行多次时可能导致问题的内容。

毕竟这是一个阶段,就算打破了,也没有人会抱怨,嗯……也不过分。 我运行脚本并查看将写入终端的内容,但错误不再可见。

又运行了几次,以确保在没有额外努力的情况下我无法重现错误,并且我开始弄清楚如何更改此脚本并使其仍然抛出错误。

脚本可以阻止 STDOUT (使用 > &-)? 还是标准错误? 最终都没有成功。

显然 systemd 以某种方式改变了运行环境,但是如何改变,为什么呢?
我打开vim并编辑 varnishreload,添加 set -x 就在 shebang 下,希望调试脚本的输出能够带来一些启发。

文件已修复,所以我重新加载清漆,发现更改完全破坏了所有内容...排气完全是一团糟,里面有大量类似 C 的代码。 即使在终端中滚动也不足以找到它的开始位置。 我完全困惑了。 调试模式会影响脚本中运行的程序的工作吗? 不说废话。 壳里有bug? 几种可能的情景像蟑螂一样在我的脑海里飞向不同的方向。 一杯富含咖啡因的饮料立刻就空了,赶紧去厨房补充一下……我们走吧。 我打开脚本并仔细查看 shebang: #!/bin/sh.

/bin/sh - 这只是一个 bash 符号链接,因此脚本以 POSIX 兼容模式解释,对吧? 它不在那里! Debian 上的默认 shell 是 dash,这正是 召回 /bin/sh.

# ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Jan 24  2017 /bin/sh -> dash

为了试验,我将shebang更改为 #!/bin/bash, 删除 set -x 并再次尝试。 最后,在随后重新加载清漆时,输出中出现了一个可以容忍的错误:

Jan 01 12:00:00 hostname varnishreload[32604]: /usr/sbin/varnishreload: line 124: echo: write error: Broken pipe
Jan 01 12:00:00 hostname varnishreload[32604]: VCL 'reload_20190101_120000_32604' compiled

124路,来了!

114 find_vcl_file() {
115         VCL_SHOW=$(varnishadm vcl.show -v "$VCL_NAME" 2>&1) || :
116         VCL_FILE=$(
117                 echo "$VCL_SHOW" |
118                 awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}' | {
119                         # all this ceremony to handle blanks in FILE
120                         read -r DELIM VCL_SHOW INDEX SIZE FILE
121                         echo "$FILE"
122                 }
123         ) || :
124
125         if [ -z "$VCL_FILE" ]
126         then
127                 echo "$VCL_SHOW" >&2
128                 fail "failed to get the VCL file name"
129         fi
130
131         echo "$VCL_FILE"
132 }

但事实证明,第 124 行相当空洞,毫无意义。 我只能假设该错误是作为从第 116 行开始的多行的一部分发生的。
最终写入变量的内容 VCL_FILE 执行上述子 shell 的结果是什么?

一开始,它发送变量的内容 VLC_SHOW,在第 115 行创建,通过管道传递到下一个命令。 然后那里会发生什么?

首先,它使用 varnishadm,它是varnish安装包的一部分,用于配置varnish而无需重新启动。

子命令 vcl.show -v 用于输出指定的整个VCL配置 ${VCL_NAME},到标准输出。

要显示当前活动的VCL配置以及仍在内存中的几个以前版本的varnish路由配置,可以使用命令 varnishadm vcl.list,其输出将类似于以下内容:

discarded   cold/busy       1 reload_20190101_120000_11903
discarded   cold/busy       2 reload_20190101_120000_12068
discarded   cold/busy       16 reload_20190101_120000_12259
discarded   cold/busy       16 reload_20190101_120000_12299
discarded   cold/busy       28 reload_20190101_120000_12357
active      auto/warm       32 reload_20190101_120000_12397
available   auto/warm       0 reload_20190101_120000_12587

变量值 ${VCL_NAME} 在脚本的另一部分设置 varnishreload 当前活动 VCL 的名称(如果有)。 在这种情况下,它将是“reload_20190101_120000_12397”。

好吧,变量。 ${VCL_SHOW} 包含清漆的完整配置,到目前为止很清楚。 现在我终于明白为什么破折号输出 set -x 事实证明如此糟糕 - 它包含了最终配置的内容。

重要的是要理解,完整的 VCL 配置通常可以由多个文件拼凑在一起。 C 风格注释用于定义一个配置文件包含在另一个配置文件中的位置,而这正是以下代码片段行的全部内容。
描述包含文件的注释语法具有以下格式:

// VCL.SHOW <NUM> <NUM> <FILENAME>

在这种情况下,数字并不重要,我们感兴趣的是文件名。

那么从第 116 行开始的命令沼泽中会发生什么呢?
让我们面对现实吧。
该命令由四部分组成:

  1. 简单 echo,显示变量的值 ${VCL_SHOW}
    echo "$VCL_SHOW"
  2. awk,它查找一行(记录),其中第一个字段在分割文本后将是“//”,第二个字段将是“VCL.SHOW”。
    awk 将写出与这些模式匹配的第一行,然后立即停止处理。

    awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}'
  3. 将由空格分隔的字段值存储到五个变量中的代码块。 第五个变量 FILE 接收该行的其余部分。 最后,最后一个echo写出变量的内容 ${FILE}.
    { read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" }
  4. 由于所有步骤 1 到 3 都包含在子 shell 中,因此值的输出 $FILE 将被写入变量 VCL_FILE.

正如第 119 行的注释所暗示的那样,这唯一的目的是可靠地处理 VCL 引用名称中含有空格字符的文件的情况。

我已经注释掉了原来的处理逻辑 ${VCL_FILE} 并试图改变命令的顺序,但没有任何结果。 一切对我来说都很顺利,在启动服务的情况下,它给出了一个错误。

手动运行脚本时,该错误似乎根本无法重现,而估计的 30 分钟已经结束了六次,此外,出现了更高优先级的任务,将其余情况推到一边。 这周剩下的时间充满了各种各样的任务,只有关于 sed 的演讲和对候选人的面试稍微稀释了一些时间。 错误问题在 varnishreload 无法挽回地消失在时间的沙滩上。

你所谓的 sed-fu...实际上...垃圾

接下来的一周有相当空闲的一天,所以我决定再次购买这张票。 我希望在我的大脑中,某个后台进程一直在寻找这个问题的解决方案,这次我一定会明白出了什么问题。

由于上次只是更改代码没有帮助,我决定从第 116 行开始重写它。 无论如何,现有的代码都很愚蠢。 并且完全没有必要使用 read.

再次查看错误:
sh: echo: broken pipe - 在这个命令中,echo 有两个地方,但我怀疑第一个地方更有可能是罪魁祸首(好吧,或者至少是同谋)。 awk 也不能激发信心。 万一真的是这样 awk | {read; echo} 设计导致了所有这些问题,为什么不更换呢? 这个一行命令并没有使用 awk 的所有功能,甚至这个额外的功能 read 在附录中。

自上周以来就有报道称 sed我想尝试我新学到的技能并简化 echo | awk | { read; echo} 变成更容易理解的 echo | sed。 虽然这绝对不是捕获 bug 的最佳方法,但我想我至少应该尝试一下 sed-fu,也许可以学到一些关于这个问题的新东西。 在此过程中,我请我的同事(sed talk 编写者)帮助我想出一个更高效的 sed 脚本。

我把内容丢了 varnishadm vcl.show -v "$VCL_NAME" 到一个文件,这样我就可以专注于编写 sed 脚本,而无需担心服务重新启动的麻烦。

关于 sed 如何处理输入的简要描述可以在 他的 GNU 手册。 在 sed 源代码中,符号 n 明确指定为行分隔符。

在我同事的建议下,经过几次迭代,我们编写了一个 sed 脚本,它给出了与整个原始第 116 行相同的结果。

以下是包含输入数据的示例文件:

> cat vcl-example.vcl
Text
// VCL.SHOW 0 1578 file with 3 spaces.vcl
More text
// VCL.SHOW 0 1578 file.vcl
Even more text
// VCL.SHOW 0 1578 file with TWOspaces.vcl
Final text

从上面的描述中可能不太明显,但我们只对第一条评论感兴趣 // VCL.SHOW,并且输入数据中可以有多个。 这就是为什么原始 awk 在第一个匹配后终止的原因。

# шаг первый, вывести только строки с комментариями
# используя возможности sed, определяется символ-разделитель с помощью конструкции '#' вместо обычно используемого '/', за счёт этого не придётся экранировать косые в искомом комментарии
# определяется регулярное выражение “// VCL.SHOW”, для поиска строк с определенным шаблоном
# флаг -n позаботится о том, чтобы sed не выводил все входные данные, как он это делает по умолчанию (см. ссылку выше)
# -E позволяет использовать расширенные регулярные выражения
> cat vcl-processor-1.sed
#// VCL.SHOW#p
> sed -En -f vcl-processor-1.sed vcl-example.vcl
// VCL.SHOW 0 1578 file with 3 spaces.vcl
// VCL.SHOW 0 1578 file.vcl
// VCL.SHOW 0 1578 file with TWOspaces.vcl

# шаг второй, вывести только имя файла
# используя команду “substitute”, с группами внутри регулярных выражений, отображается только нужная группa
# и это делается только для совпадений, ранее описанного поиска
> cat vcl-processor-2.sed
#// VCL.SHOW# {
    s#.* [0-9]+ [0-9]+ (.*)$#1#
    p
}
> sed -En -f vcl-processor-2.sed vcl-example.vcl
file with 3 spaces.vcl
file.vcl
file with TWOspaces.vcl

# шаг третий, получить только первый из результатов
# как и в случае с awk, добавляется немедленное завершения после печати первого найденного совпадения
> cat vcl-processor-3.sed
#// VCL.SHOW# {
    s#.* [0-9]+ [0-9]+ (.*)$#1#
    p
    q
}
> sed -En -f vcl-processor-3.sed vcl-example.vcl
file with 3 spaces.vcl

# шаг четвертый, схлопнуть всё в однострочник, используя двоеточия для разделения команд
> sed -En -e '#// VCL.SHOW#{s#.* [0-9]+ [0-9]+ (.*)$#1#p;q;}' vcl-example.vcl
file with 3 spaces.vcl

因此 varnishreload 脚本的内容将如下所示:

VCL_FILE="$(echo "$VCL_SHOW" | sed -En '#// VCL.SHOW#{s#.*[0-9]+ [0-9]+ (.*)$#1#p;q;};')"

上述逻辑可以概括为:
如果字符串与正则表达式匹配 // VCL.SHOW,然后贪婪地吞噬该行中包含两个数字的文本,并保存此操作后剩下的内容。 发出存储的值并结束程序。

很简单,不是吗?

我们对 sed 脚本以及它替换了所有原始代码的事实感到满意。 我所有的测试都给出了期望的结果,所以我更改了服务器上的“varnishreload”并再次运行 systemctl reload varnish。 肮脏的错误 echo: write error: Broken pipe 又当着我们的面笑了。 一个闪烁的光标正在等待在终端的黑暗空间中输入新命令......

来源: habr.com

添加评论