掉進兔子洞:一個清漆重啟錯誤的故事 - 第 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 又當著我們的面笑了。 一個閃爍的光標正在等待在終端的黑暗空間中輸入新命令......

來源: www.habr.com

添加評論