كيفية طرح إعادة هيكلة خطيرة لتحفيز مليون مستخدم؟


فيلم "طائرة" 1980.

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

أول فاكاب


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

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

بعد 20 دقيقة ، كتب المستخدمون أن التكامل لم ينجح. توقفت وظيفة إرسال البيانات إلى Google Sheet - اتضح أنه بالنسبة للتصحيح ، نرسل البيانات بتنسيقات مختلفة للمبيعات والبيئات المحلية. عند إعادة البيع ، نضغط على تنسيق البيع.

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

التنفيذ


المخطط بسيط: إذا كان المستخدم لديه علامة ممكّنة ، فانتقل إلى الرمز مع الإصدار الجديد ، إن لم يكن ، إلى الرمز مع الإصدار القديم:

if ($user->hasFeature(UserFeatures::FEATURE_1)) {
  // new version
} else {
  // old version
}

من خلال هذا النهج ، لدينا الفرصة لاختبار إعادة الهيكلة على المنتج أولاً على أنفسنا ثم صبها على المستخدمين.

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

public const ALLOW_FEATURE_1 = 0b0000001;
public const ALLOW_FEATURE_2 = 0b0000010;
public const ALLOW_FEATURE_3 = 0b0000100;

بدا الاستخدام في التعليمات البرمجية كما يلي:

If ($user->hasFeature(UserFeatures::ALLOW_FEATURE_1)) {
  // feature 1 logic
}

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

إعادة هيكلة مكان مثقل


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

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

If ($user->hasFeature(UserFeatures::CGT_REFACTORING) ||
    \in_array($cluster, Configurator::get('cgt_refactoring_cluster_ids'))) {
  // new version
} else {
  // old version
}

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

يمكن تغيير علامة cgt_refactoring_cluster_ids من خلال لوحة المشرف. في البداية ، نقوم بتعيين القيمة cgt_refactoring_cluster_ids إلى مصفوفة فارغة ، ثم نضيف مجموعة واحدة في كل مرة - [1] ، ونلقي نظرة على المقاييس لفترة من الوقت ونضيف مجموعة أخرى - [1 ، 2] حتى نختبر النظام بأكمله.

تنفيذ أداة التهيئة


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

إعادة هيكلة المواقع القديمة


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

If (Configurator::get('auth_refactoring_percentage') > \random_int(0, 99)) {
  // new version
} else {
  // old version
}

وفقًا لذلك ، قمنا بتعيين قيمة auth_refactoring_percentage في لوحة المشرف من 0 إلى 100. بالطبع ، "لطخنا" منطق التفويض بالكامل باستخدام المقاييس لنفهم أننا لم نقلل التحويل في النهاية.

المقاييس


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

المزيد عن الميزة
Facebook api. — . Widget, :

—> —> —> Facebook:



:
—> —>



“ , !” , . , “ !” id , — , id.

في السابق ، حددنا السياق بثلاث طرق ، بدا الأمر كالتالي:

function getWidgetIdContext(User $user, WatchService $watcher): int?
{
  //      
  if (null !== $user->gt_widget_id_context) {
    $watcher->logTick('cgt_match_processor_matched_via_context');

    return $user->gt_widget_id_context;
  }

  //      
  if (null !== $user->name) {
    $widgetId = $this->cgtMatchByThread($user);
    if (null !== $widgetId) {
      $watcher->logTick('cgt_match_processor_matched_via_thread');

      return $widgetId;
    }

    $widgetId = $this->cgtMatchByConversation($user);
    if (null !== $widgetId) {
      $watcher->logTick('cgt_match_processor_matched_via_conversation');

      return $widgetId;
    }
  }

  return null;
}

ترسل خدمة المراقب تحليلات في وقت المباراة ، على التوالي ، كانت لدينا مقاييس لجميع الحالات الثلاث:


عدد المرات التي تم فيها العثور على السياق من خلال طرق الربط المختلفة في الوقت المناسب.

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

function getWidgetIdContext(User $user, WatchService $watcher): int?
{
  //    
  $widgetId = $this->cgtMatchByEcho($user);
  
  if (null !== $widgetId) {
      $watcher->logTick('cgt_match_processor_matched_via_echo_message');
  }

  //    
  // ...
}

في هذه المرحلة ، نريد التأكد من أن عدد النتائج الجديدة يساوي مجموع النتائج القديمة ، لذا فقط اكتب المقياس دون إرجاع $ widgetId: يغطي


عدد السياقات التي تم العثور عليها بواسطة الطريقة الجديدة مجموع عمليات الربط بالطرق القديمة تمامًا

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

function getWidgetIdContext(User $user, WatchService $watcher): int?
{
  //    
  $widgetId = $this->cgtMatchByEcho($user);
  
  if (null !== $widgetId) {
    $watcher->logTick('cgt_match_processor_matched_by_echo_message');
  
    //    ,   
    If ($this->allowMatchingByEcho($user)) {
      return $widgetId;
    }
  }

  // ...
}

function allowMatchingByEcho(User $user): bool
{
  //    
  If ($user->hasFeature(UserFeatures::ALLOW_CGT_MATCHING_BY_ECHO)) {
    return true;
  }
  //     
  If (\in_array($this->clusterId, Configurator::get('cgt_matching_by_echo_cluster_ids'))) {
    return true;
  }

  return false;
}

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

function getWidgetIdContext(User $user, WatchService $watcher): int?
{
  $widgetId = $this->cgtMatchByEcho($user);
  
  if (null !== $widgetId) {
    $watcher->logTick('cgt_match_processor_matched_by_echo_message');
  
    return $widgetId;
  }

  return null;
}

تنفيذ ميزة العلم الجديد


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

سلبيات مثل هذه الأساليب


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

مجموع


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

في الآونة الأخيرة ، طرحت إصدارًا جديدًا من Facebook Graph API. في ثانية ، نقوم بتقديم أكثر من 3000 طلب لواجهة برمجة التطبيقات وأي خطأ باهظ الثمن بالنسبة لنا. لذلك ، طرحت التغيير تحت العلم بأقل قدر من التأثير - تمكنت من التقاط خطأ واحد غير سار ، واختبار الإصدار الجديد والتحول إليه في النهاية تمامًا دون قلق.

All Articles