在过去二十分钟里,他一直按着键盘按键,仿佛他正在为自己的生活而打字一样, ghostinushanka转向我, 眼神有些发狂,露出狡猾的微笑,“老兄,我想我明白了。
看看这个“-当他指向屏幕上的一个字符时-“我敢打赌,如果我们添加我刚刚发送给您的内容,“当他指向代码中的另一个位置时,“-不再没有错误。”
有点困惑和疲倦,我修改了我们已经计算了一段时间的sed表达式,保存了文件并运行systemctl varnish reload
。 错误消息不见了...
我的同事继续说道:“我已经与候选人交换了这些电子邮件,当他的笑容变得宽广而真诚的笑容时,“突然让我震惊的是,这确实是一个完全相同的问题!”
一切如何开始
本文假定您对bash,awk和systemd有所了解。 对Varnish的一些了解是有益的,但不是必需的。
示例片段中的时间戳已被删除。
与ghostinushanka合着。
在又一个温暖的秋天早晨,阳光透过墙壁大小的窗户照进来,一杯新鲜煮熟的含咖啡因的液体坐在键盘的侧面,耳机发出悦耳的交响曲,涵盖了机械键盘的沙沙声和积压的第一个条目在看板上,调皮地显示了命运票的标题“调查varnishreload sh: echo: I/O error
暂存中的sh: echo: I/O error
”。 每当涉及Varnish时,就不会有错误的余地,即使这一特定问题似乎并没有引起任何实际问题。
对于不熟悉varnishreload的用户来说 ,它只是一个shell脚本,用于重新加载Varnish缓存服务器的配置(也称为VCL)。
正如票证的标题所暗示的那样,其中一台登台计算机上遇到了错误,并且我很确定Varnish路由在登台环境中可以正常工作,因此我认为这必须是一个小问题。 只是将用户友好的输出消息写入封闭流中。 我拿到票,坚信我可以在30分钟之内将其标记为已解决,拍拍自己的背部以完成另一项平凡的任务,然后回到更重要的事情上。
以200kph的速度撞墙
在Debian Stretch上运行的一台受影响的服务器上打开varnishreload
文件,我发现一个Shell脚本少于200行。 简要阅读它,我发现没有什么危险可以阻止我从终端一遍又一遍地运行脚本。 毕竟,这是分阶段的,即使它崩溃了,也没有人会抱怨,嗯……就是太多了。 我运行脚本并进行观察,只是发现没有错误可以看到。 再经过几次重复运行,以确保没有任何额外的努力就无法重现该错误,并且我开始制定计划来调整和调整脚本的环境。 完全关闭脚本的STDOUT(使用> &-
)是否有帮助? 还是stderr? 两者都没有。
显然,systemd会以某种方式破坏环境,但是如何,……为什么呢? 我启动vim并编辑系统的varnishreload
,在shebang的正下方添加set -x
,希望详细的脚本运行输出能有所varnishreload
。
文件打了补丁,所以我重新加载了清漆,只是看到更改完全破坏了脚本...输出是一团糟,显示了大量的C样式代码,默认的回滚缓冲区不足以查找它的来源。 我感到困惑。 可以为shell脚本设置调试选项来中断它调用的程序吗? 不,不能。 外壳中的错误? 在我的脑海中,多种可能的情况在不同的方向疯狂运行。 一杯含咖啡因的饮料即刻完成,可以快速前往厨房补充食物,然后我们再次开始。 我打开文件并仔细查看shebang: #!/bin/sh
。
但是/bin/sh
肯定只是bash的符号链接,因此该脚本以POSIX兼容模式进行解释,对吗? 错了! Debian上的默认非交互式外壳是破折号,这正是/bin/sh
指向的内容 。
如果仅用于调试,我将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
但事实证明,第124行是相当顺利的。 我只能推测该错误是作为在第116行执行的多行命令的一部分而产生的。
那么,上面的子shell甚至产生什么存储在VCL_FILE
变量中? 在第一部分中,它将在行115上创建的VCL_SHOW
变量的内容发送到管道中。 那在那里发生了什么呢?
首先,它使用varnishadm
,这是Varnish安装的标准部分,用于配置Varnish,而无需重新启动它。 子命令vcl.show -v
用于将${VCL_NAME}
指定的整个VCL配置打印到STDOUT。
要显示当前活动的VCL配置以及仍在内存中的清漆路由的多个先前版本,可以使用另一个命令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}
现在包含Varnish的完整配置,到目前为止非常容易。 现在我终于明白了为什么带有set -x
的破折号输出看起来如此糟糕-它包含了所得清漆配置的内容。
这里重要的是,完整的VCL配置通常可以从多个文件中拼接在一起。 C风格的注释用于描述其他配置文件中包含配置文件的位置,这正是代码片段的下一行的全部含义。
文件注释的语法具有以下格式
数字在这里并不重要,我们感兴趣的是文件名。
那么从第116行开始的大量命令正在发生什么呢?
让我们分开。
该命令分为四个部分:
- 一个简单的
echo
,它打印出${VCL_SHOW}
的值
echo "$VCL_SHOW"
awk
查找第一个字段为“ //”,第二个字段为“ VCL.SHOW”的行(记录)。
指示Awk打印与这些模式匹配的第一行,然后立即停止处理。
awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}'
- 一个将空格分隔的字段读入五个变量的代码块。 第五个变量FILE获取行的其余部分。 最后,最后一个回显将打印
${FILE}
变量的内容。
{ read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" }.
- 由于第1步到第3步都被包含在一个子外壳中,因此
$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; echo}
awk | {read; echo}
构造会引起所有这些麻烦,为什么不使用其他东西呢? Awk并没有真正在单行代码上充分利用其全部功能,因此有多余的read
。
看到我们在前一周对sed
进行了内部讨论时,我想尝试一下新获得的技能并优化echo | awk | { read; echo }
echo | awk | { read; echo }
echo | awk | { read; echo }
转换为更简单的echo | sed
echo | sed
。 尽管这绝对不是进行调试的正确方法,但我认为我至少要尝试一下sed-fu,并且可能会在此过程中了解有关该问题的新知识。 在此过程中,我已经请同事-sed演讲的作者-帮助我提出更有效的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在首场比赛后退出的原因。
因此,varnishreload脚本的内容如下所示:
VCL_FILE="$(echo "$VCL_SHOW" | sed -En '\#// VCL.SHOW#{s#.*[0-9]+ [0-9]+ (.*)$#\1#p;q;};')"
上面的逻辑可以简洁地表示为:
如果一行与regex // VCL.SHOW
匹配,则贪婪地匹配包含该行上两个数字的文本,并捕获后面的内容。 发出捕获并退出。
很简单,不是吗?
我们对sed脚本及其替换的原始代码感到满意,我完成的所有测试运行均产生了预期的结果,因此我修改了服务器上的varnishreload
并再次触发了systemctl reload varnish
。 可怕的echo: write error: Broken pipe
在我们的脸上微笑。 闪烁的光标在终端的黑暗空白中等待新的命令输入...