Bagaimana cara meluncurkan refactoring berbahaya ke prod dengan sejuta pengguna?


Film "Airplane", 1980.

Itulah yang saya rasakan ketika saya menuangkan refactoring lain pada prod. Bahkan jika Anda menutupi seluruh kode dengan metrik dan log, uji fungsionalitas di semua lingkungan - ini tidak akan menghemat 100% dari fakaps setelah penyebaran.

Fakap pertama


Entah bagaimana kami refactored pemrosesan integrasi kami dengan Google Sheets. Untuk pengguna, ini adalah fitur yang sangat berharga, karena mereka menggunakan banyak alat pada saat yang bersamaan yang perlu dihubungkan bersama - mengirim kontak ke sebuah meja, mengunggah jawaban untuk pertanyaan, mengekspor pengguna, dll.

Kode integrasi tidak memperbaiki dari versi pertama dan menjadi semakin sulit untuk dipertahankan. Ini mulai mempengaruhi pengguna kami - bug lama terungkap bahwa kami takut mengedit karena rumitnya kode. Sudah waktunya untuk melakukan sesuatu tentang itu. Seharusnya tidak ada perubahan logis - cukup tulis tes, pindahkan kelas, dan sisir nama. Tentu saja, kami menguji fungsionalitas pada lingkungan dev dan mulai digunakan.

Setelah 20 menit, pengguna menulis bahwa integrasi tidak berfungsi. Fungsi pengiriman data ke Google Sheet gagal - ternyata untuk debug kami mengirim data dalam format yang berbeda untuk penjualan dan lingkungan lokal. Saat refactoring, kami menekan format untuk penjualan.

Kami memperbaiki integrasi, namun demikian, endapan dari Jumat malam yang meriah (dan Anda pikir!) Tetap ada. Dalam retrospeksi (bertemu tim untuk menyelesaikan sprint), kami mulai berpikir tentang bagaimana mencegah situasi seperti itu di masa depan - kita perlu meningkatkan praktik pengujian manual, pengujian otomatis, bekerja dengan metrik dan alarm, dan selain itu, kami mendapat ide untuk menggunakan bendera fitur untuk menguji refactoring pada Bahkan, ini akan dibahas.

Penerapan


Skema ini sederhana: jika pengguna mengaktifkan bendera, buka kode dengan versi baru, jika tidak, ke kode dengan versi lama:

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

Dengan pendekatan ini, kami memiliki kesempatan untuk menguji refactoring pada prod pertama pada diri kami dan kemudian menuangkannya pada pengguna.

Hampir sejak awal proyek, kami memiliki implementasi fitur flag yang primitif. Dalam database untuk dua entitas dasar, pengguna dan akun, bidang fitur ditambahkan, yang merupakan bit mask . Dalam kode tersebut, kami mendaftarkan konstanta baru untuk fitur, yang kemudian kami tambahkan ke mask jika fitur tertentu tersedia bagi pengguna.

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

Penggunaan dalam kode tampak seperti ini:

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

Saat refactoring, kami biasanya membuka bendera untuk tim untuk pengujian, kemudian ke beberapa pengguna yang secara aktif menggunakan fitur, dan akhirnya terbuka untuk semua, tetapi kadang-kadang skema yang lebih rumit muncul, lebih banyak tentang mereka di bawah ini.

Refactoring tempat yang kelebihan beban


Salah satu sistem kami menerima kait web Facebook dan memprosesnya melalui antrian. Pemrosesan antrian berhenti untuk mengatasi dan pengguna mulai menerima pesan tertentu dengan penundaan, yang secara kritis dapat mempengaruhi pengalaman pelanggan bot. Kami mulai memperbaiki tempat ini dengan mentransfer pemrosesan ke skema antrian yang lebih kompleks. Tempat ini kritis - berbahaya untuk menuangkan logika baru di semua server, jadi kami menutup logika baru di bawah bendera dan dapat mengujinya di prod. Tapi apa yang terjadi ketika kita membuka bendera ini? Bagaimana prilaku infrastruktur kita? Kali ini kami menggunakan pembukaan bendera di server dan mengikuti metrik.

Semua pemrosesan data penting telah kami bagi menjadi beberapa kelompok. Setiap cluster memiliki id. Kami memutuskan untuk menyederhanakan pengujian refactoring yang sedemikian kompleks dengan membuka fitur flag hanya pada server tertentu, cek dalam kode terlihat seperti ini:

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

Pertama, kami menuangkan refactoring dan membuka bendera ke tim. Kemudian kami menemukan beberapa pengguna yang secara aktif menggunakan fitur cgt, membuka bendera kepada mereka dan melihat apakah semuanya bekerja untuk mereka. Dan akhirnya, mereka mulai membuka bendera di server dan mengikuti metrik.

Bendera cgt_refactoring_cluster_ids dapat diubah melalui panel admin. Awalnya, kami menetapkan nilai cgt_refactoring_cluster_ids ke array kosong, lalu tambahkan satu kluster sekaligus - [1], lihat metrik sebentar dan tambahkan kluster lain - [1, 2] hingga kami menguji seluruh sistem.

Implementasi konfigurator


Saya akan berbicara sedikit tentang apa itu Configurator dan bagaimana penerapannya. Itu ditulis untuk dapat mengubah logika tanpa penyebaran, misalnya, seperti dalam kasus di atas, ketika kita perlu memutar balik logika dengan tajam. Kami juga menggunakannya untuk konfigurasi dinamis, misalnya, saat Anda perlu menguji waktu caching yang berbeda, Anda dapat menggunakannya untuk pengujian cepat. Untuk pengembang, ini terlihat seperti daftar bidang dengan nilai admin yang dapat diubah. Kami menyimpan semua ini dalam DB, kami cache di Redis dan statika untuk pekerja kami.

Refactoring lokasi yang sudah usang


Pada kuartal berikutnya, kami refactored logika pendaftaran, mempersiapkannya untuk transisi ke kemungkinan pendaftaran melalui beberapa layanan. Dalam kondisi kami, tidak mungkin untuk mengelompokkan logika pendaftaran sehingga pengguna tertentu terikat pada logika tertentu, dan kami tidak menghasilkan sesuatu yang lebih baik daripada menguji logika, menggelar persentase dari semua permintaan pendaftaran. Ini mudah dilakukan dengan cara yang mirip dengan flag:

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

Karenanya, kami menetapkan nilai auth_refactoring_percentage di panel admin dari 0 hingga 100. Tentu saja, kami "mengolesi" seluruh logika otorisasi dengan metrik untuk memahami bahwa kami tidak mengurangi konversi pada akhirnya.

Metrik


Untuk mengetahui bagaimana kami mengikuti metrik dalam proses membuka bendera, kami akan mempertimbangkan kasus lain secara lebih rinci. ManyChat menerima kait Facebook dari Facebook ketika pelanggan mengirim pesan ke Facebook Messenger. Kita harus memproses setiap pesan sesuai dengan logika bisnis. Untuk fitur cgt, kita perlu menentukan apakah pelanggan memulai percakapan melalui komentar di Facebook untuk mengiriminya pesan yang relevan sebagai tanggapan. Dalam kode, sepertinya menentukan konteks pelanggan saat ini, jika kita dapat menentukan widgetId, maka kita menentukan pesan respons darinya.

Lebih lanjut tentang fitur
Facebook api. β€” . Widget, :

β€”> β€”> β€”> Facebook:



:
β€”> β€”>



β€œ , !” , . , β€œ !” id , β€” , id.

Sebelumnya, kami mendefinisikan konteks dalam 3 cara, itu terlihat seperti ini:

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;
}

Layanan pengamat mengirimkan analitik pada saat pertandingan, masing-masing, kami memiliki metrik untuk ketiga kasus:


Jumlah kali konteks ditemukan oleh berbagai metode penautan dalam waktu.

Selanjutnya, kami menemukan metode pencocokan lain yang harus mengganti semua opsi lama. Untuk menguji ini, kami mendapat metrik lain:

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

  //    
  // ...
}

Pada tahap ini, kami ingin memastikan bahwa jumlah hit baru sama dengan jumlah hit lama, jadi cukup tulis metrik tanpa mengembalikan $ widgetId:


Jumlah konteks yang ditemukan oleh metode baru sepenuhnya mencakup jumlah ikatan dengan metode lama

Tapi ini tidak menjamin kami logika kecocokan yang benar pada semua kasus. Langkah selanjutnya adalah pengujian bertahap melalui pembukaan bendera:

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;
}

Kemudian proses pengujian dimulai: pada awalnya kami menguji fungsionalitas baru kami sendiri di semua lingkungan dan pada pengguna acak yang sering menggunakan pencocokan dengan membuka bendera UserFeatures :: ALLOW_CGT_MATCHING_BY_ECHO. Pada tahap ini, kami menangkap beberapa kasus ketika pertandingan bekerja dengan tidak benar dan memperbaikinya. Kemudian mereka mulai meluncurkan ke server: rata-rata, kami meluncurkan satu server dalam 1 hari selama seminggu. Sebelum pengujian, kami memperingatkan dukungan bahwa mereka melihat dengan seksama tiket yang terkait dengan fungsionalitas dan menulis kepada kami tentang segala keanehan. Berkat dukungan dan pengguna, beberapa kasing telah diperbaiki. Dan akhirnya, langkah terakhir adalah penemuan pada semua tanpa syarat:

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;
}

Implementasi fitur bendera baru


Penerapan fitur bendera yang diuraikan di awal artikel membantu kami selama sekitar 3 tahun, tetapi dengan pertumbuhan tim, menjadi tidak nyaman - kami harus menggunakan saat membuat setiap bendera dan jangan lupa untuk menghapus nilai bendera (kami menggunakan kembali nilai konstan untuk fitur yang berbeda). Baru-baru ini, komponen telah ditulis ulang dan sekarang kita dapat mengelola flag secara fleksibel melalui panel admin. Bendera dilepaskan dari bitmask dan disimpan dalam tabel terpisah - ini membuatnya mudah untuk membuat bendera baru. Setiap entri juga memiliki deskripsi dan pemilik, manajemen bendera menjadi lebih transparan.

Kontra dari pendekatan semacam itu


Pendekatan ini memiliki minus besar - ada dua versi kode dan mereka perlu didukung secara bersamaan. Saat menguji, Anda harus mempertimbangkan bahwa ada dua cabang logika, dan Anda perlu memeriksa semuanya, dan ini sangat menyakitkan. Selama pengembangan, ada situasi ketika kami memperkenalkan perbaikan ke satu logika, tetapi lupa tentang yang lain, dan di beberapa titik itu menembak. Oleh karena itu, kami menerapkan pendekatan ini hanya di tempat-tempat kritis dan berusaha untuk menyingkirkan kode versi lama secepat mungkin. Kami mencoba melakukan sisa refactoring dalam iterasi kecil.

Total


Proses saat ini terlihat seperti ini - pertama kita menutup logika di bawah kondisi bendera, kemudian menyebarkan dan mulai secara bertahap membuka bendera. Saat membentangkan bendera, kami memantau dengan cermat kesalahan dan metrik, segera setelah terjadi kesalahan - segera gulung kembali bendera dan atasi masalahnya. Nilai tambahnya adalah membuka / menutup bendera sangat cepat - itu hanya perubahan nilai pada panel admin. Setelah beberapa waktu, kami memotong kode versi lama, ini seharusnya menjadi waktu minimum untuk mencegah perubahan di kedua versi kode. Penting untuk memperingatkan kolega tentang refactoring tersebut. Kami melakukan review melalui github dan menggunakan pemilik kode selama refactoring sehingga perubahan tidak masuk ke dalam kode tanpa sepengetahuan penulis refactoring.

Baru-baru ini, saya meluncurkan versi baru dari Facebook Graph API. Dalam sedetik, kami membuat lebih dari 3000 permintaan ke API dan setiap kesalahan mahal bagi kami. Oleh karena itu, saya meluncurkan perubahan di bawah bendera dengan dampak minimal - Saya berhasil menangkap satu bug yang tidak menyenangkan, menguji versi baru dan akhirnya beralih sepenuhnya tanpa khawatir.

All Articles