SQLite الموزّعة: 5 خرافات اختبرناها بعد 12 عامًا
بعد اثني عشر عامًا من SQLite الموزّعة — Raft وLitestream وTurso وD1 — نختبر خمس معتقدات حول الاتساق والنسخ المتماثل ومتى نختار Postgres.

تحوّلت SQLite الموزّعة من مشروع عطلة نهاية أسبوع لشخص واحد إلى فئة منتجات مدعومة برأس المال الاستثماري، دون أن يلاحظ معظم المهندسين العقد الذي مرّ بينهما. في سبتمبر 2014 وضع Philip O'Toole سجل Raft أمام SQLite، ونسخ كل عملية كتابة عبر مجموعة من الخوادم، ثم رفع الكود على GitHub باسم rqlite. بعد اثني عشر عامًا صار هناك نظام بيئي كامل — dqlite وLitestream وLiteFS وcr-sqlite وlibSQL/Turso وCloudflare D1 — ومعظم ما يعتقده الناس عنه خاطئ بطرق مثيرة للاهتمام.
شغّلتُ قواعد بيانات عنقودية في الإنتاج وتعاملتُ مع أنماط أعطالها، لذا دعني أوفّر عليك بعض العناء: النقاشات حول SQLite الموزّعة نادرًا ما تدور حول SQLite نفسها. إنها تدور حول السؤال الذي تطرحه فعلًا — ماذا لو تعطّل الجهاز، أو ماذا لو تعطّل الجهاز ولا أستطيع الانتظار، أو ماذا لو لم يكن هناك جهاز أصلًا. إليك خمس خرافات، مختبرة على مدى اثني عشر عامًا من التاريخ.
الخرافة الأولى: SQLite الموزّعة فكرة حديثة
تبدأ الموجة الحالية من الكتابة عن SQLite المتماثلة النسخ حوالي عام 2021، حين ارتفع صوت Litestream وحركة «SQLite تكفي»، ووصلت Turso وCloudflare D1 بتمويل ومنشورات إطلاق. فترسّخ الاعتقاد بأن شخصًا ذكيًا لاحظ مؤخرًا أن SQLite يمكن نسخها. في الحقيقة، ظهر أول نظام Raft فوق SQLite عام 2014، ولم يكن أول نسخ لـ SQLite أصلًا — فقد كانت Expensify تفعل ذلك داخليًا بعد بروتوكول شبيه بـ Paxos لسنوات قبل أن تطلق Bedrock عام 2016، وقدّمت ActorDB لكل Erlang actor قاعدة بيانات SQLite خاصة به مُنسوخة عبر Raft في الفترة نفسها تقريبًا.
لماذا يهم هذا أكثر من مسألة الفضل؟ لأن تصميمًا يصمد اثني عشر عامًا من إعادة الاختراع المستقلة يخبرك بشيء. ما لا يقل عن ثمانية فرق مختلفة، بحاجات وبروتوكولات مختلفة، نظرت إلى المشكلة وانتهت إلى البنية نفسها: SQLite محرّكًا للتخزين، والنسخ المتماثل مُلحَق من الخارج. لم يجد أحد سببًا للتوقف. عندما ترى هذا النمط — إعادة تنفيذ متقاربة على مدى عقد — فأنت أمام فكرة جيدة فعلًا، لا دورة ضجيج. قارنها بفئات تشتعل ثم تختفي؛ هذه ظلّت تتراكم.
ويهم أيضًا لأن قرارات التصميم القديمة ما زالت تحمل الوزن. اختار rqlite أن يكون نظام CP عام 2014 — تمر الكتابات عبر القائد، ويجب أن يقرّ أغلبية، والأقلية المعزولة ترفض الكتابة — ولم يغيّر رأيه أبدًا. هذا الاختيار الواحد حدّد واجهة HTTP API ومستويات اتساق القراءة وسلوك اللقطات (snapshots). الفرق التي تعامل الاتساق كمفتاح إعدادات يُقرَّر لاحقًا تندم عادةً؛ إذ إن الفجوة بين المواصفة التي افترضتها والمواصفة التي نفّذتها هي بالضبط المكان الذي تنمو فيه أخطاء الأنظمة الموزّعة.
الخرافة الثانية: Raft فوق SQLite يمنحك Postgres موزّعة بثمن بخس
هذه أغلى خرافة في القائمة. الطرح يبدو مغريًا: خذ أكثر قواعد البيانات انتشارًا في العالم، أضف إجماعًا (consensus)، وتجاوز الثقل التشغيلي لنظام العميل-الخادم. لكن ما تحصل عليه فعليًا هو قاعدة بيانات بكاتب واحد، تدفع فيها كل عملية كتابة زمن رحلة شبكية إلى أغلبية العقد، ويبقى سطح SQL في SQLite كما هو — لا إجراءات مخزنة، ونظام أنواع نحيل، وأدوات تزامن محدودة.
مسار الكتابة هو التكلفة الصادقة. الالتزام المحلي في SQLite لا يتجاوز بضع عمليات fsync. أما عملية Raft المُنسوخة فهي: يضيف القائد السجل إلى سجله المحلي، ويرسل المدخل إلى التابعين، وينتظر أغلبية تحفظه على القرص، ثم يطبّقه على SQLite، ثم يردّ. على شبكة محلية سليمة يكون ذلك نحو ملّي ثانية أو اثنتين بدلًا من عشرات الميكروثانية. عبر المناطق الجغرافية تصبح المسألة سرعة الضوء زائد القرص. بالنسبة لأحمال القراءة الكثيفة مع معدلات كتابة متواضعة — مخازن الإعدادات، وبيانات اكتشاف الخدمات، ومستويات التحكم الصغيرة — هذا مقبول، وهو تحديدًا النيش الذي بُني له rqlite (قصة نشأته أنه كان يحتاج إلى تخزين علائقي صغير لنظام عنقودي، ولم تكن هناك إجابة جيدة لسؤال أي عقدة تحتفظ به). أما لأحمال OLTP الساخنة، فهي أداة خاطئة.
ثم هناك الواقع التشغيلي الذي لا يضعه أحد في ملف README: انتخاب القائد صار نمط عطل تمتلكه أنت الآن. منع الانقسام الدماغي (split-brain) يتم بالتصويت بالأغلبية، نعم، لكن هذا يعني أن عنقودًا من ثلاث عقد يفقد عقدتين سيرفض كل الكتابات. لقد استبدلت «قاعدة البيانات متوقفة لأن الخادم تعطّل» بـ «قاعدة البيانات متوقفة لأن النصاب (quorum) تعطّل»، والثانية أصعب في الشرح لمناوبك الليلي في الثالثة فجرًا. إن لم تفكر في اختبار النظام الذي اخترته بأسلوب Jepsen، فعليك ذلك — سأل أحد المعلّقين في Hacker News السؤال الحاد عما إذا كان rqlite قد خضع لاختبار Jepsen، لأن الأنظمة التي لم تُختبر غالبًا تخفي مفاجآت. وفي النقاش نفسه، لاحظ مهندس متمرس أن تشغيل Postgres الحقيقي أصبح سهلًا بما يكفي بفضل الأدوات الحديثة، بحيث صار «استخدم خادم قواعد بيانات» خيارًا افتراضيًا مشروعًا من جديد. وهو ليس مخطئًا.

الخرافة الثالثة: النسخ المتماثل يعني الإجماع
إذا كان هناك درس واحد يقدمه تاريخ الاثني عشر عامًا فعلًا، فهو هذا: «SQLite المُنسوخة» ليست تصميمًا واحدًا، بل ثلاث زوايا في فضاء تصميم، وقد ظهرت بترتيب له دلالة. الخلط بينها هو ما يجعل الفرق تنتهي بتشغيل عنقود Raft لحل مشكلة نسخ احتياطي.
- الإجماع (CP). rqlite وdqlite. قائد واحد، واتساق قوي، وعنقود حقيقي. يجيب عن سؤال: «ماذا لو تعطّل الجهاز ولا أستطيع الانتظار حتى الاستعادة؟». الثمن: زمن الكتابة وعمليات النصاب.
- البث المتدفق (كاتب واحد). Litestream، ولاحقًا LiteFS. لا إجماع إطلاقًا — يُبَثّ WAL الخاص بـ SQLite إلى تخزين الكائنات، فيوفّر تعافيًا من الكوارث شبه مستمر لعقدة واحدة. يجيب عن سؤال: «ماذا لو تعطّل الجهاز؟». الثمن: نافذة صغيرة لاحتمال فقدان البيانات، وزمن استعادة يُقاس بالثواني إلى الدقائق.
- الدمج (AP). cr-sqlite وحركة local-first. CRDTs كامتداد لـ SQLite، حيث يكتب كثيرون ويتباعدون ثم يُدمجون دون قائد. يجيب عن سؤال: «ماذا لو لم يكن هناك جهاز مركزي أصلًا؟». الثمن: اتساق نهائي (eventual consistency) ودلالات دمج يجب تصميمها بعناية.
حجّة Ben Johnson لصالح Litestream تستحق الاحترام، لأنها كانت صحيحة في معظم التطبيقات: هذه التطبيقات تعمل على جهاز واحد وتحتاج طريقة للتعافي حين يتعطل، لا إلى عنقود. السؤال الأول — «ماذا لو تعطّل الجهاز» — أشيع بكثير من الثاني — «ولا أستطيع الانتظار للاستعادة». لقد رأيت فرقًا تنشر عنقود Raft من ثلاث عقد لحماية قاعدة بيانات كان متطلب RTO فيها «في وقت ما قبل الغداء». هذه ليست هندسة موثوقية، بل عبادة الطقوس (cargo culting). ومن المفارقات الجميلة أن نهج Litestream في بث WAL ألهم في النهاية طريقة تعامل rqlite نفسه مع WAL في الإصدار 8 — فزاوية البث أعادت تغذية زاوية الإجماع.
لذلك قبل أن تختار أداة، اكتب السؤال. حرفيًا. «عند تعطّل العقدة X، نحتاج أن تُستأنف الكتابات خلال N ثانية، ونقبل فقدان آخر M ثانية من البيانات الملتزم بها.» إذا كانت M بضع ثوانٍ وN دقيقة، فإن Litestream إلى S3 هو جوابك، ولن يوقظك أبدًا بسبب النصاب. إذا كانت M صفرًا وN ثوانٍ، فأنت في منطقة الإجماع وستدفع ثمنه في كل عملية كتابة. هذا هو الانضباط نفسه في أي نموذج تهديد: سمِّ الفشل قبل أن تشتري الدفاع عنه، كما تفعل مع الأنظمة التي تقاوم الكوارث بأن تقرر مسبقًا أي كارثة تقاومها.
الخرافة الرابعة: «SQLite على الحافة» المُدارة تُزيل المفاضلات
المال في هذه الفئة ذهب إلى المنصات — جمعت Turso تمويلًا استثماريًا ثم استحوذت عليها Supabase؛ وأطلقت Cloudflare D1 داخل شبكتها الطرفية. الطرح أن جهة أخرى تتولى الآن الأجزاء الموزّعة الصعبة. وأحيانًا يكون ذلك صحيحًا. وأحيانًا يكون تسويقًا، ويتسرّب السر المزعج: أقرّ مطوّر من Cloudflare علنًا بأن D1 في جوهرها غلاف حول كائن Durable Object مفرد يعرض قاعدة بيانات SQLite الخاصة به، وأنك غالبًا أفضل حالًا باستخدام تخزين Durable Objects مباشرة. هذه ليست قاعدة بيانات موزّعة بالمعنى الإجماعي؛ إنها ملف SQLite واحد محاط بمنصة. ويشكو مستخدمو D1 في الإنتاج من موثوقية على القدر نفسه.
هذا لا يعني أن الخيارات المُدارة سيئة. يعني أن المفاضلات لم تختفِ — بل انتقلت خلف واجهة برمجية لا تستطيع فحصها. عندما تشغّل rqlite بنفسك، تعرف بالضبط ما يعنيه CP: ثلاث عقد، وقائد واحد، وإقرارات أغلبية، والأقلية المعزولة ترفض الكتابة. وعندما تشتري «SQLite على الحافة»، تحتاج أن تطرح الأسئلة نفسها على صندوق أسود: أين الكاتب، وماذا يحدث عند الانقسام، وما نافذة المتانة الفعلية؟ إذا كانت إجابة المورّد غامضة، فلن تكون مراجعتك للحوادث واضحة.
النسخ المتماثل كخدمة لا يلغي سؤال الاتساق. إنه فقط ينقل الجواب إلى بنية تحتية لشخص آخر، حيث لا تستطيع تدقيقه.
النمط التاريخي يستحق أن تستوعبه: الأنظمة مفتوحة المصدر (rqlite وdqlite وLitestream) حملت الفكرة ثماني سنوات قبل أن يبيعها أحد، ثم تدفق رأس المال إلى من حوّل الفكرة إلى خدمة لها فاتورة. هذا هو المصير المعتاد للبنية التحتية. الشراء غالبًا خيار صائب — لكن اشترِ وعيناك مفتوحتان على الزوايا الثلاث التي تحصل عليها فعلًا.

الخرافة الخامسة: الاتساق القوي ترقية مجانية فوق النسخ الاحتياطية
يصوغ الناس الاختيار هكذا: نسخ احتياطية (ضعيفة، رخيصة) مقابل إجماع (قوي، مكلف لكنه أفضل بالتأكيد). وهذا غير صحيح. الاتساق القوي عبر Raft منتج مختلف، لا نسخة أفضل من المنتج نفسه، ويفرض ضرائب مستمرة لا تفرضها النسخ الاحتياطية أبدًا.
الضريبة الأولى: الإنتاجية. لاحظ أحد الممارسين، ممن استخدموا النظامين، أن rqlite يمنحك ضمانات «مخزَّن في أكثر من مكان»، لكنه أبطأ بكثير من قاعدة بيانات بعقدة واحدة — وهذه هي رحلة الإجماع ذهابًا وإيابًا، ولن تختفي أبدًا. الضريبة الثانية: الدلالات الدقيقة. إذا نسخت بإرسال عبارات SQL فسترث مشكلات الحتمية: دوال مثل random() أو datetime('now') قد تتباعد بين العقد ما لم تعِد الطبقة كتابتها، ولهذا تدعم بعض التطبيقات مجموعة فرعية فقط من SQL في SQLite. وإذا نسخت على مستوى WAL بدلًا من ذلك، تتجنب هذه المشكلة، لكنك تقرن نفسك بصيغة القرص الداخلية لـ SQLite وتخطيط الصفحات. لا يوجد موضع لطبقة النسخ دون مقايضة. الضريبة الثالثة: المعاملات الموزّعة صعبة فعلًا. وصف مهندس تطور تصميمه من كون كل عبارة داخل معاملة أمرَ Raft منفصلًا — صحيح تقريبًا لكنه بطيء — إلى تصميم تُشغّل فيه العقدة البادئة المعاملة كاملة محليًا، ثم تتراجع عنها، وترسل أمر التزام واحدًا يحمل الدفعة كلها. هذا هو نوع الدرس المكتسب بالتجربة الذي لا تتعلمه إلا بتشغيل هذه الأنظمة.
إليك توضيحًا مختصرًا لفرق مسار الكتابة، باستخدام HTTP API الخاص بـ rqlite:
# Local SQLite: one process, one fsync, microseconds.
sqlite3 app.db "INSERT INTO kv VALUES ('flag', 'on')"
# rqlite: this HTTP call hides a Raft round trip —
# leader log append, replication to a majority, then apply.
curl -XPOST 'localhost:4001/db/execute?pretty' \
-H 'Content-Type: application/json' \
-d '["INSERT INTO kv VALUES (?, ?)", "flag", "on"]'
# The guarantee changed: the write now survives the loss
# of any single node. So did the latency budget.
ولاحظ ما لم تحصل عليه: توسيع القراءة. يكرر Raft السجل حتى يتمكن التابعون من تولي المهام، لكنه لا يجعل التابعين يقدمون قراءات متسقة بقوة افتراضيًا (يوفر rqlite مستويات اتساق للقراءة تحديدًا لأن هذه مفاضلة). إن جئت إلى «الموزّع» آملًا في سعة قراءة أكبر، فأنت كنت تريد نسخًا للقراءة، لا إجماعًا.
ما الذي علّمتنا إياه اثنا عشر عامًا من Raft فوق SQLite
تاريخ هذه الفئة دليل نظيف بشكل غير معتاد على كيفية نضج أفكار البنية التحتية. أولًا يبني أحدهم الشيء المبدئي لحاجة محددة (rqlite، من مشكلة تجميع حقيقية في وظيفة حقيقية). ثم يعيد آخرون بناءه وفق قيود التغليف الخاصة بهم — dqlite من Canonical مكتبة تُضمَّن لأن Canonical احتاجت قاعدة بيانات داخل منتجاتها، بينما rqlite خادم تشغّله بجانب أي منتج. ثم يجادل أحدهم بأن الحالة الشائعة أصغر مما يفترضه الحل المبدئي (Litestream)، ويثبت أنه كان محقًا، فتُعاد معايرة الحقل كله. وأخيرًا تغلّف المنصات الفكرة وتبيعها. إذا عملت في البنية التحتية مدة كافية فستشاهد هذا الفيلم مرة أخرى — ووكلاء LLM يعيدون تعلم دروس الأنظمة الموزّعة بالطريقة نفسها الآن.
توصيتي الصريحة بعد كل ما سبق: ابدأ بـ SQLite على عقدة واحدة مع بث WAL إلى تخزين الكائنات. مدى ما يوصلك إليه هذا مذهل، وقصة الفشل لديك تكون استعادة، لا تقرير ما بعد الحادث عن الإجماع. انتقل إلى نظام قائم على Raft فقط عندما تستطيع أن تصرّح في جملة واحدة بمتطلب التوفر الذي لا يستطيع البث تلبيته — واقبل فاتورة زمن الكتابة وعمليات النصاب التي تأتي معه. لجأ إلى Postgres لحظة تحتاج إلى سطح SQL الخاص به أو إنتاجية الكتابة المتعددة. وإذا لم يناسب أيٌّ من هذه الخيارات، ففي عالم قواعد البيانات المدمجة خيارات أخرى أيضًا، من قواعد البيانات البيانية المدمجة إلى زاوية CRDTs ذات الطابع local-first.
خلاصة كسر الخرافات: SQLite الموزّعة ليست جديدة، وليست Postgres رخيصة، وليست تصميمًا واحدًا، ولا يجعلها آمنة غلاف مُدار، وليست ترقية مجانية فوق النسخ الاحتياطية. إنها تصميم عمره اثنا عشر عامًا، ومُخطَّط جيدًا، بثلاث زوايا، وكل زاوية صحيحة لسؤال مختلف. المهندسون الذين يحترقون هم من لم يكتبوا السؤال أبدًا.


