ليه محتاج ده؟
- الكاش هيفشل في يوم: restart، deploy، bug في الـ invalidation، key سخن نسيته.
- الحلول اللي فاتت بتقلل الاحتمال: بس مش بتخليه صفر.
- فالداتابيز محتاجة حماية لوحدها: لو جه حمل أكبر من اللي تستحمله، ارفض جزء منه بسرعة بدل ما الكل يقع ببطء.
ظبط الـ Pool
db.mjs
import pg from "pg";export const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10, // أقصى connections من الـ instance ده connectionTimeoutMillis: 2000, // acquire timeout: متستناش أكتر من 2 ثانية connection idleTimeoutMillis: 30_000, // اقفل الـ connection اللي فاضية بعد 30 ثانية statement_timeout: 3000, // Postgres يلغي أي query أطول من 3 ثواني});جرّبتهم على Postgres 16 بجد:
statement_timeout:SELECT pg_sleep(3)مع timeout ثانية رجعتcanceling statement due to statement timeout.connectionTimeoutMillis: pool بـmax: 2وكل الـ connections مشغولة، الـconnect()التالت رجعtimeout exceeded when trying to connectبعد الوقت المحدد.
الـ max يبقى كام؟
- مش “أكبر أحسن”: الداتابيز عندها
max_connections(في Postgres الـ default بتاعه 100)، وكل connection بتاخد memory، وأكتر من عدد الـ cores بكتير بيبطّأ الكل. - الحسبة: عدد الـ instances ×
maxلازم يبقى أقل منmax_connectionsناقص connections الـ admin والـ migrations والـ jobs. - مثال: 20 pod × 10 = 200 connection. لو الداتابيز 100، هتفشل. يا تقلل الـ
max، يا تحط PgBouncer في النص. - الـ pool الصغير مع timeout قصير أحسن من pool كبير: الـ requests الزيادة بتفشل بسرعة بدل ما الداتابيز كلها تبطّأ.
الـ timeouts المهمة
| الـ timeout | بيحميك من إيه | قيمة بداية |
|---|---|---|
connectionTimeoutMillis (acquire) | الطابور في الـ pool يطول للأبد | 1–3 ثواني |
statement_timeout | query واحدة تفضل ماسكة connection | 2–10 ثواني حسب الـ endpoint |
query_timeout (في pg client) | الـ client يستنى رد مهما حصل | أطول شوية من الـ statement |
| timeout الـ HTTP request | الـ request كله | أقل من timeout الـ load balancer |
- القاعدة: كل timeout جوه لازم يبقى أقصر من اللي براه. وإلا الـ client يمشي والسيرفر لسه بيشتغل على request محدش مستنيه.
- راجع الـ docs: أسامي الـ options بتختلف بين
pgوpostgres.jsوPrisma وKnex.
ارفض بدري (Load Shedding)
- الفكرة: لو الطابور في الـ pool طويل، الـ request الجديد هيفشل غالبًا. فارفضه دلوقتي بـ
503بدل ما ياخد مكان ويفشل بعد ثانيتين. pg.Poolبيديك الأرقام:totalCountوidleCountوwaitingCount.
shed.mjs
import { pool } from "./db.mjs";const MAX_WAITING = 50;// Express middlewareexport function shedLoad(req, res, next) { if (pool.waitingCount > MAX_WAITING) { res.set("Retry-After", "2"); return res.status(503).json({ error: "busy, try again" }); } next();}- ده نوع من backpressure: السيستم بيقول للي قبله “أبطأ شوية” بدل ما يبلع كل حاجة.
Retry-After: بيقول للـ client يستنى قد إيه قبل ما يجرّب.- الـ Rate limiting: حد لكل يوزر أو IP (مثلًا في Redis أو الـ API gateway)، عشان يوزر واحد أو bot ميعملش stampede لوحده.
- حدد الأولويات: ارفض الـ recommendations قبل الـ checkout.
الـ Circuit Breaker
- الفكرة: لو الداتابيز بتفشل كتير ورا بعض، بطّل تبعتلها شوية. سيبها تفوق.
- عنده 3 حالات:
- مقفولة (closed): كله شغال عادي، وبنعد الأخطاء.
- مفتوحة (open): الأخطاء عدّت حد معين، فبنرفض على طول من غير ما نكلم الداتابيز.
- نص مفتوحة (half-open): بعد وقت، بنسيب request يجرّب. لو نجح نقفلها، لو فشل نفتحها تاني.
breaker.mjs
export class CircuitBreaker { constructor({ failureThreshold = 5, resetAfterMs = 10_000 } = {}) { this.failureThreshold = failureThreshold; this.resetAfterMs = resetAfterMs; this.failures = 0; this.state = "closed"; // closed | open | half-open this.openedAt = 0; } async call(fn) { if (this.state === "open") { if (Date.now() - this.openedAt < this.resetAfterMs) { throw new Error("circuit open: DB محمية، جرّب بعدين"); } this.state = "half-open"; // نسيب request يجرّب } try { const result = await fn(); this.failures = 0; this.state = "closed"; return result; } catch (err) { this.failures++; if (this.state === "half-open" || this.failures >= this.failureThreshold) { this.state = "open"; this.openedAt = Date.now(); } throw err; } }}// الاستخدامconst dbBreaker = new CircuitBreaker({ failureThreshold: 5, resetAfterMs: 10_000 });const product = await dbBreaker.call(() => pool.query("SELECT ...", [id]));- جرّبته: بـ threshold = 3، أول 3 أخطاء عدّت للداتابيز، والـ call الرابعة والخامسة اترفضت على طول بـ
circuit open، وبعد الـ reset أول نجاح رجّعهاclosed. - وهي مفتوحة رجّع إيه؟ القيمة الـ stale لو موجودة (فصل ٧)، أو fallback، أو
503. - النسخة دي مبسطة: في half-open ممكن كذا request يعدّوا مع بعض. للـ production استخدم مكتبة زي
opossumوراجع الـ docs بتاعتها.
افصل الأحواض (Bulkheads)
- الاسم جاي من المراكب: المركب مقسومة لأوض مقفولة، لو واحدة غرقت الباقي يفضل عايم.
- في الكود: pool منفصل لكل نوع شغل. مثلًا pool للـ checkout، وpool للتقارير والـ endpoints التقيلة.
- النتيجة: لو الـ stampede ملا pool التقارير، الـ checkout لسه عنده connections بتاعته.
- نفس الفكرة: حد أقصى للـ concurrency لكل endpoint أو لكل dependency (مثلًا مكتبة
p-limit).
الـ Retries صح
- الـ retry من غير تفكير هو اللي بيعمل retry storm (فصل ٤).
- عدد محدود: 2 أو 3 مرات بالكتير.
- الـ Backoff أُسّي: استنى 100ms، بعدين 200، بعدين 400…
- وJitter: الوقت عشوائي عشان كل الـ clients ميرجعوش مع بعض. الطريقة المعروفة بـ “full jitter”: رقم عشوائي بين 0 والـ backoff.
- متعملش retry لكل حاجة: الـ
400والـ validation errors مش هتتصلح بالـ retry. والـ503اللي معاهRetry-Afterاحترمه. - الـ retry في طبقة واحدة بس: لو الـ client بيعمل 3 والـ gateway 3 والسيرفر 3، يبقى الـ request الواحد ممكن يبقى 27.
retry.mjs
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));export async function retry(fn, { attempts = 3, baseMs = 100, maxMs = 2000 } = {}) { for (let i = 0; ; i++) { try { return await fn(); } catch (err) { if (i >= attempts - 1) throw err; const cap = Math.min(maxMs, baseMs * 2 ** i); await sleep(Math.random() * cap); // full jitter } }}افشل بسرعة (Fail Fast)
request فشل في 50ms أحسن من request فشل في 30 ثانية.- الفشل السريع بيرجّع الـ connection والـ memory بسرعة، فالـ requests التانية تلحق تشتغل.
- الفشل البطيء بيمسك موارد، ويخلي الطابور يطول، ويخلي اليوزر يعمل refresh (retry تاني).
- كل اللي في الفصل ده (timeouts قصيرة، load shedding، circuit breaker) هدفه واحد: افشل بسرعة لما لازم تفشل.
خلّصت الفصل لما
🧠 اختبر نفسك
١) 30 pod كل واحد pool max 10، والـ Postgres بـ max_connections 100. إيه المشكلة والحل؟
300 connection أكبر من 100. قلل الـ max لكل pod، أو استخدم PgBouncer.
٢) الـ circuit مفتوح ويوزر طلب المنتج. ترجّع إيه؟
القيمة القديمة من الكاش لو موجودة، أو fallback، أو 503 بسرعة من غير ما تكلم الداتابيز.
٣) ليه full jitter في الـ retry؟
عشان الـ clients اللي فشلوا مع بعض ميرجعوش يضربوا مع بعض في نفس اللحظة.