لماذا تنفد اتصالات قاعدة بياناتك على Vercel
لا تستطيع نسخة دالة بلا خوادم مشاركة مجمّع اتصالات مع التالية. وفهم ذلك هو الفرق بين اختيار مشغّل وحادث متكرر.
يصل الخطأ بعد نشر جرى على ما يرام، عادةً عند أول دفعة حركة: too many connections، أو Timed out fetching a new connection from the connection pool. قاعدة البيانات خاملة. والتطبيق لا يفعل شيئًا غير معتاد. والشيفرة نفسها
تعمل محليًا منذ شهور.
ما تغيّر هو عدد النسخ الموجودة منها.
ما الذي يختلف فعلًا
في التطوير، next dev عملية Node واحدة. تنشئ عميل قاعدة بيانات مرة، فيفتح
مجمّعًا، ويتشاركه كل طلب في تطبيقك. ذلك هو النموذج الذي يفترضه كل درس عن
التجميع، وهو صحيح.
وبعد النشر على منصة بلا خوادم، يعمل معالج المسار داخل نسخة دالة. ولكل نسخة نطاق وحدتها وذاكرتها وبالتالي مجمّعها. وعشر نسخ دافئة بمجمّع من عشرة هي مئة اتصال، ولا شيء في شيفرتك يقول الرقم مئة.
ولا تحرّك العددَ الطلباتُ المتزامنة. بل يحرّكه كم نسخة تبقيها المنصة دافئة، وهو رقم لا تتحكم به ولا تراه من داخل التطبيق.
الخطأ الذي يجعله فوريًا
// app/api/orders/route.ts
export async function GET() {
const client = new PrismaClient(); // ← مجمّع جديد، لكل طلب
...
}العميل المُنشأ داخل معالج يفتح مجمّعًا عند كل استدعاء ويتركه لجامع القمامة. وهذا يستنزف قاعدة بيانات في دقائق تحت أي حركة حقيقية.
والإصلاح على مستوى النسخة أن تنشئه مرة في نطاق الوحدة، مع التحفظ أن إعادة التحميل الساخن تعيد تقييم الوحدات وإلا تراكمت العملاء:
// lib/db.ts
import { PrismaClient } from '@prisma/client';
const globalForDb = globalThis as unknown as { db?: PrismaClient };
export const db = globalForDb.db ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForDb.db = db;هذا ضروري وغير كافٍ. يحدّ المجمّع لكل نسخة. ولا يستطيع الحدّ من عدد النسخ.
الإجابات الثلاث الحقيقية
١. pooler أمام قاعدة البيانات. PgBouncer، أو ما يسمّيه مزوّدك. دوالك تتصل به؛ وهو يوزّعها على عدد صغير من الخلفيات الحقيقية. تلك هي الإجابة المعيارية والأولى التي يُلجأ إليها.
والجزء الذي يجب إصابته هو الوضع. تجميع المعاملات هو ما ينتج أعداد الاتصالات، وهو يسلّمك خلفية لمدة المعاملة فقط - فيكفّ عن العمل كل ما يمتد عبر عبارات. وعمليًا ذلك يعني العبارات المحضَّرة على الخادم، التي تستخدمها أغلب أنظمة ORM افتراضيًا. وكل مزوّد يوثّق راية لهذا؛ اضبطها عمدًا بدل أن تكتشفها.
واحتفظ بسلسلة اتصال ثانية مباشرة للهجرات. تغييرات المخطط تحتاج جلسة حقيقية، وتمريرها عبر pooler معاملات يفشل بطرق يصعب قراءتها.
٢. مشغّل لا يحمل اتصالًا أصلًا. بعض المزوّدين يكشفون قاعدة بياناتهم عبر HTTP، فالاستعلام طلب ولا شيء يُجمَّع:
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
const rows = await sql`select id, total from orders limit 20`;هذا أنظف ملاءمة لنموذج التشغيل، وهو الشيء الوحيد الذي يعمل في بيئة حافة بلا مقابس TCP. والمقايضة أن استعلام HTTP لمرة واحدة ليس جلسة - المعاملات التفاعلية تحتاج مسار WebSocket، ومعالج يُصدر ستة استعلامات متتابعة يدفع ست رحلات حيث دفع اتصال مجمَّع واحدة.
استخدمه لمسارات القراءة باستعلام أو اثنين. واستخدم اتصالًا مجمَّعًا لأي شيء معاملاتي.
٣. اتصالات أقل، لأن استعلامات أقل. مبخوس الحق ومتاح عادةً. الصفحة التي تجري استعلامًا واحدًا بدل أربعة تحتاج ربع زمن الاتصال. والجلب في Server Component بدل معالج مسار يستدعيه عميلك يزيل قفزة كاملة. وتخزين استعلام يتغير كل ساعة يزيله من المسار الساخن.
أسرع اتصال هو الذي لا يفتحه أحد.
أين تعيش الاستعلامات، وهو جزء Next.js
يعطيك App Router عدة أماكن يمكن أن يعمل فيها استعلام، ولها سلوك مختلف تجاه الاتصالات.
Server Components تعمل أثناء الرسم، على الخادم، وتستطيع الاستعلام مباشرةً. ومكوّنان شقيقان يجلب كلٌّ منهما يعنيان استعلامين في الاستدعاء نفسه - لا بأس لمجمّع، وأسوأ لمشغّل HTTP.
Route Handlers هي الحالة التقليدية وتتصرف كأي نقطة نهاية.
Server Actions تعمل لكل إرسال. والحركة التي تكتب تريد معاملة حقيقية، وتلك هي الحالة التي يهم فيها قيد مشغّل HTTP.
الوسيط (middleware) يعمل على كل طلب مطابق، قبل أي شيء آخر. والاستعلام منه يضرب ضغط اتصالاتك في حركتك، بما في ذلك طلبات كانت ستُخدَم من التخزين المؤقت - وهو مكلف لأسباب أخرى أيضًا.
إن استضفت بنفسك
يختفي أغلب هذا. عملية Node طويلة العمر تعني نطاق وحدة واحدًا، ومجمّعًا واحدًا، وعدد اتصالات تضبطه وتستطيع التفكير فيه. وتعود إلى النموذج الذي يصفه كل مقال عن Node منذ 2015.
وتلك نقطة حقيقية لصالح تشغيله بنفسك، وهي صغيرة مقارنةً بما تتحمّله. اذكرها في المقايضة؛ ولا تختر نموذج نشر بسببها.
التشخيص، بالترتيب
١. اسأل قاعدة البيانات كم اتصالًا موجودًا ومن أين، بدل الاستنتاج. ٢. تأكّد أن العميل يُنشأ مرة لكل وحدة لا لكل طلب. ٣. تأكّد أنك تتصل بالـ pooler، وأن الهجرات لا تتصل به. ٤. افحص هل العبارات المحضَّرة معطَّلة حيث يشترط تجميع المعاملات ذلك. ٥. وعندها فقط ارفع الحد - واكتب على أي أساس بُني الرقم الجديد، لأن من يضيف مسارًا بعدك سيحتاج أن يعرف أن هناك ميزانية.
