SQLite WAL: قلّل انتظار الكتابة من 420ms لـ 70ms
مستوى القارئ: متوسط
هتعرف هنا إمتى تفعيل SQLite WAL يفرق فعلًا، وإزاي تضبطه بدون ما تسيب ملف WAL يكبر ويبطّأ القراءة بعد أسبوع تشغيل.
المشكلة باختصار
الطريقة الشائعة الغلط إنك تشوف رسالة database is locked فتزود retries وخلاص. الطريقة دي بتفشل لما عندك قراءات طويلة وكتابة قصيرة، لأنك بتعالج العرض مش السبب. الافتراض إن عندك تطبيق desktop، API صغير، queue محلية، أو edge service بتستخدم SQLite على نفس الماكينة، وفيه مثلًا 50 ألف قراءة في اليوم و500 إلى 3000 عملية كتابة.
في وضع rollback journal التقليدي، الكتابة ممكن تتداخل بشكل مؤلم مع القراءة. النتيجة إن p95 لانتظار الكتابة ممكن يوصل 420ms في اختبار ضغط بسيط. بعد تفعيل WAL، وإضافة busy_timeout، وجدولة checkpoint، ممكن تنزل نفس القراءة إلى حوالي 70-95ms حسب القرص وحجم المعاملات. الرقم هنا مثال قياس محلي، مش وعد ثابت لكل بيئة.
الفكرة بمثال بسيط
ركز في المثال ده: عندك كشك بيع صغير. فيه موظف بيقرأ الأسعار طول اليوم، وموظف تاني بيضيف عملية بيع كل دقيقة. بدل ما الموظف اللي بيكتب يقفل الدفتر كله على اللي بيقرأ، WAL يخليه يكتب في ورقة جانبية مؤقتة. القارئ يكمل من الدفتر الأساسي، والكاتب يضيف التغييرات في الورقة الجانبية. بعدين تيجي خطوة اسمها checkpoint ترجع الورقة الجانبية للدفتر الأساسي.
علميًا، SQLite WAL يعني Write-Ahead Log. التغييرات تتكتب في ملف -wal بدل تعديل ملف قاعدة البيانات الأساسي فورًا. ده يسمح للقراء والكاتب إنهم يشتغلوا في نفس الوقت في حالات كثيرة. لكن مهم جدًا: WAL لا يحوّل SQLite إلى PostgreSQL. ما زال فيه كاتب واحد فعليًا في نفس اللحظة.
الإعداد العملي القابل للنسخ
أفضل طريقة تبدأ بيها هي إعداد واضح عند فتح الاتصال، ثم قياس قبل وبعد. لو بتستخدم sqlite CLI، شغّل الأوامر دي مرة على قاعدة البيانات:
PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;
PRAGMA synchronous=NORMAL;
PRAGMA wal_autocheckpoint=1000;
ولو عندك سكربت Python صغير، خلي كل اتصال يضبط timeout ويقرأ الوضع الحالي:
import sqlite3
conn = sqlite3.connect("app.db", timeout=5)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA busy_timeout=5000")
conn.execute("PRAGMA synchronous=NORMAL")
conn.execute("PRAGMA wal_autocheckpoint=1000")
mode = conn.execute("PRAGMA journal_mode").fetchone()[0]
print("journal_mode=", mode)
busy_timeout=5000 معناه إن الاتصال يستنى لحد 5 ثواني قبل ما يرجّع lock error. ده مش علاج للبطء لوحده، لكنه يمنع فشل عمليات كتابة قصيرة بسبب تزاحم لحظي طبيعي.
الـ checkpoint هو الجزء اللي الناس بتنساه
WAL يكسبك أداء كتابة أفضل لأنه يضيف التغييرات بالتتابع. بس لو checkpoint مش بيكمل، ملف app.db-wal يكبر. مع قراءات طويلة جدًا، checkpoint ممكن يتعطل لأن قارئ قديم ما زال محتاج snapshot قديم. اللي بيحصل فعلاً إن القراءة تبدأ كويسة، وبعد ساعات تلاقي ملف WAL بقى 200MB والقراءات نفسها بقت أبطأ.
ابدأ بالافتراضي wal_autocheckpoint=1000. SQLite بيستخدم 1000 صفحة كعتبة افتراضية للـ autocheckpoint. لو عندك spikes مزعجة وقت commit، جرّب checkpoint يدوي في وقت هادي:
PRAGMA wal_checkpoint(PASSIVE);
-- في نافذة صيانة قصيرة فقط:
PRAGMA wal_checkpoint(TRUNCATE);
لا تستخدم TRUNCATE عشوائيًا كل دقيقة. ممكن يوقف كتّاب أو ينتظر قراء حسب الحالة. استخدمه في maintenance window أو بعد batch كبير.
الـ trade-off هنا
بتكسب تزامن أفضل بين القراءة والكتابة، وكتابة متتابعة أسرع، واحتمال أقل لرسائل database is locked. بتخسر بساطة التشغيل قليلًا لأن عندك ملفات إضافية مثل -wal و-shm لازم تعتبرها جزءًا من حالة قاعدة البيانات أثناء النسخ أو النقل.
synchronous=NORMAL غالبًا مناسب لتطبيقات كثيرة في WAL لأنه يقلل عمليات fsync أثناء معظم المعاملات. الثمن: لو حصل power loss، ممكن آخر transaction مؤكدة ترجع للخلف. لو عندك قيود مالية، طبية، أو لا تقبل فقد آخر كتابة، استخدم synchronous=FULL واقبل latency أعلى.
ركز كمان على إصدار SQLite. وثائق SQLite تشير إلى إصلاح bug نادر مرتبط بإعادة ضبط WAL في 3.51.3 وما بعدها، مع backports لبعض الإصدارات. لو تطبيقك يفتح أكثر من اتصال ويستخدم WAL بجدية، لا تبني القرار على نسخة قديمة مدمجة داخل runtime منسي.
متى لا تستخدم هذه الطريقة
لا تستخدم WAL لو قاعدة البيانات على network filesystem، لأن WAL يعتمد على shared memory بين القراء على نفس الماكينة. لا تستخدمه كحل لتطبيق فيه عشرات الكتّاب المتزامنين طول الوقت؛ هنا غالبًا محتاج PostgreSQL أو MySQL. ولا تستخدم synchronous=NORMAL لو فقد آخر transaction بعد انقطاع الكهرباء غير مقبول.
مصادر
- SQLite Write-Ahead Logging documentation
- SQLite PRAGMA documentation: busy_timeout, synchronous, wal_autocheckpoint, wal_checkpoint
- SQLite sqlite3_wal_checkpoint reference
الخطوة التالية
الخطوة التالية: شغّل PRAGMA journal_mode=WAL على نسخة staging، وسجّل p95 write wait وحجم ملف -wal لمدة ساعة قبل ما تنقله للإنتاج.