بک اند

طراحی API های مقیاس‌پذیر

بیشتر APIها نه به‌خاطر کمبود سرور، بلکه به‌خاطر چند تصمیم اولیه‌ی اشتباه زیر بار می‌شکنند. در این مقاله تصمیم‌هایی را مرور می‌کنیم که طراحی API مقیاس‌پذیر را از همان روز اول ارزان‌تر می‌کنند.

  • نویسنده تیم تتا
  • ۸ دقیقه مطالعه
  • به‌روزرسانی:
طراحی API های مقیاس‌پذیر

مقیاس‌پذیری در API یعنی چه؟

API مقیاس‌پذیر APIای است که وقتی ترافیک از ۱۰۰ به ۱۰٬۰۰۰ درخواست در ثانیه می‌رسد، پاسخ را با اضافه‌کردن سرور نگه می‌دارد، نه با بازنویسی. شرط اول این است که هیچ سرور خاصی چیزی را «به یاد» نداشته باشد: نشست، فایل موقت و شمارنده‌ها باید در Redis، پایگاه‌داده یا فضای ذخیره‌ی مشترک باشند.

گلوگاه معمولاً پایگاه‌داده است، نه لایه‌ی HTTP. بنابراین بیشتر تصمیم‌های این مقاله به یک هدف برمی‌گردند: کم‌کردن کار پایگاه‌داده در هر درخواست.

منابع، نسخه‌بندی و قرارداد پایدار

نشانی‌ها را حول اسم (منبع) بسازید، نه فعل: GET /orders/42 بهتر از /getOrder?id=42 است. روش HTTP معنا را حمل می‌کند و کش‌ها و پروکسی‌ها از آن استفاده می‌کنند؛ GET باید همیشه بی‌اثر و قابل‌کش باشد.

  • نسخه را در مسیر بگذارید (/v1/orders) و تغییرهای سازگار مثل افزودن فیلد را بدون نسخه‌ی جدید منتشر کنید.
  • فیلد را هرگز بی‌خبر حذف یا معنایش را عوض نکنید؛ ابتدا deprecated اعلام کنید.
  • پاسخ‌ها را کوچک نگه دارید و اجازه بدهید کلاینت با fields یا include فقط چیزی را که لازم دارد بگیرد.

صفحه‌بندی: چرا offset در مقیاس بزرگ شکست می‌خورد

LIMIT 20 OFFSET 100000 باید ۱۰۰٬۰۲۰ سطر را بخواند و ۱۰۰٬۰۰۰ تای آن را دور بریزد. هرچه صفحه عمیق‌تر شود، کندتر می‌شود. اگر هنگام ورق‌زدن سطری اضافه یا حذف شود، کاربر یک رکورد را دو بار می‌بیند یا یکی را از دست می‌دهد. راه‌حل، صفحه‌بندی مبتنی بر cursor (keyset) است:

sql
-- first page
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- next page: pass the last row of the previous page
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) < ('2026-10-01 12:00:00', 9812)
ORDER BY created_at DESC, id DESC
LIMIT 20;

یک ایندکس مرکب روی (created_at, id) این پرس‌وجو را برای میلیون‌ها سطر هم در چند میلی‌ثانیه نگه می‌دارد. cursor را به‌صورت رشته‌ی مبهم (مثلاً base64) به کلاینت بدهید تا ساختار داخلی‌تان قرارداد عمومی نشود.

مقایسه‌ی دو روش صفحه‌بندی
روشمزیتضعفمناسب برای
Offsetپرش به صفحه‌ی دلخواهکند در عمق، ناپایدار با تغییر دادهپنل‌های مدیریتی کوچک
Cursorسرعت ثابت، نتیجه‌ی پایداربدون پرش مستقیم به صفحه‌ی Nفید، لیست طولانی، API عمومی
تصویر انتزاعی و رنگارنگ از جریان درخواست‌ها که از چند لایه‌ی سرور عبور می‌کنند و به صفحه‌های کوچک تقسیم می‌شوند
هزینه‌ی هر درخواست باید به اندازه‌ی داده‌ی برگشتی باشد، نه به اندازه‌ی کل جدول.

Idempotency و محدودسازی نرخ

شبکه خطا می‌دهد و کلاینت‌ها دوباره تلاش می‌کنند. اگر POST /payments دو بار اجرا شود، مشتری دو بار شارژ می‌شود. راه‌حل استاندارد، هدر Idempotency-Key است: کلاینت برای هر عملیات یک شناسه‌ی یکتا می‌فرستد و سرور نتیجه‌ی اولین اجرا را کنار آن شناسه ذخیره و برای تکرارها همان را برمی‌گرداند.

محدودسازی نرخ (rate limiting) از API در برابر اشتباه یک کلاینت و سوءاستفاده محافظت می‌کند. الگوریتم ساده و کارآمد، شمارنده‌ی پنجره‌ای در Redis است:

js
// fixed-window limiter: 100 requests per minute per key
async function limit(redis, key) {
  const slot = Math.floor(Date.now() / 60000);
  const k = "rl:" + key + ":" + slot;
  const n = await redis.incr(k);
  if (n === 1) await redis.expire(k, 60);
  return n <= 100;
}

وقتی سقف رد شد، کد 429 Too Many Requests و هدر Retry-After برگردانید تا کلاینت درست صبر کند. شمارنده‌ها را از طریق Redis مشترک نگه دارید تا همه‌ی سرورها یک عدد را ببینند.

کش HTTP و پاسخ‌های شرطی

ارزان‌ترین درخواست، درخواستی است که اصلاً به برنامه نمی‌رسد. با Cache-Control و ETag می‌توانید به مرورگر، CDN و پروکسی بگویید پاسخ تا چه مدت معتبر است. وقتی کلاینت If-None-Match بفرستد و داده تغییر نکرده باشد، سرور فقط 304 Not Modified بدون بدنه برمی‌گرداند.

bash
GET /v1/products/42 HTTP/1.1
If-None-Match: "a7c3"

HTTP/1.1 304 Not Modified
ETag: "a7c3"
Cache-Control: public, max-age=60

برای داده‌های شخصی‌شده private بگذارید تا CDN آن را برای دیگران نگه ندارد. توضیح کامل هدرها در راهنمای کش HTTP در MDN آمده است.

کارهای سنگین را از مسیر درخواست خارج کنید

ارسال ایمیل، ساخت PDF و پردازش تصویر نباید کاربر را منتظر نگه دارد. الگوی درست این است: درخواست را اعتبارسنجی کنید، کار را در صف بگذارید و بلافاصله 202 Accepted با نشانی وضعیت برگردانید. کلاینت بعداً وضعیت را می‌پرسد یا با webhook خبردار می‌شود.

این الگو نوسان ترافیک را هم جذب می‌کند: وقتی ناگهان هزار درخواست می‌آید، صف بزرگ می‌شود ولی API کند نمی‌شود. اگر سیستم رشد کند، همین مرز می‌تواند اولین نقطه‌ی جداسازی سرویس‌ها باشد؛ بحث آن در معماری میکروسرویس‌ها آمده است.

چک‌لیست پیش از انتشار

  • سرویس stateless است و نشست‌ها بیرون از پردازش ذخیره می‌شوند.
  • همه‌ی لیست‌ها صفحه‌بندی و سقف limit دارند (مثلاً حداکثر ۱۰۰).
  • ایندکس‌ها با EXPLAIN روی پرس‌وجوهای پرتکرار بررسی شده‌اند.
  • نوشتن‌ها idempotent و محدودسازی نرخ فعال است.
  • خطاها قالب یکسان دارند و لاگ هر درخواست شناسه‌ی پیگیری دارد.
  • احراز هویت و مجوزها طبق احراز هویت امن با JWT و فهرست OWASP API Security بازبینی شده‌اند.

اگر زیرساخت API یک محصول جدید را برنامه‌ریزی می‌کنید، تیم توسعه‌ی وب ما می‌تواند طراحی و بازبینی آن را همراهی کند.

پرسش‌های متداول

API مقیاس‌پذیر چه ویژگی‌هایی دارد؟

بدون حالت است، لیست‌ها را صفحه‌بندی می‌کند، پرس‌وجوها ایندکس دارند، پاسخ‌های تکراری کش می‌شوند و کارهای سنگین در صف اجرا می‌شوند. با اضافه‌کردن سرور، ظرفیت تقریباً خطی بالا می‌رود.

صفحه‌بندی cursor بهتر است یا offset؟

برای داده‌ی بزرگ و پرتغییر cursor بهتر است، چون سرعتش با عمق صفحه کم نمی‌شود و نتیجه پایدار می‌ماند. offset فقط وقتی مناسب است که داده کم باشد و پرش به صفحه‌ی دلخواه لازم باشد.

Idempotency key چیست؟

شناسه‌ای یکتا که کلاینت همراه درخواست نوشتن می‌فرستد. سرور نتیجه‌ی اولین اجرا را نگه می‌دارد و برای تکرار همان شناسه همان پاسخ را برمی‌گرداند، بنابراین تلاش مجدد باعث ثبت دوباره نمی‌شود.

rate limit را کجا پیاده کنم؟

ترجیحاً در لبه، یعنی API gateway یا Nginx، برای حفاظت کلی، و در برنامه با شمارنده‌ی Redis برای قواعد ریزتر مثل سقف هر کاربر یا هر کلید API.

دیدگاه‌ها

    هنوز دیدگاهی ثبت نشده است. اولین نفر باشید.

    دیدگاه‌ها پس از بررسی منتشر می‌شوند.