One self-hosted console to run your entire business — commerce, ERP, HRM, CRM & manufacturing

طبقة البيانات تحت الضغط: PostgreSQL وتجميع الاتصالات وRedis وpgvector

تكبير قاعدة البيانات هو رد الفعل الأول عند ارتفاع الزيارات، وغالبًا ما يكون خاطئًا. تعلّم كيف تحدد الحجم بحسب معدل الكتابة، وتجمّع الاتصالات عبر PgBouncer، وتقسّم Redis أو Valkey بحسب الدور، وتشغّل بحث pgvector دون إبطاء عملية الدفع.

Author

Anichur Rahaman

منذ أسبوعين12 min read1 views
طبقة البيانات تحت الضغط: PostgreSQL وتجميع الاتصالات وRedis وpgvector

عند التاسعة ودقيقتين من صباح يوم تخفيضات خاطفة، وفي مشهد افتراضي، يرى مهندس المناوبة على لوحة المتابعة أن 940 من أصل 1,000 اتصال بقاعدة البيانات قيد الاستخدام، وأن عملية الدفع تتباطأ لتتجاوز ثلاث ثوانٍ. يقترح أحدهم نقل قاعدة البيانات إلى الحجم الأكبر قبل وصول الموجة الثانية.

سيكون ذلك تخمينًا. معظم تلك الاتصالات الـ 940 خاملة، تحتجزها عمليات PHP بين طلب وآخر، ومعالج قاعدة البيانات يعمل بنحو 35 بالمئة. أما الطابور الحقيقي فهو أحد عشر عاملًا ينتظرون الكتابة على الصفوف القليلة نفسها. عدد الاتصالات مجرد عَرَض، وتكبير الحجم يعالج العَرَض لا السبب.

رأيتُ النسخة الحقيقية من هذا المشهد. في منصة استعددتُ فيها لذروة مجدولة يتحرك فيها نحو 1,000 مستخدم في الدقيقة نفسها، لم تكن قاعدة البيانات المُدارة (ذاكرة 16 GB وأربع نوى vCPU وحدّ يقارب 1,000 اتصال) عنق الزجاجة قط، بل كان العنق عاملًا واحدًا يعالج المهام بالتسلسل. يتناول هذا المقال طبقة البيانات كما أخطط لها اليوم: ماذا نقيس، وكيف نجمّع الاتصالات، وما الذي تستطيعه النسخ المتماثلة وما لا تستطيعه، وكيف نمنح Redis أو Valkey دورًا واضحًا، وكيف نضيف بحثًا بالذكاء الاصطناعي دون تحميل قاعدة البيانات التشغيلية استعلامات المتجهات.

هذا هو الجزء الثالث من سلسلة «هندسة الأنظمة عالية الحمل». يتناول الجزء الأول الحافة وطبقة الويب، ويتناول الجزء الثاني الطوابير والعمّال. وهنا ننزل طبقة أخرى إلى البيانات نفسها.

حدّد الحجم بحسب معدل الكتابة لا بعدد الاتصالات

عبارة «1,000 اتصال كحدّ أقصى» تخبرك بعدد العملاء الذين يستطيعون الاتصال، لا بمقدار العمل الذي يقدر الخادم على إنجازه. أربع نوى لا تنفّذ في اللحظة نفسها إلا عددًا قليلًا من الاستعلامات. وكل ما عداها ينتظر، سواء كان متصلًا أم لا.

لذلك قِس ما يُشبع قاعدة البيانات العلائقية فعلًا: معدل الكتابة (عمليات commit في الثانية وحجم WAL)، واستهلاك المعالج، وزمن استجابة القرص، وانتظار الأقفال. أما عدد الاتصالات فلا يهم إلا بوصفه عَرَضًا لشيء آخر، مثل استعلامات بطيئة تُبقي الاتصالات مفتوحة.

قاعدتي بسيطة: لا ترقِّ قاعدة البيانات قبل أن يثبت اختبار الحمل أنها وصلت إلى حدّها. في الإعداد المذكور، كان الحل فصل عقد الويب عن عقد العمّال وتشغيل نحو 12 عملية worker. تحمّلت قاعدة البيانات الحمل الإضافي بسهولة. وبعد نحو 20 عاملًا صارت المهام تنتظر دورها في الكتابة على قاعدة البيانات، وهذا هو السقف الحقيقي الذي يجب مراقبته، لا الاتصالات. هذه الأرقام من إعداد واحد وليست معيارًا عامًا، فأجرِ اختبارك الخاص.

خريطة طبقة البيانات: PgBouncer أمام نسخة PostgreSQL أساسية ونسخة متماثلة، وRedis أو Valkey مقسّم بحسب الدور، وقاعدة pgvector منفصلة، وتخزين كائنات في المنطقة نفسها التي تعمل فيها الخوادم
طبقة واحدة وعدة مخازن، لكل منها مهمة واحدة وحدود خاصة به.

جمّع اتصالاتك

تفتح PHP اتصالات قصيرة كثيرة: كل طلب، وكل مهمة في الطابور، قد يتصل ثم ينقطع. ويشغّل PostgreSQL عملية مستقلة لكل اتصال، فيمكن لألف عميل أن يستهلكوا قدرًا كبيرًا من الذاكرة قبل أن ينفّذوا استعلامًا مفيدًا واحدًا. يقف المجمّع (pooler) مثل PgBouncer بين التطبيق وقاعدة البيانات، فتتشارك مئات اتصالات العملاء بضع عشرات من الاتصالات الفعلية بالخادم.

حساب توضيحي. عقدتا ويب بستين عملية PHP-FPM لكل منهما قادرتان على فتح 120 اتصالًا، وتضيف 12 عملية worker اثني عشر اتصالًا، ويضيف المجدوِل وبضع جلسات إدارية نحو 8. المجموع نحو 140 اتصالًا، أي 140 عملية PostgreSQL تتنافس على أربع نوى. ضع PgBouncer بوضع transaction مع مجمّع من 20 اتصالًا بالخادم، فيتشارك العملاء الـ 140 أنفسهم هذه الاتصالات العشرين. وإذا احتجزت المعاملة المتوسطة اتصالها 5 ms، فإن 20 اتصالًا تحمل 20 ÷ 0.005 = 4,000 معاملة في الثانية كحد أقصى. سيوقفك المعالج عند رقم أقل بكثير، وهذا هو المقصود: يُظهر المجمّع الحدّ الحقيقي بدل أن يخفيه تحت ضجيج فتح الاتصالات وإغلاقها.

لدى PgBouncer ثلاثة أوضاع للتجميع، والوضع الذي تختاره يحدد ما يستطيع تطبيقك فعله.

وضع التجميعيُحتجز اتصال الخادم طوالالأنسب لـانتبه إلى
Sessionمدة اتصال العميل كاملةالمستمعون طويلو العمر والأدوات التي تحتاج حالة الجلسةأقل توفير؛ العملاء الخاملون يحتفظون باتصال بالخادم
Transactionمعاملة واحدةطلبات الويب ومهام الطابور؛ الخيار المعتاد في الأحمال العاليةينكسر كل ما يعتمد على حالة الجلسة
Statementعبارة واحدةأحمال بسيطة تعمل بـ autocommit فقطلا تُسمح معاملات من عدة عبارات

بالنسبة لتطبيق ويب تحت الضغط، يمنح وضع transaction أكبر مكسب. لكن له محاذير يجدر بك معرفتها قبل التحويل.

ما الذي ينكسر في وضع transaction

لأن المعاملة التالية من العميل نفسه قد تذهب إلى اتصال آخر بالخادم، فإن أي حالة تعيش في الجلسة لا يُعتمد عليها:

  • إعدادات مستوى الجلسة. أمر SET العادي لا يبقى، أما SET LOCAL داخل المعاملة فيبقى.
  • الأقفال الاستشارية (advisory locks) وLISTEN/NOTIFY على مستوى الجلسة. استخدم لها اتصالًا مباشرًا أو وضع session.
  • الجداول المؤقتة ومؤشرات WITH HOLD التي تعيش بعد انتهاء المعاملة.
  • العبارات المُعدّة (prepared statements). لم تكن الإصدارات القديمة من PgBouncer تدعمها في وضع transaction. ومنذ الإصدار 1.21 يستطيع PgBouncer تتبعها إذا ضبطتَ الخيار max_prepared_statements على قيمة أكبر من صفر. تحقق من إصدارك ومن المشغّل (driver) بدل الافتراض.

الحل العملي مساران للاتصال: مسار مجمّع للتطبيق، ومسار مباشر لعمليات الترحيل (migrations) وأدوات المخطط وكل ما يستمع. وتجد في مرجع إعدادات PgBouncer كل الخيارات، بما فيها أحجام المجمّعات ومهلها.

النسخ المتماثلة: ما تحلّه وما يجب أن يبقى على النسخة الأساسية

النسخة المتماثلة للقراءة هي نسخة تتبع الأساسية بتأخر بسيط. وهي مناسبة للعمل الذي يتحمل التأخر قليلًا: التقارير والتصدير وقوائم البحث ولوحات المتابعة واستعلامات التحليل التي كانت ستزاحم عملية الدفع (checkout).

لكنها ليست تسريعًا شاملًا. تأخر النسخ أمر طبيعي، ويزداد مع كثرة الكتابة. أبقِ على النسخة الأساسية ما يلي:

  • كل ما يقرأ مباشرة بعد أن يكتب، كعرض صفحة الطلب بعد إتمام الدفع.
  • حجز المخزون والمدفوعات والمصادقة وأي شيء يقرر مالًا أو صلاحية دخول.
  • أي استعلام داخل معاملة تتضمن كتابة أيضًا.

عمليًا يتحول هذا إلى قاعدة توجيه قصيرة تكفيها صفحة واحدة.

مخطط انسيابي بثلاثة قرارات: الحاجة إلى حالة جلسة تعني اتصالًا مباشرًا، والكتابة أو قراءة ما كُتب للتو تعني النسخة الأساسية، وتحمّل التأخر يعني نسخة متماثلة، وإلا فالنسخة الأساسية
أين يُنفَّذ كل استعلام، والأسئلة الثلاثة التي تحسم ذلك.

الفهارس واستعلامات N+1: أرخص طاقة يمكنك شراؤها

قبل شراء عتاد إضافي، اكتشف الاستعلامات التي تستهلك أكبر وقت. تُرتّب إضافة pg_stat_statements في PostgreSQL الاستعلامات بحسب إجمالي الوقت، ويُظهر الأمر EXPLAIN مع الخيارين ANALYZE وBUFFERS هل يقرأ الاستعلام بضع صفحات أم مليونًا.

مشكلتان تسببان معظم الهدر:

  • فهارس ناقصة أو خاطئة. تصفية وفرز على أعمدة بلا فهرس مناسب يحوّلان بحثًا من أجزاء الثانية إلى مسح كامل للجدول. أنشئ فهارس مركّبة تطابق عوامل التصفية والترتيب الفعلية لديك.
  • استعلامات N+1. صفحة قائمة تنفّذ استعلامًا للقائمة ثم استعلامًا إضافيًا لكل صف. خمسون صفًا تعني واحدًا وخمسين رحلة إلى القاعدة. حمّل العلاقات التي تعرضها مسبقًا (eager loading).

Redis وValkey: خادم واحد وعدة مهام

كلمة سريعة عن الأسماء. في مارس 2024 انتقلت Redis إلى تراخيص «المصدر المتاح»، وأطلقت مؤسسة Linux Foundation مشروع Valkey، وهو تفرّع (fork) من Redis 7.2.4 يحتفظ بترخيص BSD المتساهل. يتحدث Valkey البروتوكول نفسه، لذا فكل ما يلي ينطبق على الاثنين.

الخطأ الأكثر شيوعًا وضع الذاكرة المؤقتة (cache) والجلسات ورموز API والطوابير في فضاء مفاتيح واحد. عندها تُفسد عملية مسح واحدة، أو ضيق واحد في الذاكرة، كل شيء معًا. امنح كل دور قاعدة منطقية خاصة به أو نسخة مستقلة، وبادئة مفاتيح خاصة. يمكن تفريغ الـ cache بأمان. أما الطابور فلا.

الدورماذا يعني فقدانهسياسة الإخلاءالقاعدة
Cacheبطء لحظي؛ تُعاد بناء البياناتallkeys-lruلكل شيء TTL؛ والإخلاء مقبول
الجلسات ورموز APIيُسجَّل خروج الناسvolatile-ttl أو volatile-lruاضبط الحجم بحيث لا يحدث إخلاء أبدًا
الطوابيرمهام ضائعةnoevictionتفشل عمليات الكتابة بصوت عالٍ بدل إسقاط العمل بصمت؛ ضع تنبيهًا على الذاكرة

تشرح وثائق الإخلاء في Redis كل سياسة. ونقطتان هما الأهم. مع allkeys-lru يجوز لـ Redis حذف أي مفتاح كي يبقى تحت حدّ الذاكرة، وهذا بالضبط ما تريده في الـ cache وما لا تريده أبدًا في الطابور. ومع noeviction يرفض الخادم الممتلئ الكتابة الجديدة بدل حذف البيانات، فيبقى الطابور سليمًا وتعرف بالأمر من خطأ وتنبيه.

لا تستخدم FLUSHDB أبدًا

امنع FLUSHDB وFLUSHALL في أدلة التشغيل (runbooks) وفي التطبيق. وحين يلزم تفريغ الـ cache، احذف بحسب البادئة. بهذا لا يمكن لتفريغ الـ cache أن يمحو مهام الطابور، حتى لو حدث خطأً في أسوأ لحظة.

اضبط الحجم على قدر الاستخدام

في المنصة نفسها كانت نسخة Valkey المُدارة مخصصة بسعة 16 GB وتستخدم نحو 62 MB فقط. قِس الاستهلاك الفعلي للذاكرة تحت حمل واقعي، واترك هامشًا سخيًا للطابور أثناء الذروة، ثم قلّص الحجم إلى ذلك القدر.

pgvector للبحث بالذكاء الاصطناعي، في قاعدة بيانات خاصة

إن كان في متجرك أو نظام ERP لديك بحث بالذكاء الاصطناعي أو توصيات أو مساعد يسترجع المستندات، فأنت تخزّن تمثيلات متجهية (embeddings): قوائم طويلة من الأرقام تمثّل المعنى. وتضيف إضافة pgvector إلى PostgreSQL نوعًا متجهيًا وبحثًا عن أقرب الجيران، فلا تحتاج إلى منتج متجهات منفصل في البداية.

قاعدتي في التصميم أن أشغّله في قاعدة PostgreSQL منفصلة باتصالها الخاص ومسار ترحيل خاص بها. اعتمد أحدث إعداد بنيتُه على pgvector 0.8 فوق PostgreSQL 18، الذي صدر في سبتمبر 2025. استعلامات المتجهات ثقيلة على المعالج والذاكرة، وبناء الفهارس أثقل. وفي قاعدة منفصلة لا يستطيع حمل الذكاء الاصطناعي إبطاء عملية الدفع، ويمكنك ضبط حجمها ونسخها احتياطيًا وترقيتها وفق جدولها الخاص.

مسار يبدأ من تغيير منتج إلى مهمة في الطابور، ثم worker ينشئ التمثيل المتجهي، فالتخزين في pgvector، ثم استعلام أقرب الجيران مع عوامل التصفية
ينشئ العمّال التمثيلات المتجهية بعد كل تغيير، ولا تُنشأ أبدًا داخل طلب ويب.

أنشئ التمثيلات المتجهية في العمّال

إنشاء التمثيل المتجهي يعني استدعاء نموذج، وهذا بطيء وقد يفشل. نفّذه من مهمة في الطابور تنطلق بعد commit في قاعدة البيانات، لا داخل طلب الويب. واجعل المهمة آمنة عند التكرار (idempotent): خزّن بصمة (hash) للنص المصدر وتجاوز العمل إذا لم يتغير شيء، واحتفظ بإصدار النموذج مع كل صف لتتمكن من إعادة الإنشاء في الخلفية لاحقًا. وهذا هو انضباط العمّال نفسه الذي ورد في الجزء الثاني.

HNSW أم IVFFlat

يوفّر pgvector نوعين من الفهارس التقريبية، وكلاهما يضحّي بقدر يسير من الدقة مقابل سرعة كبيرة.

HNSWIVFFlat
السرعة والاسترجاع (recall)عادةً المقايضة الأفضلجيد، ويتوقف على عدد القوائم التي تفحصها
زمن البناء والذاكرةأبطأ في البناء، ويستهلك ذاكرة أكثرأسرع في البناء، ويستهلك ذاكرة أقل
هل يحتاج بيانات أولًالا، يمكن البدء بجدول فارغنعم، يُبنى بعد تحميل بيانات تمثيلية
الاستخدام المعتادالخيار الافتراضي لمعظم البحث الحيمجموعات ضخمة نادرة التغير يهم فيها كلفة البناء

في معظم أحمال التجارة وERP أبدأ بـ HNSW. وتغطي وثائق مشروع pgvector خيارات الضبط للنوعين.

التصفية هي النقطة التي ينكسر عندها البحث بصمت

الاستعلامات الحقيقية ليست أبدًا «الأقرب إلى هذا النص» فحسب. بل هي «الأقرب، المتوفر في المخزون، في هذا المتجر، بهذه اللغة». ومع الفهرس التقريبي تُطبَّق التصفية بعد مسح الفهرس. فإذا طابق المرشّح 10% من الصفوف وكانت القيمة الافتراضية لـ hnsw.ef_search هي 40، فستحصل في المتوسط على نحو أربعة صفوف مطابقة فقط، حتى لو طلبت عشرة.

أضاف pgvector 0.8.0، الذي صدر في أواخر 2024، المسح التكراري للفهرس لهذه المشكلة بالذات. فعندما يبقى بعد التصفية عدد قليل جدًا من الصفوف، يستمر المسح حتى يجمع ما يكفي أو يبلغ حدًّا أقصى.

الإعدادوظيفته
hnsw.iterative_scanيفعّل المسح التكراري في HNSW: strict_order يحافظ على الترتيب الدقيق بحسب المسافة، وrelaxed_order يسمح باختلال طفيف مقابل استرجاع أفضل
hnsw.max_scan_tuplesالحد الأقصى لعدد مدخلات الفهرس التي قد يزورها المسح التكراري
ivfflat.iterative_scanالفكرة نفسها في IVFFlat، مع ترتيب مرن
ivfflat.max_probesالحد الأقصى لعدد القوائم التي تُفحص أثناء المسح التكراري

أبقِ تخزين الكائنات بجوار الحوسبة

الملفات أيضًا حالة يجب حفظها. صور المنتجات والفواتير وملفات التصدير مكانها تخزين كائنات متوافق مع S3، لا قرص عقدة الويب، وإلا تعذّر تشغيل عقدتين خلف موزّع الحمل.

ضع الحاوية (bucket) في المنطقة نفسها التي تعمل فيها خوادمك. في أحد اختبارات الحمل أضافت حاوية في منطقة أخرى ما بين 50 و80 ms تقريبًا إلى كل عملية رفع لملف مُولَّد. كما أن بثّ الملفات من التخزين وإليه، بدل تنزيلها أولًا على القرص المحلي، خفّض ذروة ذاكرة إحدى المهام من نحو 288 MB إلى نحو 128 MB.

قائمة تحقق لطبقة البيانات قبل الحدث

  1. شغّل اختبار حمل بالمعدل المستهدف وسجّل استهلاك معالج القاعدة وزمن استجابة القرص وانتظار الأقفال وعدد عمليات commit في الثانية. وبعدها فقط قرّر هل تكبّر الحجم.
  2. ضع PgBouncer بوضع transaction أمام التطبيق، واحتفظ باتصال مباشر لعمليات الترحيل والمستمعين.
  3. تحقق من إصدار PgBouncer ومن المشغّل لديك بخصوص دعم العبارات المُعدّة.
  4. رتّب الاستعلامات بـ pg_stat_statements، وأصلح أثقلها، وأزل أنماط N+1 من أكثر صفحاتك ازدحامًا.
  5. دوّن أي عمليات القراءة يمكن أن تذهب إلى نسخة متماثلة، وتأكد من بقاء الدفع والمخزون والمدفوعات على النسخة الأساسية.
  6. قسّم Redis أو Valkey بحسب الدور مع بادئات منفصلة، وحدّد سياسة إخلاء لكل دور، وامنع FLUSHDB.
  7. ضع تنبيهًا على ذاكرة الطوابير حتى لا يتحول noeviction إلى فشل صامت.
  8. شغّل البحث بالذكاء الاصطناعي في قاعدة PostgreSQL خاصة به، واختبر الاستعلامات المصفّاة، وفعّل المسح التكراري إذا انخفض الاسترجاع.
  9. تأكد أن تخزين الكائنات في المنطقة نفسها التي تعمل فيها الحوسبة.

نعود إلى التاسعة ودقيقتين. مع المجمّع بوضع transaction تتحول اتصالات العملاء الـ 940 إلى 20 اتصالًا بالخادم، فتختفي من اللوحة الرقم المخيف. يعمل تقرير المبيعات على النسخة المتماثلة، ويعيش الـ cache والطابور في فضاءي مفاتيح منفصلين، ويسأل مهندس المناوبة «ما الذي وصل إلى حدّه؟» بدل «ما الحجم التالي؟». لقد أجاب اختبار الحمل مسبقًا: الكتابة، وقاعدة البيانات في هامش مريح.

وحين تُضبط طبقة البيانات، تبقى حاجتك إلى رؤية ما تفعله يوم الحدث. هذا موضوع الجزء الرابع: سجلات ومقاييس وتتبّعات تستطيع التصرف على أساسها.

الخلاصة

  • حدّد حجم قاعدة البيانات بحسب معدل الكتابة والتشبّع المقيس لا بحدّ الاتصالات، ولا ترقِّها قبل أن يُظهر اختبار الحمل حاجتها إلى ذلك.
  • استخدم PgBouncer بوضع transaction للتطبيق، واعرف ما يكسره: حالة الجلسة وأقفال مستوى الجلسة وLISTEN/NOTIFY وإعدادات العبارات المُعدّة القديمة.
  • تخدم النسخ المتماثلة القراءات التي تتحمل التأخر؛ وما يقرأ بعد الكتابة أو يمس المال والمخزون يبقى على النسخة الأساسية.
  • امنح الـ cache والجلسات والطوابير قواعد وبادئات منفصلة في Redis أو Valkey، مع allkeys-lru للـ cache وnoeviction للطوابير، ولا تفرّغ القاعدة كلها أبدًا.
  • أبقِ pgvector في قاعدة PostgreSQL خاصة، وأنشئ التمثيلات المتجهية في العمّال، وابدأ بـ HNSW، واستخدم المسح التكراري في البحث المصفّى.
  • اضبط ذاكرة الـ cache على قدر الاستخدام الفعلي، وأبقِ تخزين الكائنات في المنطقة نفسها التي تعمل فيها الحوسبة.

Anichur Rahaman مهندس برمجيات معماري ومؤسس StoreConsole، يصمّم أنظمة التجارة وERP للشركات النامية، ويهتم خصوصًا بالبنية القائمة على الأحداث وسلامة البيانات وتشغيل الأنظمة على خوادم الشركة نفسها.

About the Author

Anichur Rahaman

Continue Reading