A allddd در go.dev

بهبود چشمگیر بازاستفاده از اتصالات HTTP/1 در Go 1.27

۴ دقیقه مطالعه ۲۷۵ امتیاز ۴۵ نظر ۲۳ مهر ۱۴۰۵

بهبود چشمگیر بازاستفاده از اتصالات HTTP/1 در Go 1.27 — Better HTTP/1 connection reuse in Go 1.27

خلاصه

در نسخه‌های پیشین Go، باقی‌ماندن حتی یک بایت خوانده‌نشده در بدنه پاسخ HTTP/1 باعث بسته شدن کانکشن TCP و ایجاد تاخیر مجدد در هندشیک‌ها می‌شد. نسخه Go 1.27 با تخلیه خودکار و ناهمگام بایت‌های باقیمانده، این مشکل دیرینه را حل کرده و سرعت درخواست‌های پیاپی را تا ۵ برابر افزایش می‌دهد.

ریشه مشکل: هزینه سنگین برقراری مجدد کانکشن

برقراری هر اتصال امن HTTPS روی بستر TCP و لایه TLS بار زمانی قابل‌توجهی به برنامه تحمیل می‌کند: یک رفت‌وبرگشت (1 RTT) برای TCP handshake و یک الی دو رفت‌وبرگشت (1-2 RTT) برای تبادل کلیدهای رمزنگاری در TLS. در سناریوهای واقعی و فواصل جغرافیایی، این فرایند به تنهایی صدها میلی‌ثانیه زمان می‌برد؛ رقمی که در مقایسه با زمان پردازش یک درخواست ساده HTTP بسیار سنگین است.

به همین دلیل، پروتکل HTTP/1.1 از اتصالات پایدار (Persistent Connections یا Keep-Alive) برای ارسال مجدد درخواست‌ها روی همان سوکتِ باز استفاده می‌کند. اما بر خلاف HTTP/2 که از ویژگی چندگانه‌سازی (Multiplexing) برای جداسازی جریان‌ها روی یک کانکشن بهره می‌برد، در پروتکل‌های HTTP/1.0 و HTTP/1.1 بسته‌ها و پاسخ‌ها به صورت خطی و متوالی روی کانکشن ارسال می‌شوند. این یعنی کلاینت تا زمانی که بدنه (Body) پاسخ جاری را تا آخرین بایت نخوانده باشد، نمی‌تواند سرآغاز پاسخ بعدی را پردازش کند.

تله خاموش در پکیج net/http

تا پیش از Go 1.26، الگوی بسیار رایجی در کدهای توسعه‌دهندگان باعث آسیب شدید به کارایی سیستم می‌شد:

resp, err := http.Get(url)
if err != nil {
    return err
}
defer resp.Body.Close()
return resp.StatusCode

در کدهایی شبیه به نمونه بالا، کلاینت فقط به StatusCode نیاز دارد و دیتای بدنه را نمی‌خواند. در نسخه‌های قبلی، وقتی متد resp.Body.Close() فراخوانی می‌شد، پکیج net/http تشخیص می‌داد که هنوز داده‌های خوانده‌نشده روی سوکت باقی مانده‌اند. در نتیجه به جای ریسک تداخل داده‌ها، اتصال موجود را به کل می‌بست و دور می‌انداخت و برای درخواست بعدی، کانکشن جدیدی می‌ساخت.

این مشکل حتی به شکلی ظریف‌تر هنگام پارس JSON نیز رخ می‌داد؛ کتابخانه encoding/json.Decoder به محض رسیدن به کاراکتر پایانی شیء (}) پردازش را متوقف می‌کند. اگر سرور یک کاراکتر خط جدید (\n) در انتهای بدنه ارسال کرده بود، همان یک بایت نخوانده باعث می‌شد کانکشن سالم دور ریخته شود.

راه‌حل‌های دست‌وپاگیر قدیمی

توسعه‌دهندگان سال‌ها برای فرار از این افت کارایی، توابع کمکی عجیبی می‌نوشتند:

  • تخلیه بدنه با کپی کردن آن درون حافظه دورریز: io.Copy(io.Discard, resp.Body)
  • محدود کردن سقف حجم تخلیه (مثلاً ۲۵۶ کیلوبایت)، تا اگر فایل حجیمی باقی مانده بود، پهنای باند هدر نرود.
  • قرار دادن تایم‌اوت زمانی (مثلاً ۵۰ میلی‌ثانیه) در یک Goroutine مجزا برای اینکه سرورهای کند کل فرآیند را قفل نکنند.

این کدهای طولانی و پیچیده نه تنها تمیزی اکوسیستم Go را مخدوش می‌کردند، بلکه باعث ایجاد بدفهمی‌هایی هم شده بودند؛ از جمله توسعه‌دهندگانی که همین منطق را روی درخواست‌های HTTP/2 نیز اعمال می‌کردند، در حالی که ساختار مالتی‌پلکس HTTP/2 اساساً نیازی به چنین کاری ندارد.

راهکار Go 1.27: تخلیه خودکار و بهینه‌سازی بدون مداخله دستی

از نسخه Go 1.27، قابلیت net/http.Transport تمام این فرایند را به شکل درون‌ساخت به دست می‌گیرد. هنگام بستن بدنه با resp.Body.Close، سیستم به شکل خودکار و ناهمگام (Asynchronous) در پس‌زمینه تا سقف ۲۵۶ کیلوبایت یا حداکثر ۵۰ میلی‌ثانیه از بدنه باقیمانده را می‌خواند و تخلیه (Drain) می‌کند تا سوکت به سرعت به Connection Pool بازگردد. همچنین هوشمندی مضاعفی اعمال شده است؛ مثلاً اگر هدر Connection: close از سرور دریافت شده باشد، موتور HTTP می‌داند که تلاش برای تخلیه بی‌ثمر است و بلافاصله آن را رها می‌کند.

بنچمارک‌ها نشان می‌دهد که ۱۰۰ درخواست پشت‌سرهم با بستن عادیِ بدنه، در Go 1.26 حدود ۷۱۰ میلی‌ثانیه زمان می‌برد، در حالی که همین کد در Go 1.27 ظرف حدود ۱۵۰ میلی‌ثانیه اجرا می‌شود (تقریباً ۵ برابر سریع‌تر).

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

در گفتگوهای Hacker News، برنامه‌نویسان عمدتاً از بازنشسته شدن کدهای کمکی حجیم خود استقبال کردند. البته بحث‌های حاشیه‌ای دیگری نیز مطرح شد؛ از جمله سوالاتی درباره زمان پشتیبانی رسمی از HTTP/3 در پکیج استاندارد. برخی کاربران نیز اشاره کردند که تغییر رفتار در پس‌زمینه به این معنی است که سیستم ممکن است برای بستن یک درخواست، تا ۵۰ میلی‌ثانیه ترافیک ناخواسته دانلود کند، اما در عمل همگی متفق‌القول بودند که تعادل انتخاب‌شده (Trade-off) برای اکثریت مطلق سرویس‌های وب ایده‌آل است.

این مطلب به‌صورت کاملاً خودکار و با هوش مصنوعی از مقالهٔ اصلی و بحث هکرنیوز تهیه شده و ممکن است خطا داشته باشد. حقوق مطلب اصلی متعلق به نویسندهٔ آن است. قوانین و گزارش مشکل

خبرهای فناوری در قالب پست و ویدیوی کوتاه، در اینستاگرام هکرنیوز بلاگ. به‌زودی
هر خبر به محض انتشار، به‌همراه پادکست روزانه در کانال تلگرام هکرنیوز بلاگ.