سرور از دسترس خارج شده، اما CPU فقط ۳۵ درصد است، RAM هنوز فضای خالی دارد و دیسک هم پر نشده است. برنامه را Restart میکنید و همهچیز فوراً به حالت عادی برمیگردد؛ چند ساعت یا چند روز بعد همان مشکل دوباره تکرار میشود. اگر این سناریو برایتان آشناست، ممکن است با Connection Leak یا نشتی اتصال روبهرو باشید.
Connection Leak معمولاً سروصدای زیادی ایجاد نمیکند. یک اتصال باز میشود، کارش تمام میشود، اما به استخر برنمیگردد یا بسته نمیشود. این اتفاق در ابتدا هیچ اثر محسوسی ندارد. با تکرار درخواستها، تعداد اتصالهای بلااستفاده بیشتر میشود تا سرانجام برنامه دیگر نتواند اتصال جدید بگیرد. از دید کاربر، سرویس ناگهان کند یا از دسترس خارج شده است؛ درحالیکه مشکل ساعتها قبل آغاز شده بود.
الگوی کلاسیک Connection Leak این است: مصرف یک منبع محدود پیوسته رشد میکند، Restart موقتاً مشکل را حل میکند و بدون اصلاح کد یا تنظیمات، خرابی دوباره برمیگردد.
در این مقاله یاد میگیریم Connection Leak دقیقاً چیست، با ترافیک طبیعی چه تفاوتی دارد، چگونه در لینوکس و دیتابیس آن را پیدا کنیم، چه متریکهایی برایش Alert بسازیم و چطور جلوی تکرارش را بگیریم.
Connection Leak دقیقاً چیست؟
برنامهها برای ارتباط با دیتابیس، API خارجی، Redis، Message Broker یا سرویسهای دیگر اتصال ایجاد میکنند. هر اتصال بخشی از منابع محدود سیستم را مصرف میکند: سوکت، حافظه، File Descriptor، یک Slot در Connection Pool و گاهی یک Thread یا Process در سمت سرور.
در حالت سالم، چرخه اتصال مشخص است:
- برنامه اتصال را از Pool میگیرد یا یک اتصال جدید باز میکند.
- عملیات موردنظر را انجام میدهد.
- اتصال را آزاد میکند تا دوباره استفاده شود یا آن را میبندد.
در Connection Leak مرحله سوم در بعضی مسیرهای اجرا اتفاق نمیافتد. برای مثال Query خطا میدهد، تابع زودتر return میکند، درخواست Timeout میشود یا Exception رخ میدهد؛ اما کد آزادسازی اتصال اجرا نمیشود. اتصال ممکن است از دید برنامه همچنان «در حال استفاده» باشد، درحالیکه دیگر هیچ کار مفیدی انجام نمیدهد.
Connection Leak فقط به دیتابیس مربوط نیست
| نوع اتصال | منبعی که تمام میشود | نشانه رایج |
|---|---|---|
| Database Pool | Slotهای Pool یا سقف اتصال دیتابیس | Timeout هنگام دریافت Connection و Queryهای معطل |
| TCP Socket | سوکت، پورت موقت و حافظه Kernel | رشد غیرعادی ESTABLISHED، CLOSE_WAIT یا TIME_WAIT |
| HTTP Client | Socketهای Agent یا اتصال به سرویس مقصد | درخواستهای Pending و Timeoutهای زنجیرهای |
| Redis / Broker | Client Connectionهای سرویس | افزایش Clients بدون رشد متناسب Traffic |
| File و Stream | File Descriptor | خطای EMFILE: too many open files |
File Descriptor Leak از نظر فنی همیشه Connection Leak نیست، اما اثر و روش تشخیص آن بسیار شبیه است. در لینوکس، Socket نیز یک File Descriptor است؛ به همین دلیل نشتی سوکت میتواند در نهایت برنامه را به محدودیت فایلهای باز برساند.
چرا این مشکل بیسروصدا رشد میکند؟
فرض کنید Pool دیتابیس ۵۰ اتصال دارد و فقط یک درصد درخواستها اتصال خود را آزاد نمیکنند. در تست کوتاه همهچیز سالم به نظر میرسد. حتی تست Load دهدقیقهای ممکن است مشکل را نشان ندهد. اما در محیط واقعی، هر اتصال نشتکرده برای همیشه یکی از ۵۰ Slot را اشغال میکند. ظرفیت Pool ابتدا به ۴۹، بعد به ۴۸ و در نهایت به صفر اتصال قابلاستفاده میرسد.
از آن لحظه، درخواستهای جدید پشت Pool منتظر میمانند. Latency بالا میرود، Timeout ایجاد میشود، Retryها ترافیک بیشتری تولید میکنند و یک مشکل کوچک به خرابی زنجیرهای تبدیل میشود. اگر Health Check فقط زندهبودن Process را بسنجد، ممکن است سرور همچنان Healthy گزارش شود.
اعداد نمودار آموزشیاند، اما رابطه آن مهم است: اگر تعداد اتصالها بالا میرود درحالیکه نرخ درخواست تقریباً ثابت مانده و پس از پایان درخواستها پایین نمیآید، باید به Leak، Queryهای طولانی، تراکنش باز یا تنظیم نادرست Pool شک کنید.
نشانههایی که باید جدی بگیرید
- تعداد Connectionها در طول زمان پلهپله بالا میرود و به Baseline قبلی برنمیگردد.
- تعداد اتصالهای Pool نزدیک سقف است، اما CPU و Throughput رشد مشابهی ندارند.
- زمان انتظار برای گرفتن Connection از Pool افزایش مییابد.
- خطاهایی مثل
connection pool exhausted،too many connections،remaining connection slots are reservedیاEMFILEدیده میشوند. - تعداد درخواستهای Pending بیشتر میشود و Latency ناگهان در چند Endpoint بالا میرود.
- تعداد سوکتهای
CLOSE_WAITبرای یک Process دائماً رشد میکند. - Restart برنامه مشکل را حل میکند، اما بعد از بازهای تقریباً مشابه برمیگردد.
تفاوت Connection Leak با ترافیک بالا چیست؟
زیادبودن Connection بهتنهایی اثبات Leak نیست. ممکن است افزایش واقعی ترافیک، یک Query کند، تراکنشهای طولانی، تنظیم Keep-Alive یا کوچکبودن Pool علت باشد. برای تشخیص باید Connection را کنار Traffic، Latency و وضعیت اتصال ببینید.
| رفتار | ترافیک واقعی | Connection Leak |
|---|---|---|
| رابطه با Request Rate | Connection همراه Traffic بالا و پایین میرود | Connection با Traffic ثابت نیز رشد میکند |
| پس از کاهش بار | اتصالها به Baseline برمیگردند | بخشی از اتصالها باقی میمانند |
| Restart | ظرفیت واقعی را بیشتر نمیکند | موقتاً منابع را آزاد و مشکل را پنهان میکند |
| روند بلندمدت | متناسب با الگوی مصرف است | شکل پلهای یا صعودی مداوم دارد |
گام اول تشخیص: وضعیت Socketهای لینوکس را ببینید
دستور ss نقطه شروع خوبی برای دیدن Socketهای TCP است. این دستورها را با دسترسی مناسب روی همان Host یا داخل Namespace کانتینر اجرا کنید:
# خلاصه وضعیت سوکتها
ss -s
# شمارش اتصالها براساس State
ss -tan | awk 'NR>1 {count[$1]++} END {for (s in count) print s, count[s]}' | sort
# اتصالهای یک پورت مشخص؛ برای مثال PostgreSQL
ss -tanp '( sport = :5432 or dport = :5432 )'
# تعداد CLOSE_WAITها
ss -tan state close-wait | wc -l
# تعداد اتصالهای برقرار
ss -tan state established | wc -l
یک Snapshot کافی نیست. خروجی را در چند بازه زمانی ثبت کنید و آن را کنار تعداد درخواستها بگذارید. برای بررسی سریع میتوانید دستور را با watch تکرار کنید، اما برای تشخیص قطعی به نمودار تاریخی نیاز دارید.
Stateها چه چیزی میگویند؟
- ESTABLISHED: اتصال باز است؛ زیادبودن آن ممکن است طبیعی یا ناشی از Pool بزرگ باشد.
- CLOSE_WAIT: سمت مقابل اتصال را بسته، اما برنامه محلی هنوز Socket را نبسته است. رشد مداوم آن معمولاً نیازمند بررسی کد برنامه است.
- TIME_WAIT: بخشی طبیعی از چرخه TCP در سمتی است که اتصال را فعالانه بسته است. تعداد بالا همیشه Leak نیست و ممکن است از ایجاد اتصال کوتاهعمر و استفادهنکردن از Keep-Alive ناشی شود.
- SYN_SENT: برنامه برای برقراری اتصال منتظر پاسخ است؛ رشد آن میتواند مشکل شبکه، Firewall یا سرویس مقصد را نشان دهد.
اشتباه رایج این است که هر TIME_WAIT زیادی را Connection Leak بدانیم. Leak واقعی را باید با روند زمانی، مالک Socket، Traffic و چرخه عمر Connection اثبات کرد.
گام دوم: Process مالک اتصال را پیدا کنید
# تعداد File Descriptorهای یک Process
ls /proc/<PID>/fd | wc -l
# سقف File Descriptor همان Process
cat /proc/<PID>/limits | grep 'open files'
# مشاهده فایلها و Socketهای باز Process
lsof -nP -p <PID>
# فقط اتصالهای شبکه همان Process
lsof -nP -a -p <PID> -i
بهجای تمرکز روی یک عدد، نرخ رشد را دنبال کنید. اگر File Descriptorهای Process با بار ثابت از ۳۰۰ به ۶۰۰ و سپس ۱۲۰۰ میرسند و پایین نمیآیند، یک سرنخ جدی دارید. بالابردن ulimit در این وضعیت درمان نیست؛ فقط زمان خرابی را عقب میاندازد.
گام سوم: Pool برنامه را مانیتور کنید
مانیتورینگ فقط سمت دیتابیس کافی نیست. Pool داخل برنامه باید حداقل این متریکها را ارائه کند:
pool.total: کل اتصالهای ساختهشدهpool.activeیاused: اتصالهای تحویلدادهشده به درخواستهاpool.idle: اتصالهای آماده استفادهpool.pendingیاwaiting: درخواستهای منتظر Connection- زمان انتظار برای دریافت Connection
- تعداد Timeout و خطا هنگام Acquire
قویترین سیگنال معمولاً فقط Active Connection نیست؛ ترکیب Active نزدیک سقف + Idle نزدیک صفر + Waiting رو به رشد نشان میدهد Pool دیگر ظرفیت پاسخگویی ندارد. سپس باید مشخص کنید اتصالها واقعاً مشغول Query هستند یا در کد گم شدهاند.
گام چهارم: سمت دیتابیس را بررسی کنید
PostgreSQL
-- تعداد Connectionها براساس وضعیت و برنامه
SELECT application_name, state, count(*)
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY application_name, state
ORDER BY count(*) DESC;
-- اتصالهای قدیمی و تراکنشهای باز
SELECT pid, application_name, client_addr, state,
now() - backend_start AS connection_age,
now() - xact_start AS transaction_age,
wait_event_type, wait_event,
left(query, 120) AS query
FROM pg_stat_activity
WHERE datname = current_database()
ORDER BY backend_start ASC;
به وضعیت idle in transaction حساس باشید. این وضعیت صرفاً یک اتصال Idle معمولی نیست؛ تراکنش باز مانده و میتواند Lock، نگهداری نسخههای قدیمی Row و مشکلات Vacuum ایجاد کند. برای تشخیص، سن اتصال و تراکنش را همراه نام برنامه و آدرس Client بررسی کنید.
MySQL
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';
SHOW VARIABLES LIKE 'max_connections';
SHOW FULL PROCESSLIST;
Threads_connected تعداد Clientهای متصل را نشان میدهد، درحالیکه Threads_running اتصالهایی را نشان میدهد که Sleep نیستند. اگر Connected بالا میرود ولی Running و Throughput ثابتاند، تنظیم Pool، اتصالهای Sleep و چرخه آزادسازی Clientها را بررسی کنید. باز هم یک Snapshot کافی نیست؛ روند تاریخی اهمیت بیشتری دارد.
Redis
redis-cli INFO clients
redis-cli CLIENT LIST
در Redis متریک connected_clients را کنار نرخ Commandها، تعداد Clientهای Blocked و سقف maxclients ببینید. ساخت یک Client جدید برای هر Request بهجای استفاده مجدد از اتصال، میتواند تعداد Clientها و هزینه Handshake را بهشدت افزایش دهد.
یک نمونه واقعی در Node.js: اتصال کجا نشت میکند؟
در مثال زیر، اگر Query خطا بدهد یا یکی از مسیرهای شرطی زودتر Return کند، ممکن است Connection هرگز به Pool برنگردد:
async function getUser(userId) {
const connection = await pool.getConnection()
const [rows] = await connection.query(
'SELECT * FROM users WHERE id = ?',
[userId]
)
if (!rows.length) {
return null // connection.release() اجرا نمیشود
}
connection.release()
return rows[0]
}
نسخه امنتر، آزادسازی را داخل finally قرار میدهد تا موفقیت، خطا یا Return زودهنگام تفاوتی ایجاد نکند:
async function getUser(userId) {
const connection = await pool.getConnection()
try {
const [rows] = await connection.query(
'SELECT * FROM users WHERE id = ?',
[userId]
)
return rows.length ? rows[0] : null
} finally {
connection.release()
}
}
اگر کتابخانه اجازه میدهد Query ساده را مستقیماً روی Pool اجرا کنید، معمولاً خود Pool چرخه Acquire و Release را مدیریت میکند. دریافت دستی Connection را به تراکنشها یا سناریوهایی محدود کنید که واقعاً به یک Session ثابت نیاز دارند.
تراکنشها مسیر خطای جداگانه دارند
async function transfer(fromId, toId, amount) {
const connection = await pool.getConnection()
try {
await connection.beginTransaction()
await debit(connection, fromId, amount)
await credit(connection, toId, amount)
await connection.commit()
} catch (error) {
await connection.rollback()
throw error
} finally {
connection.release()
}
}
Rollback جای Release را نمیگیرد. Rollback تراکنش را پایان میدهد و Release اتصال را به Pool برمیگرداند؛ هر دو لازماند.
چه چیزهایی ظاهراً مشکل را حل میکنند، اما درمان نیستند؟
- Restart دورهای: منابع را آزاد میکند، اما علت را باقی میگذارد.
- بزرگکردن Pool: زمان رسیدن به سقف را بیشتر میکند و ممکن است فشار بیشتری به دیتابیس وارد کند.
- افزایش max_connections: بدون محاسبه ظرفیت دیتابیس میتواند مصرف حافظه و رقابت را بالا ببرد.
- افزایش ulimit: برای ظرفیت واقعی مفید است، اما Leak را اصلاح نمیکند.
- کاهش Timeout بهتنهایی: خرابی را سریعتر آشکار میکند، ولی اتصال گمشده را لزوماً آزاد نمیکند.
Alert مناسب برای Connection Leak چگونه ساخته میشود؟
روی یک عدد ثابت و جهانی Alert نسازید. Pool با سقف ۲۰ و Pool با سقف ۵۰۰ رفتار یکسانی ندارند. هشدار را نسبتی، پایدار و چندسیگناله طراحی کنید.
| سیگنال | هشدار پیشنهادی | دلیل |
|---|---|---|
| Pool Utilization | بیش از ۸۰٪ برای یک بازه پایدار | نزدیکشدن به محدودیت را زودتر نشان میدهد |
| Pending Acquires | بزرگتر از صفر و رو به رشد | درخواستها منتظر منبعاند |
| Acquire Duration | افزایش p95 نسبت به Baseline | فشار Pool را پیش از Timeout آشکار میکند |
| Connection / Traffic | رشد Connection با Traffic ثابت | الگوی رفتاری Leak را پیدا میکند |
| CLOSE_WAIT | رشد پیوسته برای یک Process | احتمال بستهنشدن Socket محلی |
| Open FD Ratio | نزدیکشدن FD باز به سقف Process | از خطای نهایی EMFILE جلوگیری میکند |
مقدار ۸۰ درصد فقط نقطه شروع است، نه قانون همگانی. آن را با ظرفیت، Burst معمول، زمان لازم برای واکنش و Baseline سرویس خود تنظیم کنید. هشدار بحرانی بهتر است چند شرط را ترکیب کند؛ مثلاً Pool بالای ۸۵ درصد، Idle نزدیک صفر و Pending رو به رشد برای پنج دقیقه.
Runbook پنجدقیقهای هنگام پرشدن Connection Pool
- اثر را تأیید کنید: Latency، Error Rate و Endpointهای درگیر را ببینید.
- Pool را بررسی کنید: Active، Idle، Pending، Limit و Acquire Time را ثبت کنید.
- Traffic را مقایسه کنید: آیا Request Rate واقعاً بیشتر شده است؟
- سمت مقصد را ببینید: Query طولانی، Lock، تراکنش باز یا Clientهای Sleep وجود دارد؟
- Process را مشخص کنید: PID، تعداد FD و State سوکتهای آن را بررسی کنید.
- تغییر اخیر را پیدا کنید: Release جدید، تغییر Pool یا Dependency تازه را بررسی کنید.
- شواهد را قبل از Restart ذخیره کنید: نمودارها، خروجی Queryها، Thread Dump یا Profile لازم را نگه دارید.
- کاهش اثر را انجام دهید: در صورت نیاز Rollback، محدودکردن Traffic یا Restart کنترلشده؛ سپس علت کد را اصلاح کنید.
Restart ممکن است برای بازیابی فوری سرویس لازم باشد، اما اگر قبل از آن شواهد را جمع نکنید، مهمترین سرنخها از بین میروند و Incident بعدی دوباره از نقطه صفر بررسی میشود.
چطور Connection Leak را قبل از Production پیدا کنیم؟
- تست را به چند دقیقه محدود نکنید؛ Soak Test چندساعته اجرا کنید.
- مسیرهای خطا، Timeout، Cancel و Return زودهنگام را عمداً فعال کنید.
- در پایان تست بررسی کنید Active Connection و FDها به Baseline برگشتهاند.
- قطعشدن دیتابیس یا سرویس مقصد را شبیهسازی کنید و رفتار Reconnect را ببینید.
- برای Pool یک سقف کوچک در محیط تست قرار دهید تا Leak زودتر آشکار شود.
- متریک تعداد Acquire و Release را مقایسه کنید؛ اختلاف دائمی نیازمند بررسی است.
- Shutdown برنامه را تست کنید تا Connectionها و Timerها بهدرستی بسته شوند.
چکلیست پیشگیری در کد و معماری
- آزادسازی Connection، Stream و Socket را در
finallyانجام دهید. - برای دریافت Connection و اجرای عملیات Timeout جداگانه داشته باشید.
- یک Pool مشترک برای هر Process بسازید؛ داخل هر Request Pool جدید نسازید.
- Pool را متناسب با تعداد Instanceها و ظرفیت دیتابیس تنظیم کنید؛ سقف هر Instance را جداگانه ضرب کنید.
- نام برنامه، Instance و Environment را در اتصال دیتابیس ثبت کنید تا منشأ Client مشخص باشد.
- Retry را محدود و همراه Backoff و Jitter پیادهسازی کنید تا هنگام خرابی Connection Storm نسازید.
- Keep-Alive را آگاهانه تنظیم کنید؛ خاموشکردن یا روشنکردن آن بدون مشاهده Traffic راهحل عمومی نیست.
- متریکهای Pool و File Descriptor را از روز اول ثبت کنید، نه بعد از Incident.
Watchlog چطور به تشخیص Connection Leak کمک میکند؟
برای تشخیص نشتی اتصال باید چند لایه را کنار هم ببینید: تعداد Connectionهای سرویس، منابع Host، وضعیت دیتابیس، Latency و Error Rate برنامه، Log خطاها و زمان Release. در Watchlog میتوانید متریکهای سرور و Integrationهای MongoDB، PostgreSQL، MySQL و Redis را کنار APM، Trace و Logهای برنامه بررسی کنید.
هدف فقط دیدن عدد Connection نیست. وقتی افزایش اتصال با زمان انتشار نسخه، Endpoint کند، Query مرتبط و تعداد File Descriptorهای Host در یک Timeline قرار بگیرد، فاصله بین «سرور کند شده» و «کدام مسیر اتصال را آزاد نکرده» بسیار کوتاهتر میشود.
سؤالات متداول
آیا زیادبودن تعداد Connection همیشه نشانه Leak است؟
خیر. ترافیک بالا، Query طولانی، تراکنش باز، Pool بزرگ یا Keep-Alive میتوانند تعداد Connection را افزایش دهند. Leak زمانی محتملتر است که Connection با Traffic ثابت رشد کند، پس از کاهش بار به Baseline برنگردد و Restart موقتاً آن را برطرف کند.
تفاوت Connection Leak و Connection Pool Exhaustion چیست؟
Connection Leak یکی از علتهای Pool Exhaustion است. Pool ممکن است بهدلیل Leak پر شود، اما Queryهای بسیار کند، Lock، ترافیک بیشتر از ظرفیت یا Pool بسیار کوچک نیز میتوانند همه اتصالها را مشغول کنند. Exhaustion نتیجه است؛ Leak یکی از علتهای احتمالی آن.
آیا TIME_WAIT زیاد یعنی برنامه Connection Leak دارد؟
نه لزوماً. TIME_WAIT بخشی طبیعی از بستهشدن TCP است و تعداد بالای آن میتواند از اتصالهای کوتاهعمر فراوان ناشی شود. رشد CLOSE_WAIT برای یک Process معمولاً سرنخ مستقیمتری از بستهنشدن Socket محلی است، اما آن هم باید با کد و روند زمانی بررسی شود.
آیا افزایش max_connections مشکل را حل میکند؟
اگر ظرفیت واقعاً کم باشد شاید لازم باشد، اما در صورت وجود Leak فقط خرابی را عقب میاندازد. همچنین سقف بالاتر میتواند حافظه و رقابت دیتابیس را افزایش دهد. ابتدا علت مصرف Connection را پیدا کنید و سپس ظرفیت را براساس تعداد Instanceها و توان دیتابیس تنظیم کنید.
مهمترین متریک برای تشخیص Connection Leak چیست؟
یک متریک واحد کافی نیست. روند Active و Idle Connection، Pending Acquire، زمان دریافت Connection، Request Rate، وضعیت Socketها و تعداد File Descriptor باید کنار هم دیده شوند. رشد Connection در برابر Traffic ثابت یکی از مهمترین الگوهاست.
چرا Restart مشکل را موقتاً حل میکند؟
با پایان Process، سیستمعامل Socketها و File Descriptorهای متعلق به آن را آزاد میکند و Pool جدید از صفر ساخته میشود. چون مسیر معیوب کد هنوز وجود دارد، منابع دوباره بهمرور نشت میکنند و مشکل بازمیگردد.
جمعبندی
Connection Leak یک خرابی ناگهانی نیست؛ یک بدهی در حال انباشتهشدن است. هر Connection آزادنشده مقدار کمی از ظرفیت محدود سرور یا دیتابیس را مصرف میکند تا روزی Pool، Socket یا File Descriptor دیگری باقی نماند. در آن لحظه CPU و RAM ممکن است کاملاً عادی باشند، اما سرویس دیگر توان پاسخگویی ندارد.
برای پیدا کردن آن فقط به تعداد Connection نگاه نکنید. روند اتصال را کنار Traffic ببینید، State سوکتها را بررسی کنید، Pool داخل برنامه را مانیتور کنید، اتصالهای قدیمی و تراکنشهای باز دیتابیس را پیدا کنید و مسیرهای خطا را در کد با try/finally ایمن کنید. مهمتر از همه، پیش از Restart شواهد را نگه دارید؛ چون همان شواهد تفاوت میان یک راهحل موقت و اصلاح واقعی علت را مشخص میکنند.





