Kegagalan krusial membuat layanan tidak dapat memberikan hasil yang bermanfaat. Misalnya, dalam situs web e-niaga, jika kueri database untuk informasi produk gagal, situs web tidak dapat menampilkan halaman produk. Layanan Amazon harus menangani hampir semua kegagalan krusial agar dapat diandalkan. Terdapat empat kategori strategi yang luas untuk menangani kegagalan krusial:

Coba lagi: Lakukan lagi aktivitas yang gagal, baik dalam waktu dekat atau setelah beberapa penundaan.
Coba lagi proaktif: Lakukan aktivitas beberapa kali secara paralel dan gunakan yang pertama untuk menyelesaikan.
Failover: Lakukan aktivitas lagi terhadap salinan berbeda dari titik akhir, atau, sebaiknya lakukan beberapa salinan paralel dari aktivitas untuk meningkatkan peluang setidaknya satu dari mereka berhasil.
Fallback: Gunakan mekanisme berbeda untuk mendapatkan hasil yang sama.

Artikel ini membahas strategi fallback dan alasan kami hampir tidak pernah menggunakannya di Amazon. Anda mungkin terkejut mendengarnya. Namun, para teknisi seringkali menggunakan dunia nyata sebagai titik awal desain mereka. Selain itu, di dunia nyata, strategi fallback seringkali direncanakan di awal dan digunakan jika perlu. Misalnya papan pengumuman bandara mati. Rencana darurat (seperti manusia yang menulis informasi penerbangan di papan tulis) harus ada untuk menangani situasi ini karena penumpang masih perlu mencari tahu tentang gerbang keberangkatannya. Namun, pertimbangkan betapa buruknya rencana darurat itu: orang kesulitan membaca di papan tulis, sulitnya memperbarui informasi, dan risiko bahwa manusia akan menambahkan informasi yang salah. Strategi fallback papan tulis diperlukan tetapi penuh dengan masalah.

Dalam dunia sistem terdistribusi, strategi fallback adalah salah satu tantangan paling sulit untuk ditangani, terutama untuk layanan yang peka terhadap waktu. Hal yang memperumit kesulitan ini adalah bahwa strategi fallback yang buruk dapat memakan waktu yang lama (bahkan bertahun-tahun) untuk dapat memberikan dampak, dan perbedaan antara strategi yang baik dengan strategi yang buruk cukup tipis. Dalam artikel ini, fokusnya adalah bagaimana strategi fallback dapat menyebabkan lebih banyak masalah daripada memperbaiki. Kami akan menyertakan contoh ketika strategi fallback menyebabkan masalah di Amazon. Akhirnya, kami akan membahas alternatif untuk fallback yang kami gunakan di Amazon.

Menganalisis strategi fallback untuk layanan tidaklah intuitif dan efek riaknya sulit diperkirakan dalam sistem terdistribusi, jadi mari kita mulai dengan terlebih dahulu melihat strategi fallback untuk aplikasi mesin tunggal.

Fallback mesin tunggal

Pertimbangkan cuplikan kode C berikut yang menggambarkan pola umum untuk menangani kegagalan alokasi memori di banyak aplikasi. Kode ini mengalokasikan memori menggunakan fungsi malloc(), dan kemudian menyalin buffer gambar ke dalam memori itu sambil melakukan semacam transformasi:
pixel_ranges = malloc(image_size); // allocates memory
if (pixel_ranges == NULL) {
  // On error, malloc returns NULL
  exit(1);
}
for (i = 0; i < image_size; i++) {
  pixel_ranges[i] = xform(original_image[i]);
}

Kode tidak pulih dengan baik dari kasus tempat malloc gagal. Dalam praktiknya, panggilan ke malloc jarang gagal, jadi pengembang sering mengabaikan kegagalannya dalam kode. Mengapa strategi ini sangat umum? Alasannya, pada mesin tunggal, jika malloc gagal, mesin mungkin kehabisan memori. Sehingga ada masalah yang lebih besar dari satu panggilan malloc gagal, yaitu mesin mungkin segera crash. Selain itu, seringkali pada mesin tunggal alasan itu yang masuk akal. Banyak aplikasi yang tidak cukup penting yang sepadan dengan usaha untuk memecahkan masalah yang sulit. Tetapi, bagaimana jika Anda ingin menangani kesalahan ini? Mencoba melakukan sesuatu yang bermanfaat dalam situasi tersebut cukup rumit. Misalnya, kami menerapkan metode kedua yang disebut malloc2 yang mengalokasikan memori secara berbeda, dan kami memanggil malloc2 jika implementasi malloc default gagal:

pixel_ranges = malloc(image_size);
if (pixel_ranges == NULL) {
  pixel_ranges = malloc2(image_size);
}

Pada awalnya, kode ini sepertinya bisa berfungsi, tetapi ada masalah, beberapa kode kurang terlihat dibanding yang lain. Untuk memulai, logika fallback itu sulit diuji. Kami dapat menghadang panggilan ke malloc dan menyuntikkan kegagalan, tetapi itu mungkin tidak secara akurat menyimulasikan apa yang akan terjadi di lingkungan produksi. Dalam produksi, jika malloc gagal, mesin kemungkinan besar kehabisan atau kekurangan memori. Bagaimana Anda menyimulasikan masalah memori yang lebih luas? Bahkan jika Anda dapat menghasilkan lingkungan dengan memori rendah untuk menjalankan tes (misalnya, dalam kontainer Docker), bagaimana Anda mengatur waktu kondisi memori rendah agar bertepatan dengan eksekusi kode fallback malloc2?

Masalah lainnya adalah fallback itu sendiri dapat gagal. Kode fallback sebelumnya tidak menangani kegagalan malloc2, jadi program ini tidak memberikan manfaat sebanyak yang Anda kira. Strategi fallback mungkin membuat kemungkinan kegagalan total lebih kecil, tetapi bukan tidak mungkin. Di Amazon, kami telah menemukan bahwa membelanjakan sumber daya teknik untuk membuat kode primer (non-fallback) lebih dapat diandalkan dan biasanya lebih meningkatkan peluang keberhasilan kami dibanding berinvestasi dalam strategi fallback yang jarang digunakan.

Selain itu, jika ketersediaan adalah prioritas tertinggi kita, strategi fallback mungkin tidak sepadan dengan risikonya. Mengapa repot-repot menggunakan malloc jika malloc2 memiliki peluang lebih tinggi untuk berhasil? Secara logika, malloc2 harus melakukan pertukaran sebagai imbalan dari ketersediaannya yang lebih tinggi. Mungkin hal ini dapat mengalokasikan memori dalam penyimpanan berbasis SSD dengan latensi yang lebih tinggi, tetapi lebih besar. Tetapi hal ini menimbulkan pertanyaan, mengapa malloc2 diperbolehkan untuk melakukan pertukaran ini? Mari kita pertimbangkan urutan kejadian potensial yang mungkin terjadi dengan strategi fallback ini. Pertama, pelanggan menggunakan aplikasi tersebut. Tiba-tiba (karena malloc gagal), malloc2 aktif, dan aplikasi melambat. Ini buruk: Apakah berjalan lebih lambat itu tidak apa-apa? Selain itu, masalahnya tidak berhenti di sini. Pertimbangkan bahwa mesin kemungkinan besar kehabisan (atau sangat rendah) memori. Pelanggan saat ini mengalami dua masalah (aplikasi dan mesin lebih lambat) dan bukan satu. Efek samping dari beralih ke malloc2 mungkin bahkan membuat seluruh masalah memburuk. Misalnya, subsistem lain juga mungkin bersaing untuk penyimpanan berbasis SSD yang sama.

Logika fallback juga akan menempatkan muatan yang tidak dapat diprediksi pada sistem. Bahkan logika umum sederhana seperti menulis pesan kesalahan ke log dengan jejak tumpukan tidak berbahaya di permukaan, tetapi jika sesuatu berubah tiba-tiba sehingga menyebabkan kesalahan itu terjadi pada kecepatan tinggi, aplikasi yang terikat CPU mungkin tiba-tiba berubah menjadi aplikasi terikat I/O. Selain itu, jika disk tidak disediakan untuk menangani penulisan pada tingkat itu atau untuk menyimpan jumlah data itu, disk dapat memperlambat atau merusak aplikasi.

Bukan hanya strategi fallback yang memperburuk masalah, hal ini akan muncul sebagai bug laten. Sangat mudah untuk mengembangkan strategi fallback yang jarang memicu dalam produksi. Mungkin perlu bertahun-tahun sebelum bahkan satu mesin pelanggan benar-benar kehabisan memori pada saat yang tepat untuk memicu baris kode tertentu dengan fallback ke malloc2 yang ditunjukkan sebelumnya. Jika ada bug dalam logika fallback atau semacam efek samping yang membuat masalah keseluruhan menjadi lebih buruk, para teknisi yang menulis kode kemungkinan akan lupa bagaimana cara kerjanya, dan kode akan lebih sulit untuk diperbaiki. Untuk aplikasi mesin tunggal, ini mungkin merupakan pertukaran bisnis yang dapat diterima, tetapi dalam sistem terdistribusi, konsekuensinya jauh lebih signifikan, seperti yang akan kita bahas nanti.

Semua masalah ini sangat mengganggu, tetapi dalam pengalaman kami, mereka dapat seringkali diabaikan dengan aman dalam aplikasi mesin tunggal. Solusi paling umum adalah yang disebutkan sebelumnya: Biarkan kesalahan alokasi memori merusak aplikasi. Kode yang mengalokasi memori tersebut memiliki kondisi yang sama dengan mesin lainnya, dan tampaknya mesin lain juga akan gagal dalam kasus ini. Bahkan jika kondisinya tidak sama, aplikasi saat ini akan berada dalam keadaan yang tidak terduga, dan gagal dengan cepat adalah strategi yang baik. Pertukaran bisnis itu masuk akal.

Untuk aplikasi mesin tunggal penting yang harus bekerja jika terjadi kegagalan alokasi memori, salah satu solusinya adalah dengan pra-alokasi semua memori tumpukan pada startup dan tidak pernah bergantung pada malloc lagi, bahkan dalam kondisi kesalahan. Amazon telah menerapkan strategi ini beberapa kali; misalnya, dalam memantau daemon yang dijalankan pada server produksi dan daemon Amazon Elastic Compute Cloud (Amazon EC2) yang memantau lonjakan CPU pelanggan.

Fallback terdistribusi

Di Amazon, kami tidak membiarkan sistem terdistribusi, terutama sistem yang dimaksudkan untuk merespons secara real time, melakukan pertukaran yang sama dengan aplikasi mesin tunggal. Salah satu alasan kami adalah kurangnya jumlah kondisi yang sama dengan pelanggan. Kita dapat mengasumsikan bahwa aplikasi sedang berjalan pada mesin yang terletak di depan pelanggan. Jika aplikasi kehabisan memori, pelanggan mungkin tidak menduga aplikasi akan terus berjalan. Layanan tidak berjalan pada mesin yang digunakan pelanggan secara langsung, sehingga ekspektasinya berbeda. Selain itu, pelanggan biasanya menggunakan layanan dengan tepat karena mereka lebih tersedia dibanding menjalankan aplikasi pada satu server, jadi kita perlu membuatnya seperti demikian. Secara teori, hal ini akan mengarahkan kita untuk mengimplementasikan fallback sebagai cara untuk membuat layanan lebih andal. Sayangnya, fallback terdistribusi memiliki semua masalah yang sama, dan banyak lagi, ketika terjadi kegagalan sistem penting.

Strategi fallback terdistribusi lebih sulit untuk diuji. Layanan fallback lebih rumit daripada kasus aplikasi mesin tunggal, karena beberapa mesin dan layanan hilir berperan dalam kegagalan tersebut. Mode kegagalan itu sendiri, seperti skenario kelebihan beban, sulit untuk direplikasi dalam pengujian, bahkan jika orkestrasi pengujian di beberapa mesin sudah tersedia. Kombinatorika juga meningkatkan jumlah kasus untuk diuji, sehingga Anda membutuhkan lebih banyak tes dan lebih sulit untuk dibuat.

Strategi fallback terdistribusi itu sendiri dapat gagal. Walaupun mungkin tampak bahwa strategi fallback menjamin kesuksesan, dalam pengalaman kami, strategi ini biasanya hanya meningkatkan peluang keberhasilan.

Strategi fallback terdistribusi sering kali membuat pemadaman semakin buruk. Dalam pengalaman kami, strategi fallback meningkatkan cakupan dampak kegagalan serta meningkatkan waktu pemulihan.

Strategi fallback terdistribusi seringkali tidak sepadan dengan risikonya. Seperti halnya malloc2, strategi fallback sering membuat semacam pertukaran; jika tidak, kami akan menggunakannya setiap saat. Mengapa menggunakan fallback yang lebih buruk, ketika ada sesuatu yang salah?

Strategi fallback terdistribusi sering memiliki bug laten yang muncul hanya saat terjadinya serangkaian kejadian kebetulan yang tidak mungkin, berpotensi terjadi selama berbulan-bulan atau bertahun-tahun setelah diperkenalkan.
Pemadaman besar-besaran di dunia nyata yang dipicu oleh mekanisme fallback di situs ritel Amazon menggambarkan semua masalah ini. Pemadaman terjadi sekitar tahun 2001 dan disebabkan oleh fitur baru yang menyediakan kecepatan pengiriman terkini untuk semua produk yang ditampilkan di situs web. Fitur baru tersebut tampak seperti ini:

Pada saat itu, arsitektur situs web hanya memiliki dua tingkatan, dan karena data ini disimpan dalam database rantai pasokan, server web diperlukan untuk melakukan kueri database secara langsung. Tetapi, database tidak dapat mengimbangi volume permintaan dari situs web. Situs web ini memiliki volume lalu lintas yang tinggi, dan beberapa halaman akan menampilkan 25 produk atau lebih, dengan kecepatan pengiriman untuk setiap produk yang ditampilkan sejajar. Sehingga, kami menambahkan lapisan caching yang berjalan sebagai proses terpisah pada setiap server web (mirip Memcached):

Langkah ini berfungsi dengan baik, tetapi tim juga berusaha menangani kasus di mana cache (proses terpisah) gagal karena beberapa alasan. Dalam skenario ini, server web kembali membuat kueri database secara langsung. Dalam pseudo-code, kami menulis:

if (cache_healthy) {
  shipping_speed = get_speed_via_cache(sku);
} else {
  shipping_speed = get_speed_from_database(sku);
}

Kembali ke kueri database langsung adalah solusi intuitif yang berfungsi selama beberapa bulan. Namun akhirnya, semua cache gagal pada waktu yang bersamaan, yang berarti bahwa setiap server web langsung mengakses database. Hal ini menciptakan beban yang cukup untuk sepenuhnya mengunci database. Seluruh situs web mati karena semua proses server web diblokir pada database. Database rantai pasokan ini juga penting untuk pusat pemenuhan, sehingga pemadaman menyebar lebih jauh, dan semua pusat pemenuhan di seluruh dunia terhenti hingga masalah diperbaiki.

Semua masalah yang kami lihat dalam kasus mesin tunggal terdapat dalam kasus terdistribusi dengan konsekuensi yang lebih mengerikan. Sulit untuk menguji kasus fallback terdistribusi; bahkan jika kami telah menyimulasikan kegagalan cache, kami tidak akan menemukan masalah, yang memerlukan kegagalan di beberapa mesin agar dapat memicu. Selain itu, dalam kasus ini strategi fallback itu sendiri memperparah masalah dan lebih buruk dibanding tidak menggunakan strategi fallback sama sekali. Fallback membuat sebagian website padam (tidak bisa menampilkan kecepatan pengiriman) menjadi pemadaman situs total (tidak ada halaman dimuat sama sekali) dan menjatuhkan seluruh jaringan pemenuhan Amazon di backend.

Pemikiran di balik strategi fallback kami dalam kasus ini tidak masuk akal. Jika menyasar database secara langsung lebih dapat diandalkan daripada melalui cache, mengapa repot-repot menggunakan cache sejak awal? Kami takut tidak menggunakan cache akan mengakibatkan kelebihan database, tetapi mengapa repot-repot memiliki kode fallback jika berpotensi berbahaya? Kami mungkin telah melihat kesalahan kami di awal, tetapi bug ini laten, dan situasi tersebut dapat menyebabkan pemadaman terjadi lagi beberapa bulan setelah peluncuran.

Cara Amazon menghindari fallback

Mengingat perangkap yang kami temui dalam fallback terdistribusi, saat ini kami cenderung lebih suka alternatif daripada fallback. Berikut ini garis besarnya.

Meningkatkan keandalan kasus non-fallback

Seperti yang disebutkan sebelumnya, strategi fallback hanya mengurangi kemungkinan kegagalan total. Layanan dapat jauh lebih tersedia jika kode utama (non-fallback) dibuat lebih tangguh. Misalnya, sebagai ganti menerapkan logika fallback antara dua penyimpanan data yang berbeda, tim dapat berinvestasi dalam menggunakan database dengan ketersediaan inheren yang lebih tinggi, seperti Amazon DynamoDB. Strategi ini seringkali berhasil digunakan di seluruh Amazon. Misalnya, pembicaraan ini menjelaskan penggunaan DynamoDB untuk mendayai amazon.com di Prime Day 2017.

Membiarkan pemanggil mengatasi kesalahan

Salah satu solusi untuk kegagalan sistem penting adalah tidak menggunakan fallback, tetapi membiarkan sistem panggilan menangani kegagalan (misalnya, dengan mencoba kembali). Ini adalah strategi yang disukai untuk layanan AWS, di mana CLI dan SDK kami sudah memiliki logika coba lagi bawaan. Jika memungkinkan, kami lebih suka strategi ini, terutama dalam situasi ketika upaya yang cukup telah dilakukan agar kondisi sama dan mengurangi kemungkinan kegagalan kasus utama (dan logika fallback akan sangat tidak mungkin untuk meningkatkan ketersediaan). 

Mendorong data secara proaktif

Langkah lain yang kami gunakan untuk menghindari fallback adalah mengurangi jumlah komponen yang bergerak saat merespons permintaan. Misalnya, jika suatu layanan membutuhkan data untuk memenuhi permintaan, dan data tersebut sudah ada secara lokal (tidak perlu diambil), tidak perlu menggunakan strategi failover. Contoh sukses dari langkah ini adalah implementasi peran AWS Identity and Access Management (IAM) untuk Amazon EC2. Layanan IAM perlu memberikan kredensial yang telah ditandatangani dan dirotasi untuk menjalankan kode pada instans EC2. Untuk menghindari terjadi fallback, kredensial secara proaktif didorong ke setiap instans dan tetap valid selama berjam-jam. Hal ini berarti bahwa permintaan terkait peran IAM tetap berfungsi jika terjadi gangguan dalam mekanisme dorong. 

Mengonversi fallback menjadi failover

Salah satu hal terburuk tentang fallback adalah bahwa fallback tidak digunakan secara teratur dan cenderung gagal atau meningkatkan cakupan dampak ketika dipicu selama pemadaman. Keadaan yang memicu fallback mungkin tidak terjadi secara alami selama berbulan-bulan atau bahkan bertahun-tahun! Untuk mengatasi masalah kegagalan laten dari strategi fallback, penting untuk menggunakannya secara teratur dalam produksi. Layanan harus berjalan di logika fallback dan non-fallback secara berkelanjutan. Hal ini bukan hanya menjalankan kasus fallback, tetapi juga memperlakukannya secara setara sebagai sumber data yang valid. Misalnya, layanan mungkin secara acak memilih antara respons fallback dan non-fallback (ketika terdapat keduanya) untuk memastikan keduanya berfungsi. Tetapi pada titik ini, strategi tidak lagi dapat dianggap sebagai fallback dan secara jelas masuk ke kategori failover.

Memastikan bahwa percobaan ulang dan batas waktu tidak menjadi fallback

Percobaan ulang dan batas waktu dibahas di artikel Batas Waktu, Coba Lagi, dan Mundur bersama Jitter. Artikel tersebut menjelaskan bahwa percobaan ulang adalah mekanisme yang andal untuk memberikan ketersediaan tinggi saat menghadapi kesalahan sementara dan acak. Dengan kata lain, percobaan ulang dan batas waktu memberikan asuransi terhadap kegagalan tidak berkala akibat masalah kecil seperti kehilangan paket palsu, kegagalan mesin tunggal yang tidak berkorelasi, dan sejenisnya. Namun percobaan ulang dan batas waktu bisa saja salah. Seringkali layanan berlangsung berbulan-bulan atau lebih lama tanpa perlu banyak percobaan ulang, dan hal ini mungkin akhirnya muncul selama skenario yang tim Anda belum pernah uji. Karena alasan ini, kami mempertahankan metrik yang memantau tingkat percobaan ulang secara keseluruhan dan alarm yang mengingatkan tim kami jika percobaan ulang sering terjadi.

Salah satu cara lain untuk menghindari percobaan ulang menjadi fallback adalah dengan menjalankannya sepanjang waktu dengan percobaan ulang proaktif (juga dikenal sebagai hedging atau permintaan paralel). Teknik ini secara inheren dibangun di dalam sistem yang melakukan kuorum membaca atau menulis, di mana suatu sistem mungkin memerlukan jawaban dari dua dari tiga server untuk merespons. Percobaan ulang proaktif mengikuti pola desain pekerjaan konstan. Karena permintaan redundansi selalu dibuat, tidak ada tambahan muatan dari percobaan ulang yang ditambahkan ke sistem saat kebutuhan permintaan redundansi meningkat.

Penutup

Di Amazon, kami menghindari fallback dalam sistem kami karena sulit untuk dibuktikan dan efektivitasnya sulit untuk diuji. Strategi fallback memperkenalkan mode operasional di mana sistem masuk hanya pada saat-saat paling kacau ketika beberapa hal mulai bermasalah, dan beralih ke mode ini hanya menambah kekacauan. Sering terjadi penundaan yang lama antara waktu strategi fallback yang diterapkan dan waktu yang ditemui dalam lingkungan produksi.

Sebagai gantinya, kami lebih memilih jalur kode yang digunakan dalam produksi secara terus-menerus dibandingkan yang jarang. Kami berfokus pada peningkatan ketersediaan sistem utama kami, menggunakan pola seperti mendorong data ke sistem yang membutuhkannya alih-alih menarik dan mempertaruhkan kegagalan panggilan jarak jauh pada waktu yang krusial. Terakhir, kami memerhatikan perilaku samar dalam kode kami yang dapat mengubahnya menjadi mode operasi seperti fallback, seperti melakukan terlalu banyak percobaan ulang.

Jika fallback penting dalam sistem, kami menggunakannya sesering mungkin dalam produksi, sehingga fallback berperilaku andal dan dapat diprediksi, sama seperti mode pengoperasian utama.


Tentang penulis

Jacob Gabrielsonr adalah Senior Principal Engineer di Amazon Web Services. Ia telah bekerja di Amazon selama 17 tahun, terutama pada platform layanan mikro internal. Selama 8 tahun terakhir ia telah bekerja di EC2 dan ECS, termasuk sistem penerapan perangkat lunak, layanan plane kendali, Spot market, Lightsail, dan yang terakhir, kontainer. Pemrograman sistem, bahasa pemrograman, dan komputasi terdistribusi menjadi bidang yang diminatinya. Ia paling tidak suka dengan perilaku sistem mode ganda, terutama dalam kondisi kegagalan. Ia meraih gelar sarjana dalam bidang Ilmu Komputer dari University of Washington di Seattle.

Batas waktu, percobaan ulang, dan kemunduran dengan gangguan Tantangan dan strategi Caching