في مقال تقسيم الجداول Partitioning والتوزيع Sharding شرحنا فكرة التوزيع على عدة سيرفرات: مفتاح التوزيع، وجدول التوجيه، والجزء الساخن، والاستعلام الذي يسأل كل السيرفرات، وذكرنا أن كتابة ذلك بنفسك في كود التطبيق عمل كبير. وفي هذا المقال سوف نأخذ الأداة الأشهر التي تفعل ذلك داخل PostgreSQL نفسه، وهي Citus: إضافة Extension تجعل عدة سيرفرات PostgreSQL تعمل كقاعدة واحدة، فيتصل التطبيق بسيرفر واحد ويكتب SQL عادياً، وتوزع Citus الجداول على السيرفرات وتوجه كل استعلام إلى مكانه. وسوف نبني عنقوداً Cluster صغيراً من منسق وعاملين، ونوزع جداول تطبيق SaaS متعدد المستأجرين، ونرى بالأمر EXPLAIN أين يذهب كل استعلام، ثم نضيف سيرفراً ثالثاً وننقل إليه الأجزاء.
وسوف نناقش في هذا المقال ما يلي:
- مكونات Citus: المنسق Coordinator والعمال Workers والأجزاء Shards.
- الجداول الموزعة Distributed Tables والجداول المرجعية Reference Tables، واختيار عمود التوزيع، والتوزيع المشترك Co-location.
- متى تناسبك Citus ومتى لا تناسبك، وترخيصها.
- مختبر حقيقي: منسق وعاملان في Docker، وتوزيع جداول المستأجرين، وقراءة توجيه الاستعلامات، والأخطاء التي ظهرت لنا.
- إضافة عامل جديد وإعادة توزيع الأجزاء Rebalance، والإعداد الذي أفشلها في المحاولة الأولى.
- التشغيل في الإنتاج: التوفر العالي والنسخ الاحتياطي، والأخطاء الشائعة.
كيف تعمل Citus؟
Citus ليست نسخة معدلة من PostgreSQL، وإنما إضافة تثبت عليه، والعنقود يتكون من نوعين من العقد Nodes كما في وثائق المفاهيم:
- المنسق Coordinator: السيرفر الذي يتصل به التطبيق، ويحفظ البيانات الوصفية Metadata: أي جدول موزع، وبأي عمود، وأين يوجد كل جزء، ثم يخطط كل استعلام ويرسله إلى العمال ويجمع النتائج.
- العمال Workers: سيرفرات PostgreSQL عادية تحمل الأجزاء، وكل جزء Shard جدول عادي اسمه مثل
ads_102099فيه جزء من صفوف الجدول الأصلي.
وعندما توزع جدولاً بعمود مثل company_id، تحسب Citus قيمة Hash لكل قيمة في العمود، وتقسم مدى ال Hash إلى 32 جزءاً افتراضياً بحسب الإعداد citus.shard_count، فكل صفوف الشركة الواحدة في جزء واحد. الصورة التالية تبين ذلك:

الجداول الموزعة والمرجعية، وعمود التوزيع
في Citus ثلاثة أنواع من الجداول:
- الجدول الموزع Distributed Table: صفوفه مقسمة على الأجزاء بعمود التوزيع Distribution Column، وهو للجداول الكبيرة التي تنمو مع العملاء أو الأحداث.
- الجدول المرجعي Reference Table: جدول صغير منسوخ كاملاً في كل عقدة، مثل الدول والعملات وأنواع الاشتراك، حتى يتم الربط JOIN معه محلياً في كل عامل، كما في وثائق الجداول المرجعية، والثمن أن كل كتابة فيه تذهب إلى كل العقد.
- الجدول المحلي Local Table: جدول عادي على المنسق وحده، لما لا يحتاج توزيعاً.
واختيار عمود التوزيع هو القرار الأصعب، والسبب أن تغييره بعد ذلك يعني إعادة توزيع كل البيانات. وتقول وثائق نمذجة البيانات إن العمود الجيد هو الذي يظهر في أغلب الاستعلامات، ويوزع البيانات بالتساوي، ويجمع الصفوف التي تربط معاً. وفي تطبيق SaaS متعدد المستأجرين يكون غالباً معرف المستأجر tenant_id أو company_id، وقد شرحنا نماذج عزل المستأجرين في مقال تعدد المستأجرين Multi-Tenancy، ومشكلة المستأجر الضخم الذي يجعل جزءاً واحداً ساخناً في مقال التقسيم والتوزيع.
ثم يأتي التوزيع المشترك Co-location: إذا وزعت companies وcampaigns وads كلها بنفس العمود ونفس عدد الأجزاء، فصفوف الشركة رقم 42 في الجداول الثلاثة تكون في نفس العامل، فيتم الربط بينها داخله دون نقل بيانات عبر الشبكة. الصورة التالية تبين الفرق، والأرقام من المختبر:

متى تناسبك Citus، ومتى لا تناسبك؟
قبل أي شيء: أغلب التطبيقات لا تحتاج Citus، والسبب أن سيرفر PostgreSQL واحداً بذاكرة كبيرة وقرص سريع مع الفهارس الصحيحة والتقسيم Partitioning يحمل مئات الجيجابايتات وآلاف الطلبات في الثانية، كما رأينا في مقال الجداول الضخمة، وCitus تضيف سيرفرات تحتاج تشغيلاً ونسخاً ومراقبة. وهي تناسب حالات محددة تذكرها وثائقها:
- تطبيقات SaaS متعددة المستأجرين: كل استعلام تقريباً فيه
company_id، فيذهب إلى عامل واحد، وتتوسع بإضافة عمال، وتنقل المستأجر الكبير إلى عامل خاص به، كما في دليل التطبيقات متعددة المستأجرين. - التحليلات اللحظية Real-time Analytics: جداول أحداث ضخمة، واستعلامات تجميع تحتاج أن يعمل عليها أكثر من سيرفر بالتوازي.
- الخدمات المصغرة Microservices: منذ Citus 12 يوجد التوزيع بالمخطط Schema-based Sharding، فيكون كل مخطط Schema في عقدة كما في إعلان Citus 12، وهو مناسب أيضاً لنموذج «مخطط لكل مستأجر».
ولا تناسبك في هذه الحالات:
- القاعدة تدخل في سيرفر واحد، أو تستطيع شراء سيرفر أكبر بسعر معقول.
- استعلامات كثيرة تربط جداول كبيرة بأعمدة مختلفة، ولا يوجد عمود واحد يجمعها، فكل ربط يصبح نقلاً للبيانات بين العمال.
- تحتاج معاملات Transactions كثيرة تلمس صفوفاً لمستأجرين مختلفين في نفس الوقت، فالمعاملة الموزعة ممكنة ولكنها أبطأ من المحلية.
- المشكلة توفر عال وليست حجماً: Citus لا تعطي انتقالاً تلقائياً Failover وحدها، وما تحتاجه هو نسخة متماثلة مع Patroni أو CloudNativePG كما في مقال عناقيد قواعد البيانات.
الترخيص
Citus مرخصة بالترخيص GNU AGPL-3.0، وقد كانت بعض ميزاتها مثل إعادة توزيع الأجزاء دون توقف في نسخة تجارية فقط، ثم أصبحت كلها مفتوحة منذ Citus 11 في 2022. وترخيص AGPL يعني أنك إذا عدلت كود Citus نفسه وقدمته كخدمة عبر الشبكة فعليك نشر تعديلاتك، أما استخدامها كما هي كقاعدة بيانات لتطبيقك فلا يفرض شيئاً على كود تطبيقك، وتفاصيل التراخيص في مقال تراخيص البرمجيات مفتوحة المصدر. وتطورها Microsoft، وتذكر صفحة Azure Cosmos DB for PostgreSQL المبنية عليها أن تلك الخدمة في طريقها إلى الإيقاف، وأن البديل هو Elastic Clusters في Azure Database for PostgreSQL الذي يستخدم Citus أيضاً، فالإضافة نفسها مستمرة وما يتغير هو اسم الخدمة المدارة.
المختبر: منسق وعاملان
استخدمنا الصورة الرسمية citusdata/citus:14.2.0 وفيها Citus 14.2.0 على PostgreSQL 18.4، وملف Compose كما يلي:
# Citus 14.2 (PostgreSQL 18.4): one coordinator and two workers on a private Docker network.
# Lab only: the nodes trust each other inside this network and no port is published (use docker exec);
# in production use passwords in ~/.pgpass or certificates, and TLS between nodes.
name: lab-citus
x-node: &node
image: citusdata/citus:14.2.0
environment:
POSTGRES_PASSWORD: lab-only-password
POSTGRES_HOST_AUTH_METHOD: trust
# online shard moves (rebalancing) use logical replication, so every node needs wal_level=logical
command: ["postgres", "-c", "wal_level=logical"]
shm_size: 256mb
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
retries: 20
services:
coord:
<<: *node
container_name: lab-citus-coord
hostname: coord
volumes: [lab-citus-coord:/var/lib/postgresql]
w1:
<<: *node
container_name: lab-citus-w1
hostname: w1
volumes: [lab-citus-w1:/var/lib/postgresql]
w2:
<<: *node
container_name: lab-citus-w2
hostname: w2
volumes: [lab-citus-w2:/var/lib/postgresql]
volumes:
lab-citus-coord: { name: lab-citus-coord }
lab-citus-w1: { name: lab-citus-w1 }
lab-citus-w2: { name: lab-citus-w2 }في الإعداد أعلاه لاحظ التالي:
- العقد تثق ببعضها داخل الشبكة
trustولا يوجد أي منفذ منشور على السيرفر، فنستخدمdocker exec، وهذا للمختبر فقط، وفي الإنتاج تضع كلمات المرور في الملف~/.pgpassعلى كل عقدة أو تستخدم الشهادات مع TLS بين العقد. - ال Volume مربوط على
/var/lib/postgresqlوليس/var/lib/postgresql/data، وفي أول تشغيل كتبناه بالمسار القديم فتوقفت العقد الثلاث بالرسالةError: in 18+, these Docker images are configured to store database data in a format which is compatible with "pg_ctlcluster"، وهو نفس التغيير في صور PostgreSQL 18 الذي شرحناه في مقال PostgreSQL للفرق. wal_level=logicalعلى كل العقد، وسوف نرى لماذا عند إضافة العامل الثالث.
بعد التشغيل نخبر المنسق بعنوانه وبالعمال:
SELECT citus_version();
SELECT citus_set_coordinator_host('coord', 5432);
SELECT citus_add_node('w1', 5432);
SELECT citus_add_node('w2', 5432);
SELECT * FROM citus_get_active_worker_nodes();
SHOW citus.shard_count;والمخرج سوف يكون كما يلي:
citus_version
----------------------------------------------------------------------------------------
Citus 14.2.0 on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
citus_add_node
----------------
2
citus_add_node
----------------
3
node_name | node_port
-----------+-----------
w1 | 5432
w2 | 5432
citus.shard_count
-------------------
32الخطأ الأول: مفتاح أساسي دون عمود التوزيع
الطريقة البسيطة أن تأخذ الجداول كما هي من تطبيقك وتوزعها، ولكن لنرى ماذا يحدث لجدول مفتاحه الأساسي id فقط:
CREATE TABLE wrong_ads (id bigint PRIMARY KEY, company_id bigint NOT NULL);
SELECT create_distributed_table('wrong_ads', 'company_id');ERROR: cannot create constraint on "wrong_ads"
DETAIL: Distributed relations cannot have UNIQUE, EXCLUDE, or PRIMARY KEY constraints that do not include the partition column (with an equality operator if EXCLUDE).والسبب منطقي: كل جزء يستطيع أن يتحقق من التفرد داخله فقط، ولا يعرف ما في الأجزاء الأخرى، فإذا لم يكن عمود التوزيع جزءاً من المفتاح فقد يتكرر id في جزأين دون أن يعلم أحد. لذلك يصبح المفتاح (company_id, id)، وكذلك المفاتيح الأجنبية Foreign Keys بين الجداول الموزعة، وهذا هو التغيير الأكبر الذي تحتاجه أغلب التطبيقات قبل الانتقال إلى Citus، ومن الأسهل أن تصمم الجداول هكذا من البداية إذا كان التوزيع متوقعاً.
توزيع جداول المستأجرين
-- Multi-tenant ad platform: every tenant table carries company_id and is distributed by it.
CREATE TABLE companies (
id bigint PRIMARY KEY,
name text NOT NULL,
country char(2) NOT NULL
);
CREATE TABLE campaigns (
company_id bigint NOT NULL,
id bigint NOT NULL,
name text NOT NULL,
budget numeric(12,2) NOT NULL,
PRIMARY KEY (company_id, id)
);
CREATE TABLE ads (
company_id bigint NOT NULL,
id bigint NOT NULL,
campaign_id bigint NOT NULL,
clicks int NOT NULL DEFAULT 0,
impressions int NOT NULL DEFAULT 0,
PRIMARY KEY (company_id, id),
FOREIGN KEY (company_id, campaign_id) REFERENCES campaigns (company_id, id)
);
-- small lookup table that every node needs in full
CREATE TABLE countries (code char(2) PRIMARY KEY, name text NOT NULL);
SELECT create_distributed_table('companies', 'id');
SELECT create_distributed_table('campaigns', 'company_id', colocate_with => 'companies');
SELECT create_distributed_table('ads', 'company_id', colocate_with => 'companies');
SELECT create_reference_table('countries');ثم أضفنا 200 شركة، و25 حملة لكل شركة، و2500 إعلان لكل شركة، أي نصف مليون إعلان:
INSERT INTO countries VALUES ('SA','Saudi Arabia'),('EG','Egypt'),('AE','United Arab Emirates'),('JO','Jordan');
INSERT INTO companies SELECT g, 'company ' || g, (ARRAY['SA','EG','AE','JO'])[1 + g % 4] FROM generate_series(1, 200) g;
INSERT INTO campaigns SELECT c, n, 'campaign ' || n, 1000 + n FROM generate_series(1, 200) c, generate_series(1, 25) n;
INSERT INTO ads SELECT c, a, 1 + a % 25, (random() * 100)::int, (random() * 10000)::int
FROM generate_series(1, 200) c, generate_series(1, 2500) a;والآن ننظر إلى ما حدث:
SELECT table_name, citus_table_type, distribution_column, colocation_id, shard_count FROM citus_tables ORDER BY 1;
SELECT nodename, count(*) AS shards, pg_size_pretty(sum(shard_size)) AS size FROM citus_shards GROUP BY nodename ORDER BY 1; table_name | citus_table_type | distribution_column | colocation_id | shard_count
------------+------------------+---------------------+---------------+-------------
companies | distributed | id | 2 | 32
campaigns | distributed | company_id | 2 | 32
ads | distributed | company_id | 2 | 32
countries | reference | <none> | 3 | 1
nodename | shards | size
----------+--------+-------
coord | 1 | 32 kB
w1 | 49 | 26 MB
w2 | 49 | 22 MBلاحظ أن الجداول الثلاثة لها نفس colocation_id، أي أنها موزعة معاً، وأن كل عامل يحمل 48 جزءاً من الجداول الموزعة (ثلاثة جداول في 16 جزءاً) ونسخة من الجدول المرجعي، والمنسق يحمل نسخة من الجدول المرجعي فقط.
أين يذهب كل استعلام؟
الأمر EXPLAIN على المنسق يبين خطة Citus: كم مهمة Task، وعلى أي عامل، وما الخطة داخل كل جزء. أولاً استعلام لشركة واحدة:
EXPLAIN (COSTS OFF)
SELECT c.name, sum(a.clicks)
FROM campaigns c JOIN ads a ON a.company_id = c.company_id AND a.campaign_id = c.id
WHERE c.company_id = 42
GROUP BY c.name; Custom Scan (Citus Adaptive)
Task Count: 1
Tasks Shown: All
-> Task
Node: host=w2 port=5432 dbname=postgres
-> HashAggregate
Group Key: c.name
-> Hash Join
Hash Cond: (a.campaign_id = c.id)
-> Bitmap Heap Scan on ads_102099 a
Recheck Cond: (company_id = 42)
-> Bitmap Index Scan on ads_pkey_102099
Index Cond: (company_id = 42)
-> Hash
-> Seq Scan on campaigns_102067 c
Filter: (company_id = 42)مهمة واحدة Task Count: 1 على العامل w2، والربط كله داخله بين الجزأين ads_102099 وcampaigns_102067، وهذا هو النمط الذي يجعل تطبيقات SaaS تتوسع: كل طلب يشغل عاملاً واحداً، فإضافة عمال تعني خدمة مستأجرين أكثر. ولاحظ أن الكتابة تتبع نفس القاعدة:
EXPLAIN (COSTS OFF) UPDATE ads SET clicks = clicks + 1 WHERE company_id = 7 AND id = 10;
Custom Scan (Citus Adaptive)
Task Count: 1
-> Task
Node: host=w1 port=5432 dbname=postgres
-> Update on ads_102080 ads
-> Index Scan using ads_pkey_102080 on ads_102080 ads
Index Cond: ((company_id = 7) AND (id = 10))أما الاستعلام الذي يسأل عن كل الشركات، مع ربط بالجدول المرجعي:
EXPLAIN (COSTS OFF)
SELECT co.name AS country, sum(a.clicks)
FROM ads a JOIN companies c ON c.id = a.company_id JOIN countries co ON co.code = c.country
GROUP BY co.name; HashAggregate
Group Key: remote_scan.country
-> Custom Scan (Citus Adaptive)
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=w1 port=5432 dbname=postgres
-> HashAggregate
Group Key: co.name
-> Hash Join
Hash Cond: (c.country = co.code)
-> Hash Join
Hash Cond: (a.company_id = c.id)
-> Seq Scan on ads_102072 a
-> Hash
-> Seq Scan on companies_102008 c
-> Hash
-> Seq Scan on countries_102104 coهنا 32 مهمة، واحدة لكل جزء، وكل عامل يجمع أجزاءه جزئياً ويربطها بنسخته المحلية من countries، ثم يجمع المنسق النتائج الجزئية في HashAggregate الأعلى. الصورة التالية تبين النمطين:

وهذا النوع من الاستعلامات الشاملة مفيد للتحليلات لأن العمال يعملون بالتوازي، ولكنه مكلف إذا كان في كل طلب من المستخدمين، والسبب الذي شرحناه في مقال التقسيم والتوزيع: زمن الطلب يصبح زمن أبطأ عامل، وكل طلب يشغل كل العمال.
الخطأ الثاني: جدول موزع بعمود آخر
لنفرض أن مطوراً أضاف جدول أحداث الإعلانات ووزعه بعمود ad_id لأنه بدا منطقياً:
CREATE TABLE ad_events (ad_id bigint NOT NULL, company_id bigint NOT NULL, at timestamptz NOT NULL DEFAULT now());
SELECT create_distributed_table('ad_events', 'ad_id');
INSERT INTO ad_events (ad_id, company_id) SELECT id, company_id FROM ads WHERE company_id <= 5;
SELECT count(*) FROM ad_events e JOIN ads a ON a.id = e.ad_id AND a.company_id = e.company_id WHERE a.campaign_id = 3;INSERT 0 12500
ERROR: the query contains a join that requires repartitioning
HINT: Set citus.enable_repartition_joins to on to enable repartitioningCitus ترفض الربط افتراضياً لأن صفوف الجدولين ليست في نفس العمال، وإذا فعلت الإعداد الذي يقترحه التلميح:
SET citus.enable_repartition_joins = on;
count
-------
500
Time: 15788.844 ms (00:15.789)نجح الاستعلام بعد 15.8 ثانية لأن Citus أعادت توزيع الصفوف بين العمال أثناء التنفيذ. ثم أنشأنا نفس الجدول موزعاً بالعمود company_id مع colocate_with => 'ads' ونفس البيانات، فأخذ نفس الاستعلام 50 مللي ثانية. أي أن اختيار العمود الخطأ جعل الاستعلام أبطأ بثلاثمئة مرة، ولا يوجد إعداد يصلح ذلك، فلا تقم بتفعيل enable_repartition_joins لتسكت الخطأ في استعلامات التطبيق، وإنما اجعل الجداول التي تربط معاً موزعة بنفس العمود.
إضافة عامل وإعادة توزيع الأجزاء
عندما يمتلئ العاملان تضيف ثالثاً، ولكن إضافته لا تنقل إليه شيئاً، فالأجزاء الموجودة تبقى مكانها حتى تطلب إعادة التوزيع Rebalance، والفكرة العامة لنقل الأجزاء دون إيقاف الكتابة شرحناها في مقال التقسيم والتوزيع. شغلنا عاملاً ثالثاً w3 بنفس الصورة في نفس الشبكة ثم:
SELECT citus_add_node('w3', 5432);
SELECT citus_rebalance_start();
SELECT citus_rebalance_wait();وفي المحاولة الأولى ظهر خطآن متتاليان. الأول قبل أن يبدأ أي نقل:
ERROR: cannot use logical replication to transfer shards of the relation ad_events since it doesn't have a REPLICA IDENTITY or PRIMARY KEY
DETAIL: UPDATE and DELETE commands on the shard will error out during logical replication unless there is a REPLICA IDENTITY or PRIMARY KEY.
HINT: If you wish to continue without a replica identity set the shard_transfer_mode to 'force_logical' or 'block_writes'.والسبب أن Citus تنقل الجزء والكتابة مستمرة عبر النسخ المنطقي Logical Replication، وهذا يحتاج أن يعرف كيف يحدد الصف عند التحديث والحذف، فحذفنا الجدول التجريبي، وجعلنا للجدول الآخر الذي لا مفتاح له REPLICA IDENTITY FULL. والخطأ الثاني كان أسوأ لأنه لم يظهر مباشرة: بدأت المهمة في الخلفية، وانتظر الأمر citus_rebalance_wait أكثر من 33 دقيقة ثم انتهى بالحالة failed، وفي جدول المهام:
SELECT task_id, status, retry_count, left(message, 300) FROM pg_dist_background_task ORDER BY task_id;
task_id | status | retry_count | left
---------+-------------+-------------+-------------------------------------------------------------------
1 | error | 33 | ERROR: ERROR: logical decoding requires "wal_level" >= "logical"+
| | | CONTEXT: while executing command on coord:5432 +
2 | unscheduled | 0 |
3 | unscheduled | 0 | النقل المنطقي يحتاج wal_level = logical على العقد، والقيمة الافتراضية replica، والمهمة أعادت المحاولة 33 مرة بصمت. لذلك أضفنا الإعداد إلى ملف Compose لكل العقد، وأعدنا تشغيلها، ثم:
NOTICE: Scheduled 10 moves as job 2
citus_rebalance_wait
----------------------
Time: 81860.643 ms (01:21.861)
state
----------
finished
nodename | shards | size
----------+--------+-------
coord | 1 | 32 kB
w1 | 45 | 15 MB
w2 | 45 | 13 MB
w3 | 41 | 24 MBنقلت Citus عشرة أجزاء في 82 ثانية، وكل نقل يأخذ أجزاء الجداول الموزعة معاً لنفس مدى ال Hash، لذلك وصل إلى w3 أربعون جزءاً من أربعة جداول مع نسخة الجدول المرجعي، وبقي عدد الإعلانات 500000 وبقي استعلام الشركة 42 يعمل. والدرس العملي: اجعل wal_level = logical ومفاتيح أساسية لكل الجداول من اليوم الأول، وراقب citus_rebalance_status() أثناء النقل بدل الانتظار، كما في وثائق إدارة العنقود.
التشغيل في الإنتاج
- التوفر العالي High Availability: كل عقدة في Citus سيرفر PostgreSQL عادي، وإذا توقف عامل توقفت الاستعلامات التي تحتاج أجزاءه، لذلك تحتاج كل عقدة نسخة متماثلة Replica وانتقالاً تلقائياً، وأداة Patroni تدعم Citus مباشرة، وكذلك بعض مشغلات Kubernetes.
- النسخ الاحتياطي: كل عقدة تنسخ مثل أي PostgreSQL بأداة مثل pgBackRest كما في مقال الاستعادة إلى نقطة زمنية، ولكن الاستعادة إلى لحظة واحدة متسقة على كل العقد تحتاج نقاط استعادة مشتركة بالدالة
citus_create_restore_pointثم استعادة كل عقدة إلى نفس النقطة. - الترقية: ترقية Citus تكون على كل العقد ثم
ALTER EXTENSION citus UPDATEكما في دليل الترقية، وتابع أي إصدار من PostgreSQL تدعمه النسخة قبل ترقية PostgreSQL نفسه، فالصورة التي استخدمناها كانت على 18.4 وليس آخر إصدار صغير. - الاتصالات: كل استعلام شامل يفتح اتصالات من المنسق إلى كل عامل، فضع PgBouncer أمام المنسق وانتبه لعدد الاتصالات على العمال، كما في مقال تجميع الاتصالات.
أخطاء شائعة
- Citus قبل الحاجة: سيرفر واحد أكبر أبسط بكثير حتى يثبت القياس أنه لا يكفي.
- عمود توزيع لا يظهر في أغلب الاستعلامات: كل استعلام يصبح شاملاً على كل العمال.
- جداول مرتبطة موزعة بأعمدة مختلفة: ربط يحتاج نقل البيانات، وكان أبطأ بثلاثمئة مرة في المختبر.
- تفعيل
enable_repartition_joinsللتطبيق: يخفي خطأ التصميم بدل أن يصلحه. - جداول بلا مفتاح أساسي و
wal_levelالافتراضي: تكتشفهما يوم تحتاج إضافة عامل. - جدول كبير يكتب فيه كثيراً كجدول مرجعي: كل كتابة تذهب إلى كل العقد.
- الاعتقاد أن Citus تعطي التوفر العالي: هي تعطي التوسع، والتوفر العالي إعداد منفصل لكل عقدة.
حذف المختبر
docker rm -f lab-citus-w3 && docker volume rm lab-citus-w3
docker compose down -vالخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- Citus إضافة تجعل عدة سيرفرات PostgreSQL قاعدة واحدة: منسق يعرف مكان كل جزء ويوجه الاستعلامات، وعمال يحملون الأجزاء، والتطبيق يكتب SQL عادياً.
- عمود التوزيع هو القرار الأهم، ويجب أن يدخل في كل مفتاح أساسي وفريد، والجداول التي تربط معاً توزع بنفس العمود حتى يتم الربط داخل العامل.
- في المختبر ذهب استعلام المستأجر الواحد إلى عامل واحد، وتوزع الاستعلام الشامل على 32 مهمة، وكان الربط بين جدولين موزعين بأعمدة مختلفة أبطأ بثلاثمئة مرة.
- إضافة عامل تحتاج إعادة توزيع، وهي تعمل دون إيقاف الكتابة بشرط
wal_level = logicalومفتاح أساسي أوREPLICA IDENTITYلكل جدول. - Citus تناسب SaaS متعدد المستأجرين والتحليلات على بيانات ضخمة، ولا تغني عن التوفر العالي والنسخ الاحتياطي لكل عقدة، وترخيصها AGPL-3.0.
دليلتقسيم الجداول Partitioning وتوزيعها على السيرفرات Shardingمتى تقسم الجدول داخل سيرفر واحد، ومتى توزع البيانات على عدة سيرفرات، وكيف تضيف سيرفراً وتتعامل مع العميل الضخم.
دليلتطبيق SaaS متعدد المستأجرين عملياً مع Row-Level Securityنبني على PostgreSQL تطبيق SaaS يعزل كل مستأجر Tenant عن غيره بسياسات Row-Level Security، ونرى أين تتسرب البيانات رغم ذلك.
دليلعناقيد قواعد البيانات Database Clustering ومتى تحتاجهاماذا تعني كلمة Cluster في كل قاعدة بيانات، ومتى تحتاج نسخاً أو انتقالاً تلقائياً، ومتى يكفيك سيرفر واحد مع نسخ احتياطية جيدة.
دليلPostgreSQL للجداول الضخمة: عشرات الملايين من الصفوفماذا تفعل عندما يصل جدول الطلبات إلى عشرات الملايين من الصفوف وتبطئ لوحة التحكم ويمتلئ القرص، وكيف تقيس قبل أن تصلح.سجل التحديثات
- أكتوبر 2026: كتابة المقال واختباره على Citus 14.2.0 مع PostgreSQL 18.4 (الصورة citusdata/citus:14.2.0) بمنسق وثلاثة عمال على Docker Engine 29.8.0.