Falling Down the Rabbit Hole: داستان یک خطای راه اندازی مجدد لاک - قسمت 1

گوستینوشانکا، بعد از کوبیدن دکمه ها در 20 دقیقه قبل که انگار زندگی اش به آن بستگی دارد، با حالتی نیمه وحشی در چشمانش و پوزخندی حیله گر به سمت من برمی گردد - "رفیق، فکر کنم فهمیدم."

او با اشاره به یکی از شخصیت‌های روی صفحه می‌گوید: «اینجا را نگاه کن، «من شرط می‌بندم که اگر آنچه را که برایت فرستادم اینجا اضافه کنیم» - با اشاره به بخش دیگری از کد - «این خطا دیگر وجود ندارد. نمایش داده شود."

کمی متحیر و خسته، دستور sed را که مدتی روی آن کار می‌کردیم تغییر می‌دهم، فایل را ذخیره می‌کنم و اجرا می‌کنم. systemctl varnish reload. پیام خطا ناپدید شد...

«ایمیل هایی که با نامزد رد و بدل کردم»، همکارم در حالی که پوزخند او به لبخندی واقعی و پر از شادی تبدیل می شد، ادامه داد: «ناگهان متوجه شدم که این دقیقاً همان مشکل است!»

چگونه همه چیز شروع شد

این مقاله درک نحوه عملکرد bash، awk، sed و systemd را فرض می کند. آشنایی با لاک ترجیح داده می شود اما الزامی نیست.
مهرهای زمانی در قطعه ها تغییر کرده است.
نوشته شده با گوستینوشانکا.
این متن ترجمه ای است از نسخه اصلی که دو هفته پیش به زبان انگلیسی منتشر شده است. ترجمه boyikoden.

در یکی دیگر از صبح‌های گرم پاییزی، خورشید از پنجره‌های پانوراما می‌درخشد، یک فنجان نوشیدنی کافئین‌دار تازه دم‌شده کنار صفحه‌کلید قرار دارد، سمفونی مورد علاقه صداها در هدفون بر سر صدای خش‌خش کیبوردهای مکانیکی پخش می‌شود، و اولین ورودی در فهرست بلیت‌های عقب‌افتاده روی تابلوی کانبان با عنوان سرنوشت‌ساز «بررسی varnishreload sh: echo: خطای I/O در مرحله‌بندی» («varnishreload sh: echo: I/O error» در مرحله‌بندی را بررسی کنید) می‌درخشد. در مورد لاک زدن، هیچ اشتباهی وجود ندارد و نمی تواند وجود داشته باشد، حتی اگر مانند این مورد مشکلی ایجاد نکند.

برای کسانی که آشنایی ندارند بارگیری لاک الکل، این یک اسکریپت پوسته ساده است که برای بارگذاری مجدد پیکربندی استفاده می شود لاک زدن - همچنین VCL نامیده می شود.

همانطور که از عنوان بلیط پیداست، خطا در یکی از سرورهای مرحله رخ داده است، و از آنجایی که من مطمئن بودم که مسیریابی وارنیش در استیج به درستی کار می کند، تصور کردم که این یک اشتباه جزئی است. بنابراین، فقط یک پیام است که وارد یک جریان خروجی از قبل بسته شده است. بلیطی برای خودم می‌گیرم، با اطمینان کامل که در کمتر از 30 دقیقه آن را آماده می‌کنم، برای پاک کردن زباله‌های بعدی روی شانه‌ام می‌زنم و به چیزهای مهم‌تر برمی‌گردم.

برخورد با دیوار با سرعت 200 کیلومتر بر ساعت

باز کردن یک فایل varnishreload، در یکی از سرورهایی که Debian Stretch را اجرا می کند، یک پوسته اسکریپت کمتر از 200 خط دیدم.

با اجرای اسکریپت، چیزی ندیدم که هنگام اجرای چندین بار مستقیماً از ترمینال، مشکلی ایجاد کند.

بالاخره این یک مرحله است، حتی اگر شکسته شود، هیچ کس شکایت نمی کند، خوب ... نه زیاد. من اسکریپت را اجرا می کنم و می بینم که چه چیزی در ترمینال نوشته می شود، اما خطاها دیگر قابل مشاهده نیستند.

چند بار دیگر اجرا می‌شود تا مطمئن شوم که نمی‌توانم خطا را بدون تلاش اضافی بازتولید کنم، و شروع به کشف نحوه تغییر این اسکریپت و ایجاد خطا در آن می‌کنم.

آیا اسکریپت می تواند STDOUT (با استفاده از > &-)؟ یا STDERR؟ هیچ کدام در نهایت کار نکردند.

واضح است که systemd به نوعی محیط اجرا را تغییر می دهد، اما چگونه و چرا؟
vim رو روشن میکنم و ویرایش میکنم varnishreload، اضافه كردن set -x درست در زیر shebang، امیدواریم که اشکال زدایی خروجی اسکریپت کمی روشن شود.

فایل ثابت است، بنابراین من دوباره وارنیش را بارگذاری می کنم و می بینم که تغییر به طور کامل همه چیز را شکست ... اگزوز کاملاً به هم ریخته است، با هزاران کد C مانند. حتی پیمایش در ترمینال برای یافتن نقطه شروع آن کافی نیست. من کاملا گیج هستم. آیا حالت اشکال زدایی می تواند بر کار برنامه های اجرا شده در یک اسکریپت تأثیر بگذارد؟ نه، مزخرف اشکال در پوسته؟ چندین سناریو ممکن مانند سوسک ها در جهات مختلف در سرم پرواز می کنند. یک فنجان نوشیدنی پر از کافئین فوراً خالی می شود، یک سفر سریع به آشپزخانه برای تامین مجدد و... بیایید برویم. فیلمنامه را باز می‌کنم و نگاه دقیق‌تری به شبنگ می‌اندازم: #!/bin/sh.

/bin/sh - این فقط یک پیوند نمادین bash است، بنابراین اسکریپت در حالت سازگار با POSIX تفسیر می شود، درست است؟ آنجا نبود! پوسته پیش‌فرض در دبیان dash است که دقیقاً همان چیزی است اشاره دارد /bin/sh.

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

به خاطر محاکمه، شبنگ را به تغییر دادم #!/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 در نتیجه اجرای زیر پوسته فوق؟

در ابتدا محتویات متغیر را ارسال می کند VLC_SHOW، ایجاد شده در خط 115، به دستور بعدی از طریق لوله. و سپس در آنجا چه اتفاقی می افتد؟

اول، استفاده می کند varnishadm، که بخشی از بسته نصب لاک است، برای پیکربندی لاک بدون راه اندازی مجدد.

فرمان فرعی vcl.show -v برای خروجی کل پیکربندی VCL مشخص شده در خروجی استفاده می شود ${VCL_NAME}، به 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} شامل پیکربندی کامل برای لاک، تا کنون روشن است. حالا بالاخره متوجه شدم که چرا خروجی خط تیره با 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 - در این دستور، اکو در دو مکان است، اما من گمان می کنم که اولی مقصر بیشتر باشد (خوب، یا حداقل همدست). Awk نیز اعتماد به نفس را القا نمی کند. و در صورتی که واقعا اینطور باشد awk | {read; echo} طراحی منجر به همه این مشکلات می شود، چرا آن را جایگزین نکنید؟ این دستور یک خطی از تمام ویژگی های awk و حتی این اضافی استفاده نمی کند read در ضمیمه

از هفته گذشته گزارشی در مورد sedمن می خواستم مهارت های تازه به دست آمده ام را امتحان کنم و ساده کنم echo | awk | { read; echo} قابل درک تر echo | sed. در حالی که این قطعاً بهترین روش برای یافتن باگ نیست، فکر کردم حداقل sed-fu خود را امتحان کنم و شاید چیز جدیدی در مورد مشکل یاد بگیرم. در طول راه، از همکارم، نویسنده sed talk خواهش کردم که به من کمک کند تا فیلمنامه sed کارآمدتری پیدا کنم.

من مطالب را رها کردم varnishadm vcl.show -v "$VCL_NAME" به یک فایل، بنابراین من می توانم بر روی نوشتن اسکریپت sed تمرکز کنم بدون هیچ زحمتی در راه اندازی مجدد سرویس.

شرح مختصری از اینکه دقیقا چگونه sed ورودی را کنترل می کند را می توان در آن یافت راهنمای گنو او. در منابع 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

اضافه کردن نظر