برنامه نویسی

یافتن سوزن در انبار کاه: تعمیر دروازه بد مرموز

معرفی

در سازمان کنونی من، ما برنامه‌های بسیاری را در پشت دروازه API اجرا می‌کنیم که با استفاده از REST API با دنیای خارج تعامل دارد. برای ردیابی خرابی‌های سمت سرور، ما یک سیستم هشدار ایجاد کردیم که هر بار که خطای 5XX را دریافت می‌کنیم، تلنگر می‌دهد.

به محض اینکه سیستم هشدار را فعال کردیم، متوجه شدیم که چند خطای HTTP 502 “Bad Gateway” هر روز اما به طور متناوب رخ می دهد.

مطابق با RFC7231، کد وضعیت 502 (Bad Gateway) نشان می‌دهد که سرور در حالی که به عنوان دروازه یا پروکسی عمل می‌کند، پاسخ نامعتبری را از سرور ورودی که به آن دسترسی داشت در حین تلاش برای انجام درخواست دریافت کرده است.

با اطلاعات بسیار کمی شناخته شده، من شروع به تعقیب RCA برای این شکست کردم.

اشکال زدایی

تصویر

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

ما همچنین یک سرور پراکسی داریم که بین دروازه API ما و برنامه قرار دارد، بنابراین گزارش‌های آن را نیز بررسی کردم و خوشبختانه، توانستم درخواست را در آنجا ردیابی کنم.

من درخواست را مشاهده کردم علیرغم اینکه از دروازه API آمدم، پاسخ ارسال شده 502 بود، حتی بدون ارتباط با برنامه من.

در مرحله بعد، زمانی که چنین پاسخی ارسال شد، توان عملیاتی را بررسی کردم زیرا فکر می‌کردم ممکن است محدودیتی در تعداد درخواست‌های همزمان وجود داشته باشد که برنامه ما می‌تواند رسیدگی کند. اما نتیجه باز هم تعجب آور بود زیرا حتی زمانی که توان عملیاتی کمتر از 1000 TPM (معاملات در دقیقه) بود، خطای 502 همچنان ادامه داشت و ما قبلاً مشاهده کرده بودیم که برنامه هایمان حتی در TPM بیش از 5000 به خوبی کار می کنند.

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

غواصی در اتصال TCP

در حوزه ارتباطات وب، HTTP (پروتکل انتقال ابرمتن) یک اتصال منحصر به فرد TCP (پروتکل کنترل انتقال) را برای هر درخواست آغاز می کند. راه اندازی یک اتصال TCP به یک فرآیند سه مرحله ای دست دادن با سرور قبل از انتقال داده نیاز دارد. هنگامی که انتقال داده به پایان رسید، یک فرآیند چهار مرحله ای برای پایان دادن به همان اتصال پیاده سازی می شود.

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

این مکانیسم به عنوان HTTP keep-alive شناخته می شود که به مشتریان اجازه می دهد از اتصالات موجود برای چندین درخواست مجدد استفاده کنند. تصمیم گیری در مورد زمان خاتمه دادن به این سوکت های TCP باز توسط تنظیمات مهلت زمانی در مشتری یا سرور هدف یا هر دو مدیریت می شود. این رویکرد کارایی ارتباطات وب را بهبود می بخشد و تأخیر را کاهش می دهد.

تصویری دیگر

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

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

این نشان می دهد که برنامه ما ممکن است تلاش کرده باشد اتصال TCP را قطع کند، اما سرور پروکسی از این موضوع بی اطلاع بوده و به ارسال درخواست به همان سوکت ادامه داده است. این به ما یک اشاره انتقادی در تحقیقات ما ارائه کرد.

اشکال زدایی، بیشتر

خطای 502 Bad Gateway ممکن است زمانی رخ دهد که سرور پروکسی درخواستی را به برنامه ارسال می کند و به طور همزمان، سرویس با ارسال پیام، اتصال را قطع می کند. FIN قطعه به همان سوکت. سوکت پروکسی تایید می کند FIN و یک فرآیند دست دادن جدید را آغاز می کند.

در همین حال، سوکت سمت سرور به تازگی یک درخواست داده با اشاره به اتصال قبلی (اکنون بسته) دریافت کرده است. چون قادر به مدیریت آن نیست، a را ارسال می کند RST به سرور پروکسی برگردید. سپس سرور پروکسی یک خطای 502 را به کاربر برمی گرداند.

بر اساس این فرضیه، زمان پیش‌فرض حفظ زنده HTTP را پیدا کردیم. در حالی که زمان حفظ زنده بودن سرور پروکسی روی 75 ثانیه تنظیم شده بود، برنامه ما تنها 5 ثانیه زمان نگهداری داشت. این نشان می دهد که پس از 5 ثانیه، برنامه ما می تواند اتصال را ببندد و در حالی که این روند در حال انجام است، سرور پروکسی می تواند یک درخواست دریافت کند. همانطور که هنوز آن را دریافت نکرده است FIN، درخواست را به اتصال مرده ارسال می کند، چیزی در پاسخ دریافت نمی کند و بنابراین خطای 502 را می اندازد.

برای تأیید این فرضیه، من زمان ماندن زنده HTTP برنامه خود را به 1 میلی ثانیه در محیط توسعه خود تغییر دادم. سپس تست بارگذاری را روی API های خود انجام دادیم و تعداد قابل توجهی از خرابی های API را به دلیل همان خطای 502 پیدا کردیم. این فرضیه اولیه ما را تایید کرد.

ثابت

یک راه حل ممکن این است که اطمینان حاصل شود که زمان توقف اتصال بیکار بالادستی (برنامه) طولانی تر از زمان پایان اتصال پایین دستی (پراکسی) است.

در عمل، با افزایش زمان زنده نگه داشتن سرور برنامه به مقداری بیشتر از زمان پایان اتصال پروکسی می توان به این امر دست یافت. به عنوان مثال، در راه اندازی ما، من زمان نگه داشتن سرور را از 5 ثانیه به 76 ثانیه افزایش داده ام، که 1 ثانیه بیشتر از زمان پایان اتصال سرور پراکسی است.

با انجام این کار، سرور پایین‌دست (پراکسی) به جای سرور بالادست، به بستن اتصالات تبدیل می‌شود. این کار باعث می شود که هیچ شرط مسابقه ای بین دو سرور وجود نداشته باشد و احتمال خطای 502 ناشی از اتصال بسته را از بین می برد. بنابراین، تنظیم بازه های زمانی بالادستی و پایین دستی مناسب برای عملکرد روان برنامه های کاربردی وب بسیار مهم است.

مثال زیر نحوه تغییر زمان پیش‌فرض نگه‌داشتن زنده را برای یک برنامه Express نشان می‌دهد:

const express = require('express');
const app = express();
const server = app.listen(3000);

server.keepAliveTimeout = 76 * 1000 // Time in ms
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نتیجه

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

در مورد من، این سرور پروکسی بود که از قبل به همه سرورهای باطن متصل می شود و اتصال TCP برای استفاده مجدد برای درخواست های HTTP بیشتر ادامه می یابد. زمان Keep-Alive این اتصال 75 ثانیه از پروکسی و 5 ثانیه از سرور برنامه بود.

این باعث ایجاد شرایطی می شد که در آن پروکسی فکر می کند یک اتصال باز است، اما باطن آن را می بندد. و هنگامی که درخواست را به جای دریافت TCP، همان اتصال را پایین می فرستد ACK برای درخواست ارسال شده، پروکسی TCP دریافت می کند FIN (و بالاخره RST) از بالادست.

سپس سرور پروکسی از چنین درخواست هایی صرف نظر می کند و بلافاصله با خطای HTTP 502 به مشتری پاسخ می دهد. و این در سمت برنامه کاملاً نامرئی است! به همین دلیل هیچ گزارش برنامه برای ما قابل مشاهده نبود.

برای کاهش این مشکل، می‌توانید مطمئن شوید که مهلت زمانی اتصال بی‌کار (برنامه) طولانی‌تر از زمان‌بندی اتصال پایین‌دست (پراکسی) است یا سعی‌های مجدد را روی خطاهای HTTP 5xx از دروازه API اجرا کنید.

امیدوارم برای شما مفید بوده باشد. نظرات یا اصلاحات همیشه پذیرفته می شود.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا