قطع شدن WebSocket در HAProxy هر ۵۰ ثانیه؛ علت و راهحل
اگر WebSocket یا SSE پشت HAProxy هر ۵۰ ثانیه قطع میشود، مقصر تایماوتهای پیشفرض فایل نمونه است. در این مقاله علت ریشهای و پیکربندی درست timeout tunnel را بررسی میکنیم.

شرح مسئله
سرویسی با WebSocket (چت، نوتیفیکیشن، داشبورد زنده) یا SSE را پشت HAProxy گذاشتهاید. همهچیز در ابتدا درست کار میکند، اما بعد از چند دقیقه همهی کلاینتها تقریباً همزمان دوباره وصل میشوند. نشانههای کلاسیک این مشکل:
- در کنسول مرورگر، اتصال با close code
1006(بستهشدن غیرعادی) قطع میشود و فاصلهی بین قطع و وصل مجدد تقریباً ثابت است؛ چیزی حدود ۵۰ ثانیه، آن هم دقیقاً وقتی هیچ دادهای رد و بدل نمیشود. - در لاگ HAProxy در حالت
mode httpخطی شبیه این میبینید (فقط بخش مهم آن آورده شده):
haproxy[2137]: 10.20.30.40:52184 [21/May/2025:10:11:13.001] fe_web be_ws/app1 0/0/0/50213/50219 101 0 - - cD-- 12/12/0/0/0 0/0
کد وضعیت 101 یعنی ارتقای پروتکل انجام شده و اتصال WebSocket برقرار بوده است؛ اما مقدار تایمرها حدود ۵۰٬۰۰۰ میلیثانیه است، یعنی نشست دقیقاً بعد از ۵۰ ثانیه خاتمه یافته. مهمتر از همه، کد خاتمه cD-- است: تایماوت کلاینت در فاز انتقال داده.
- درخواستهای HTTP طولانی (long-polling یا پاسخهای کند) با کد
504برمیگردند و در لاگ کد خاتمهsD--ثبت میشود. - در حالت
mode tcpهیچ کد وضعیتی ثبت نمیشود و اتصال بدون هیچ پیام مشخصی ریست میشود؛ همین موضوع عیبیابی را سختتر میکند.
علت ریشهای
مقصر، همان چند خطی است که تقریباً همه از فایل نمونهی /etc/haproxy/haproxy.cfg کپی میکنند: timeout client 50s و timeout server 50s. این تایماوتها مطلق (absolute) نیستند؛ تایماوت بیکاری (inactivity) هستند و با هر بار انتقال داده در هر جهت ریست میشوند.
مشکل اینجاست که یک اتصال WebSocket معمولاً بیشتر وقتها بیکار است. کاربر در چت چیزی تایپ نمیکند، داشبورد زنده دوباره داده نمیفرستد و هیچ بستهای رد و بدل نمیشود. بعد از ۵۰ ثانیه سکوت، HAProxy اتصال را میبندد؛ بدون هیچ خطای سمت سرور اپلیکیشن، چون خودش این کار را کرده است.
وقتی سرور پاسخ 101 Switching Protocols میدهد و اتصال به یک تونل دوسویه تبدیل میشود، HAProxy از timeout tunnel استفاده میکند. اما اگر این تایماوت در پیکربندی تعریف نشده باشد، HAProxy به همان timeout client و timeout server برمیگردد. نتیجه دقیقاً همان چیزی است که در لاگ دیدید: هر ۵۰ ثانیه سکوت، یک اتصال بسته.
برای SSE ماجرا کمی متفاوت است: SSE یک تونل نیست، بلکه یک پاسخ HTTP طولانی است که سرور باز نگه میدارد؛ پس timeout server مستقیماً روی آن اعمال میشود. timeout http-keep-alive هم فقط برای فاصلهی بین دو درخواست روی یک اتصال keep-alive است و اینجا نقش اصلی را ندارد.
راهحل گامبهگام
۱. تأیید تشخیص از روی لاگ و سوکت مدیریتی
اول مطمئن شوید واقعاً تایماوت عامل ماجراست، نه کرش اپلیکیشن یا ریاستارت پادها:
# کدهای خاتمه مربوط به تایماوت
sudo grep -E "cD--|sD--" /var/log/haproxy.log | tail -20
# آمار لحظهای backendها از سوکت مدیریتی
echo "show stat" | sudo socat stdio /var/run/haproxy.sock | awk -F, 'NR==1 || $2=="app1" || $2=="app2" {print $1, $2, $5, $18}'
اگر خطوط پر از cD-- بود و ستون status سرورها UP بود، تشخیص تأیید میشود. اگر سوکت مدیریتی تنظیم نشده باشد، این خط را به بخش global اضافه کنید: stats socket /var/run/haproxy.sock mode 660 level admin.
۲. تعریف درست تایماوتها
تایماوتها را به سه دسته تقسیم کنید: کوتاه برای محافظت (اتصال و دریافت درخواست)، متوسط برای نشست HTTP، و بلند برای تونل. نمونهی کامل و قابل اجرا:
global
log /dev/log local0
maxconn 20000
stats socket /var/run/haproxy.sock mode 660 level admin
stats timeout 30s
defaults
mode http
log global
option httplog
option dontlognull
# محافظت: کوتاه بماند تا از هدررفتن منابع جلوگیری شود
timeout connect 5s
timeout http-request 10s
timeout http-keep-alive 10s
# نشست HTTP: از تایماوت نمونه (50s) خیلی بزرگتر
timeout client 5m
timeout server 5m
# کلید ماجرا: تونل WebSocket
timeout tunnel 1h
retries 2
frontend fe_web
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
default_backend be_ws
backend be_ws
balance roundrobin
server app1 10.0.1.11:8080 check
server app2 10.0.1.12:8080 check
نکته: مقدار 0 برای timeout tunnel یعنی بینهایت. این کار مشکل را حل میکند اما اتصالهای مرده تا ابد در حافظه میمانند و ظرفیت maxconn را میخورند؛ توصیه نمیشود. عددی مثل 1h یا 4h انتخاب معقولی است.
۳. جداسازی ترافیک WebSocket (توصیهشده)
تحمیل تایماوت بلند به همهی ترافیک ایدهی خوبی نیست؛ چون مهاجم میتواند با بازکردن اتصالهای HTTP بیکار، منابع را اشغال کند. WebSocket را روی یک backend جدا ببرید:
frontend fe_web
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
acl is_ws path_beg /ws
use_backend be_ws if is_ws
default_backend be_app
backend be_ws
balance roundrobin
timeout tunnel 4h
server app1 10.0.1.11:8080 check
server app2 10.0.1.12:8080 check
backend be_app
balance roundrobin
timeout server 30s
server app1 10.0.1.11:8080 check
server app2 10.0.1.12:8080 check
۴. اعتبارسنجی و reload بدون قطعی
قبل از هر reload، پیکربندی را چک کنید:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
# خروجی مورد انتظار: Configuration file is valid
sudo systemctl reload haproxy
اگر خودتان reload را بهصورت دستی مدیریت میکنید، از -sf استفاده کنید تا پروسهی قبلی اتصالهای فعال را تا پایان کارشان نگه دارد؛ برای WebSocket این تفاوت بزرگی میکند:
sudo haproxy -Ws -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -sf $(cat /run/haproxy.pid)
۵. سقف اتصال و file descriptor
هر اتصال WebSocket یک slot از maxconn و یک file descriptor مصرف میکند. اگر تعداد کاربران زیاد است، هم maxconn را بالا ببرید و هم سقف توصیفگر فایل سرویس را:
echo "show info" | sudo socat stdio /var/run/haproxy.sock | grep -E "Maxconn|CurrConns|Maxsock"
برای systemd یک override بسازید (/etc/systemd/system/haproxy.service.d/limits.conf):
[Service]
LimitNOFILE=65536
این تغییر به restart نیاز دارد، پس در پنجرهی نگهداری انجامش دهید.
چطور مطمئن شویم مشکل حل شده است
- تست اتصال بیکار با
wscat. هدف این است که عمداً بیش از ۵۰ ثانیه هیچ دادهای نفرستید:
npm install -g wscat
date -Ins
wscat -c wss://example.com/ws
# هیچ ورودیای تایپ نکنید و چند دقیقه صبر کنید
date -Ins
در حالت سالم، اتصال تا وقتی خودتان Ctrl+C نزنید باز میماند. اگر باز هم حدود ۵۰ ثانیه قطع شد، پیکربندی جدید لود نشده یا یک پروکسی دیگر در مسیر (مثل nginx با proxy_read_timeout) دارد اتصال را میبندد.
- برای SSE، یک درخواست خام بفرستید و ببینید پاسخ بعد از ۵۰ ثانیه قطع نمیشود:
curl -N -H "Accept: text/event-stream" https://example.com/events - لاگ را دوباره پایش کنید؛ در نشستهای عادی دیگر نباید
cD--یاsD--ببینید:sudo tail -f /var/log/haproxy.log | grep -E "cD--|sD--" - با
show statمقدارscurرویbe_wsرا با تعداد کلاینتهای واقعی مقایسه کنید؛ این عدد باید با گذشت زمان ثابت بماند و مدام صفر نشود. - اگر میخواهید مطمئن شوید پیکربندی جدید فعال است، شناسهی پروسه و uptime را از سوکت بخوانید:
show infoبعد از reload بایدPidتازه وUptimeکوچک نشان دهد.
پیشگیری و بهترین روشها
- تایماوتهای نمونه را کپی نکنید.
50sفقط یک مقدار نمایشی است. هر frontend و backend باید تایماوتهای صریح و متناسب با نوع ترافیک خودش داشته باشد. - برای هر تونل،
timeout tunnelبگذارید. این تنها تایماوتی است که رفتار اتصالهای ارتقایافته را کنترل میکند و اگر نباشد، تایماوتهای نشست جای آن را میگیرند. - ترافیک WebSocket را جدا کنید. یک backend اختصاصی با تایماوت بلند، و تایماوتهای کوتاه برای ترافیک معمولی.
- درخواستهای keep-alive را با
timeout http-keep-aliveکنترل کنید، نه با بالا بردنtimeout client. بالا بردن بیدلیلtimeout clientباعث تجمع اتصالهای نیمهباز میشود. - Heartbeat سطح اپلیکیشن اضافه کنید. یک پیام ping هر ۲۰ تا ۳۰ ثانیه، هم اتصالهای مرده را در مسیرهای میانی زودتر تشخیص میدهد و هم به load balancerهای میانی (مثل ALB) اجازه نمیدهد اتصال را بیکار ببینند.
- تایماوتهای بقیهی لایهها را هم همراستا کنید. اگر nginx یا یک Ingress جلوی HAProxy است،
proxy_read_timeoutوproxy_send_timeoutآن هم باید از بازهی بیکاری WebSocket بزرگتر باشد. - مانیتورینگ را جدی بگیرید. روی نرخ
5xx، رسیدنscurبهslim(سقف نشست) و پر شدنCurrConnsنسبت بهMaxconnهشدار تعریف کنید. - در کلاینت، reconnect با backoff نمایی پیاده کنید. حتی با پیکربندی درست، قطعی شبکه رخ میدهد؛ رفتار درست کلاینت باعث میشود قطعی کوتاه، به یک طوفان اتصال تبدیل نشود.
نویسنده
تیم محتوای کلودیکپ
تیم فنی و محتوای کلودیکپ؛ مهندسانی که هر روز با DevOps، Kubernetes و زیرساخت ابری کار میکنند و تجربههایشان را اینجا مینویسند.
سوالات متداول این مقاله
نه. این تایماوت فقط بعد از تبدیل اتصال به یک تونل دوسویه (مثلاً بعد از پاسخ 101 برای WebSocket) اعمال میشود. ترافیک HTTP عادی همچنان تابع timeout client و timeout server است، مگر اینکه آنها را در همان backend بازنویسی کنید.
احتمالاً یک لایهی دیگر در مسیر اتصال را میبندد (nginx، Ingress، فایروال سازمانی یا خود اپلیکیشن). کد خاتمه در لاگ HAProxy تعیین میکند مقصر کدام سمت است: cD-- یعنی HAProxy سمت کلاینت را تایماوت کرده، sD-- یعنی سمت سرور. اگر هیچکدام را نمیبینید، اتصال جای دیگری بسته شده است.
خیر. مقدار 0 یعنی بینهایت؛ این کار قطعیهای ۵۰ ثانیهای را از بین میبرد اما اتصالهای مرده هرگز آزاد نمیشوند و بهسرعت ظرفیت maxconn و حافظه را اشغال میکنند. یک مقدار محدود مانند 1h یا 4h به همراه heartbeat سطح اپلیکیشن، انتخاب امنتری است.
نه لزوماً. SSE یک تونل دوسویه نیست؛ یک پاسخ HTTP طولانی است که سرور باز نگه میدارد، پس timeout server روی آن اثر میگذارد. اگر پاسخ SSE بیشتر از timeout server باز میماند، همان مقدار را در backend مربوطه بیشتر کنید.
کلودیکپ در این زمینه چه کمکی میکند؟
DevSecOps و امنیت زیرساخت
تزریق امنیت در تمام مراحل توسعه و استقرار، از کد تا زیرساخت.
مشاهده جزئیات خدمتمعماری زیرساخت ابری
طراحی زیرساخت مقیاسپذیر، امن و مقرونبهصرفه روی AWS، Azure یا Google Cloud.
مشاهده جزئیات خدمتخدمات مدیریتشده DevOps
برونسپاری کامل عملیات زیرساخت به تیمی متخصص، با پشتیبانی مستمر.
مشاهده جزئیات خدمت
پرشدن دیسک PostgreSQL با WAL اسلات replication غیرفعال
یک replication slot غیرفعال میتواند pg_wal را تا پر شدن کامل دیسک نگه دارد و دیتابیس را عملاً از کار بیندازد. در این مقاله ریشهیابی، رفع و پیشگیری از آن را گامبهگام بررسی میکنیم.

«idle in transaction» در PostgreSQL: قاتل خاموش autovacuum
نشستهای idle in transaction تراکنش را باز نگه میدارند، جلوی autovacuum را میگیرند و جدولها را باد میکنند تا جایی که دیسک پر و اتصالها تمام میشود. این مقاله تشخیص و درمان کامل آن است.
در پیادهسازی به کمک نیاز دارید؟
تیم کلودیکپ همین کار را هر روز برای تیمهای دیگر انجام میدهد. اگر جایی گیر کردهاید، با ما صحبت کنید.