Ketahanan dalam perangkat lunak mengacu pada kemampuan aplikasi untuk terus berfungsi dengan lancar dan andal, bahkan saat menghadapi masalah atau kegagalan yang tidak terduga. Dalam proyek Fintech, ketahanan menjadi sangat penting karena beberapa alasan. Pertama, perusahaan wajib memenuhi persyaratan regulasi dan regulator keuangan menekankan ketahanan operasional untuk menjaga stabilitas dalam sistem. Selain itu, menjamurnya perangkat digital dan ketergantungan pada penyedia layanan pihak ketiga membuat bisnis Fintech menghadapi ancaman keamanan yang lebih tinggi. Ketahanan juga membantu mengurangi risiko pemadaman yang disebabkan oleh berbagai faktor seperti ancaman siber, pandemi, atau peristiwa geopolitik, serta menjaga operasi bisnis inti dan aset penting.
Dengan pola ketahanan, kita memahami serangkaian praktik dan strategi terbaik yang dirancang untuk memastikan bahwa perangkat lunak dapat bertahan terhadap gangguan dan mempertahankan operasinya. Pola-pola ini bertindak seperti jaring pengaman, menyediakan mekanisme untuk menangani kesalahan, mengelola beban, dan memulihkan dari kegagalan, sehingga memastikan bahwa aplikasi tetap tangguh dan dapat diandalkan dalam kondisi yang buruk.
Strategi ketahanan yang paling umum meliputi bulkhead, cache, fallback, retry, dan circuit breaker. Dalam artikel ini, saya akan membahasnya lebih rinci, dengan contoh masalah yang dapat dipecahkan.
Mari kita lihat pengaturan di atas. Kita memiliki aplikasi yang sangat biasa dengan beberapa backend di belakang kita untuk mendapatkan beberapa data. Ada beberapa klien HTTP yang terhubung ke backend ini. Ternyata semuanya berbagi kumpulan koneksi yang sama! Dan juga sumber daya lain seperti CPU dan RAM.
Apa yang akan terjadi, jika salah satu backend mengalami beberapa jenis masalah yang mengakibatkan latensi permintaan yang tinggi? Karena waktu respons yang tinggi, seluruh kumpulan koneksi akan terisi penuh oleh permintaan yang menunggu respons dari backend1. Akibatnya, permintaan yang ditujukan untuk backend2 dan backend3 yang sehat tidak akan dapat dilanjutkan karena kumpulan tersebut telah habis. Ini berarti bahwa kegagalan di salah satu backend kita dapat menyebabkan kegagalan di seluruh aplikasi. Idealnya, kita hanya ingin fungsionalitas yang terkait dengan backend yang gagal mengalami penurunan, sementara aplikasi lainnya terus beroperasi secara normal.
Apa itu Pola Sekat?
Istilah pola sekat berasal dari pembuatan kapal, yang melibatkan pembuatan beberapa kompartemen terisolasi di dalam kapal. Jika terjadi kebocoran di satu kompartemen, kompartemen tersebut akan terisi air, tetapi kompartemen lainnya tidak terpengaruh. Isolasi ini mencegah seluruh kapal tenggelam karena satu kebocoran.
Pola Bulkhead dapat digunakan untuk mengisolasi berbagai jenis sumber daya dalam suatu aplikasi, mencegah kegagalan di satu bagian agar tidak memengaruhi seluruh sistem. Berikut cara menerapkannya pada masalah kita:
Misalkan sistem backend kita memiliki probabilitas rendah untuk mengalami kesalahan secara individual. Namun, ketika suatu operasi melibatkan kueri semua backend ini secara paralel, masing-masing dapat secara independen menghasilkan kesalahan. Karena kesalahan ini terjadi secara independen, probabilitas keseluruhan kesalahan dalam aplikasi kita lebih tinggi daripada probabilitas kesalahan dari setiap backend tunggal. Probabilitas kesalahan kumulatif dapat dihitung menggunakan rumus P_total=1−(1−p)^n, di mana n adalah jumlah sistem backend.
Misalnya, jika kita memiliki sepuluh backend, masing-masing dengan probabilitas kesalahan p=0,001 (sesuai dengan SLA 99,9%), probabilitas kesalahan yang dihasilkan adalah:
Jumlah P = 1 - (1 - 0,001) ^ 10 = 0,009955
Ini berarti SLA gabungan kami turun hingga sekitar 99%, yang menggambarkan bagaimana keandalan keseluruhan menurun saat melakukan kueri pada beberapa backend secara paralel. Untuk mengurangi masalah ini, kami dapat menerapkan cache dalam memori.
Cache dalam memori berfungsi sebagai buffer data berkecepatan tinggi, menyimpan data yang sering diakses dan menghilangkan kebutuhan untuk mengambilnya dari sumber yang berpotensi lambat setiap saat. Karena cache yang disimpan dalam memori memiliki peluang kesalahan 0% dibandingkan dengan pengambilan data melalui jaringan, cache tersebut secara signifikan meningkatkan keandalan aplikasi kita. Selain itu, caching mengurangi lalu lintas jaringan, yang selanjutnya menurunkan peluang kesalahan. Akibatnya, dengan memanfaatkan cache dalam memori, kita dapat mencapai tingkat kesalahan yang lebih rendah dalam aplikasi kita dibandingkan dengan sistem backend kita. Selain itu, cache dalam memori menawarkan pengambilan data yang lebih cepat daripada pengambilan berbasis jaringan, sehingga mengurangi latensi aplikasi—keuntungan yang penting.
Untuk data yang dipersonalisasi, seperti profil pengguna atau rekomendasi, penggunaan cache dalam memori juga bisa sangat efektif. Namun, kita perlu memastikan semua permintaan dari pengguna secara konsisten masuk ke instans aplikasi yang sama untuk memanfaatkan data yang di-cache untuk mereka, yang memerlukan sesi lengket. Menerapkan sesi lengket bisa jadi menantang, tetapi untuk skenario ini, kita tidak memerlukan mekanisme yang rumit. Penyeimbangan ulang lalu lintas kecil dapat diterima, jadi algoritme penyeimbangan beban yang stabil seperti hashing yang konsisten akan cukup.
Terlebih lagi, jika terjadi kegagalan node, hashing yang konsisten memastikan bahwa hanya pengguna yang terkait dengan node yang gagal yang akan mengalami penyeimbangan ulang, sehingga meminimalkan gangguan pada sistem. Pendekatan ini menyederhanakan pengelolaan cache yang dipersonalisasi dan meningkatkan stabilitas dan kinerja aplikasi kita secara keseluruhan.
Jika data yang ingin kami simpan dalam cache bersifat penting dan digunakan dalam setiap permintaan yang ditangani sistem kami, seperti kebijakan akses, paket langganan, atau entitas penting lainnya di domain kami—sumber data ini dapat menimbulkan titik kegagalan yang signifikan dalam sistem kami. Untuk mengatasi tantangan ini, salah satu pendekatan adalah mereplikasi data ini sepenuhnya langsung ke dalam memori aplikasi kami.
Dalam skenario ini, jika volume data dalam sumber dapat dikelola, kita dapat memulai proses dengan mengunduh cuplikan data ini di awal aplikasi kita. Selanjutnya, kita dapat menerima peristiwa pembaruan untuk memastikan data yang di-cache tetap sinkron dengan sumbernya. Dengan mengadopsi metode ini, kita meningkatkan keandalan akses ke data penting ini, karena setiap pengambilan data terjadi langsung dari memori dengan probabilitas kesalahan 0%. Selain itu, pengambilan data dari memori sangat cepat, sehingga mengoptimalkan kinerja aplikasi kita. Strategi ini secara efektif mengurangi risiko yang terkait dengan ketergantungan pada sumber data eksternal, memastikan akses yang konsisten dan andal ke informasi penting untuk operasi aplikasi kita.
Namun, kebutuhan untuk mengunduh data saat aplikasi dijalankan, sehingga menunda proses memulai, melanggar salah satu prinsip 'aplikasi 12 faktor' yang menganjurkan aplikasi dijalankan dengan cepat. Namun, kita tidak ingin kehilangan manfaat dari penggunaan caching. Untuk mengatasi dilema ini, mari kita telusuri solusi yang mungkin.
Startup yang cepat sangat penting, terutama untuk platform seperti Kubernetes, yang mengandalkan migrasi aplikasi yang cepat ke berbagai node fisik. Untungnya, Kubernetes dapat mengelola aplikasi yang lambat saat startup menggunakan fitur seperti startup probes.
Tantangan lain yang mungkin kita hadapi adalah memperbarui konfigurasi saat aplikasi sedang berjalan. Sering kali, penyesuaian waktu cache atau batas waktu permintaan diperlukan untuk mengatasi masalah produksi. Bahkan jika kita dapat dengan cepat menerapkan berkas konfigurasi yang diperbarui ke aplikasi kita, penerapan perubahan ini biasanya memerlukan restart. Dengan waktu startup setiap aplikasi yang diperpanjang, restart bergulir dapat secara signifikan menunda penerapan perbaikan kepada pengguna kita.
Untuk mengatasi hal ini, salah satu solusinya adalah menyimpan konfigurasi dalam variabel bersamaan dan meminta utas latar belakang memperbaruinya secara berkala. Namun, parameter tertentu, seperti batas waktu permintaan HTTP, mungkin memerlukan inisialisasi ulang klien HTTP atau basis data saat konfigurasi terkait berubah, sehingga menimbulkan tantangan potensial. Namun, beberapa klien, seperti driver Cassandra untuk Java, mendukung pemuatan ulang konfigurasi secara otomatis, sehingga menyederhanakan proses ini.
Menerapkan konfigurasi yang dapat dimuat ulang dapat mengurangi dampak negatif dari waktu mulai aplikasi yang lama dan menawarkan manfaat tambahan, seperti memfasilitasi penerapan fitur. Pendekatan ini memungkinkan kami untuk mempertahankan keandalan dan responsivitas aplikasi sambil mengelola pembaruan konfigurasi secara efisien.
Sekarang mari kita lihat masalah lain: dalam sistem kami, saat permintaan pengguna diterima dan diproses dengan mengirimkan kueri ke backend atau basis data, terkadang, respons kesalahan diterima alih-alih data yang diharapkan. Selanjutnya, sistem kami merespons pengguna dengan 'kesalahan'.
Namun, dalam banyak skenario, mungkin lebih baik untuk menampilkan data yang sedikit kedaluwarsa bersama dengan pesan yang menunjukkan adanya penundaan penyegaran data, daripada membiarkan pengguna dengan pesan kesalahan berwarna merah besar.
Untuk mengatasi masalah ini dan meningkatkan perilaku sistem, kita dapat menerapkan pola Fallback. Konsep di balik pola ini melibatkan sumber data sekunder, yang mungkin berisi data dengan kualitas atau kesegaran yang lebih rendah dibandingkan dengan sumber utama. Jika sumber data utama tidak tersedia atau menghasilkan kesalahan, sistem dapat kembali mengambil data dari sumber sekunder ini, memastikan bahwa beberapa bentuk informasi disajikan kepada pengguna alih-alih menampilkan pesan kesalahan.
Jika Anda melihat gambar di atas, Anda akan melihat kesamaan antara masalah yang kita hadapi sekarang dan masalah yang kita temui pada contoh cache.
Untuk mengatasinya, kita dapat mempertimbangkan penerapan pola yang dikenal sebagai retry. Alih-alih mengandalkan cache, sistem dapat dirancang untuk secara otomatis mengirim ulang permintaan jika terjadi kesalahan. Pola retry ini menawarkan alternatif yang lebih sederhana dan secara efektif dapat mengurangi kemungkinan kesalahan dalam aplikasi kita. Tidak seperti caching, yang sering kali memerlukan mekanisme pembatalan cache yang kompleks untuk menangani perubahan data, mencoba ulang permintaan yang gagal relatif mudah untuk diterapkan. Karena pembatalan cache secara luas dianggap sebagai salah satu tugas yang paling menantang dalam rekayasa perangkat lunak, mengadopsi strategi retry dapat menyederhanakan penanganan kesalahan dan meningkatkan ketahanan sistem.
Namun, mengadopsi strategi percobaan ulang tanpa mempertimbangkan konsekuensi potensial dapat menimbulkan komplikasi lebih lanjut.
Bayangkan salah satu backend kita mengalami kegagalan. Dalam skenario seperti itu, memulai percobaan ulang pada backend yang gagal dapat mengakibatkan peningkatan volume lalu lintas yang signifikan. Lonjakan lalu lintas yang tiba-tiba ini dapat membanjiri backend, memperburuk kegagalan, dan berpotensi menyebabkan efek berjenjang di seluruh sistem.
Untuk mengatasi tantangan ini, penting untuk melengkapi pola coba lagi dengan pola pemutus sirkuit. Pemutus sirkuit berfungsi sebagai mekanisme pengamanan yang memantau tingkat kesalahan layanan hilir. Ketika tingkat kesalahan melampaui ambang batas yang telah ditetapkan, pemutus sirkuit menghentikan permintaan ke layanan yang terpengaruh selama durasi tertentu. Selama periode ini, sistem menahan diri untuk tidak mengirim permintaan tambahan untuk memberi waktu layanan yang gagal pulih. Setelah interval yang ditentukan, pemutus sirkuit dengan hati-hati mengizinkan sejumlah permintaan terbatas untuk lewat, memverifikasi apakah layanan telah stabil. Jika layanan telah pulih, lalu lintas normal dipulihkan secara bertahap; jika tidak, sirkuit tetap terbuka, terus memblokir permintaan hingga layanan melanjutkan operasi normal. Dengan mengintegrasikan pola pemutus sirkuit di samping logika coba lagi, kita dapat secara efektif mengelola situasi kesalahan dan mencegah kelebihan beban sistem selama kegagalan backend.
Kesimpulannya, dengan menerapkan pola ketahanan ini, kita dapat memperkuat aplikasi kita terhadap keadaan darurat, mempertahankan ketersediaan tinggi, dan memberikan pengalaman yang lancar kepada pengguna. Selain itu, saya ingin menekankan bahwa telemetri adalah alat lain yang tidak boleh diabaikan saat menyediakan ketahanan proyek. Log dan metrik yang baik dapat meningkatkan kualitas layanan secara signifikan dan memberikan wawasan berharga tentang kinerjanya, membantu membuat keputusan yang tepat untuk meningkatkannya lebih lanjut.