Durability: الـ WAL والـ fsync
الداتابيز ردت عليك "COMMIT تم" وبعدها بمللي ثانية الكهربا قطعت. التعديلات لسه موجودة؟ الدرس ده بيشرح القاعدة اللي بتضمن ده: الـ Write-Ahead Log. وليه الكتابة في Log أسرع من الكتابة في الجداول، ويعني إيه fsync، وإزاي الداتابيز بتجمّع ألف COMMIT في fsync واحد، والإعدادات اللي بتبيع فيها شوية أمان مقابل سرعة.
- تفهم قاعدة الـ Write-Ahead Logging وليه بتضمن الـ Durability
- تعرف ليه الكتابة في الـ Log أسرع من الكتابة في صفحات الجدول
- تفهم يعني إيه fsync والـ Page Cache
- تفهم الـ Group Commit والـ Checkpoints والـ Torn Pages
- تختار بين
innodb_flush_log_at_trx_commitوsynchronous_commitعن فهم
1. ليه مانكتبش الصفحات على الديسك مع كل COMMIT؟
أبسط طريقة للـ Durability: مع كل COMMIT، اكتب كل صفحة اتعدّلت على الديسك. بس دي فكرة وحشة جداً:
- ترانزاكشن عدّلت 5 صفوف في 5 جداول = 5 صفحات (+ صفحات الـ Indexes) في أماكن عشوائية على الديسك. والكتابة العشوائية أبطأ بكتير من المتسلسلة.
- الصفحة حجمها 16 KB (InnoDB) أو 8 KB (PostgreSQL)، حتى لو اتغير فيها 4 bytes بس.
- لو الكهربا قطعت وإنت كاتب 3 صفحات من 5، الداتا تبقى نصها متعدّل.
2. الـ Write-Ahead Log
الاسم في PostgreSQL WAL، وفي InnoDB Redo Log، والفكرة واحدة. والـ COMMIT بيمشي كده:
- التعديلات بتتعمل على الصفحات في الذاكرة (Buffer Pool / Shared Buffers)، والصفحات دي بقت Dirty.
- سجلات بتوصف التعديلات بتتكتب في الـ Log Buffer في الذاكرة.
- مع الـ COMMIT: الـ Log Buffer بيتكتب في آخر ملف الـ Log، وبيتعمل fsync.
- الداتابيز بترد “COMMIT تم”.
- الصفحات الـ Dirty بتتكتب على الديسك بعدين في الخلفية، على راحتها.
ولو الكهربا قطعت بين 4 و 5؟ الصفحات في الذاكرة ضاعت، بس الـ Log على الديسك فيه كل التعديلات. فلما الداتابيز تقوم، بتقرا الـ Log وتعيد تطبيق التعديلات (ده درس الـ Recovery).
ليه ده أسرع؟
| كتابة الصفحات مع كل COMMIT | WAL | |
|---|---|---|
| نوع الكتابة | عشوائي في أماكن كتير | متسلسل في آخر ملف واحد |
| الحجم | صفحات كاملة | التعديلات بس (غالباً bytes قليلة لكل صف) |
| عدد الـ fsync | صفحة صفحة | واحد للـ Log |
| تعديلات كتير على نفس الصفحة | كل مرة كتابة | الصفحة بتتكتب مرة واحدة بعدين |
الـ WAL بيحوّل الـ Durability من “اكتب كل حاجة في مكانها دلوقتي” لـ “اكتب وصف التغيير في آخر ملف واحد دلوقتي، وحط الحاجات في مكانها بعدين”. والكتابة المتسلسلة أسرع بكتير.
3. fsync: إمتى الداتا تبقى على الديسك فعلاً؟
لما برنامج يعمل write() على ملف، نظام التشغيل مش بيكتب على الديسك. بيحط الداتا في الـ Page Cache (في الـ RAM) ويرجع على طول. ولو الكهربا قطعت، اللي في الـ Page Cache ضاع.
والسلسلة كاملة: Log Buffer (ذاكرة الداتابيز) ← write() ← Page Cache (ذاكرة نظام التشغيل) ← fsync() ← Disk Cache (ذاكرة الديسك نفسه) ← الديسك. والـ Durability الحقيقية محتاجة الداتا تعدّي كل ده، بما فيهم Cache الديسك، وده محتاج ديسك بيحترم الـ Flush أو عنده حماية من انقطاع الكهربا (Power-loss protection / BBU).
4. Group Commit
لو كل COMMIT محتاج fsync، والـ fsync بياخد 1 مللي ثانية، يبقى أقصى حاجة 1000 COMMIT في الثانية؟ لأ.
فلو 200 ترانزاكشن عملوا COMMIT في نفس المللي ثانية، دول fsync واحد مش 200. وده سبب إن الداتابيز بتستحمل آلاف الـ COMMITs في الثانية على ديسك عادي. وفي PostgreSQL فيه commit_delay يخلّي الداتابيز تستنى ميكروثواني قليلة عشان تجمّع أكتر، وده بيفيد بس تحت ضغط عالي جداً.
5. Checkpoints
الـ Log مش هيفضل يكبر للأبد، والـ Recovery مش هيقرا Log من سنة. فكل فترة الداتابيز بتعمل Checkpoint:
وفيه Trade-off: Checkpoints متقاربة = Recovery أسرع بعد الـ Crash، بس كتابة على الديسك أكتر أثناء الشغل العادي. وCheckpoints متباعدة = كتابة أقل (الصفحة اللي اتعدّلت 100 مرة بتتكتب مرة)، بس Recovery أطول. في PostgreSQL ده بيتظبط بـ checkpoint_timeout و max_wal_size، وفي InnoDB بحجم الـ Redo Log (innodb_redo_log_capacity).
6. Torn Pages
مشكلة خبيثة: صفحة الداتابيز 16 KB، بس الديسك ونظام التشغيل بيضمنوا إن الكتابة الذرية تبقى 4 KB أو 512 bytes. لو الكهربا قطعت وإنت بتكتب صفحة، ممكن يتكتب أول 8 KB منها والباقي يفضل قديم: Torn Page، صفحة نصها جديد ونصها قديم ومتبوظة. والـ Log مش هيعرف يصلحها، لأن سجلات الـ Log بتوصف تغيير على صفحة سليمة.
| الحل | |
|---|---|
| InnoDB | Doublewrite Buffer: كل صفحة بتتكتب الأول في منطقة مخصوصة على الديسك، وبعدين في مكانها. لو الكتابة الأولى اتقطعت، الأصل لسه سليم. ولو التانية اتقطعت، فيه نسخة سليمة في الـ Doublewrite |
| PostgreSQL | full_page_writes: أول ما صفحة تتعدّل بعد كل Checkpoint، بتتكتب كاملة في الـ WAL. في الـ Recovery بيبدأ منها بدل الصفحة اللي ممكن تكون متقطعة |
7. تبيع أمان مقابل سرعة
الـ fsync مع كل COMMIT هو أغلى حاجة في الكتابة. والداتابيزين بيسمحوا تضعّفه:
INSERT INTO orders ...; COMMIT;مستني الـ fsync…1- 2
redo/WAL: write + fsyncعلى الديسك ✓ -- "COMMIT تم"اتأكد للعميل3⚡ الكهربا قطعتCrash- 5
Recovery: replay من الـ Logالأوردر موجود
INSERT INTO orders ...; COMMIT;رد فوري1- 2
redo/WAL: في الذاكرة / Page Cacheلسه مش على الديسك INSERT INTO orders ...; COMMIT;رد فوري3⚡ الكهربا قطعتCrash قبل الـ fsync- 5
Recovery: replay من الـ Logالأوردرين مش موجودين
دوس ▶ شغّل، أو امشي خطوة خطوة بالأسهم.
| الإعداد | القيمة | المعنى | ممكن تخسر |
|---|---|---|---|
InnoDB innodb_flush_log_at_trx_commit |
1 (الافتراضي) | write + fsync مع كل COMMIT | ولا حاجة |
| 2 | write لنظام التشغيل مع كل COMMIT، و fsync كل ثانية | آخر ثانية لو الجهاز وقع (مش لو MySQL بس وقع) | |
| 0 | write + fsync كل ثانية | آخر ثانية حتى لو MySQL بس وقع | |
MySQL sync_binlog |
1 (الافتراضي) | fsync للـ Binary Log مع كل COMMIT | ولا حاجة (والـ Replicas متسقة) |
PostgreSQL synchronous_commit |
on (الافتراضي) | يستنى fsync الـ WAL (ولو فيه Synchronous Standby يستناه كمان) | ولا حاجة |
| off | يرد قبل الـ fsync | آخر جزء من الثانية (لحد 3 × wal_writer_delay) |
|
| remote_apply | يستنى الـ Standby يطبّق التعديل كمان | ولا حاجة، والقراية من الـ Standby بتشوف التعديل فوراً |
إمتى تضعّف الـ Durability؟ لما الداتا ممكن تتعاد من مصدر تاني: Import بتعيده لو فشل، أو Logs وإحصائيات خسارة ثانية منها مش مشكلة. وفي PostgreSQL ممكن تعمل ده لترانزاكشن واحدة: SET LOCAL synchronous_commit = off; جوه الترانزاكشن، فالفلوس تفضل محمية والعدادات تبقى سريعة.
الافتراضي في الداتابيزين Durable بالكامل. أي إعداد بيرد قبل الـ fsync بيكسب سرعة ويخاطر بآخر ثانية من الـ COMMITs. ومتلمسش fsync = off في الإنتاج.
8. أسئلة الإنترفيو
فكّر في إجابتك الأول، وبعدين افتح السؤال.
01اشرح الـ Write-Ahead Logging.
قبل ما أي تعديل يوصل لصفحات الجداول على الديسك، لازم سجل بيوصفه يتكتب في Log متسلسل على الديسك. الـ COMMIT بيتعتبر تم لما سجله يوصل للديسك ويتعمل fsync، والصفحات نفسها بتتكتب بعدين. ولو حصل Crash، الـ Log بيتقري ويتعاد تطبيقه.
02ليه الكتابة في الـ WAL أسرع من كتابة الصفحات مع كل COMMIT؟
لأن الـ WAL كتابة متسلسلة في آخر ملف واحد، وفيها التعديلات بس، و fsync واحد. أما الصفحات فكتابة عشوائية في أماكن كتير، وكل صفحة 8 أو 16 KB حتى لو اتغير فيها bytes قليلة. وكمان الصفحة اللي بتتعدّل كتير بتتكتب مرة واحدة في الـ Checkpoint.
03يعني إيه Group Commit؟
لما كذا ترانزاكشن يعملوا COMMIT في نفس الوقت تقريباً، الداتابيز بتكتب سجلاتهم وتعمل fsync واحد للكل وبعدين ترد عليهم. ده بيخلي عدد الـ COMMITs في الثانية أكبر بكتير من عدد الـ fsync اللي الديسك يقدر عليه.
04الفرق بين innodb_flush_log_at_trx_commit = 1 و 2 و 0؟
1 (الافتراضي): write و fsync مع كل COMMIT، فمفيش خسارة. 2: write لنظام التشغيل مع كل COMMIT و fsync كل ثانية، فممكن تخسر آخر ثانية لو الجهاز وقع بس مش لو MySQL وقع. 0: write و fsync كل ثانية، فممكن تخسر آخر ثانية حتى لو MySQL بس وقع.
05يعني إيه Torn Page؟ وإزاي InnoDB و PostgreSQL بيحموا منها؟
صفحة اتكتب نصها بس لأن الكهربا قطعت أثناء كتابتها، لأن الديسك بيضمن كتابة ذرية لـ 4 KB أو أقل والصفحة أكبر. InnoDB بيستخدم Doublewrite Buffer: الصفحة بتتكتب في منطقة مخصوصة الأول. PostgreSQL بيستخدم full_page_writes: أول تعديل على الصفحة بعد كل Checkpoint بيكتبها كاملة في الـ WAL.