چرا بعضی APIها کند میشوند؟ بررسی Bottleneckهای واقعی در Backend
مقدمه؛ چرا یک API ناگهان کند میشود؟
فرض کنید یک API در محیط توسعه تقریباً بلافاصله پاسخ میدهد:
GET /api/products
Response: 80msهمهچیز خوب به نظر میرسد.
اما بعد از انتشار پروژه و افزایش تعداد کاربران، زمان پاسخ به 800ms، 2 ثانیه یا حتی بیشتر میرسد.
در این شرایط معمولاً اولین تصور این است که:
«سرور ضعیف شده.»
اما در بسیاری از پروژهها مشکل واقعاً خود سرور نیست.
ممکن است یک Query ساده به دیتابیس باعث تأخیر شود، تعداد زیادی Request همزمان ایجاد شود، Connection Pool به سقف برسد، یک API خارجی کند باشد یا حتی مقدار زیادی داده بدون نیاز واقعی از Database دریافت شود.
به همین دلیل، کند بودن API یک مشکل واحد نیست؛ بلکه نتیجه یک یا چند Bottleneck در مسیر پردازش Request است.
در این مقاله بررسی میکنیم Bottleneck چیست، کدام قسمتهای Backend بیشتر باعث کندی API میشوند و چگونه میتوانیم محل واقعی مشکل را پیدا کنیم.
Bottleneck در Backend چیست؟
Bottleneck یا «گلوگاه» بخشی از سیستم است که ظرفیت آن نسبت به سایر قسمتها محدودتر است و در نهایت سرعت کل سیستم را کاهش میدهد.
مسیر ساده یک Request را در نظر بگیرید:
Client
↓
Network
↓
Load Balancer
↓
Backend
↓
Business Logic
↓
Database / External API
↓
Backend
↓
Clientاگر Database برای اجرای یک Query به 700ms زمان نیاز داشته باشد، حتی اگر تمام بخشهای دیگر در چند میلیثانیه اجرا شوند، API نمیتواند سریعتر از آن Query پاسخ دهد.
بنابراین:
API Response Time
=
Network
+ Server Processing
+ Database
+ External Services
+ Serializationاین فرمول البته سادهسازی شده است، اما یک نکته مهم را نشان میدهد:
برای سریعتر کردن API ابتدا باید بدانیم زمان دقیقاً کجا مصرف میشود.
۱. Database؛ یکی از رایجترین Bottleneckها
در بسیاری از Backendها، Database یکی از اولین قسمتهایی است که باید هنگام بررسی Performance به سراغ آن برویم.
فرض کنید API زیر را داریم:
GET /api/productsو Backend این Query را اجرا میکند:
SELECT *
FROM products
ORDER BY created_at DESC;اگر جدول فقط چند هزار رکورد داشته باشد، احتمالاً مشکلی احساس نمیکنیم.
اما با بزرگ شدن جدول، اجرای Query ممکن است هزینه بیشتری داشته باشد؛ مخصوصاً اگر ستون موردنظر Index مناسبی نداشته باشد.
راهحل چیست؟
یکی از اولین اقدامات، بررسی Query و Execution Plan است.
مثلاً به جای اینکه صرفاً Query را تغییر دهیم، باید بفهمیم Database واقعاً چگونه آن را اجرا میکند.
در PostgreSQL میتوان از مواردی مانند:
EXPLAIN ANALYZEبرای بررسی Query استفاده کرد.
مواردی که باید بررسی شوند:
Full Table Scan
Missing Index
بیش از حد بودن دادههای خواندهشده
Sortهای سنگین
Joinهای پرهزینه
تعداد دفعات اجرای Query
زمان اجرای واقعی Query
۲. مشکل معروف N+1 Query
یکی از Bottleneckهای رایج در Backend، مخصوصاً هنگام کار با ORMها، مشکل N+1 Query است.
فرض کنید میخواهیم لیست سفارشها را دریافت کنیم:
Orders
├── User
├── User
├── User
└── Userممکن است Backend ابتدا یک Query برای دریافت سفارشها اجرا کند:
SELECT * FROM orders;سپس برای هر سفارش یک Query جداگانه برای User اجرا کند.
اگر 100 سفارش داشته باشیم:
1 Query برای Orders
+
100 Query برای Users
=
101 Queryدر حالی که میتوان با طراحی بهتر Query یا استفاده صحیح از امکانات ORM، تعداد Queryها را کاهش داد.
چرا N+1 خطرناک است؟
چون در محیط Development ممکن است کاملاً عادی به نظر برسد.
اما وقتی تعداد دادهها افزایش پیدا کند:
10 records → 11 queries
100 records → 101 queries
1000 records → 1001 queriesهزینه به شکل قابل توجهی افزایش پیدا میکند.
بنابراین هنگام بررسی Performance، فقط به زمان کلی API نگاه نکنید؛ تعداد Queryهای Database را هم بررسی کنید.
۳. دریافت بیش از حد داده
یک API ممکن است کند باشد، نه به این دلیل که Database نمیتواند سریع پاسخ دهد، بلکه به این دلیل که Backend بیش از نیاز داده دریافت میکند.
مثلاً:
SELECT *
FROM users;در حالی که Frontend فقط به این موارد نیاز دارد:
id
name
avatarاگر جدول User دهها ستون داشته باشد و تعداد کاربران نیز زیاد باشد، انتقال داده اضافی میتواند هزینه ایجاد کند.
راه بهتر:
SELECT id, name, avatar
FROM users;این مفهوم در API Design نیز اهمیت دارد.
به جای:
GET /api/usersکه حجم زیادی از اطلاعات را برمیگرداند، میتوان Response را متناسب با نیاز واقعی طراحی کرد.
۴. Pagination را فراموش نکنید
یکی از اشتباهات رایج:
GET /api/productsو برگرداندن تمام محصولات موجود در Database.
فرض کنید یک فروشگاه 500,000 محصول دارد.
آیا منطقی است یک Request تمام این محصولات را دریافت کند؟
طبیعتاً خیر.
به جای آن:
GET /api/products?page=1&limit=20یا در سیستمهای بزرگتر، Pagination مبتنی بر Cursor استفاده میشود.
مثلاً:
GET /api/products?cursor=abc123&limit=20Pagination علاوه بر کاهش حجم Response، میتواند فشار روی Database و Network را نیز کاهش دهد.
۵. Connection Pool؛ وقتی Database دیگر Connection ندارد
Backend برای ارتباط با Database معمولاً از Connection Pool استفاده میکند.
به جای اینکه برای هر Request یک Connection جدید ساخته شود، تعدادی Connection آماده در Pool نگهداری میشوند.
مثلاً:
Connection Pool
┌─────────────────────┐
│ Connection 1 │
│ Connection 2 │
│ Connection 3 │
│ Connection 4 │
│ Connection 5 │
└─────────────────────┘حالا تصور کنید Backend با حجم زیادی از Request مواجه شود:
1000 Requests
↓
Connection Pool
↓
Only 20 Connectionsدر این شرایط Requestهای اضافی ممکن است مجبور شوند منتظر آزاد شدن Connection بمانند.
نتیجه:
Queueing
↓
Higher Latency
↓
Timeout
↓
Failed Requestsبنابراین صرفاً افزایش اندازه Pool همیشه راهحل نیست.
اگر Database ظرفیت مدیریت Connectionهای بیشتر را نداشته باشد، افزایش بیحساب Pool حتی میتواند وضعیت را بدتر کند.
۶. APIهای خارجی؛ Bottleneckای که کنترل کامل آن دست شما نیست
فرض کنید Backend شما برای پاسخ دادن به یک Request باید به یک سرویس خارجی متصل شود:
Client
↓
Your API
↓
Payment API
↓
Responseاگر سرویس خارجی 1.5 ثانیه زمان نیاز داشته باشد، API شما نیز تحت تأثیر قرار میگیرد.
این مشکل در سرویسهایی مانند:
Payment
Email
SMS
Maps
Authentication
Shipping
Third-party APIs
زیاد دیده میشود.
یک اشتباه رایج
گاهی Backend چند API خارجی را به صورت Sequential صدا میزند:
API A → 300ms
↓
API B → 500ms
↓
API C → 400ms
Total ≈ 1200msاگر این عملیات مستقل از یکدیگر باشند، میتوان در معماری مناسب آنها را به شکل Concurrent اجرا کرد:
┌── API A → 300ms
Backend ├── API B → 500ms
└── API C → 400ms
Total ≈ 500msالبته اجرای همزمان فقط زمانی منطقی است که وابستگی بین عملیات وجود نداشته باشد و محدودیتهای سرویسهای مقصد نیز رعایت شود.
۷. Sequential Processing؛ وقتی Backend کارها را پشت سر هم انجام میدهد
فرض کنید یک Request باید سه عملیات انجام دهد:
Validate User
↓
Fetch Product
↓
Fetch Recommendationsاگر هر عملیات زمان قابل توجهی داشته باشد، زمان نهایی تقریباً مجموع آنها خواهد بود.
اما اگر بعضی عملیات مستقل باشند، میتوان آنها را همزمان اجرا کرد.
در JavaScript/Node.js مثلاً:
const [products, recommendations] = await Promise.all([
getProducts(),
getRecommendations()
]);در این حالت عملیات مستقل میتوانند به صورت Concurrent پیش بروند.
اما نکته مهم این است که:
Concurrency همیشه به معنی سریعتر شدن نیست.
اگر Database یا سرویس مقصد خودش Bottleneck باشد، ایجاد درخواستهای بیشتر میتواند فشار را افزایش دهد.
۸. CPU Bottleneck؛ وقتی مشکل Database نیست
همه مشکلات Performance مربوط به Database نیستند.
گاهی Backend به دلیل پردازش سنگین CPU کند میشود.
برای مثال:
پردازش تصویر
تبدیل ویدئو
رمزنگاری سنگین
محاسبات پیچیده
پردازش فایلهای بزرگ
عملیات سنگین روی JSON
الگوریتمهای پرهزینه
ممکن است CPU را برای مدت زیادی درگیر کنند.
مثلاً اگر یک Request چنین کاری انجام دهد:
Request
↓
Large File
↓
Heavy Processing
↓
Responseاجرای این عملیات داخل Request اصلی میتواند باعث افزایش Latency شود.
در چنین شرایطی ممکن است استفاده از:
Background Jobs
Queue
Workerراهکار مناسبتری باشد.
۹. Memory و Garbage Collection
در Runtimeهایی مانند Node.js، مصرف زیاد Memory میتواند روی Performance تأثیر بگذارد.
فرض کنید یک API حجم بزرگی از داده را یکجا در Memory نگه میدارد:
const users = await getAllUsers();اگر تعداد دادهها بسیار زیاد باشد، مصرف Memory افزایش پیدا میکند.
در چنین شرایطی ممکن است:
High Memory Usage
↓
More Garbage Collection
↓
CPU Pressure
↓
Higher Latencyایجاد شود.
راهکار بسته به سناریو میتواند شامل مواردی مانند:
Pagination
Streaming
محدود کردن حجم داده
پردازش مرحلهای
جلوگیری از نگهداری Objectهای غیرضروری
باشد.
۱۰. Caching؛ وقتی لازم نیست هر بار از اول محاسبه کنیم
یکی از قدرتمندترین ابزارهای Backend برای کاهش Latency، Caching است.
فرض کنید API زیر مرتباً درخواست میشود:
GET /api/categoriesاگر اطلاعات Categories به ندرت تغییر کند، منطقی نیست برای هر Request دوباره Database را Query کنیم.
میتوان نتیجه را Cache کرد:
Request
↓
Cache
├── HIT → Return Data
│
└── MISS
↓
Database
↓
Cache
↓
Responseبرای چنین سناریوهایی ابزارهایی مانند Redis میتوانند استفاده شوند.
اما Caching نیز یک هزینه مهم دارد:
Cache Invalidation
یعنی وقتی داده اصلی تغییر کرد، باید بدانیم نسخه Cache شده چه زمانی و چگونه باید منقضی یا بهروزرسانی شود.
۱۱. Serialization و حجم Response
فرض کنید Backend اطلاعات زیادی تولید میکند و سپس آنها را به JSON تبدیل میکند.
مثلاً:
{
"id": 1,
"name": "Product",
"description": "...",
"metadata": "...",
"history": "...",
"relatedProducts": [...]
}اگر Response بیش از حد بزرگ باشد، چند مشکل ایجاد میشود:
Database
↓
Backend
↓
Serialization
↓
Network
↓
Client Parsingبنابراین Performance فقط زمان اجرای Backend نیست.
حجم Response هم بخشی از Performance است.
راهکارها میتوانند شامل:
کاهش فیلدهای غیرضروری
Pagination
Compression
استفاده مناسب از Cache
طراحی بهتر API Response
باشند.
۱۲. Network Latency را نادیده نگیرید
گاهی Backend شما واقعاً سریع است، اما Client و Server فاصله زیادی دارند.
مثلاً:
User
↓
Europe
↓
US Server
↓
Databaseحتی اگر پردازش Server سریع باشد، Network Latency میتواند روی زمان نهایی تأثیر بگذارد.
به همین دلیل در سیستمهای بزرگ، مواردی مانند:
CDN
Edge Computing
Geographic Distribution
Region Selection
Connection Reuse
میتوانند اهمیت پیدا کنند.
۱۳. مشکل فقط یک Request نیست؛ Load میتواند همهچیز را تغییر دهد
ممکن است API در حالت عادی کاملاً سریع باشد:
10 Requests/sec
→ 100msاما با افزایش Load:
100 Requests/sec
→ 400ms
500 Requests/sec
→ 1.5s
1000 Requests/sec
→ Timeoutاین موضوع نشان میدهد که Performance باید تحت Load واقعی نیز بررسی شود.
اینجاست که Load Testing اهمیت پیدا میکند.
ابزارهایی مانند:
k6
Apache JMeter
Artillery
میتوانند برای شبیهسازی Load مورد استفاده قرار بگیرند.
۱۴. چگونه Bottleneck واقعی را پیدا کنیم؟
یکی از بدترین روشها این است که بدون اندازهگیری شروع به تغییر معماری کنیم.
مثلاً:
«API کند است؛ پس Redis اضافه کنیم.»
یا:
«Database کند است؛ پس Server را قویتر کنیم.»
این تصمیمها ممکن است مشکل واقعی را حل نکنند.
روش بهتر:
Measure
↓
Find Bottleneck
↓
Form Hypothesis
↓
Optimize
↓
Measure Againیعنی:
اول اندازهگیری، بعد بهینهسازی.
۱۵. Observability؛ بدون آن پیدا کردن مشکل سخت میشود
برای تشخیص Bottleneck در Production، داشتن اطلاعات مناسب بسیار مهم است.
سه بخش مهم Observability عبارتاند از:
Logs
برای فهمیدن اینکه چه اتفاقی افتاده است.
مثلاً:
Request /api/orders
Status: 200
Duration: 842msMetrics
برای مشاهده وضعیت سیستم در طول زمان:
CPU
Memory
Requests/sec
Error Rate
Latency
Database ConnectionsTracing
Tracing به شما اجازه میدهد مسیر یک Request را مرحلهبهمرحله ببینید:
HTTP Request 10ms
↓
Auth 15ms
↓
Database 620ms
↓
External API 180ms
↓
Serialization 20msحالا Bottleneck تقریباً مشخص است:
Database = 620msدر چنین شرایطی به جای حدس زدن، میدانیم کجا باید تحقیق کنیم.
۱۶. Latency را فقط با Average اندازه نگیرید
یکی از اشتباهات رایج در بررسی Performance استفاده از Average Latency است.
فرض کنید 1000 Request داشتهایم.
ممکن است Average برابر با:
120msباشد.
اما شاید وضعیت واقعی این باشد:
p50 = 80ms
p95 = 300ms
p99 = 1200msیعنی 1 درصد از Requestها بیشتر از یک ثانیه زمان بردهاند.
در سیستمهای واقعی، Percentileها اطلاعات ارزشمندتری درباره Tail Latency ارائه میکنند.
معمولاً:
p50 → رفتار میانه
p95 → تجربه بخش بزرگی از کاربران
p99 → Requestهای بسیار کند
را نشان میدهد.
۱۷. Timeout و Retry؛ دو شمشیر دولبه
فرض کنید یک سرویس خارجی کند شده است.
Backend برای آن Timeout تعریف نکرده باشد:
Request
↓
External API
↓
Waiting...
↓
Waiting...
↓
Waiting...در نهایت Connectionها اشغال میشوند.
از طرف دیگر، Retry بدون طراحی مناسب نیز میتواند مشکل را تشدید کند.
مثلاً:
100 Requests
↓
Service Fails
↓
Each Request retries 3 times
↓
300 additional requestsدر شرایط بحرانی این اتفاق میتواند فشار زیادی روی سرویس مقصد ایجاد کند.
برای همین در سیستمهای Production باید مفاهیمی مانند:
Timeout
Exponential Backoff
Jitter
Circuit Breaker
Idempotency
متناسب با سناریو در نظر گرفته شوند.
چکلیست پیدا کردن Bottleneck در API
وقتی یک API کند است، این موارد را به ترتیب بررسی کنید:
Database
آیا Query کند است؟
آیا Index مناسب وجود دارد؟
آیا N+1 Query داریم؟
آیا داده بیشتری از نیاز دریافت میکنیم؟
آیا Pagination داریم؟
Backend
آیا Business Logic پردازش سنگینی دارد؟
آیا عملیات Sequential غیرضروری وجود دارد؟
آیا CPU یا Memory بالا است؟
آیا Garbage Collection مشکل ایجاد میکند؟
Network
Response چقدر بزرگ است؟
Server در چه Regionای قرار دارد؟
آیا سرویس خارجی کند است؟
Connections
Connection Pool پر شده؟
Connectionها دیر آزاد میشوند؟
Timeout مناسب داریم؟
Caching
آیا دادهای داریم که دائماً تکرار میشود؟
Cache Hit Rate چقدر است؟
آیا Cache Invalidation درست طراحی شده؟
Production
p95 و p99 چقدر هستند؟
Error Rate چقدر است؟
در Load بالا چه اتفاقی میافتد؟
آیا Trace برای Requestها داریم؟
یک مثال واقعی از پیدا کردن Bottleneck
فرض کنیم:
GET /api/dashboardدر Development:
Response Time = 120msاما در Production:
Response Time = 1.8sTracing را بررسی میکنیم:
Authentication 20ms
Database Query #1 80ms
Database Query #2 900ms
External API 600ms
Serialization 40msحالا مشخص است که مشکل یک چیز واحد نیست.
دو Bottleneck اصلی داریم:
Database Query #2 → 900ms
External API → 600msدر این شرایط به جای افزایش CPU سرور، باید Query را بررسی کنیم و نحوه ارتباط با External API را نیز اصلاح کنیم.
ممکن است بعد از Optimization به این وضعیت برسیم:
Authentication 15ms
Database Query #1 30ms
Database Query #2 90ms
External API 120ms
Serialization 20ms
Total ≈ 275msاین همان تفاوت بین حدس زدن مشکل و اندازهگیری Bottleneck واقعی است.
جمعبندی
کند شدن API معمولاً یک علت ساده ندارد.
ممکن است Bottleneck در:
Database
↓
Connection Pool
↓
Business Logic
↓
External API
↓
CPU / Memory
↓
Network
↓
Response Sizeباشد.
به همین دلیل، بهینهسازی Backend نباید با حدس و تغییرات تصادفی انجام شود.
یک فرآیند حرفهای بیشتر شبیه این است:
Measure
↓
Observe
↓
Find Bottleneck
↓
Optimize
↓
Load Test
↓
Measure Againمهمترین نکته این است:
اول بفهمید API دقیقاً کجا زمان مصرف میکند؛ بعد همان قسمت را بهینه کنید.
چون سریعتر کردن بخشی از سیستم که Bottleneck واقعی نیست، ممکن است تقریباً هیچ تأثیری روی Performance نهایی نداشته باشد.
در Backend حرفهای، Performance فقط به معنی «کد سریعتر» نیست؛ بلکه نتیجه طراحی درست Database، API، Cache، Concurrency، Network، Infrastructure و Observability در کنار یکدیگر است.
