بنناجح
المرحلة 2: من جوه · الدرس 2 من 3 · 30 دقيقة

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 بيمشي كده:

  1. التعديلات بتتعمل على الصفحات في الذاكرة (Buffer Pool / Shared Buffers)، والصفحات دي بقت Dirty.
  2. سجلات بتوصف التعديلات بتتكتب في الـ Log Buffer في الذاكرة.
  3. مع الـ COMMIT: الـ Log Buffer بيتكتب في آخر ملف الـ Log، وبيتعمل fsync.
  4. الداتابيز بترد “COMMIT تم”.
  5. الصفحات الـ 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 هو أغلى حاجة في الكتابة. والداتابيزين بيسمحوا تضعّفه:

الكهربا قطعت بعد COMMITجرّب بنفسك
في الداتابيزorders on disk0orders acknowledged0
الأبلكيشن⏱الديسك
  1. INSERT INTO orders ...; COMMIT;مستني الـ fsync…1
  2. 2redo/WAL: write + fsyncعلى الديسك ✓
  3. -- "COMMIT تم"اتأكد للعميل3
  4. ⚡ الكهربا قطعتCrash
  5. 5Recovery: 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.

علّم عليه لما تقدر تجاوب أسئلة الإنترفيو من غير ما تبص.