استفاده مجدد از اتصال HTTP/1 بهتر در Go 1.27
آدرس مقاله: https://go.dev/blog/http-connection-reuse آدرس نظرات: https://news.ycombinator.com/item?id=50090369 امتیاز: 276 # نظرات: 45
نیکلاس هوسین و دیمین نیل 14 اکتبر 2026
یک درخواست GET به url ارسال می کند و کد وضعیت HTTP را برمی گرداند.
با استفاده از HTTP/1 (GODEBUG=http2client=0)، 100 بار فراخوانی این تابع در برابر index.golang.org حدود 710 میلی ثانیه در Go 1.26 در دستگاه من طول می کشد. در Go 1.27، همان برنامه 150 میلی ثانیه طول می کشد.
یعنی تقریبا 5 برابر سریعتر! چگونه چنین تفاوت عملکرد فاحشی ممکن است؟
برای درک تفاوت، در نظر بگیرید که وقتی درخواست HTTP میدهیم در شبکه چه اتفاقی میافتد.
بسته net/http Go از HTTP/1.0، HTTP/1.1 و HTTP/2 پشتیبانی می کند. هر سه از طریق TCP و معمولا روی TLS هنگام صحبت با نقاط پایانی HTTPS اجرا میشوند. درخواست از طریق اتصال جدید چیزی شبیه به این است:
این سفرهای رفت و برگشت (به علاوه مقداری از زمان CPU صرف شده برای دست دادن TLS) به سرعت جمع می شوند. در ارتباطات متقابل منطقه ای، تأخیرهای یک طرفه اغلب به 50 میلی ثانیه (100 میلی ثانیه RTT) نشان داده شده در نمودار بالا می رسد یا از آن فراتر می رود. در مقایسه با 300 میلیثانیه صرف شده برای راهاندازی اتصال، سرویس درخواست HTTP میتواند ارزان باشد و زمانی که پهنای باند زیاد است یا پاسخ HTTP کم است، فقط کمی بیش از 1 RTT میگیرد.
حتی در شرایط مطلوب تر، باز کردن یک اتصال جدید همچنان می تواند گران باشد. جستجوهای معیار افتتاحیه ما index.golang.org، که در سراسر جهان توزیع شده است و از TLS 1.3 پشتیبانی می کند. این بدان معنی است که تأخیر نسبتا کم است و دست دادن TLS در واقع می تواند تنها در 1 RTT به پایان برسد. با این وجود، تفاوت عملکرد هنوز بسیار محسوس است.
برای جلوگیری از پرداخت این هزینه راه اندازی در هر درخواست، سرویس گیرندگان و سرورهای HTTP سعی می کنند در صورت امکان از اتصالات موجود در چندین درخواست استفاده مجدد کنند. مکانیک این بر اساس نسخه HTTP متفاوت است:
در HTTP/1 (هر دو HTTP/1.0 و HTTP/1.1)، چندین درخواست و پاسخ در یک اتصال به ترتیب روی سیم ارسال میشوند. اگر دو درخواست GET از طریق یک اتصال TCP به index.golang.org ارسال شود، پاسخ ها به صورت یک جریان واحد از بایت ها دریافت می شوند:
همانطور که در نمودار نشان داده شده است، net/http.Get خط وضعیت و هدرها را از جریان بایت می خواند و تجزیه می کند تا net/http.Response را پر کند، سپس بلافاصله برمی گردد. بدنه پاسخ در اتصال خوانده نشده باقی می ماند و تنها زمانی که تماس گیرنده از net/http.Response.Body می خواند مصرف می شود.
از آنجایی که پاسخها پشت سر هم در یک جریان ارسال میشوند، net/http فقط میتواند شروع پاسخ بعدی را پس از خواندن متن پاسخ فعلی تا انتها پیدا کند (همانطور که توسط هدر Content-Length یا Transfer-Encoding تعیین میشود).
اکنون به برنامه مثال خود نگاه کنید، جایی که ما هرگز resp.Body را نمی خوانیم:
از طریق Go 1.26، هنگامی که resp.Body.Close فراخوانی می شود، net/http می بیند که هنوز یک بدنه پاسخ خوانده نشده وجود دارد. با درک اینکه نمی تواند از اتصال مجدد استفاده کند، اتصال موجود را رها می کند و اتصالات جدیدی را برای درخواست های بعدی خود ایجاد می کند و باعث می شود که بارها هزینه ایجاد اتصالات جدید را متحمل شود.
(HTTP/2 این مشکل را ندارد. از آنجایی که به جای ارسال پاسخها پشت سر هم، جریانهای فریمها را روی یک اتصال چندگانه میکند، بدنه پاسخ خوانده نشده در یک جریان، درخواستهای دیگر را از اشتراکگذاری اتصال مسدود نمیکند.)
این ظرافت استفاده مجدد از اتصال HTTP/1 می تواند باعث کاهش عملکرد بی صدا در کدهای به ظاهر بی ضرر شود.
همانطور که در مثال ابتدایی ما، یک کلاینت ممکن است فقط به کد وضعیت پاسخ API اهمیت دهد. با این حال، نقطه پایانی API که با آن صحبت میکند ممکن است بدنههایی را ارسال کند که اطلاعات دقیقتری اما در نهایت استفادهنشده، مانند {"موفقیت": درست، "پیام": "بهروزرسانی پردازش شد"} ارائه میدهند. این متأسفانه باعث عدم استفاده مجدد از اتصال می شود.
حتی برنامههایی که بدنه پاسخ را میخوانند، اگر کمی از انتهای بدنه متوقف شوند، میتوانند با این مشکل مواجه شوند. به عنوان مثال، encoding/json.Decoder به محض مشاهده بسته شدن } یک شی JSON، خواندن را متوقف می کند. اگر یک سرور یک پاسخ JSON را ارسال کند که با یک خط جدید خاتمه یافته است، رمزگشایی شی JSON باعث می شود خط جدید خوانده نشده باقی بماند. تا زمانی که آن خط جدید مصرف نشود، نمی توان از اتصال برای درخواست جدید استفاده مجدد کرد. بنابراین، یک اتصال سالم ممکن است در نهایت رها شود، حتی اگر تنها یک بایت با قابلیت استفاده مجدد فاصله داشت.
مشکل بدنه خوانده نشده یک لبه تیز طولانی مدت بوده است. در طول سال ها، بسیاری از توسعه دهندگان Go راه حل های خود را پیدا کردند. معمولا این کار با تخلیه بدنه پاسخ قبل از بستن آن انجام می شود:
در حالی که این برای پاسخ های کوچک کار می کند، اگر بدنه خوانده نشده بزرگ باشد، ایجاد یک اتصال TCP جدید می تواند سریعتر از دانلود و دور انداختن بقیه بدنه باشد. یک روش بهتر این است که تعداد بایت هایی را که می خواهیم تخلیه کنیم محدود کنیم:
با این حال، یک سرور ممکن است یک بدنه پاسخ ارسال کند که به آرامی وارد می شود، بنابراین ما همچنین باید زمان تخلیه را محدود کنیم:
البته ما همچنین نمی خواهیم هنگام بستن بدنه پاسخ تا 50 میلی ثانیه مسدود کنیم. در عوض، میتوانیم بدن را به صورت ناهمزمان در پسزمینه تخلیه کنیم و اجازه دهیم با درخواستهای بعدی مقابله کند:
همانطور که می بینیم، تخلیه ایمن بدنه های پاسخ به طرز شگفت آوری مشکل و دست و پا گیر است. بدتر از آن، با گسترش عمل تخلیه بدنه های پاسخ، برخی از تفاوت های ظریف اغلب از بین می رفت. این منجر به سوء تفاهم هایی مانند تلاش برای تخلیه بدنه های پاسخ برای اتصالات HTTP/2 شده است، در حالی که انجام این کار غیر ضروری است.
با شروع در Go 1.27، net/http.Transport بهطور خودکار تمام کارهایی را که در کد بالا پیادهسازی کردهایم انجام میدهد: به طور ناهمزمان یک بدنه پاسخ خوانده نشده HTTP/1 را تا 50 میلیثانیه یا 256 کیلوبایت تخلیه میکند. همچنین بهینه سازی های اضافی را پیاده سازی می کند. به عنوان مثال، هنگامی که یک پاسخ حاوی هدر Connection: Close باشد، تخلیه بدنه تضمین می شود که کار هدر می رود، و بنابراین از انجام این کار صرف نظر می کند. توجه داشته باشید که این رفتار ممکن است در نسخههای بعدی Go تغییر کند، زیرا سایر پیشرفتهای احتمالی آشکار میشوند.
بهعنوان بهترین روش کلی، اکنون توصیه میکنیم که کاربران به سادگی بدنههای HTTP خود را هنگامی که دیگر به آنها نیاز ندارند، ببندند، مانند مثال اصلی ما:
برو همه چیز در مورد سادگی است. اجازه دهید کد شما تمیز و ساده بماند و net/http از طرف شما با پیچیدگیها مقابله کند.
متن اصلی (انگلیسی)
Better HTTP/1 connection reuse in Go 1.27
Article URL: https://go.dev/blog/http-connection-reuse Comments URL: https://news.ycombinator.com/item?id=50090369 Points: 276 # Comments: 45