下兔子洞:一个varnishreload错误的故事-第1部分

在过去二十分钟里,他一直按着键盘按键,仿佛他正在为自己的生活而打字一样, 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 指向的内容


 # 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行执行的多行命令的一部分而产生的。


那么,上面的子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风格的注释用于描述其他配置文件中包含配置文件的位置,这正是代码片段的下一行的全部含义。


文件注释的语法具有以下格式


 // 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获取行的其余部分。 最后,最后一个回显将打印${FILE}变量的内容。
     { read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" }. 
  4. 由于第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在首场比赛后退出的原因。


 # step 1, capture just the comment lines # using sed capability to specify delimiter character with '\#' instead of the commonly used '/' so there is no need to escape slashes themselves # and the “address” capability defined as regex “// VCL.SHOW” to search for lines with specific pattern # -n flag makes sure that the sed does not print all as it does by default (see above link) # -E switches to the extended regex > 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 # step 2, only print out the file name # using the “substitute” command with regex capture groups to print just that group # and this is done only for the matches of the previous search > 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 # step 3, make sure to only get the first result # same as with the awk before, add an immediate exit after the first processed match is printed > 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 # step 4, wrap it up into a one-liner using the colon to separate commands > 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;};')" 

上面的逻辑可以简洁地表示为:
如果一行与regex // VCL.SHOW匹配,则贪婪地匹配包含该行上两个数字的文本,并捕获后面的内容。 发出捕获并退出。


很简单,不是吗?


我们对sed脚本及其替换的原始代码感到满意,我完成的所有测试运行均产生了预期的结果,因此我修改了服务器上的varnishreload并再次触发了systemctl reload varnish 。 可怕的echo: write error: Broken pipe在我们的脸上微笑。 闪烁的光标在终端的黑暗空白中等待新的命令输入...

Source: https://habr.com/ru/post/zh-CN475698/


All Articles