Di Slash, kami mengelola pengeluaran senilai miliaran dolar setiap tahun. Dengan pertumbuhan lebih dari 1000% dalam 12 bulan terakhir, kami terus menghadapi tantangan baru dengan skala yang semakin besar. Seiring bertambahnya volume data dan kompleksitas sistem, asumsi yang kami buat tentang hubungan data seringkali tidak lagi berlaku. Kami menghadapi masalah dari berbagai sumber: data yang tidak akurat, bug dalam kode produksi, dan kesalahan kecil selama migrasi data. Seiring waktu, semakin sulit untuk memverifikasi keakuratan data kami, yang mendorong kami untuk membangun apa yang kini kami sebut Health Checks — sistem yang dirancang untuk secara terus-menerus memverifikasi keakuratan data dalam lingkungan produksi.

Pemeriksaan kesehatan adalah alat yang membantu kita memverifikasi invarians dalam basis kode kita. Saat kita membangun sistem, kita selalu merancangnya dengan seperangkat invarians implisit dan eksplisit, yang merupakan aturan/asumsi yang kita harapkan selalu benar sehingga kita dapat membangun sistem yang lebih kompleks di atasnya.

Contoh sederhana dari suatu invarian: dalam Kartu Tabel dalam basis data kami, di mana setiap baris mewakili kartu kredit, terdapat sebuah status kolom di mana salah satu nilainya dapat berupa “tertutup” dan a Alasan ditutup kolom yang dapat bernilai NULL. Salah satu sifat tetapnya adalah bahwa status kolom dianggap "tertutup" **jika dan hanya jika** Alasan ditutup Kolom ini tidak boleh kosong.

Contoh di atas bukanlah kondisi yang secara khusus memerlukan penulisan health check, tetapi merupakan contoh dari invariant yang kami pastikan tetap berlaku saat bekerja dengan bagian kode ini.

Desain pemeriksaan kesehatan awal kami

Iterasi pertama dari pemeriksaan kesehatan kami terlihat seperti berikut ini:

/**
 * This health check ensures that for every declined AuthorizationSet, there are no transfer intents
 */
export const hcrForDeclinedAuthSets = createHealthCheckRoutineDefinition({
  programmaticName: 'chargeCard.authSets.declinedAuthSets',
  title: 'Charge card declined auth sets',
  description:
    'This health check ensures for every declined AuthorizationSet, there are no transfer intents',
  schedule: {
    type: 'interval',
    interval: 'minutes',
    frequency: 30,
  },
})
  .bulkCheckFn((deps: DefineDependencies<[HealthCheckModule.Service]>) => {
    return async (
      cursor:
        | HealthCheckRoutineCursor<{
            authorizationSetId: string;
          }>
        | undefined
    ) => {
	    // getErrorAccounts -> a query to Snowflake
      const errorAccounts = await getErrorAccounts(cursor, deps);

      return {
        data: errorAccounts.data.map((val) => ({
          key: val.authorizationSetId,
        })),
        nextCursor: errorAccounts.nextCursor,
      };
    };
  })
  .singleCheckFn(() => async (params) => {
    const singleCheckRes = await runQuery(
      sql`...`
    );

    return {
      success: singleCheckRes.length === 0,
    };
  });

Kami awalnya merancang pemeriksaan kesehatan (health checks) untuk didefinisikan oleh kueri SQL. Kami akan menjalankan kueri SQL terhadap sebuah tabel untuk menemukan "kesalahan potensial". Namun, karena menjalankan kueri SQL terhadap tabel besar sangat mahal, kami menjalankan pemeriksaan ini terhadap replika baca yang konsisten secara eventual (dalam kasus kami, ini adalah Snowflake). Untuk setiap hasil yang dikembalikan oleh kueri, kami kemudian menjalankan pemeriksaan terhadap basis data produksi utama kami untuk memastikan itu bukan false positive. Desain ini memiliki beberapa masalah:

  1. Kami harus mempertahankan dua kueri. Kueri pertama akan dijalankan terhadap replika baca untuk seluruh tabel. Pengelola juga harus secara eksplisit mempertimbangkan dan menangani paginasi untuk kueri tersebut. Kueri kedua akan dijalankan terhadap produksi untuk setiap hasil individu yang dikembalikan oleh kueri pertama.
  2. Query terhadap seluruh tabel akan menjadi semakin kompleks dan sulit dibaca / sulit dipelihara karena kita akan mencoba mengkodekan sejumlah logika bisnis ke dalam satu query SQL.

Iterasi kedua pemeriksaan kesehatan

Kami memutuskan:

  1. Pemeriksaan kesehatan harus selalu dilakukan pada basis data produksi utama kami. Jika tidak, kami mungkin tidak dapat mendeteksi semua masalah yang sebenarnya terjadi.
  2. Melakukan "full table scan" dalam query SQL adalah hal yang harus dihindari, tetapi melakukan "full table scan" yang terkontrol dengan mengiterasi setiap baris dan melakukan pemeriksaan kesehatan sebenarnya boleh-boleh saja — asalkan beban kerja konstan, dapat diprediksi, dan tidak melonjak, biasanya tidak ada masalah.
  3. Melakukan iterasi seiring waktu pada tabel di lingkungan produksi (dan menyembunyikan proses tersebut dari pengembang) sangat mempermudah pengalaman pengembangan (DX) kami:
    1. Kami tidak lagi perlu memelihara dua kueri SQL yang kompleks dengan paginasi. Kami hanya perlu mendefinisikan logika aplikasi untuk memeriksa kesehatan satu item.
    2. Setiap pemeriksaan kesehatan didefinisikan untuk seluruh tabel (di masa depan, kita mungkin memilih untuk memperluas pemeriksaan kesehatan sehingga dapat beriterasi secara spesifik atas definisi indeks).

Salah satu pelajaran penting yang kami pelajari sebagai tim seiring berjalannya waktu adalah bahwa sistem yang dapat diprediksi dan memberikan beban konstan adalah yang ideal. Penyebab utama masalah produksi biasanya adalah perubahan mendadak dan tidak terduga, seperti lonjakan besar dalam beban database, atau perubahan mendadak dalam perencanaan kueri internal database.

Aspek yang bertentangan dengan intuisi tentang memberikan beban konstan pada sistem, terutama terkait dengan basis data dan antrian, adalah bahwa hal itu tampak boros. Dulu saya percaya lebih baik tidak memberikan beban pada sistem secara default dan hanya melakukan pekerjaan saat dibutuhkan beberapa kali sehari. Namun, hal ini dapat menyebabkan beban kerja yang fluktuatif, yang kadang-kadang dapat merusak sistem kita secara tiba-tiba. Pada kenyataannya, kami menemukan bahwa biasanya lebih baik ada beban konstan yang kecil yang berjalan terus-menerus terhadap database selama periode waktu yang lama, bahkan jika beban konstan tersebut hanya berkontribusi sekitar 1-5% penggunaan CPU secara keseluruhan 24/7. Ketika beban dapat diprediksi, mudah untuk memantau tingkat penggunaan secara keseluruhan. Kemampuan memprediksi ini memungkinkan kami untuk melakukan skalabilitas horizontal dengan percaya diri dan merencanakan ahead untuk potensi masalah kinerja di masa depan.

Ada artikel yang menarik tentang hal ini yang ditulis oleh tim di AWS: Keandalan, kerja yang konsisten, dan secangkir kopi yang enak

Versi kedua dari pemeriksaan kesehatan sekarang terlihat seperti:

export const savingsAccount = createHealthCheckGroup({
  over: SlashAccountGroup,
  name: 'savings_account_interest_entities',
  schedules: {
    full_scan: {
      type: 'full_scan',
      config: {},
      props: {
        numberOfItemsToPaginate: 2000,
        minimumIntervalSinceStartOfLastRun: 3 * 60 * 60 * 1000,
      },
    },
  },
  checks: {
    interest_limit_rule_should_exist: {
      async check(group, ctx) {
        if (group.groupType === 'savings') {
          const activeInterestLimits = await from(SlashAccountGroupLimitRule)
            .leftJoin(LimitRule, {
              on: (_) => _.lr.id.equals(_.slr.limitRuleId),
            })
            .where((_) =>
              _.slr.slashAccountGroupId
                .equals(group.id)
                .and(_.slr.isActive.equals(true))
                .and(_.lr.type.equals('interest'))
            );

          ctx.shouldHaveLengthOf(activeInterestLimits, 1, {
            message:
              'Savings accounts should have exactly one active interest limit',
          });
        }
      },
    },
    // other checks
    ...
  }
});

Dengan desain ini, sangat mudah untuk menambahkan pemeriksaan kesehatan baru ke entitas tunggal karena setiap pemeriksaan kesehatan didefinisikan sebagai fungsi asinkron tunggal. Kita dapat menjalankan pemeriksaan kesehatan ini berulang kali sepanjang hari tanpa perlu mengorbankan frekuensi, karena beban kerjanya tetap konstan. Untuk pemeriksaan kesehatan di atas, setiap entitas melakukan pencarian cepat ke tabel `LimitRule`. Pemeriksaan ini berjalan secara terus-menerus, memberikan beban minimal namun konstan pada database pada setiap saat.

Bagaimana sistem ini sebenarnya beroperasi di balik layar dapat disederhanakan menjadi desain berikut ini:

  1. Kami menjalankan pemeriksaan kami di atas Temporal agar kami dapat menjamin eksekusi yang pasti. Kami menggunakan produk "schedules" dari Temporal untuk memastikan bahwa cron kami dijalankan setiap menit.
  2. Setiap menit, kami memeriksa semua tugas yang harus dijadwalkan SEKARANG, dan menjalankan tugas-tugas tersebut dalam antrian tugas. Kami mengatur kecepatan antrian tugas kami agar tidak pernah melebihi batas RPS tertentu yang dikirim ke basis data kami.
  3. Setiap aktivitas yang dijalankan melakukan pekerjaan "konstan". Tidak ada kompleksitas waktu O(N) atau lainnya yang meningkat seiring dengan jumlah total entitas dalam database. Hal ini memastikan bahwa aktivitas berjalan dengan cepat, mudah diprediksi, dan tidak akan memblokir antrian tugas itu sendiri.

Apa selanjutnya?

Pemeriksaan kesehatan saat ini menjadi dasar penting dalam alur pengujian kami. Ini merupakan landasan untuk pengujian verifikasi produksi yang berkelanjutan. Seiring pertumbuhan kami, semakin banyak pengujian umum yang diperlukan untuk menjaga produk kami tetap stabil dan berfungsi dengan baik. Butuh beberapa kali percobaan bagi kami untuk menyempurnakannya. Kami telah berencana untuk menerapkan bentuk pengujian ledger / verifikasi data di lingkungan produksi selama tiga tahun terakhir. Namun, baru setahun setengah yang lalu kami akhirnya berhasil mengembangkan solusi tersebut. Secara keseluruhan, beberapa pelajaran penting yang kami pelajari adalah:

  1. Kirimkan sesuatu, apa saja, dan terus kembangkan seiring waktu. Kami tidak akan bisa mencapai iterasi kedua dari pemeriksaan kesehatan kami tanpa telah mengirimkan iterasi pertamanya.
  2. Jaga agar tetap sederhana — kami ingin memudahkan para insinyur dalam membangun dan memelihara pemeriksaan kesehatan. Logika bisnis yang kompleks yang tertanam dalam beberapa kueri SQL tidak pernah menyenangkan untuk ditangani.
  3. Lakukan pemeriksaan kami langsung pada lingkungan produksi — itulah sumber kebenaran kami, dan jika kami melakukan pemeriksaan terhadap sesuatu yang lain, hal itu secara inheren akan lebih kompleks dan lebih rentan terhadap hasil positif palsu/negatif palsu.

Hari ini, pemeriksaan kesehatan merupakan langkah pertama yang kami ambil untuk menguji dan memverifikasi sistem kami secara lebih baik di luar metode tradisional seperti pengujian dan pemantauan. Seiring dengan pertumbuhan kami, kami akan terus mengevaluasi dan mencari cara baru untuk menjaga stabilitas produk kami.

Read more from us