Jatuh ke Lubang Kelinci: Kisah satu kesalahan reboot pernis - Bagian 1

hantuinushanka, yang telah menekan tombol-tombol selama 20 menit sebelumnya seolah-olah hidupnya bergantung padanya, menoleh ke arahku dengan tatapan agak liar di matanya dan seringai licik - “Bung, sepertinya aku mengerti.”

“Lihat di sini,” katanya, menunjuk ke salah satu simbol di layar, “Saya yakin topi merah saya bahwa jika kita menambahkan di sini apa yang baru saja saya kirimkan kepada Anda,” sambil menunjuk ke bagian kode lain, “kesalahan tidak akan lagi terjadi. akan ditampilkan."

Sedikit bingung dan lelah, saya memodifikasi ekspresi sed yang telah kami kerjakan selama beberapa waktu, menyimpan file dan menjalankannya systemctl varnish reload. Pesan kesalahan telah hilang...

“Email yang saya tukarkan dengan kandidat tersebut,” rekan saya melanjutkan, ketika seringainya berubah menjadi senyum kebahagiaan yang tulus, “Tiba-tiba saya sadar bahwa ini adalah masalah yang sama!”

Bagaimana semua ini dimulai

Artikel ini mengasumsikan pemahaman tentang cara kerja bash, awk, sed, dan systemd. Pengetahuan tentang pernis lebih disukai, tetapi tidak wajib.
Stempel waktu dalam cuplikan telah diubah.
Ditulis dengan hantuinushanka.
Teks ini adalah terjemahan dari teks asli yang diterbitkan dalam bahasa Inggris dua minggu lalu; terjemahan boikoden.

Matahari bersinar melalui jendela panorama di pagi musim gugur yang hangat, secangkir minuman segar kaya kafein terletak jauh dari keyboard, simfoni suara favorit Anda terdengar di headphone Anda, meredam gemerisik keyboard mekanis, dan entri pertama dalam daftar tiket backlog di papan Kanban bersinar dengan judul yang menentukan "Selidiki vernishreload" sh: echo: I/O error in staging" (Selidiki "varnishreload sh: echo: I/O error" dalam staging). Dalam hal pernis, tidak ada ruang untuk kesalahan, meskipun kesalahan tersebut tidak menimbulkan masalah seperti dalam kasus ini.

Bagi yang belum familiar pernisreload, ini adalah skrip shell sederhana yang digunakan untuk memuat ulang konfigurasi pernis - juga disebut VCL.

Sesuai dengan judul tiketnya, kesalahan terjadi pada salah satu server di panggung, dan karena saya yakin perutean pernis di panggung berfungsi dengan baik, saya berasumsi bahwa ini adalah kesalahan kecil. Jadi, hanya sebuah pesan yang berakhir di aliran keluaran yang sudah ditutup. Saya mengambil tiket itu untuk diri saya sendiri, dengan keyakinan penuh bahwa saya akan menandainya sebagai siap dalam waktu kurang dari 30 menit, menepuk punggung saya karena sudah membersihkan papan dari sampah-sampah lain dan kembali ke hal-hal yang lebih penting.

Menabrak tembok dengan kecepatan 200 km/jam

Membuka file varnishreload, di salah satu server yang menjalankan Debian Stretch, saya melihat skrip shell yang panjangnya kurang dari 200 baris.

Setelah membaca skripnya, saya tidak melihat apa pun yang dapat mengakibatkan masalah saat menjalankannya berkali-kali langsung dari terminal.

Bagaimanapun, ini adalah sebuah panggung, meskipun rusak, tidak ada yang akan mengeluh, yah... tidak terlalu banyak. Saya menjalankan skrip dan melihat apa yang akan ditulis ke terminal, tetapi kesalahan tidak lagi terlihat.

Beberapa langkah lagi untuk memastikan bahwa saya tidak dapat mereproduksi kesalahan tanpa upaya tambahan apa pun, dan saya mulai mencari cara untuk mengubah skrip ini dan membuatnya tetap menimbulkan kesalahan.

Bisakah skrip mengganti STDOUT (menggunakan > &-)? Atau STDERR? Pada akhirnya tidak ada satu pun yang berhasil.

Rupanya systemd entah bagaimana mengubah lingkungan startup, tapi bagaimana, dan mengapa?
Saya membuka vim dan mengedit varnishreload, menambahkan set -x tepat di bawah shebang, berharap keluaran debug dari skrip akan memberi sedikit pencerahan.

File telah diperbaiki, jadi saya memuat ulang pernis dan melihat bahwa perubahan tersebut benar-benar merusak segalanya... Knalpotnya benar-benar berantakan, di dalamnya terdapat banyak sekali kode mirip C. Bahkan menggulir terminal saja tidak cukup untuk menemukan di mana ia dimulai. Saya benar-benar bingung. Bisakah mode debugging memengaruhi pengoperasian program yang diluncurkan dalam skrip? Tidak, itu tidak masuk akal. Bug di cangkangnya? Beberapa kemungkinan skenario berkecamuk di kepala saya seperti kecoak ke berbagai arah. Secangkir minuman berkafein langsung dikosongkan, perjalanan singkat ke dapur untuk mengisi kembali stok dan... berangkatlah. Saya membuka skrip dan melihat lebih dekat shebangnya: #!/bin/sh.

/bin/sh - ini hanya symlink ke bash, jadi skrip diinterpretasikan dalam mode yang kompatibel dengan POSIX, bukan? Tidak begitu! Shell default di Debian adalah tanda hubung, dan seperti itulah tampilannya. mengacu /bin/sh.

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

Sebagai ujian, saya mengubah shebang menjadi #!/bin/bash, dihapus set -x dan mencoba lagi. Akhirnya, setelah pernis di-boot ulang berikutnya, kesalahan yang dapat ditoleransi muncul pada keluaran:

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

Baris 124, ini dia!

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 }

Namun ternyata jalur 124 cukup kosong dan tidak ada minat. Saya hanya dapat berasumsi bahwa kesalahan terjadi sebagai bagian dari string multiline yang dimulai pada baris 116.
Apa yang pada akhirnya ditulis ke variabel? VCL_FILE sebagai hasil dari mengeksekusi sub-shell di atas?

Pada awalnya, ia mengirimkan isi variabel VLC_SHOW, dibuat pada baris 115, mengikuti perintah melalui pipa. Lalu apa yang terjadi di sana?

Pertama, digunakan di sana varnishadm, yang merupakan bagian dari paket pemasangan pernis, untuk memasang pernis tanpa memulai ulang.

Sub-tim vcl.show -v digunakan untuk menampilkan seluruh konfigurasi VCL yang ditentukan dalam ${VCL_NAME}, ke STDOUT.

Untuk menampilkan konfigurasi VCL yang aktif saat ini, serta beberapa konfigurasi routing pernis versi sebelumnya yang masih ada di memori, Anda dapat menggunakan perintah varnishadm vcl.list, outputnya akan serupa dengan yang di bawah ini:

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

Nilai variabel ${VCL_NAME} dipasang di bagian lain skrip varnishreload dengan nama VCL yang sedang aktif, jika ada. Dalam hal ini akan menjadi “reload_20190101_120000_12397”.

Bagus, bervariasi ${VCL_SHOW} berisi konfigurasi lengkap untuk pernis, jelas untuk saat ini. Sekarang saya akhirnya mengerti mengapa keluaran dasbornya seperti itu set -x ternyata sangat rusak - termasuk isi konfigurasi yang dihasilkan.

Penting untuk dipahami bahwa konfigurasi VCL yang lengkap sering kali dapat dibuat dari beberapa file. Komentar gaya C digunakan untuk mengidentifikasi di mana file konfigurasi tertentu telah disertakan dalam file konfigurasi lain, dan itulah inti dari cuplikan kode berikut.
Sintaks untuk komentar yang menjelaskan file yang disertakan adalah dalam format berikut:

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

Angka tidak penting dalam konteks ini, kami tertarik pada nama file.

Apa yang akhirnya terjadi di rawa perintah yang dimulai pada baris 116?
Mari kita cari tahu.
Tim ini terdiri dari empat bagian:

  1. Sederhana echo, yang mencetak nilai variabel ${VCL_SHOW}
    echo "$VCL_SHOW"
  2. awk, yang mencari baris (record) dengan field pertama, setelah teksnya dipecahkan, adalah “//”, dan yang kedua adalah “VCL.SHOW”.
    Awk akan menuliskan baris pertama yang cocok dengan pola ini dan kemudian segera menghentikan pemrosesan.

    awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}'
  3. Blok kode yang menyimpan nilai bidang menjadi lima variabel, dipisahkan dengan spasi. Variabel FILE kelima menerima sisa baris. Terakhir, echo terakhir menuliskan isi variabel ${FILE}.
    { read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" }
  4. Karena semua langkah 1 hingga 3 terlampir dalam subkulit, menghasilkan nilai $FILE akan ditulis ke variabel VCL_FILE.

Seperti yang disarankan oleh komentar pada baris 119, ini hanya bertujuan untuk menangani kasus dengan andal di mana VCL akan mereferensikan file dengan spasi di namanya.

Saya telah mengomentari logika pemrosesan asli ${VCL_FILE} dan mencoba mengubah urutan perintah, tetapi tidak menghasilkan apa-apa. Semuanya berfungsi dengan baik bagi saya, tetapi ketika saya memulai layanan, terjadi kesalahan.

Tampaknya kesalahan tersebut tidak dapat direproduksi saat menjalankan skrip secara manual, sementara waktu yang seharusnya 30 menit telah berakhir enam kali dan, sebagai tambahan, tugas dengan prioritas lebih tinggi telah muncul, mengesampingkan masalah lain. Sisa minggu itu diisi dengan berbagai tugas dan hanya sedikit diencerkan dengan laporan tentang sed dan wawancara dengan seorang kandidat. Masalah dengan kesalahan dalam varnishreload telah hilang tak dapat diperbaiki lagi di pasir waktu.

Apa yang kamu sebut sed-fu... sebenarnya... sampah

Minggu berikutnya saya mempunyai satu hari yang cukup bebas, jadi saya memutuskan untuk menangani tiket ini lagi. Saya berharap di otak saya, beberapa proses latar belakang telah mencari solusi untuk masalah ini selama ini, dan kali ini saya pasti mengerti apa yang sedang terjadi.

Karena mengubah kode saja tidak membantu terakhir kali, saya memutuskan untuk menulis ulang mulai dari baris 116. Bagaimanapun, kode yang ada itu bodoh. Dan sama sekali tidak perlu menggunakannya read.

Melihat kesalahannya lagi:
sh: echo: broken pipe — echo muncul di dua tempat dalam perintah ini, tapi saya curiga yang pertama kemungkinan besar adalah pelakunya (atau setidaknya kaki tangan). Awk juga tidak menginspirasi kepercayaan diri. Dan seandainya itu benar awk | {read; echo} desainnya menyebabkan semua masalah ini, mengapa tidak menggantinya? Perintah satu baris ini tidak menggunakan semua fitur awk, dan bahkan fitur tambahan ini read Selain itu.

Sejak minggu lalu ada laporan tentang sed, saya ingin mencoba keterampilan yang baru saya peroleh dan menyederhanakannya echo | awk | { read; echo} menjadi lebih mudah dimengerti echo | sed. Meskipun ini jelas bukan pendekatan terbaik untuk mengidentifikasi bug, saya pikir setidaknya saya akan mencoba sed-fu saya dan mungkin mempelajari sesuatu yang baru tentang masalah tersebut. Sepanjang jalan, saya meminta rekan saya, penulis sed talk, untuk membantu saya membuat skrip sed yang lebih efisien.

Aku menjatuhkan isinya varnishadm vcl.show -v "$VCL_NAME" ke sebuah file, jadi saya bisa fokus menulis skrip sed tanpa perlu repot melakukan reboot layanan.

Deskripsi singkat tentang bagaimana sed memproses input dapat ditemukan manual GNU-nya. Di sed sumber simbol n secara eksplisit ditentukan sebagai pemisah garis.

Dalam beberapa langkah dan dengan rekomendasi rekan saya, kami menulis skrip sed yang memberikan hasil yang sama dengan keseluruhan baris asli 116.

Di bawah ini adalah contoh file dengan data input:

> 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

Ini mungkin tidak terlihat jelas dari uraian di atas, tetapi kami hanya tertarik pada komentar pertama // VCL.SHOW, dan mungkin ada beberapa di antaranya di data masukan. Inilah sebabnya mengapa awk asli berakhir setelah pertandingan pertama.

# шаг первый, вывести только строки с комментариями
# используя возможности 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

Jadi, isi script pernisreload akan terlihat seperti ini:

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

Logika di atas dapat diungkapkan secara singkat sebagai berikut:
Jika string cocok dengan ekspresi reguler // VCL.SHOW, lalu dengan rakus melahap teks yang menyertakan kedua angka di baris ini, dan simpan semua yang tersisa setelah operasi ini. Keluarkan nilai yang disimpan dan akhiri program.

Sederhana, bukan?

Kami senang dengan skrip sed dan fakta bahwa skrip tersebut menggantikan semua kode asli. Semua pengujian saya memberikan hasil yang diinginkan, jadi saya mengubah “varnishreload” di server dan menjalankannya lagi systemctl reload varnish. Kesalahan buruk echo: write error: Broken pipe tertawa di wajah kami lagi. Kursor yang berkedip sedang menunggu perintah baru dimasukkan dalam kegelapan terminal...

Sumber: www.habr.com

Tambah komentar