چرا بعضی APIها کند می‌شوند؟ بررسی Bottleneckهای واقعی در Backend

چرا بعضی APIها کند می‌شوند؟ بررسی Bottleneckهای واقعی در Backend

توسعه‌دهنده بک‌اند

کند بودن API یک مشکل واحد نیست؛ بلکه نتیجه یک یا چند Bottleneck در مسیر پردازش Request است.

چرا بعضی APIها کند می‌شوند؟ بررسی Bottleneckهای واقعی در Backend

صارم توکلی

۱۰ دقیقه مطالعه

۱ نفر

۱۴۰۵/۶/۳۰

چرا بعضی 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=20

Pagination علاوه بر کاهش حجم 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: 842ms

Metrics

برای مشاهده وضعیت سیستم در طول زمان:

CPU
Memory
Requests/sec
Error Rate
Latency
Database Connections

Tracing

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.8s

Tracing را بررسی می‌کنیم:

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 در کنار یکدیگر است.