Linux و مدیریت سرورمهندسانسطح متوسط

قطع شدن WebSocket در HAProxy هر ۵۰ ثانیه؛ علت و راه‌حل

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

تحریریه کلودیکپ ۷ دقیقه مطالعه
قطع شدن WebSocket در HAProxy هر ۵۰ ثانیه؛ علت و راه‌حل

شرح مسئله

سرویسی با 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 نیاز دارد، پس در پنجره‌ی نگهداری انجامش دهید.

چطور مطمئن شویم مشکل حل شده است

  1. تست اتصال بی‌کار با wscat. هدف این است که عمداً بیش از ۵۰ ثانیه هیچ داده‌ای نفرستید:
npm install -g wscat

date -Ins
wscat -c wss://example.com/ws
# هیچ ورودی‌ای تایپ نکنید و چند دقیقه صبر کنید
date -Ins

در حالت سالم، اتصال تا وقتی خودتان Ctrl+C نزنید باز می‌ماند. اگر باز هم حدود ۵۰ ثانیه قطع شد، پیکربندی جدید لود نشده یا یک پروکسی دیگر در مسیر (مثل nginx با proxy_read_timeout) دارد اتصال را می‌بندد.

  1. برای SSE، یک درخواست خام بفرستید و ببینید پاسخ بعد از ۵۰ ثانیه قطع نمی‌شود:
    curl -N -H "Accept: text/event-stream" https://example.com/events
    
  2. لاگ را دوباره پایش کنید؛ در نشست‌های عادی دیگر نباید cD-- یا sD-- ببینید:
    sudo tail -f /var/log/haproxy.log | grep -E "cD--|sD--"
    
  3. با show stat مقدار scur روی be_ws را با تعداد کلاینت‌های واقعی مقایسه کنید؛ این عدد باید با گذشت زمان ثابت بماند و مدام صفر نشود.
  4. اگر می‌خواهید مطمئن شوید پیکربندی جدید فعال است، شناسه‌ی پروسه و 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 مربوطه بیشتر کنید.

ادامه مطالعه

مقالات مرتبط

پرشدن دیسک PostgreSQL با WAL اسلات replication غیرفعال
مانیتورینگ و پایداریمتوسط

پرشدن دیسک PostgreSQL با WAL اسلات replication غیرفعال

یک replication slot غیرفعال می‌تواند pg_wal را تا پر شدن کامل دیسک نگه دارد و دیتابیس را عملاً از کار بیندازد. در این مقاله ریشه‌یابی، رفع و پیشگیری از آن را گام‌به‌گام بررسی می‌کنیم.

تحریریه کلودیکپ ۶ دقیقه مطالعه
«idle in transaction» در PostgreSQL: قاتل خاموش autovacuum
مانیتورینگ و پایداریمتوسط

«idle in transaction» در PostgreSQL: قاتل خاموش autovacuum

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

تحریریه کلودیکپ ۸ دقیقه مطالعه

در پیاده‌سازی به کمک نیاز دارید؟

تیم کلودیکپ همین کار را هر روز برای تیم‌های دیگر انجام می‌دهد. اگر جایی گیر کرده‌اید، با ما صحبت کنید.