تحليل ثابت للكميات الكبيرة من كود Python: تجربة Instagram. الجزء 1

تتم كتابة رمز خادم Instagram حصريًا في بيثون. حسنا ، هذا هو الأساس. نستخدم Cython قليلاً ، وتشمل التبعيات الكثير من C ++ - رمز يمكن تشغيله من Python كما هو الحال مع C-extensions.



تطبيق الخادم الخاص بنا هو مجموعة متجانسة ، وهي عبارة عن قاعدة رمز كبيرة تتكون من عدة ملايين من الخطوط وتشمل عدة آلاف من نقاط النهاية لـ Django ( هنا نتحدث عن استخدام Django على Instagram). كل هذا يتم تحميله ويعمل ككيان واحد. تم تخصيص العديد من الخدمات من المتراصة ، لكن خطتنا لا تتضمن فصلًا قويًا عن المتراصة.

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

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

تركز هذه المادة على الطريقة التي نستخدم بها الوبر وإعادة المعالجة التلقائية لتسهيل إدارة قاعدة بيانات Python.

إذا كنت مهتمًا بتجربة بعض الأفكار المذكورة في هذه المقالة ، فعليك أن تعلم أننا انتقلنا مؤخرًا إلى فئة المشاريع مفتوحة المصدر LibCST ، والتي تكمن وراء العديد من أدواتنا الداخلية للبطانة وإعادة التجهيز التلقائي للرموز.

الجزء الثاني

الوبر: الوثائق التي تظهر عند الحاجة


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


أصناف من الوبر

تعد Linting مجرد واحد من أنواع تحليل الشفرة الثابتة العديدة التي نستخدمها على Instagram.

الطريقة الأكثر بدائية لتطبيق قواعد الفحص هي استخدام التعبيرات العادية. التعبيرات العادية سهلة الكتابة ، لكن بيثون ليست لغة "عادية" . نتيجة لذلك ، من الصعب للغاية (وأحيانًا يكون من المستحيل) البحث بشكل موثوق عن الأنماط الموجودة في شفرة Python باستخدام التعبيرات المعتادة.

إذا تحدثنا عن أكثر الطرق تعقيدًا وتقدماً في تطبيق linter ، فهناك أدوات مثل mypy و Pyre . هذان نظامان للتحقق بشكل ثابت من أنواع أكواد بيثون التي يمكنها إجراء تحليل عميق للبرنامج. يستخدم Instagram بير. هذه أدوات قوية ، لكن من الصعب توسيعها وتخصيصها.

عندما نتحدث عن الفحص على Instagram ، فإننا نعني عادةً العمل باستخدام قواعد بسيطة تستند إلى شجرة بناء جملة مجردة. إنه شيء مثل هذا بالضبط الذي يقوم عليه قواعد الفحص الخاصة برمز الخادم.

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


شجرة بيثون تحليل (صيغة مختلفة من CST) التي تم إنشاؤها بواسطة lib2to3

لسوء الحظ ، يؤدي هذا النهج إلى إنشاء شجرة معقدة ، مما يجعل من الصعب استخراج المعلومات الدلالية التي تهمنا منها.

يجمع Python شجرة التحليل في شجرة بناء جملة مجردة (AST). يتم فقد بعض المعلومات حول الكود المصدري أثناء هذا التحويل. نحن نتحدث عن "المعلومات النحوية الإضافية" - مثل التعليقات ، الأقواس ، الفواصل. ومع ذلك ، يتم الاحتفاظ دلالات رمز في AST.


بيثون شجرة بناء الجملة التي تم إنشاؤها بواسطة وحدة ast

قمنا بتطوير LibCST - مكتبة توفر لنا أفضل ما في العالم من CST و AST. إنه يقدم تمثيلًا للرمز الذي يتم فيه تخزين جميع المعلومات المتعلقة به (كما هو الحال في CST) ، ولكن من السهل استخراج المعلومات الدلالية حوله من مثل هذا التمثيل للرمز (كما هو الحال عند العمل مع AST).


تمثيل شجرة بناء جملة LibCST محددة

تستخدم قواعد الفحص الخاصة بنا شجرة بناء جملة LibCST للعثور على أنماط في الكود. من السهل استكشاف شجرة بناء الجملة هذه ، على مستوى عالٍ ، فهي تسمح لك بالتخلص من المشكلات التي تصاحب العمل باللغة "غير النظامية".

افترض أنه في وحدة نمطية معينة ، هناك تبعية دورية بسبب نوع الاستيراد. بايثون يحل هذه المشكلة عن طريق وضع أوامر استيراد الكتابة في كتلة if TYPE_CHECKING . هذه هي الحماية ضد استيراد أي شيء في وقت التشغيل.

 #    from typing import TYPE_CHECKING from util import helper_fn #    if TYPE_CHECKING:    from circular_dependency import CircularType 

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

 #    from typing import TYPE_CHECKING from util import helper_fn #    if TYPE_CHECKING:    from circular_dependency import CircularType if TYPE_CHECKING: #   !    from other_package import OtherType 

يمكنك التخلص من هذا التكرار باستخدام قاعدة linter!

لنبدأ من خلال تهيئة عداد الكتل "الواقية" الموجودة في الكود.

 class OnlyOneTypeCheckingIfBlockLintRule(CstLintRule):    def __init__(self, context: Context) -> None:        super().__init__(context)        self.__type_checking_blocks = 0 

بعد ذلك ، مع تلبية الشرط المقابل ، نقوم بزيادة العداد ، ونتأكد من أنه لن يكون هناك أكثر من كتلة واحدة في الكود. إذا لم يتم استيفاء هذا الشرط ، فإننا نصدر تحذيرًا في المكان المناسب في الكود ، ندعو الآلية المساعدة المستخدمة لإنشاء مثل هذه التحذيرات.

 def visit_If(self, node: cst.If) -> None:    if node.test.value == "TYPE_CHECKING":        self.__type_checking_blocks += 1        if self.__type_checking_blocks > 1:            self.context.report(                node,                "More than one 'if TYPE_CHECKING' section!"            ) 

تعمل قواعد الفحص المماثلة من خلال النظر إلى شجرة LibCST وجمع المعلومات. في linter لدينا ، يتم تنفيذ هذا باستخدام نمط الزائر. كما لاحظت ، تتجاوز القواعد طرق visit وتترك الأساليب المرتبطة بنوع العقدة. تسمى هذه "الزوار" بترتيب معين.

 class MyNewLintRule(CstLintRule):    def visit_Assign(self, node):        ... #      def visit_Name(self, node):        ... #        def leave_Assign(self, name):        ... #      


تسمى طرق الزيارة قبل زيارة أحفاد العقد. تسمى أساليب الإجازة بعد زيارة جميع الأحفاد

نلتزم بمبادئ العمل ، والتي بموجبها يتم حل المهام البسيطة أولاً. تم تنفيذ أول قاعدة linter الخاصة بنا في ملف واحد ، وتضمنت "زائرًا" واستخدمت حالة مشتركة.


ملف واحد ، "زائر" واحد ، باستخدام حالة مشتركة

يجب أن تحتوي فئة Single Visitor على معلومات حول حالة ومنطق جميع قواعد الفحص التي لا علاقة لها بها. علاوة على ذلك ، ليس من الواضح دائمًا الحالة التي تتوافق مع قاعدة معينة. يُظهر هذا النهج نفسه جيدًا في الموقف الذي يوجد فيه عدد قليل من قواعد الفرش الخاصة بك ، ولكن لدينا حوالي مائة من هذه القواعد ، مما يعقد إلى حد كبير دعم نمط single-visitor .


من الصعب معرفة الحالة والمنطق المقترنين بكل من عمليات الفحص.

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


يمكن لكل قاعدة linter اجتياز شجرة مرارا وتكرارا. عند معالجة ملف ، يتم تنفيذ القواعد بالتتابع. ومع ذلك ، فإن هذا النهج ، الذي يعبر الشجرة في كثير من الأحيان ، من شأنه أن يؤدي إلى انخفاض خطير في الأداء.

بدلاً من إدراك شيء كهذا ، استلهمنا من اللينتات المستخدمة في النظم الإيكولوجية للغات البرمجة الأخرى - مثل ESLint من JavaScript ، وقمنا بإنشاء سجل مركزي "للزائرين" (سجل الزوار).


سجل مركزي من "الزوار". يمكننا تحديد العقدة التي تهتم بشكل فعال بكل قاعدة من قواعد linter ، مما يوفر الوقت على العقد غير المهتمة به.

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

هذا يقلل من استهلاك النظام من موارد الحوسبة عند إضافة قواعد جديدة لينت. نحن نتحقق عادة مع عدد قليل من الملفات التي تم تعديلها مؤخرًا. ولكن يمكننا التحقق من جميع القواعد الموجودة على قاعدة كود خادم Instagram بالكامل بالتوازي في 26 ثانية فقط.

بعد حل المشكلات المتعلقة بالأداء ، أنشأنا إطار عمل للاختبار كان يهدف إلى الالتزام بتقنيات البرمجة المتقدمة ، والتي تتطلب اختبارات في المواقف التي يجب أن يكون فيها شيء ما ذا جودة وفي المواقف التي لا ينبغي أن يكون فيها شيء ما بجودة ينبغي.

 class MyCustomLintRuleTest(CstLintRuleTest):    RULE = MyCustomLintRule       VALID = [        Valid("good_function('this should not generate a report')"),        Valid("foo.bad_function('nor should this')"),    ]       INVALID = [        Invalid("bad_function('but this should')", "IG00"),    ] 

استمرار → الجزء الثاني

أعزائي القراء! هل تستخدم الشراب؟


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


All Articles