LuaVela: تطبيق Lua 5.1 استنادًا إلى LuaJIT 2.0

منذ بعض الوقت ، أعلنا إصدارًا عامًا وفتحنا بموجب ترخيص MIT الكود المصدري لـ LuaVela - تطبيق Lua 5.1 ، استنادًا إلى LuaJIT 2.0. بدأنا العمل عليه في عام 2015 ، وبحلول بداية عام 2017 تم استخدامه في أكثر من 95 ٪ من مشاريع الشركة. الآن أريد أن ننظر إلى الوراء على الطريق سافر. ما الظروف التي دفعتنا إلى تطوير تطبيقنا الخاص بلغة البرمجة؟ ما هي المشاكل التي واجهناها وكيف حلها؟ كيف يختلف LuaVela عن شوكات LuaJIT الأخرى؟

قبل التاريخ


يعتمد هذا القسم على تقريرنا على HighLoad ++. بدأنا في استخدام Lua بنشاط لكتابة منطق الأعمال لمنتجاتنا في عام 2008. في البداية كان الفانيليا لوا ، ومنذ عام 2009 - LuaJIT. وضع بروتوكول RTB إطارًا محكمًا لمعالجة الطلب ، وبالتالي فإن الانتقال إلى التنفيذ السريع للغة كان حلًا منطقيًا وضروريًا من نقطة ما.

مع مرور الوقت ، أدركنا أن هناك بعض القيود على بنية LuaJIT. الشيء الأكثر أهمية بالنسبة لنا هو أن LuaJIT 2.0 يستخدم مؤشرات بدقة 32 بت. أدى هذا بنا إلى موقف حيث يعمل على Linux 64 بت قصر حجم مساحة العنوان الافتراضية لذاكرة العملية على غيغا بايت واحد (في الإصدارات الأحدث من kernel Linux ، تم رفع هذا الحد إلى غيغابايت 2):

/* MAP_32BIT    4  x86-64: */ void *ptr = mmap((void *)MMAP_REGION_START, size, MMAP_PROT, MAP_32BIT | MMAP_FLAGS, -1, 0); 

أصبح هذا القيد مشكلة كبيرة - بحلول عام 2015 ، توقف استخدام 1-2 غيغابايت من الذاكرة لتكون كافية للعديد من المشاريع لتحميل البيانات التي عمل عليها المنطق. تجدر الإشارة إلى أن كل مثيل لجهاز Lua الظاهري مرتبط بشكل فردي ولا يعرف كيفية مشاركة البيانات مع مثيلات أخرى - وهذا يعني أنه في الممارسة العملية ، يمكن لكل جهاز ظاهري أن يطالب بحجم ذاكرة لا يتجاوز 2 جيجابايت / ن ، حيث n هو عدد تدفقات العمل لخادمنا التطبيقات.

لقد مررنا بالعديد من الحلول للمشكلة: قمنا بتقليل عدد مؤشرات الترابط في خادم التطبيقات الخاص بنا ، وحاولنا تنظيم الوصول إلى البيانات من خلال LuaJIT FFI ، واختبرنا الانتقال إلى LuaJIT 2.1. لسوء الحظ ، كانت كل هذه الخيارات إما غير مواتية اقتصاديًا أو لم تكن جيدة على المدى الطويل. كان الشيء الوحيد المتبقي بالنسبة لنا هو اغتنام الفرصة وشوكة LuaJIT. في هذه اللحظة ، اتخذنا قرارات حددت إلى حد كبير مصير المشروع.

أولاً ، قررنا على الفور عدم إجراء تغييرات على بناء الجملة ودلالات اللغة ، مع التركيز على إزالة القيود المعمارية لـ LuaJIT ، والتي تحولت إلى مشكلة بالنسبة للشركة. بالطبع ، مع تطور المشروع ، بدأنا في إضافة ملحقات (سنناقش هذا أدناه) - لكننا عزلنا جميع واجهات برمجة التطبيقات الجديدة من مكتبة اللغات القياسية.

بالإضافة إلى ذلك ، لقد تخلينا عن النظام الأساسي عبر دعم نظام Linux x86-64 فقط ، منصة الإنتاج الوحيدة لدينا. لسوء الحظ ، لم يكن لدينا ما يكفي من الموارد لاختبار كمية التغييرات العملاقة التي كنا بصدد إجراؤها على المنصة.

نظرة سريعة تحت غطاء المنصة


لنرى من أين يأتي تقييد حجم المؤشرات. بادئ ذي بدء ، نوع الرقم في Lua 5.1 هو (مع بعض التحذيرات الطفيفة) نوع C مزدوج ، والذي بدوره يتوافق مع نوع الدقة المزدوجة المحددة في معيار IEEE 754. في تشفير هذا النوع 64 بت ، يتم تسليط الضوء على مجموعة من القيم للعرض نان. على وجه الخصوص ، كيف أي قيمة في النطاق [0xFFF8000000000000 ؛ 0xFFFFFFFFFFFFFFFF].

وبالتالي ، يمكننا أن نضع في قيمة 64 بت واحدة إما رقم مزدوج حقيقي "حقيقي" ، أو كيان ما ، والذي من وجهة نظر النوع المزدوج سيتم تفسيره على أنه NaN ، ومن وجهة نظر نظامنا الأساسي ، سيكون شيء أكثر فائدة - على سبيل المثال ، حسب نوع الكائن (ارتفاع 32 بت) ومؤشر لمحتوياته (32 بت منخفض):

 union TValue { double n; struct object { void *payload; uint32_t type; } o; }; 

تسمى هذه التقنية أحيانًا وضع علامات NaN (أو ملاكمة NaN) ، ويصف TValue أساسًا كيف تمثل LuaJIT القيم المتغيرة في Lua. يحتوي TValue أيضًا على أقنوم ثالث يستخدم لتخزين مؤشر لوظيفة ومعلومات لإعادة لف رصة Lua ، أي في التحليل النهائي ، تبدو بنية البيانات كما يلي:

 union TValue { double n; struct object { void *payload; uint32_t type; } o; struct frame { void *func; uintptr_t link; } f; }; 

حقل frame.link في التعريف أعلاه هو من النوع uintptr_t ، لأنه في بعض الحالات يخزن مؤشرًا ، وفي حالات أخرى يكون عددًا صحيحًا. والنتيجة هي تمثيل مضغوط للغاية لمكدس الجهاز الظاهري - في الحقيقة ، إنه عبارة عن صفيف TValue ، ويتم تفسير كل عنصر من عناصر المصفوفة على أنه إما رقم ، ثم كمؤشر مكتوب على كائن ، أو كبيانات حول إطار مكدس Lua.

لنلقِ نظرة على مثال. تخيل أننا بدأنا برمز LuaJIT هذا LuaJit وقمنا بتعيين نقطة توقف داخل وظيفة الطباعة:

 local function foo(x) print("Hello, " .. x) end local function bar(a, b) local n = math.pi foo(b) end bar({}, "Lua") 

ستظهر بنية Lua في هذه المرحلة على النحو التالي:

كومة لوا باستخدام LuaJIT 2.0

وسيكون كل شيء على ما يرام ، ولكن هذه التقنية تبدأ بالفشل بمجرد أن نحاول البدء في x86-64. في حالة تشغيلنا في وضع التوافق للتطبيقات ذات 32 بت ، فإننا نواجه قيود mmap التي سبق ذكرها أعلاه. ولن تعمل مؤشرات 64 بت خارج منطقة الجزاء على الإطلاق. ما يجب القيام به لإصلاح المشكلة ، كان علي:

  1. قم بتمديد TValue من 64 إلى 128 بت: بهذه الطريقة نحصل على فراغ "صادق" * على نظام أساسي 64 بت.
  2. تصحيح رمز الجهاز الظاهري وفقا لذلك.
  3. قم بإجراء تغييرات على برنامج التحويل البرمجي JIT.

لقد تبين أن إجمالي حجم التغييرات كان مهمًا للغاية وتم عزلنا تمامًا عن LuaJIT الأصلي. تجدر الإشارة إلى أن امتداد TValue ليس هو الطريقة الوحيدة لحل المشكلة. في LuaJIT 2.1 ، ذهبنا في الاتجاه الآخر من خلال تطبيق وضع LJ_GC64. بيتر كولي ، الذي قدم مساهمة هائلة في تطوير هذا النمط من العمليات ، قرأ عنه في اجتماع في لندن. حسنًا ، في حالة LuaVela ، يبدو مكدس المثال نفسه كما يلي:

لوا المكدس عند استخدام LuaVela

النجاحات الأولى وتحقيق الاستقرار في المشروع


بعد أشهر من التطوير النشط ، حان الوقت لتجربة LuaVela في المعركة. كتجربة ، اخترنا المشاريع الأكثر إشكالية من حيث استهلاك الذاكرة: كمية البيانات التي كان عليها أن تعمل ، تجاوزت بوضوح 1 غيغا بايت ، لذلك اضطروا إلى استخدام الحلول المختلفة. كانت النتائج الأولى مشجعة: كانت LuaVela مستقرة وأظهرت أداء أفضل مقارنة بتكوين LuaJIT المستخدم في هذه المشاريع نفسها.

في الوقت نفسه ، نشأت مسألة الاختبار. لحسن الحظ ، لم يكن علينا أن نبدأ من نقطة الصفر ، لأنه منذ اليوم الأول للتطوير ، بالإضافة إلى خوادم التدريج ، كان لدينا تحت تصرفنا:

  • الاختبارات الوظيفية والتكامل لخادم التطبيقات الذي ينفذ منطق الأعمال لجميع مشاريع الشركة.
  • اختبارات المشاريع الفردية.

كما أوضحت الممارسة ، كانت هذه الموارد كافية لتصحيح الأخطاء وإيصال المشروع إلى حالة مستقرة كحد أدنى (لقد قاموا بتجميع dev - تم طرحه على مراحل - إنه يعمل ولا يتعطل). من ناحية أخرى ، كان مثل هذا الاختبار من خلال مشاريع أخرى غير مناسب تمامًا على المدى الطويل: مشروع على مستوى من التعقيد مثل تنفيذ لغة برمجة لا يمكن أن يكون له اختباراته الخاصة. بالإضافة إلى ذلك ، أدى عدم وجود اختبارات مباشرة في المشروع إلى تعقيد عملية البحث عن الأخطاء وتصحيحها تقنيًا.

في عالم مثالي ، أردنا اختبار ليس فقط تنفيذنا ، ولكن أيضًا لدينا مجموعة من الاختبارات التي من شأنها أن تسمح لنا بالتحقق من صحتها في دلالات اللغة . لسوء الحظ ، انتظرنا بعض خيبة الأمل في هذا الأمر. على الرغم من حقيقة أن مجتمع Lua ينشئ عن طيب خاطر شوكًا للتطبيقات الحالية ، حتى وقت قريب ، كانت هناك مجموعة مماثلة من اختبارات التحقق من الصحة مفقودة. تغير الوضع للأفضل عندما أعلن فرانسوا براد في نهاية عام 2018 عن مشروع lua-Harness.

في النهاية ، قمنا بإغلاق مشكلة الاختبار من خلال دمج مجموعات الاختبار الأكثر اكتمالا وتمثيلا في النظام البيئي لوا في مستودعنا:

  • اختبارات مكتوبة من قبل المبدعين من اللغة لتنفيذ لوا 5.1.
  • الاختبارات التي يقدمها المجتمع من قبل مؤلف LuaJIT مايك بال.
  • لوا تسخير
  • مجموعة فرعية من اختبارات مشروع MAD الجاري تطويره بواسطة CERN.
  • مجموعتان من الاختبارات التي قمنا بإنشائها في IPONWEB ولا يزال يتم تجديدها حتى الآن: واحدة للاختبار الوظيفي للنظام الأساسي ، والآخر باستخدام إطار cmocka لاختبار C API وكل ما يفتقر إلى الاختبار على مستوى رمز Lua.

سمح لنا إدخال كل مجموعة من الاختبارات باكتشاف وتصحيح 2-3 أخطاء حرجة - لذلك من الواضح أن جهودنا قد آتت ثمارها. على الرغم من أن موضوع اختبار أوقات تشغيل البرامج ومجمعيها (سواء الثابت أو الديناميكي) هو بلا حدود حقًا ، إلا أننا نعتقد أننا وضعنا أساسًا قويًا للتطوير المستقر للمشروع. تحدثنا عن مشكلات اختبار تطبيقنا الخاص بـ Lua (بما في ذلك موضوعات مثل العمل مع مقاعد الاختبار وتصحيح postmortem ) مرتين ، في Lua في موسكو 2017 وفي HighLoad ++ 2018 ، كل من يهتم بالتفاصيل مرحب به لمشاهدة فيديو لهذه التقارير. حسنًا ، انظر إلى دليل الاختبارات في مستودعنا ، بالطبع.

ميزات جديدة


وبالتالي ، كان لدينا تحت تصرفنا تطبيق مستقر لـ Lua 5.1 لنظام Linux x 86-64 ، تم تطويره بواسطة قوى فريق صغير ، والذي "أتقن" تدريجياً تراث LuaJIT وخبراته المتراكمة. في مثل هذه الظروف ، أصبحت الرغبة في توسيع النظام الأساسي وإضافة ميزات ليست موجودة في Vanilla Lua ولا في LuaJIT ، والتي من شأنها أن تساعدنا في حل المشكلات الملحة الأخرى ، أمرًا طبيعيًا تمامًا.

يتم توفير وصف مفصل لجميع الملحقات في الوثائق بتنسيق RST (استخدم cmake. && إنشاء مستندات لإنشاء نسخة محلية بتنسيق HTML). يمكن العثور على وصف كامل لملحقات Lua API على هذا الرابط ، و C API في هذا الرابط . لسوء الحظ ، من المستحيل التحدث في مقال المراجعة عن كل شيء ، لذلك توجد قائمة بالوظائف الأكثر أهمية:

  • DataState - القدرة على تنظيم الوصول المشترك إلى كائن من عدة مثيلات مستقلة للأجهزة الظاهرية Lua.
  • القدرة على تحديد مهلة ل coroutine ومقاطعة تنفيذ تلك التي تعمل لفترة أطول من ذلك.
  • مجموعة من تحسينات برنامج التحويل البرمجي JIT المصممة لمكافحة الزيادة الأسية في عدد الآثار عند نسخ البيانات بين الكائنات - تحدثنا عن ذلك في HighLoad ++ 2017 ، لكن قبل بضعة أشهر فقط كانت لدينا أفكار عمل جديدة لم يتم توثيقها بعد.
  • مجموعة أدوات جديدة: أخذ العينات Profiler. dumpanalyze مترجم إخراج التصحيح محلل ، الخ

تستحق كل من هذه الميزات مقالة منفصلة - اكتب التعليقات التي ترغب في قراءة المزيد عنها.

أريد هنا أن أتحدث أكثر قليلاً عن كيفية خفض الحمل على أداة تجميع النفايات. يسمح لك الختم بجعل كائن غير قابل للوصول إلى أداة تجميع مجمعي البيانات المهملة. في مشروعنا النموذجي ، فإن معظم البيانات (ما يصل إلى 80 ٪) داخل الجهاز الظاهري لوا هي بالفعل قواعد العمل ، والتي هي جدول لوا معقدة. عمر هذا الجدول (بالدقائق) أطول بكثير من عمر الطلبات التي تمت معالجتها (عشرات الميلي ثانية) ، ولا تتغير البيانات الموجودة فيه أثناء معالجة الاستعلام. في مثل هذه الحالة ، لا معنى لإجبار أداة تجميع مجمعي البيانات المهملة على التنقل حول بنية البيانات الضخمة هذه مرارًا وتكرارًا. للقيام بذلك ، نقوم بشكل متكرر "بختم" الكائن وإعادة ترتيب البيانات بطريقة لا يصل بها جامع البيانات المهملة أبدًا إلى الكائن "المختوم" أو محتوياته. في vanilla Lua 5.4 ، سيتم حل هذه المشكلة عن طريق دعم أجيال من الكائنات في مجموعة البيانات المهملة للأجيال.

من المهم أن تضع في اعتبارك أن الأشياء "المختومة" يجب ألا تكون قابلة للكتابة. يؤدي عدم مراعاة هذا الثابت إلى ظهور مؤشرات متدلية: على سبيل المثال ، يشير الكائن "المختوم" إلى كائن عادي ، ومجمع القمامة ، الذي يتخطى كائن "مختوم" أثناء عبور كومة ، يتخطى كائنًا منتظمًا - مع اختلاف أنه لا يمكن تحرير كائن "مختوم" ، ويمكن للشخص المعتاد. بعد تنفيذ الدعم لهذا الثابت ، نحصل أساسًا على دعم مجاني للمناعة للكائنات ، وغالبًا ما يندب غيابها في لوا. أؤكد أن الأجسام الثابتة و "المختومة" ليست هي نفس الشيء. الخاصية الثانية تعني الأولى ، ولكن ليس العكس.

وألاحظ أيضًا أنه في Lua 5.1 يمكن تنفيذ الحصانة باستخدام metatables - الحل يعمل جيدًا ، ولكن ليس الأكثر ربحية من حيث الأداء. يمكن العثور على مزيد من المعلومات حول "الختم" ، والحصانة ، وكيف نستخدمها في الحياة اليومية في هذا التقرير.

النتائج


في الوقت الحالي ، نحن راضون عن الاستقرار ومجموعة الفرص المتاحة لتنفيذنا. وعلى الرغم من القيود الأولية ، فإن تطبيقنا أدنى من Vanilla Lua و LuaJIT بدرجة كبيرة من حيث قابلية النقل ، فهو يحل العديد من مشكلاتنا - نأمل أن تكون هذه الحلول مفيدة لشخص آخر.

بالإضافة إلى ذلك ، حتى لو لم تكن LuaVela مناسبة للإنتاج ، فنحن ندعوك لاستخدامها كنقطة بداية لفهم كيفية عمل LuaJIT أو شوكةها. بالإضافة إلى حل المشكلات وتوسيع الوظائف ، قمنا على مدار الأعوام بإعادة تشكيل جزء مهم من قاعدة الشفرة وكتبنا مقالات تدريبية حول الهيكل الداخلي للمشروع - كثير منها لا ينطبق على LuaVela فقط ، ولكن أيضًا على LuaJIT.

شكرا لاهتمامكم ، نحن في انتظار طلبات السحب!

Source: https://habr.com/ru/post/ar465441/


All Articles