في البداية كان هناك كتلة متراصة: كيف نغير هندستنا دون التدخل في الأعمال





مرحبا! اسمي إيغور نارازين ، أنا قائد فريق فريق الخدمات اللوجستية لنادي Delivery Club. أريد أن أخبركم كيف نبني ونحول بنيتنا المعمارية وكيف تؤثر على عمليات التطوير لدينا.



ينمو Now Delivery Club (بالإضافة إلى سوق تكنولوجيا الطعام بالكامل) بسرعة كبيرة ، مما يخلق عددًا كبيرًا من التحديات للفريق الفني ، والتي يمكن تلخيصها من خلال اثنين من أهم المعايير:



  • من الضروري ضمان الاستقرار العالي وتوافر جميع أجزاء المنصة.
  • في الوقت نفسه ، حافظ على وتيرة عالية لتطوير ميزات جديدة.


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



لكننا نجحنا (حتى الآن) كليهما. كيف نفعل هذا سوف تناقش أكثر.



أولاً ، سأخبرك عن نظامنا الأساسي : كيف نقوم بتحويله مع مراعاة الكميات المتزايدة باستمرار من البيانات ، وما هي المعايير التي نطبقها على خدماتنا وما هي المشاكل التي نواجهها على طول الطريق



ثانيًا ، سأشارك كيف نحل مشكلة تقديم الميزات دون التعارض مع التغييرات في النظام الأساسي ودون أي تدهور غير ضروري للنظام.



لنبدأ بالمنصة.



في البداية كان هناك منليث



تمت كتابة الأسطر الأولى من كود Delivery Club منذ 11 عامًا ، وفي أفضل تقاليد هذا النوع ، كانت الهندسة المعمارية متراصة في PHP. لمدة 7 سنوات ، كانت مليئة بالمزيد والمزيد من الوظائف حتى واجهت المشاكل الكلاسيكية للهندسة المعمارية المتجانسة.



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



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


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



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



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



النظام البيئي



كما قال Andrey Evsyukov في مقال حول فرقنا ، فقد سلطنا الضوء على المجالات الرئيسية لمجالات المجال: البحث والتطوير ، والخدمات اللوجستية ، والمستهلك ، والبائع ، والداخلية ، والمنصة. ضمن هذه المناطق ، تتركز مجالات المجال الرئيسية التي تعمل بها الخدمات بالفعل: على سبيل المثال ، بالنسبة للخدمات اللوجستية ، هذه هي شركات النقل والطلبات ، وبالنسبة للبائعين - المطاعم والمواقف.



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



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



أحمال منخفضة ، طلبات متزامنة ، كل شيء يعمل بشكل رائع.



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





أحمال عالية ، طلبات متزامنة: يعاني الجميع ، حتى مستخدمو نطاقات مختلفة تمامًا - سعاة.



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



مركبة البيانات



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



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



  • mobile-gatewayوهي الواجهة الخلفية للواجهة الأمامية لتطبيق الهاتف المحمول ؛
  • courier-tracker، الذي يخزن منطق استلام الإحداثيات وإرسالها ؛
  • logistics-couriersالذي يخزن هذه الإحداثيات. يتم إرسالها من تطبيقات الهاتف المحمول.






في المخطط الأصلي ، كان كل هذا يعمل بشكل متزامن: تم إرسال الطلبات من تطبيق الهاتف المحمول مرة واحدة في الدقيقة mobile-gatewayإلى الخدمة courier-trackerالتي وصلت إلى logistics-couriersالإحداثيات واستلمتها. بالطبع ، لم يكن الأمر بهذه البساطة في هذا المخطط ، ولكن في النهاية اختصر الأمر كله في استنتاج بسيط: كلما زادت الطلبات النشطة لدينا ، زاد عدد الطلبات الواردة للإحداثيات logistics-couriers.



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



المواصلات



لقد استخدمنا بالفعل RabbitMQ ، بما في ذلك التواصل بين الخدمات. ولكن باعتبارها وسيلة النقل الرئيسية ، فقد استقرنا على الأداة التي أثبتت جدواها بالفعل - Apache Kafka. سنكتب مقالة مفصلة منفصلة حول هذا الموضوع ، لكنني الآن أود أن أتحدث بإيجاز عن كيفية استخدامنا له.



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



لكن هذا النهج له عيب كبير: الرسائل لا تحتوي على أي كتابة والتحقق من الصحة - نحن لا نعرف على وجه اليقين تنسيق الرسالة الذي نقرأه من الموضوع.



هذا يزيد من مخاطر الأخطاء وعدم الاتساق بين الخدمات التي توفر البيانات وتلك التي تستهلكها.



لحل هذه المشكلة ، كتبنا غلافًا - خدمة صغيرة في Go ، تخفي كافكا وراء واجهة برمجة التطبيقات الخاصة بها. أضاف هذا ميزتين:



  • التحقق من صحة البيانات في وقت الإرسال والاستلام. في الواقع ، هذه هي نفس DTOs ، لذلك نحن واثقون دائمًا من تنسيق البيانات المتوقعة.
  • تكامل سريع لخدماتنا مع هذا النقل.


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



دعنا نعود إلى المثال



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



يجب على الخدمة courier-trackerفقط تجميع الإحداثيات بالمبلغ المطلوب وللمدة المطلوبة. نتيجة لذلك ، تصبح نقطة النهاية لدينا بسيطة قدر الإمكان: خذ البيانات من قاعدة بيانات الخدمة وأعطها لتطبيق الهاتف المحمول. الزيادة في الحمل عليها أصبحت الآن آمنة بالنسبة لنا.







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



الاتساق في نهاية المطاف



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



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







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



عدم التطابق



مثال آخر: لدينا آلية للإشعارات الموجهة إلى شركات التوصيل - هذه هي الرسائل التي ستصل إليهم في التطبيق. على الجانب الخلفي ، هناك واجهة برمجة تطبيقات قوية لإرسال مثل هذه الإشعارات. في ذلك ، يمكنك تكوين عوامل تصفية البريد: من ساعي معين إلى مجموعات من السعاة وفقًا لمعايير معينة.



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



  • إنشاء نقطة نهاية على جانب الخدمة - معالج بيانات البريد السريع ( logistics-couriers) ، والذي سيكون قادرًا على تصفية وإرجاع الناقلات الضرورية بواسطة الحقول المرسلة ؛
  • تخزين جميع المعلومات الضرورية مباشرة في الخدمة ، واستهلاكها من الموضوع ذي الصلة وتخزين البيانات التي سنحتاج إلى تصفيتها في المستقبل.


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



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


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



نتيجة لذلك ، قمنا بصياغة عدة مبادئ مهمة لتصميم الخدمات:



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


وهكذا ، من خلال التحويل المتكرر لنظامنا وفقًا للمبادئ والأساليب المقبولة ، توصلنا إلى البنية التالية:







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



كيف نخطط لتطوير هندستنا



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



من الآن فصاعدًا ، سنستمر في تنفيذ الأساليب المطورة لجميع خدمات Delivery Club: لبناء أنظمة بيئية للخدمة حول منصة مع النقل في شكل ناقل بيانات.



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



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



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







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



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



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



كيفية القيام بإعادة البناء بشفافية للعملاء



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



  1. أولاً ، داخل الخدمة ، نذهب مباشرةً إلى نسخة طبق الأصل من قاعدة بيانات monolith الخاصة بنا ونحصل على البيانات من هناك.
  2. ثم نبدأ في دفق البيانات التي نحتاجها من خلال Debezium وتجميعها في قاعدة بيانات الخدمة نفسها.






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



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



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



اللجنة المعمارية



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



يؤدي هذا إلى ظهور مشاكل حقيقية (الخدمة لا تستطيع تحمل العبء بسبب التصميم غير المناسب) والمشاكل المفاهيمية ("دعنا نسير بشكل متزامن هنا ، الحمل صغير").



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



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



نتيجة لذلك ، أغلقنا مشكلة التحكم في التغييرات الكبيرة ، ويبقى السؤال عن النهج العام لجودة الكود في Delivery Club: يمكن حل مشكلات معينة في الكود أو إطار العمل في فرق مختلفة بطرق مختلفة. لقد توصلنا إلى نقابات على نموذج Spotify: هذه نقابات لأشخاص ليسوا غير مبالين ببعض التكنولوجيا. على سبيل المثال ، هناك نقابات Go و PHP و Frontend.



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



كود المنتج



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



تشير قائمة التحقق عادةً إلى:



  • مسؤول عن الخدمة (عادة ما يكون هو القائد الفني للخدمة) ؛
  • روابط إلى لوحة القيادة مع تنبيهات مخصصة ؛
  • وصف الخدمة ورابط إلى Swagger ؛
  • وصف للخدمات التي ستتفاعل معها ؛
  • الحمل المقدر على الخدمة ؛
  • health-check. URL, . Health-check - : 200, , - . , health check URL’ , , , PostgreSQL Redis.


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



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



للقياسات ، نستخدم Graylog و Prometheus ، ونبني لوحات المعلومات وننشئ التنبيهات في Grafana.



على الرغم من حجم التحضير ، فإن تسليم الخدمات للإنتاج سريع جدًا: يتم حزم جميع الخدمات مبدئيًا في Docker ، ويتم طرحها تلقائيًا بعد تشكيل مخطط Kubernetes المكتوب ، ثم يتم تحديد كل شيء بواسطة زر في Jenkins.



يتمثل إطلاق خدمة جديدة في إنتاجها في تعيين مهمة للمسؤولين في Jira ، والتي توفر جميع المعلومات التي أعددناها مسبقًا.



تحت الغطاء



لدينا الآن 162 خدمة مصغرة مكتوبة بلغة PHP و Go. تم توزيعها بين الخدمات حوالي 50٪ إلى 50٪. في البداية ، أعدنا كتابة بعض الخدمات عالية التحميل في Go. ثم أصبح من الواضح أن Go أسهل في الصيانة والمراقبة في الإنتاج ، وله حد دخول منخفض ، لذلك قمنا مؤخرًا بكتابة الخدمات فيه فقط. لا يوجد أي غرض لإعادة كتابة خدمات PHP المتبقية في Go: فهي تتواءم مع وظائفها بنجاح كبير.



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

$courierResponse = $this->courierProtocol->get($courierRequest);

نحن هنا نشكل DTO للطلب ( $courierRequest) ، نسمي طريقة كائن البروتوكول لخدمة معينة ، وهي عبارة عن غلاف فوق نقطة نهاية محددة. تحت الغطاء ، يتم $courierRequestتحويل كائننا إلى كائن طلب ، ممتلئ بحقول من DTO. كل هذا مرن: يمكن إدراج الحقول في كل من العناوين وفي عنوان URL للطلب نفسه. بعد ذلك ، يتم إرسال الطلب من خلال cURL ، نحصل على كائن الاستجابة ونحوله مرة أخرى إلى الكائن الذي نتوقعه $courierResponse.



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



لكن هذه العملية لها عيب كبير: من الصعب الحفاظ على المستودعات مع SDK ، لأن جميع DTOs مكتوبة يدويًا ، وإنشاء الكود المريح ليس بالأمر السهل: كانت هناك محاولات ، ولكن في النهاية ، نظرًا للانتقال إلى Go ، لم يستثمروا الوقت فيها.



نتيجة لذلك ، يمكن أن تتحول التغييرات في بروتوكول الخدمة إلى عدة طلبات سحب: إلى الخدمة نفسها ، إلى SDK ، وإلى خدمة تحتاج إلى هذا البروتوكول. في الأخير ، نحتاج إلى رفع إصدار SDK الذي تم استيراده حتى تصل التغييرات إلى هناك. غالبًا ما يثير هذا أسئلة من المطورين الجدد: "لقد قمت للتو بتغيير المعلمة ، لماذا أحتاج إلى تقديم ثلاثة طلبات إلى ثلاثة مستودعات مختلفة؟!"



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



رادار تقني







إلى جانب Go و PHP المذكورين بالفعل ، نستخدم عددًا كبيرًا من التقنيات الأخرى. وهي تختلف من اتجاه إلى آخر وتعتمد على مهام محددة. في الأساس ، في الخلفية نستخدم:



  • Python، والتي يكتب عنها فريق Data Science.
  • Kotlinو Swift- لتطوير تطبيقات الهاتف المحمول.
  • PostgreSQLكقاعدة بيانات ، لكن بعض الخدمات القديمة ما زالت تشغل MySQL. في الخدمات المصغرة ، نستخدم عدة طرق: كل خدمة لها قاعدة بياناتها الخاصة ولا تشارك شيئًا - نحن لا نذهب إلى قواعد البيانات التي تتجاوز الخدمات ، فقط من خلال واجهة برمجة التطبيقات الخاصة بهم.
  • ClickHouse - للخدمات عالية التخصص المتعلقة بالتحليلات.
  • Redisو Memcachedكما التخزين في الذاكرة.




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



قصة طويلة قصيرة



نتيجة لذلك ، انتقلنا من بنية متجانسة إلى خدمة مصغرة ، والآن لدينا بالفعل مجموعات من الخدمات توحدها الاتجاهات (مناطق المجال) حول النظام الأساسي ، وهو الأساس الرئيسي والبيانات.



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



وبفضل النقل النشط للمعرفة والعملية الرسمية لإجراء التغييرات ، نحن قادرون على تقديم عدد كبير من الميزات التي لا تبطئ عملية تحويل بنيتنا المعمارية.



هذا كل شيء بالنسبة لي ، شكرا على القراءة!



All Articles