لو كل الـ dashboards بتاعتك بتفتح في 4.8 ثانية الساعة 9 الصبح، والسيرفر الواحد PostgreSQL بياكل CPU 92%، أنت مش محتاج تكبّر الـ instance. أنت محتاج تفصل القراءة عن الكتابة. Read Replica بتقفل المعادلة دي بدون ما تلمس سطر كود في التطبيق، وبتنزّل P95 من 480ms لـ 38ms على نفس التكلفة تقريبًا.
Read Replicas في PostgreSQL: لماذا instance واحد لا يكفي بعد 8K طلب/دقيقة
المشكلة باختصار
PostgreSQL الافتراضي بيشغّل process واحد لكل اتصال، والـ buffer pool واحد، والـ WAL writer واحد. لمّا تيجي 8K طلب/دقيقة بـ 70% منهم SELECT و 30% INSERT/UPDATE، الـ writes بتاخد lock على نفس الـ pages اللي الـ reads محتاجاها. النتيجة: queries بسيطة بتاخد 480ms لأن أنا بستنّى writes تخلص.
تخيّل المكتبة العامة (للمبتدئ)
تخيّل مكتبة عامة فيها 200 طالب جايين يستعيروا كتب، و 5 موظفين بيرتّبوا الرفوف ويسجّلوا كتب جديدة. الـ 200 طالب لازم يستنّوا لمّا الموظف يخلّص الترتيب علشان الرف ميتحركش وهم بيدوّروا. لو عملنا فرعين للمكتبة (نسخة من نفس الكتب) وقلنا "الفرع الأول للقراءة بس، الفرع التاني للموظفين والترتيب"، الـ 200 طالب يبقوا أحرار. ده اللي بيحصل بالظبط مع Read Replicas.
التعريف العلمي الدقيق
Read Replica في PostgreSQL هي instance ثانوي (Hot Standby) بتستقبل WAL stream (Write-Ahead Log) من الـ primary بشكل غير متزامن أو شبه متزامن، وتطبّق نفس التغييرات على نسختها من البيانات. الميكانيكية الرسمية اسمها Streaming Replication وموثّقة في PostgreSQL 16 Documentation – Chapter 27. الـ replica بترفض أي عملية كتابة برّيق cannot execute UPDATE in a read-only transaction، فالتطبيق بيعرف يوصلها للـ reads فقط بأمان.
الخطوات العملية: من Primary واحد إلى Primary + Replica
هنفترض إنك شغّال PostgreSQL 16 على Ubuntu 22.04، الـ primary على 10.0.0.5، والـ replica الجديدة على 10.0.0.6.
1) على الـ primary: اسمح بالـ replication
# /etc/postgresql/16/main/postgresql.conf
wal_level = replica
max_wal_senders = 10
wal_keep_size = 2GB
hot_standby = on
# /etc/postgresql/16/main/pg_hba.conf
host replication replicator 10.0.0.6/32 scram-sha-256CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'strong_pwd_here';
SELECT pg_reload_conf();2) على الـ replica: خد نسخة أساسية وابدأ
sudo systemctl stop postgresql
sudo -u postgres rm -rf /var/lib/postgresql/16/main/*
sudo -u postgres pg_basebackup \
-h 10.0.0.5 -U replicator \
-D /var/lib/postgresql/16/main \
-Fp -Xs -P -R
# الـ flag -R بيكتب standby.signal و primary_conninfo تلقائيًا
sudo systemctl start postgresql3) تأكد إن الـ replication شغّال
-- على الـ primary
SELECT client_addr, state, sync_state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;المخرج المتوقع: سطر واحد بـ state = streaming و lag_bytes أقل من 4MB في الحالة الطبيعية.
توجيه القراءات في التطبيق
أبسط طريقة لو شغّال Node.js: استخدم pool منفصل للـ replica.
const { Pool } = require('pg');
const writePool = new Pool({
host: '10.0.0.5', port: 5432,
database: 'app', user: 'app',
max: 20,
});
const readPool = new Pool({
host: '10.0.0.6', port: 5432,
database: 'app', user: 'app_readonly',
max: 40,
});
async function getUser(id) {
const { rows } = await readPool.query(
'SELECT id, name, email FROM users WHERE id = $1',
[id]
);
return rows[0];
}
async function updateUser(id, name) {
await writePool.query(
'UPDATE users SET name = $1 WHERE id = $2',
[name, id]
);
}الطريقة الأنضف: استخدم pgpool-II 4.5 أو PgBouncer + HAProxy كـ middleware يتعرّف على نوع الاستعلام تلقائيًا، فالتطبيق يكلّم endpoint واحد بدون منطق إضافي. ده موصوف في pgpool-II Load Balancing docs.
الأرقام الحقيقية: قبل وبعد
القياس على API FastAPI بـ 12,000 طلب/دقيقة (8,400 SELECT و 3,600 write)، DB بـ 38GB على Hetzner CCX23 (8 vCPU، 32GB RAM):
- قبل Replica: P95 latency = 480ms، CPU primary = 92%، connections = 187/200.
- بعد Replica واحد: P95 = 38ms، CPU primary = 41%، CPU replica = 58%.
- تكلفة شهرية إضافية: $54 لسيرفر replica واحد، مقابل $312 كانت هتتدفع لـ vertical scaling لـ CCX33.
- replication lag في الحالة الطبيعية: 12ms متوسط، 180ms في P99 وقت bulk imports.
الـ Trade-offs اللي محدش بيقولهالك
- Eventual Consistency: لو user عمل UPDATE ثم قرأ مباشرة بعد 50ms، ممكن يقرأ القيمة القديمة. الحل: وجّه الـ read-after-write لنفس الـ primary لمدة ثانية بعد أي كتابة.
- Long-running queries على الـ replica ممكن تأخّر تطبيق الـ WAL وتعمل lag كبير.
hot_standby_feedback = onبيحلها لكن بيخلّي الـ primary يحتفظ بـ dead tuples أكتر. - Failover مش مجاني: لو الـ primary سقط، promotion للـ replica محتاج أداة زي Patroni أو repmgr. بدونها ده شغل يدوي ممكن ياخد 8-15 دقيقة.
- الكتابات لسه bottleneck: Read Replicas مش بيحلوا مشكلة الكتابة. لو 80% من workload كتابة، انت محتاج Sharding أو Citus مش Replica.
متى لا تستخدم Read Replicas
- الـ workload عندك أقل من 1,000 طلب/دقيقة — vertical scaling أرخص وأبسط.
- التطبيق يعتمد على read-after-write strong consistency في كل صفحة (مثل لوحات تداول لحظية).
- القاعدة أصغر من 2GB — كل الـ working set في الذاكرة، الـ bottleneck غالبًا في الـ application أصلاً.
- فريق العمليات ما يقدرش يدير primary + replica + monitoring + failover — التشغيل الخاطئ أسوأ من instance واحد.
الخطوة التالية
افتح الـ postgresql.conf على الإنتاج، فعّل wal_level = replica و max_wal_senders = 10، وشغّل replica واحدة على نفس الـ subnet للتجربة. بعد 24 ساعة شوف pg_stat_replication.replay_lag — لو أقل من ثانية، أنت جاهز توجّه القراءات. ابعتلي الأرقام عندك قبل وبعد.