قواعد البيانات في الإنتاج

إدارة الاتصالات والتجميع

18 دقيقة الدرس 6 من 30

إدارة الاتصالات والتجميع

كل اتصال بـ PostgreSQL أو MySQL هو عملية نظام تشغيل ثقيلة (أو خيط). عند ذروة حركة المرور، تفتح التصميمات التطبيقية السطحية اتصالًا جديدًا لكل طلب — وعلى نطاق واسع، يتحول ذلك إلى توقف في الإنتاج أسرع من أي نمط مضاد آخر لقواعد البيانات. فهم كيفية إدارة الاتصالات فعليًا، ولماذا يجب أن يجلس موزّع الاتصالات بين تطبيقك وقاعدة بياناتك، مهارة لا غنى عنها لأي مهندس يشغّل قواعد بيانات على نطاق إنتاجي.

ما الذي يعنيه max_connections فعلًا

في PostgreSQL، max_connections هو سقف صارم على إجمالي عدد عمليات الخادم الخلفي التي سيقبلها الخادم — بما يشمل تطبيقك، ونظيراتك الثانوية للنسخ المتماثل، والعمال الخلفيين، وجلساتك الإدارية. الافتراضي هو 100. كل اتصال يستهلك ما بين 5 إلى 10 ميغابايت من ذاكرة الوصول العشوائي لعملية الخادم الخلفي الخاصة به بالإضافة إلى حمل الذاكرة المشتركة. على نسخة بـ 16 غيغابايت، السماح بـ 500 اتصال يستهلك من 2.5 إلى 5 غيغابايت قبل تنفيذ أي استعلام واحد.

الفهم الجوهري: معظم الاتصالات خاملة معظم الوقت. تطبيق ويب نموذجي به 200 مستخدم متزامن لا يحتاج إلى 200 اتصال بقاعدة بيانات نشط — بل يحتاج إلى جزء صغير من ذلك في أقصى الأحوال، لأن عمليات قاعدة البيانات تستغرق ميلي ثوانٍ. لكن بدون موزّع، تفتح معظم الأطر اتصالًا واحدًا لكل خيط أو goroutine وتحتفظ به، مما يعني أن 200 خيط يعني 200 اتصال.

تستخدم MySQL max_connections بنفس الطريقة، لكن التكلفة لكل اتصال تختلف لأن MySQL تستخدم نموذج الخيوط بدلًا من نموذج العمليات المتفرعة. العمليات الحسابية للذاكرة لا تزال حاسمة — تخصص MySQL مخازن مؤقتة لكل خيط (sort_buffer_size، join_buffer_size، إلخ) عند الطلب، لذا فإن 1000 اتصال تنفّذ عمليات فرز كبيرة يمكن أن تستنفد ذاكرة الوصول العشوائي فجأة.

تسلسل "اتصالات كثيرة جدًا": عندما تصل قاعدة البيانات إلى max_connections، تُرفض محاولات الاتصال الجديدة بخطأ — لا تُوضع في قائمة انتظار. يرمي تطبيقك الآن استثناءات لكل طلب. تولّد الاستثناءات عمليات إعادة محاولة، والتي تولّد المزيد من محاولات الاتصال، والتي تُرفض أيضًا. هذه الحلقة المرتدة الإيجابية يمكن أن تُوقف الخدمة لدقائق حتى بعد حل السبب الجذري. موزّع الاتصالات يمنع ذلك بوضع الطلبات الجديدة في قائمة انتظار على مستوى الموزّع بدلًا من قصف قاعدة البيانات.

طبقة الموزّع: PgBouncer و ProxySQL

PgBouncer هو الموزّع الافتراضي الأكثر استخدامًا لـ PostgreSQL. إنه عملية C أحادية الخيط تجلس بين تطبيقك وـ PostgreSQL. تحافظ PgBouncer على عدد صغير من اتصالات الخادم طويلة الأمد وتُوزّع آلاف الاتصالات قصيرة الأمد من العملاء عليها. إنها خفيفة بما يكفي للتشغيل على نفس مضيف قاعدة البيانات دون تأثير معنوي على وحدة المعالجة المركزية.

تعمل PgBouncer في ثلاثة أوضاع:

  • تجميع الجلسة: يُخصص اتصال خادم للعميل طوال مدة جلسة العميل. هذا هو الوضع الأأمن ومتوافق مع جميع ميزات PostgreSQL (بما في ذلك SET وLISTEN والجمل المحضّرة)، لكنه يوفر أقل فائدة تعددية.
  • تجميع المعاملات: يُخصص اتصال خادم فقط لمدة معاملة، ثم يُعاد إلى التجمّع. هذا هو الوضع الإنتاجي الموصى به لمعظم أعباء العمل — يسمح لمئات اتصالات العملاء بمشاركة تجمّع خوادم صغير. القيود: يجب إدارة الجمل المحضّرة والأقفال الاستشارية على مستوى التطبيق، أو استخدام PgBouncer 1.21+ مع تتبع الجمل المحضّرة من جانب الخادم.
  • تجميع الجمل: يُعاد اتصال الخادم بعد كل جملة. نادرًا ما يُستخدم لأنه يكسر المعاملات متعددة الجمل.

ProxySQL هو المعادل لـ MySQL وMariaDB، لكن بمجموعة ميزات أوسع بكثير: قواعد توجيه الاستعلامات، وتقسيم القراءة/الكتابة، وإعادة كتابة الاستعلامات، والاتصالات المتعددة، وإمكانية المراقبة الغنية عبر جداول الإحصاءات الداخلية. تُستخدم ProxySQL بشكل شائع أمام مجموعات Galera وMySQL Group Replication لتوجيه الكتابات إلى الأساسي والقراءات إلى النسخ المتماثلة بشكل شفاف.

Connection pooler architecture App Pod 1 App Pod 2 App Pod 3 App Pod 4 App Pod 5 500 client conns PgBouncer Transaction mode pool_size = 25 25 server conns PostgreSQL Primary max_connections=100 500 App Connections → 25 DB Server Connections
PgBouncer في وضع المعاملات: 500 اتصال عميل من التطبيق موزَّعة على 25 اتصال خادم PostgreSQL حقيقي.

إعداد PgBouncer للإنتاج

الإعداد الأدنى لـ PgBouncer في الإنتاج لأساسي PostgreSQL. هذا الملف يقع في /etc/pgbouncer/pgbouncer.ini:

[databases] # توجيه appdb على منفذ 5432 من pgbouncer إلى مضيف قاعدة البيانات الحقيقي appdb = host=pg-primary.internal port=5432 dbname=appdb [pgbouncer] listen_addr = 0.0.0.0 listen_port = 5432 # استخدام auth_query بدلًا من ملف مسطح في الإعدادات الحديثة auth_type = scram-sha-256 auth_file = /etc/pgbouncer/userlist.txt # وضع تجميع المعاملات: الوضع الموصى به لمعظم أعباء الويب pool_mode = transaction # الحد الأقصى لاتصالات العميل التي يقبلها PgBouncer max_client_conn = 2000 # اتصالات الخادم لكل زوج قاعدة بيانات/مستخدم default_pool_size = 25 # اتصالات للحفاظ على دفئها (لتجنب طفرات زمن البداية الباردة) min_pool_size = 5 # تجمّع احتياطي للاستخدام الإداري/الطوارئ reserve_pool_size = 5 reserve_pool_timeout = 3 # إنهاء اتصالات الخادم الخاملة بعد 10 دقائق (تجنب TCP القديم) server_idle_timeout = 600 # إنهاء اتصالات العملاء الخاملة لمدة 5 دقائق (يمنع الاتصالات الوهمية) client_idle_timeout = 300 # مهل لمنع العملاء البطيئين من الاحتفاظ باتصالات الخادم server_connect_timeout = 10 query_timeout = 30 query_wait_timeout = 10 # تسجيل الاستعلامات البطيئة فقط (تقليل الضوضاء) log_connections = 0 log_disconnections = 0 log_pooler_errors = 1 # واجهة المشرف — تقتصر على localhost admin_users = pgbouncer_admin stats_users = stats_user
ملف userlist: أنشئ تجزئات MD5 أو SCRAM لـ PgBouncer باستخدام echo "md5$(echo -n 'passwordusername' | md5sum | awk '{print $1}')". بالنسبة لـ SCRAM، استخدم psql -c "SELECT rolpassword FROM pg_authid WHERE rolname = 'appuser';" على قاعدة البيانات وانسخ السلسلة SCRAM-SHA-256$... مباشرة إلى userlist.txt. في الإنتاج، استخدم auth_query لجعل PgBouncer تستعلم من قاعدة البيانات عن بيانات الاعتماد مباشرة، مما يلغي مشكلة المزامنة كليًا.

إعداد ProxySQL لـ MySQL

تُعدَّل ProxySQL عبر واجهتها الإدارية (اتصال بروتوكول MySQL على المنفذ 6032) بدلًا من ملف إعداد، مما يجعلها مناسبة للإدارة البرمجية وTerraform. الجداول الرئيسية هي mysql_servers وmysql_users وmysql_query_rules.

-- الاتصال بمشرف ProxySQL (المنفذ 6032، ليس 3306) -- mysql -u admin -padmin -h 127.0.0.1 -P 6032 -- إضافة خوادم الخلفية INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight, max_connections) VALUES (10, 'mysql-primary.internal', 3306, 1000, 200), -- مجموعة الكتابة (20, 'mysql-replica1.internal', 3306, 500, 400), -- مجموعة القراءة (20, 'mysql-replica2.internal', 3306, 500, 400); -- إضافة مستخدم التطبيق (يجب أن يكون موجودًا على خادم MySQL الخلفي أيضًا) INSERT INTO mysql_users (username, password, default_hostgroup, transaction_persistent) VALUES ('appuser', 'hashed_password_here', 10, 1); -- transaction_persistent=1: الاحتفاظ بالمعاملة كاملة على خادم واحد -- توجيه الكتابة إلى الأساسي (مجموعة 10)، والقراءة إلى النسخ (مجموعة 20) INSERT INTO mysql_query_rules (rule_id, active, match_digest, destination_hostgroup, apply) VALUES (1, 1, '^SELECT', 20, 1), -- SELECT تذهب إلى مجموعة القراءة (2, 1, '.*', 10, 1); -- كل شيء آخر يذهب إلى مجموعة الكتابة -- تطبيق وحفظ LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK; LOAD MYSQL USERS TO RUNTIME; SAVE MYSQL USERS TO DISK; LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK; -- مراقبة حالة تجمّع الاتصالات SELECT hostgroup, srv_host, status, ConnUsed, ConnFree, ConnOK, ConnERR FROM stats_mysql_connection_pool;

تحديد حجم التجمّع: المعادلة والواقع

أكثر قاعدة تحديد حجم تجمّع استشهادًا تأتي من وثائق HikariCP، والتي تستمد بدورها من أبحاث اختبار قواعد البيانات: حجم التجمّع = (عدد الأنوية × 2) + عدد الأقراص الفعّال. بالنسبة لخادم تطبيق نموذجي بـ 8 أنوية وتخزين SSD، ينتج ذلك تجمّعًا من حوالي 17 اتصالًا. هذا الرقم يصدم المهندسين الذين يفترضون أن الأكبر أفضل.

المنطق: وحدة المعالجة المركزية يمكنها تنفيذ خيط واحد فقط في وقت واحد لكل نواة. إذا كان لديك اتصالات قاعدة بيانات أكثر من أنوية وحدة المعالجة المركزية على مضيف قاعدة البيانات، فأنت ببساطة تتسلسل العمل على جانب قاعدة البيانات بمزيد من الحمل الزائد (تبديل السياق، وضغط الذاكرة) مما كانت ستستهلكه اتصالات أقل. إشباع وحدة المعالجة المركزية لقاعدة البيانات هو وضع الفشل، ليس الهدف.

في الممارسة العملية، حدّد حجم تجمّعك بالبدء من المعادلة والتحقق من الإشارات التالية في الإنتاج:

  • عدد الاتصالات النشطة في pg_stat_activity — كم عدد الاتصالات التي تنفّذ استعلامات فعلًا (state = 'active') مقابل الخاملة؟
  • cl_waiting في PgBouncer — عملاء ينتظرون اتصال خادم. إذا كان هذا غير صفري، فحجم تجمّعك أصغر من اللازم لذروة الحمل.
  • استخدام وحدة المعالجة المركزية لقاعدة البيانات — إذا كان أقل من 60% في الذروة، فلديك مساحة؛ إذا كان أعلى من 80%، فإضافة اتصالات تجمّع ستجعل زمن الاستجابة أسوأ، لا أفضل.
  • زمن استجابة الاستعلام p99 — يجب ألا يتدهور مع زيادة حجم التجمّع. إذا تدهور، فأنت تُرهق قاعدة البيانات.
حدود اتصالات RDS/Aurora: تحدّ AWS من max_connections بناءً على ذاكرة الوصول العشوائي للنسخة باستخدام المعادلة LEAST({DBInstanceClassMemory/9531392}, 5000). تحصل db.t3.micro (1 غيغابايت ذاكرة وصول عشوائي) على ما يقرب من 87 اتصالًا. مع 3 خوادم تطبيق تحتفظ كل منها بتجمّع من 30، تكون قد استخدمت جميعها. يوجد RDS Proxy (مبني على معمارية PgBouncer/ProxySQL) لحل هذه المشكلة بالضبط، وهو الحل الموصى به لأعباء Lambda أو ECS التي تفتح اتصالات كثيرة قصيرة الأمد.

أوضاع الفشل الإنتاجي وكيفية اكتشافها

ثلاثة أوضاع فشل متعلقة بالاتصالات تتكرر عبر حوادث الإنتاج على كل المقاييس:

1. تسرّب الاتصالات: يحصل كود التطبيق على اتصال ويفشل في تحريره (استثناء غير ملتقَط، كتلة finally مفقودة، خلل في ORM). تتراكم الاتصالات حتى ينضب التجمّع. اكتشفه بـ: SELECT count(*), state FROM pg_stat_activity GROUP BY state; — عدد خامل متزايد بدون حمل مقابل هو تسرب.

2. الخمول في المعاملة لفترة طويلة: يبدأ الاتصال معاملة، يقوم ببعض العمل، ثم يتوقف (ينتظر إدخال المستخدم، أو محجوب على استدعاء API خارجي). المعاملة تحتجز الأقفال. تنتظر الاستعلامات الأخرى خلفها. التجمّع يبدو "ممتلئًا" رغم أن معظم الاتصالات خاملة. اكتشفه بـ:

-- إيجاد الاتصالات الخاملة في المعاملة لأكثر من 30 ثانية SELECT pid, usename, application_name, state, wait_event_type, wait_event, now() - state_change AS idle_duration, query FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - state_change > interval '30 seconds' ORDER BY idle_duration DESC; -- إنهاء خادم خلفي مخالف معين (استبدل PID) SELECT pg_terminate_backend(12345); -- PgBouncer: اضبط هذا لإنهاء اتصالات idle-in-transaction تلقائيًا -- في pgbouncer.ini: -- idle_transaction_timeout = 30

3. نضوب التجمّع تحت حركة مرور متفجرة: طفرة مفاجئة في معدل الطلبات تجعل جميع اتصالات التجمّع مشغولة في وقت واحد. تنتظر الطلبات الجديدة عند الموزّع. إذا تجاوزت قائمة الانتظار query_wait_timeout، يُعيد PgBouncer خطأً للعميل. هذا سلوك صحيح — الموزّع يحمي قاعدة البيانات — لكن على التطبيق معالجة هذا الخطأ بلطف (تراجع أسي، قاطع دارة) بدلًا من التعطل.

نمط الحاوية المصاحبة في Kubernetes: في Kubernetes، انشر PgBouncer كحاوية مصاحبة في كل حجرة تطبيق بدلًا من خدمة مشتركة. هذا يبسّط المصادقة (تتصل كل حجرة بقاعدة البيانات باستخدام بياناتها الاعتمادية الخاصة عبر الحاوية المصاحبة) ويلغي الموزّع كنقطة فشل واحدة. المقايضة هي أن قاعدة البيانات ترى مصادر اتصال أكثر. بالنسبة لـ PostgreSQL مع RDS، استخدم RDS Proxy بدلًا من ذلك — إنه المكافئ المُدار من AWS ويتكامل مع مصادقة IAM.

إدارة الاتصالات ليست بنية تحتية جذابة. نادرًا ما تُطرح في مراجعات المعمارية. لكن استنفاد التجمّع هو أحد أكثر أسباب الانقطاعات الناجمة عن قواعد البيانات شيوعًا على نطاق واسع — وموزّع اتصالات مُضبط بشكل صحيح أمام قاعدة بياناتك هو أحد أعلى تحسينات الموثوقية أثرًا يمكنك تنفيذه في بعد ظهيرة واحدة.

ES
Edrees Salih
منذ ساعة

We are still cooking the magic in the way!