يميل المستخدمون إلى ملاحظة تراجع الأداء في الحالات ذات التزامن المنخفض بسهولة أكبر، بينما يكون من الصعب غالبًا إدراك التحسينات في الأداء في الحالات ذات التزامن العالي. لذلك، فإن الحفاظ على أداء التزامن المنخفض أمر بالغ الأهمية، حيث يؤثر بشكل مباشر على تجربة المستخدم واستعداده للترقية [1].
وفقًا لتغذية راجعة واسعة من المستخدمين، بعد الترقية إلى MySQL 8.0، لاحظ المستخدمون عمومًا تراجعًا في الأداء، وخاصة في عمليات الإدراج الجماعي وعمليات الانضمام. لقد أصبح هذا الاتجاه التنازلي أكثر وضوحًا في الإصدارات الأعلى من MySQL. بالإضافة إلى ذلك، أبلغ بعض عشاق MySQL والمختبرين عن تدهور الأداء في عدة اختبارات sysbench بعد الترقية.
هل يمكن تجنب هذه المشكلات في الأداء؟ أو، بشكل أكثر تحديدًا، كيف ينبغي علينا تقييم الاتجاه المستمر لتراجع الأداء بشكل علمي؟ هذه أسئلة مهمة يجب مراعاتها.
على الرغم من أن الفريق الرسمي يواصل تحسين الأمور، لا يمكن تجاهل التدهور التدريجي في الأداء. في بعض السيناريوهات، قد يبدو أن هناك تحسنًا، لكن هذا لا يعني أن الأداء في جميع السيناريوهات قد تم تحسينه بشكل متساوٍ. علاوة على ذلك، من السهل أيضًا تحسين الأداء لسيناريوهات محددة على حساب تدهور الأداء في مجالات أخرى.
الأسباب الجذرية لتراجع أداء MySQL
عمومًا، مع إضافة المزيد من الميزات، ينمو قاعدة الشيفرة، ومع التوسع المستمر في الوظائف، يصبح من الصعب بشكل متزايد السيطرة على الأداء.
يفشل مطورو MySQL في كثير من الأحيان في ملاحظة تراجع الأداء، حيث يؤدي كل إضافة جديدة إلى قاعدة الشفرة إلى انخفاض بسيط جدًا في الأداء. ومع ذلك، مع مرور الوقت، تتراكم هذه الانخفاضات البسيطة، مما يؤدي إلى تأثير تراكمي كبير، والذي يجعل المستخدمين يلاحظون تدهورًا ملحوظًا في الأداء في الإصدارات الأحدث من MySQL.
على سبيل المثال، تُظهر الشكل التالي أداء عملية الوصلة الفردية البسيطة، حيث يظهر انخفاض في الأداء في MySQL 8.0.40 مقارنة بـ MySQL 8.0.27:
ويُظهر الشكل التالي اختبار أداء إدخال الدفعات تحت التوازن الفردي، مع انخفاض في الأداء في MySQL 8.0.40 مقارنة بالإصدار 5.7.44:
من الرسمين أعلاه، يُمكن رؤية أن أداء الإصدار 8.0.40 ليس جيدًا.
الآن، دعونا نحلل سبب تدهور الأداء في MySQL من مستوى الشفرة. أدناه وظيفة PT_insert_values_list::contextualize
في MySQL 8.0:
وتكون الوظيفة المقابلة PT_insert_values_list::contextualize
في MySQL 5.7 كما يلي:
من خلال مقارنة الشفرات، يبدو أن MySQL 8.0 لديها شفرة أكثر أناقة، ويبدو أنها تحقق تقدمًا.
للأسف، في كثير من الأحيان، يكون من دوافع هذه التحسينات في الشفرة هي التي تؤدي بالضبط إلى تدهور الأداء. فريق MySQL الرسمي قام بتبديل الهيكل البياني السابق List
بـ deque
، والذي أصبح واحدًا من جذور تدهور الأداء التدريجي. دعونا نلقي نظرة على وثائق deque
:
std::deque (double-ended queue) is an indexed sequence container that allows fast insertion and deletion at both its
beginning and its end. In addition, insertion and deletion at either end of a deque never invalidates pointers or
references to the rest of the elements.
As opposed to std::vector, the elements of a deque are not stored contiguously: typical implementations use a sequence
of individually allocated fixed-size arrays, with additional bookkeeping, which means indexed access to deque must
perform two pointer dereferences, compared to vector's indexed access which performs only one.
The storage of a deque is automatically expanded and contracted as needed. Expansion of a deque is cheaper than the
expansion of a std::vector because it does not involve copying of the existing elements to a new memory location. On
the other hand, deques typically have large minimal memory cost; a deque holding just one element has to allocate its
full internal array (e.g. 8 times the object size on 64-bit libstdc++; 16 times the object size or 4096 bytes,
whichever is larger, on 64-bit libc++).
The complexity (efficiency) of common operations on deques is as follows:
Random access - constant O(1).
Insertion or removal of elements at the end or beginning - constant O(1).
Insertion or removal of elements - linear O(n).
كما هو موضح في الوصف أعلاه، في حالات متطرفة، يتطلب الاحتفاظ بعنصر واحد تخصيص الصف الكامل، مما يؤدي إلى كفاءة ذاكرة منخفضة جدًا. على سبيل المثال، في إدراجات الجملة، حيث يحتاج إلى إدراج عدد كبير من السجلات، يقوم التنفيذ الرسمي بتخزين كل سجل في صف ديك مختلف. حتى إذا كان محتوى السجل بحد أدنى، يجب لا يزال تخصيص ديك. تقوم تنفيذية MySQL ديك بتخصيص 1KB من الذاكرة لكل ديك لدعم البحث السريع.
The implementation is the same as classic std::deque: Elements are held in blocks of about 1 kB each.
تستخدم التنفيذة الرسمية 1KB من الذاكرة لتخزين معلومات الفهرس، وحتى إذا لم يكن طول السجل كبيرًا ولكن هناك العديد من السجلات، فإن عناوين وصول الذاكرة قد تصبح غير متتالية، مما يؤدي إلى عدم ودية الكاش. كان هذا التصميم مقصودًا لتحسين ودية الكاش، ولكنه لم يكن فعالًا بشكل كامل.
من الجدير بالذكر أن التنفيذ الأصلي كان يستخدم بنية بيانات List، حيث كانت تتم تخصيص الذاكرة من خلال حوض ذاكرة، مما يوفر مستوى معين من ودية الكاش. على الرغم من أن الوصول العشوائي أقل كفاءة، إلا أن الأمر يتحسن بشكل كبير للوصول التسلسلي إلى عناصر List مما يحسن أداء النظام بشكل كبير.
خلال الترقية إلى MySQL 8.0، لاحظ المستخدمون انخفاضًا كبيرًا في أداء إدراج الدفعات، وكان أحد الأسباب الرئيسية هو التغيير الكبير في البنية الداخلية للبيانات.
بالإضافة إلى ذلك، بينما قام الفريق الرسمي بتحسين آلية سجل إعادة القيد، فقد أدى ذلك أيضًا إلى انخفاض كفاءة عملية تأكيد MTR. مقارنة ب MySQL 5.7، يقلل الكود الإضافي بشكل كبير من أداء العمليات الفردية، على الرغم من تحسين إجمالي معدل كتابة البيانات بشكل كبير.
لنفحص العملية الأساسية execute
لعملية تأكيد MTR في MySQL 5.7.44:
لنفحص عملية execute
الأساسية للتأكد من التعهد MTR في MySQL 8.0.40:
من المقارنة، يتضح أن في MySQL 8.0.40، أصبحت عملية التنفيذ في التأكد MTR أكثر تعقيدًا بكثير، مع مزيد من الخطوات المتضمنة. هذا التعقيد هو أحد الأسباب الرئيسية لانخفاض أداء الكتابة عند القليل من التنافر.
بشكل خاص، العمليات m_impl->m_log.for_each_block(write_log)
و log_wait_for_space_in_log_recent_closed(*log_sys, handle.start_lsn)
تحملان عبء كبير. تمت إجراء هذه التغييرات لتعزيز أداء التنافر العالي، ولكن جاءت على حساب أداء التنافر القليل.
أولوية سجل الإعادة (redo log) لوضعية التنافر العالي تؤدي إلى أداء ضعيف لأعباء العمل التنافر القليل. على الرغم من أن إدخال innodb_log_writer_threads
كان يهدف إلى التخفيف من مشاكل أداء التنافر القليل، إلا أنه لا يؤثر على تنفيذ الوظائف المذكورة أعلاه. نظرًا لأن هذه العمليات أصبحت أكثر تعقيدًا وتتطلب التأكد المتكرر من MTR، فإن الأداء قد انخفض بشكل كبير.
دعونا نلقي نظرة على تأثير ميزة الإضافة/الإسقاط الفوري على الأداء. فيما يلي وظيفة rec_init_offsets_comp_ordinary
في MySQL 5.7:
وظيفة rec_init_offsets_comp_ordinary
في MySQL 8.0.40 كما يلي:
من الشيفرة أعلاه، يتضح أن مع إدخال ميزة العمود الجديدة الفورية / الإسقاط، أصبحت وظيفة rec_init_offsets_comp_ordinary
أكثر تعقيدًا بشكل ملحوظ، مما يقدم المزيد من استدعاءات الوظائف وإضافة بيان switch الذي يؤثر بشكل كبير على تحسين ذاكرة الكاش. نظرًا لتكرار استدعاء هذه الوظيفة، فإن ذلك يؤثر مباشرة على أداء تحديث الفهرس، وإدراج الدفعات، والانضمام، مما يؤدي إلى تأثير كبير على الأداء.
علاوة على ذلك، فان الانخفاض في الأداء في MySQL 8.0 ليس مقتصرًا على ما سبق؛ بل هناك العديد من المجالات الأخرى التي تسهم في تدهور الأداء الشامل، خاصة التأثير على توسيع الوظائف الداخلية. على سبيل المثال، يؤثر الشيفرة التالية على توسيع الوظائف الداخلية:
ووفقًا لاختباراتنا، فإن بيان ib::fatal
يتداخل بشكل كبير مع تحسين الوظائف الداخلية. بالنسبة للوظائف التي يتم الوصول إليها بشكل متكرر، من النصيحة تجنب البيانات التي تتداخل مع تحسين الوظائف الداخلية.
الآن، دعونا نلقي نظرة على قضية مماثلة. تُستدعى وظيفة row_sel_store_mysql_field
بشكل متكرر، حيث تعد row_sel_field_store_in_mysql_format
وظيفة ساخنة ضمنها. الشيفرة المحددة هي كما يلي:
تستدعي وظيفة row_sel_field_store_in_mysql_format
في النهاية row_sel_field_store_in_mysql_format_func
.
لا يمكن تضمين وظيفة row_sel_field_store_in_mysql_format_func
بسبب وجود الشيفرة ib::fatal
.
الوظائف غير الفعالة التي تُستدعى بشكل متكرر، وتنفذ عشرات الملايين من المرات في الثانية، يمكن أن تؤثر بشكل كبير على أداء الانضمام.
لنواصل استكشاف أسباب انخفاض الأداء. في الواقع، الأداء الرسمي الأمثل هو واحد من جذور انخفاض أداء الانضمام. على الرغم من أنه قد يتم تحسين بعض الاستعلامات، إلا أنها لا تزال بعض الأسباب لتدهور الأداء لعمليات الانضمام العادية.
تتجاوز مشاكل MySQL هذه. كما هو موضح في التحاليل أعلاه، انخفاض الأداء في MySQL ليس بدون سبب. سلسلة من المشاكل الصغيرة، عند تراكمها، يمكن أن تؤدي إلى تدهور الأداء الملحوظ الذي يواجهه المستخدمون. ومع ذلك، غالبًا ما تكون هذه المشاكل صعبة التحديد، مما يجعل من الصعب حتى حلها.
ما يسمى ‘التحسين المبكر’ هو جذر كل شر، وهذا لا ينطبق في تطوير MySQL. تطوير قواعد البيانات هو عملية معقدة، وتجاهل الأداء مع مرور الوقت يجعل تحسينات الأداء اللاحقة أكثر تحديًا بشكل كبير.
حلول للتخفيف من انخفاض أداء MySQL
الأسباب الرئيسية للانخفاض في أداء الكتابة تتعلق بمشاكل التأكيد MTR، إضافة/إسقاط الأعمدة فورًا، وعوامل أخرى عدة. هذه من الصعب تحسينها بالطرق التقليدية. ومع ذلك، يمكن للمستخدمين تعويض انخفاض الأداء من خلال تحسين PGO. باستراتيجية مناسبة، يمكن الحفاظ على الأداء بشكل عام.
بالنسبة لتدهور أداء إدراج الدُفعة، تقوم النسخة مفتوحة المصدر لدينا [2] بتبديل الdeque الرسمي بتنفيذ قائمة محسن. يتناول هذا في المقام الأول قضايا كفاءة الذاكرة ويمكن أن يخفف جزئيًا من تدهور الأداء. من خلال دمج تحسين PGO مع النسخة مفتوحة المصدر لدينا، يمكن لأداء إدراج الدُفعة أن يقترب من ذلك في MySQL 5.7.
يمكن للمستخدمين أيضًا الاستفادة من عدة خيوط لمعالجة الدُفعات المتزامنة، مستفيدين تمامًا من تحسين التزامن المحسن لسجل الإعادة، مما يمكن أن يعزز بشكل كبير أداء إدراج الدُفعات.
بالنسبة لقضايا تحديث الفهرس، نظرًا لإضافة الكود الجديد اللازمة، يمكن لتحسين PGO المساعدة في التخفيف من هذه المشكلة. يمكن لنسختنا PGO [2] التخفيف بشكل كبير من هذه المشكلة.
بالنسبة لأداء القراءة، وبخاصة أداء الانضمام، قمنا بتحسينات كبيرة، بما في ذلك إصلاح مشكلات الinline وإجراء تحسينات أخرى. مع إضافة PGO، يمكن زيادة أداء الانضمام بأكثر من 30٪ مقارنة بالنسخة الرسمية.
سنستمر في استثمار الوقت في تحسين أداء القليل من التزامن. هذه العملية طويلة ولكنها تشمل العديد من المجالات التي تحتاج إلى تحسين.
النسخة مفتوحة المصدر متاحة للفحص، وسيستمر الجهد في تحسين أداء MySQL..
المراجع
[1] بن وانغ (2024). فن حل المشكلات في هندسة البرمجيات: كيفية جعل MySQL أفضل.
Source:
https://dzone.com/articles/mysql-80-performance-degradation-analysis