Kegagalan Terjadi

Setiap kali sebuah layanan atau sistem memanggil layanan atau sistem lainnya, kegagalan dapat terjadi. Kegagalan tersebut dapat disebabkan oleh beragam faktor. Faktor tersebut meliputi server, jaringan, penyeimbang muatan, perangkat lunak, sistem operasi, atau bahkan kesalahan dari operator sistem. Kami merancang sistem untuk mengurangi probabilitas kegagalan, tetapi tidak mungkin membuat sistem yang tidak akan pernah gagal. Jadi di Amazon, kami mendesain sistem untuk menoleransi dan mengurangi probabilitas kegagalan, serta menghindari memperluas persentase kegagalan kecil menjadi pemadaman sepenuhnya. Untuk membangun sistem yang tangguh, kami menggunakan tiga alat penting: batas waktu, percobaan ulang, dan kemunduran.

Banyak jenis kegagalan menjadi tampak jelas saat permintaan membutuhkan waktu lebih lama daripada biasanya, dan berpotensi tidak akan pernah selesai. Saat klien menunggu lebih lama daripada biasanya untuk menyelesaikan sebuah permintaan, klien juga menahan sumber daya yang digunakannya untuk permintaan tersebut dalam waktu yang lebih lama. Saat sejumlah permintaan menahan sumber daya dalam waktu yang lama, server dapat kehabisan sumber daya tersebut. Sumber daya ini dapat meliputi memori, thread, koneksi, port ephemeral, atau hal lain yang dibatasi. Untuk menghindari situasi ini, klien mengatur batas waktu. Batas waktu adalah lamanya waktu maksimum klien menunggu permintaan selesai.

Seringnya, mencoba ulang permintaan yang sama akan menyebabkan permintaan berhasil. Ini terjadi karena jenis sistem yang kita buat tidak sering gagal sebagai unit tunggal. Jenis sistem tersebut justru mengalami kegagalan parsial atau sementara. Kegagalan parsial adalah saat persentase permintaan berhasil. Kegagalan sementara adalah saat permintaan mengalami kegagalan dalam waktu singkat. Percobaan ulang memungkinkan klien berhasil melewati kegagalan parsial acak dan kegagalan sementara dengan usia pendek ini dengan mengirim permintaan yang sama lagi.

Percobaan ulang tidak selalu aman. Percobaan ulang dapat meningkatkan muatan pada sistem yang dipanggil, jika sistem telah mengalami kegagalan karena hampir mengalami kelebihan muatan. Untuk menghindari masalah ini, kami mengimplementasikan klien kami untuk menggunakan kemunduran. Ini akan meningkatkan waktu antara percobaan ulang berikutnya, yang memastikan muatan pada backend tetap seimbang. Masalah lain dengan percobaan ulang adalah beberapa panggilan jarak jauh memiliki efek samping. Batas waktu atau kegagalan tidak selalu berarti efek samping belum terjadi. Jika melakukan beberapa efek samping memang diinginkan, praktik terbaik adalah merancang API agar idempoten, yang berarti API dapat dicoba ulang dengan aman.

Terakhir, lalu lintas tidak memasuki layanan Amazon dalam laju yang konstan. Laju kedatangan permintaan justru secara rutin memiliki lonjakan besar. Lonjakan ini dapat disebabkan oleh perilaku klien, pemulihan kegagalan, dan bahkan dengan hal sederhana seperti tugas cron berkala. Jika kesalahan disebabkan oleh muatan, percobaan ulang dapat menjadi tidak efektif jika semua klien melakukan percobaan ulang pada saat yang bersamaan. Untuk menghindari masalah ini, kami menggunakan gangguan. Ini adalah lama waktu acak sebelum membuat atau mencoba ulang permintaan untuk membantu mencegah lonjakan besar dengan menyebar laju kedatangan.

Setiap solusi ini dibahas dalam bagian berikutnya.

Batas Waktu

Praktik terbaik di Amazon adalah mengatur batas waktu pada setiap panggilan jarak jauh, dan secara umum pada setiap panggilan di seluruh proses bahkan pada kotak yang sama. Ini meliputi batas waktu koneksi dan batas waktu permintaan. Banyak klien standar menawarkan kemampuan batas waktu bawaan yang tangguh.
Umumnya, masalah yang paling sulit adalah memilih nilai batas waktu untuk diatur. Mengatur batas waktu terlalu tinggi akan mengurangi kegunaannya, karena sumber daya masih digunakan saat klien menunggu batas waktu. Mengatur batas waktu terlalu rendah memiliki dua risiko:
 
• Peningkatan lalu lintas pada backend dan peningkatan latensi karena terlalu banyak permintaan yang dicoba ulang.
• Peningkatan latensi backend kecil menyebabkan pemadaman total, karena semua permintaan mulai dicoba ulang.
 
Praktik yang baik untuk memilih batas waktu untuk panggilan dalam Wilayah AWS adalah memulai dengan metrik latensi layanan downstream. Jadi di Amazon, saat kami membuat satu panggilan layanan ke layanan lainnya, kami memilih laju batas waktu palsu yang dapat diterima (misalnya, 0,1%). Kemudian kami melihat persentil latensi yang sesuai pada layanan downstream (dalam contoh ini p99.9). Pendekatan ini berfungsi dengan baik dalam sebagian besar kasus, tetapi ada beberapa kesulitan, seperti yang dijelaskan berikut:
 
• Pendekatan ini tidak berfungsi di kasus-kasus dengan klien memiliki latensi jaringan yang substansial, misalnya melalui internet. Dalam kasus ini, kami mempertimbangkan latensi jaringan kemungkinan terburuk yang masuk akal, sambil tetap mengingat bahwa klien dapat menjangkau seluruh area.
• Pendekatan ini juga tidak berfungsi dengan layanan yang memiliki batas latensi yang ketat, dengan p99.9 dekat dengan p50. Dalam kasus ini, menambahkan sejumlah ruang kosong akan membantu kita terhindar dari kenaikan latensi kecil yang menyebabkan banyak batas waktu.
• Kami menghadapi kesulitan yang lazim terjadi saat mengimplementasikan batas waktu. SO_RCVTIMEO dari Linux andal, tetapi memiliki beberapa kerugian yang membuatnya tidak sesuai sebagai batas waktu socket menyeluruh. Beberapa bahasa, seperti Java, mengekspos kendali ini secara langsung. Bahasa lain, seperti Go, memberikan mekanisme batas waktu yang tangguh.
• Terdapat juga implementasi ketika batas waktu tidak mencakup semua panggilan jarak jauh, seperti handshake DNS atau TLS. Secara umum, kami memilih untuk menggunakan batas waktu yang disertakan dalam klien yang diuji dengan baik. Jika kita mengimplementasikan batas waktu kita sendiri, kita sangat memerhatikan arti sebenarnya dari opsi socket batas waktu, dan pekerjaan apa yang sedang dikerjakan.
 
Dalam satu sistem yang saya kerjakan di Amazon, kami melihat beberapa batas waktu yang bicara ke dependensi segera setelah penerapan. Batas waktunya diatur sangat rendah, hingga sekitar 20 milidetik. Di luar penerapan, bahkan dengan nilai batas waktu rendah ini, kami tidak melihat batas waktu terjadi secara berkala. Setelah menelitinya, saya menemukan bahwa pengatur waktu yang disertakan membuat koneksi aman baru, yang digunakan kembali pada permintaan berikutnya. Karena pembuatan koneksi memerlukan waktu lebih dari 20 milidetik, kami melihat beberapa jumlah batas waktu permintaan saat server baru memasuki layanan setelah penerapan. Dalam beberapa kasus, permintaan dicoba ulang dan berhasil. Awalnya kami berusaha menyelesaikan masalah ini dengan meningkatkan nilai batas waktu seandainya koneksi dibuat. Kemudian, kami meningkatkan sistem dengan membuat koneksi ini saat sebuah proses dimulai, tetapi sebelum menerima lalu lintas. Ini membuat kami berhasil menyelesaikan masalah batas waktu sekaligus.

Percobaan ulang dan kemunduran

Percobaan ulang bersifat “egois.” Dengan kata lain, saat satu klien mencoba ulang, klien tersebut menghabiskan lebih banyak waktu server untuk mendapatkan peluang berhasil yang lebih tinggi. Ini bukan masalah jika kegagalan jarang terjadi atau bersifat sementara. Ini karena angka keseluruhan permintaan yang dicoba ulang kecil, dan pertukaran meningkatkan ketersediaan yang jelas bekerja dengan baik. Saat kegagalan disebabkan oleh kelebihan muatan, percobaan ulang yang meningkatkan muatan dapat membuat banyak hal menjadi jauh bertambah buruk. Hal ini bahkan dapat menunda pemulihan dengan membuat muatan tetap tinggi setelah masalah awal diselesaikan. Percobaan ulang serup dengan obat yang manjur -- berguna dalam dosis yang tepat, tetapi dapat menyebabkan kerusakan besar jika digunakan terlalu berlebihan. Sayangnya, dalam sistem terdistribusi, nyaris tidak ada cara untuk berkoordinasi antara semua klien untuk mencapai angka percobaan ulang yang tepat.

Solusi pilihan yang kami gunakan di Amazon adalah kemunduran. Sebagai ganti dengan segera dan agresif mencoba ulang, klien menunggu beberapa saat di antara percobaan ulang. Pola yang paling umum adalah kemunduran eksponensial, dengan waktu tunggu meningkat secara eksponensial setelah setiap percobaan. Kemunduran eksponensial dapat menyebabkan waktu kemunduran yang amat panjang, karena fungsi eksponensial tumbuh dengan cepat. Untuk menghindari mencoba ulang terlalu lama, implementasi umumnya memberi batas untuk kemundurannya ke nilai maksimum. Ini sudah bisa ditebak disebut kemunduran eksponensial dengan batasan. Meski demikian, kemunduran ini menyebabkan masalah lainnya. Kini semua klien terus-menerus mencoba di laju yang diberi batas. Dalam hampir semua kasus, solusi kami adalah membatasi berapa kali klien mencoba ulang, dan menangani kegagalan yang dihasilkan sebelumnya dalam arsitektur berorientasi layanan. Dalam sebagian besar kasus, klien akan menyerah dalam panggilan, karena memiliki batas waktunya sendiri.

Terdapat masalah lain dengan percobaan ulang, seperti yang dijelaskan berikut:

• Sistem terdistribusi sering memiliki beberapa lapisan. Pertimbangkan sebuah sistem dengan panggilan pelanggan menyebabkan lima tumpukan panggilan layanan. Ini diakhiri dengan kueri ke database, dan tiga percobaan ulang di setiap lapisan. Apa yang terjadi saat database mulai memutuskan kueri di bawah muatan gagal? Jika setiap lapisan mencoba ulang secara independen, muatan pada database akan meningkat 243x, membuatnya cenderung tidak akan pernah pulih. Ini karena percobaan ulang pada setiap lapisan akan dilipatgandakan -- tiga percobaan ulang pertama, kemudian sembilan percobaan ulang, dan seterusnya. Di sisi lain, percobaan ulang pada lapisan tertinggi dalam tumpukan dapat menyia-nyiakan pekerjaan dari panggilan sebelumnya, yang mengurangi efisiensi. Secara umum, untuk bidang kendali biaya rendah dan pengoperasian bidang data, praktik terbaik kami adalah mencoba ulang pada satu titik di tumpukan.
• Muatan. Bahkan dengan satu lapisan percobaan ulang, lalu lintas akan secara signifikan meningkat saat kesalahan dimulai. Sakelar pemutus tenaga, tempat panggilan ke layanan downstream berhenti sepenuhnya saat ambang batas kesalahan terlampaui, didukung penuh untuk mengatasi masalah ini. Sayangnya, sakelar pemutus tenaga menyebabkan perilaku mode ke dalam sistem yang sulit untuk diuji, dan dapat menyebabkan tambahan waktu yang signifikan untuk pemulihan. Kami telah menemukan bahwa kita dapat memitigasi risiko ini dengan membatasi percobaan ulang secara lokal menggunakan bucket token. Ini juga memungkinkan semua panggilan untuk dicoba ulang selama ada token, kemudian mencoba ulang pada laju tetap setelah token tidak mampu lagi. AWS menambahkan perilaku ini ke AWS SDK pada tahun 2016. Jadi pelanggan yang menggunakan SDK ini memiliki perilaku throttling bawaan ini.
• Memutuskan kapan harus mencoba ulang. Secara umum, pandangan kami adalah bahwa API dengan efek samping tidak aman untuk dicoba ulang kecuali API tersebut memberikan idempotensi. Ini menjamin efek samping hanya terjadi satu kali, seberapa sering pun Anda mencoba ulang. API hanya baca umumnya idempoten, sedangkan API pembuatan sumber daya mungkin tidak demikian. Beberapa API, seperti API RunInstances Amazon Elastic Compute Cloud (Amazon EC2), memberikan mekanisme berbasis token eksplisit untuk menyediakan idempotensi dan membuatnya aman untuk dicoba ulang. Desain API yang baik dan kehati-hatian saat mengimplementasikan klien, diperlukan guna mencegah efek samping duplikat.
• Mengetahui kegagalan mana yang layak dicoba ulang. HTTP memberikan perbedaan jelas antara kesalahan klien dan server. HTTP mengindikasikan bahwa kesalahan klien sebaiknya tidak dicoba ulang dengan permintaan yang sama karena tidak akan berhasil nanti, sedangkan kesalahan server dapat berhasil pada percobaan berikutnya. Sayangnya, konsistensi akhir dalam sistem secara signifikan mengaburkan batasan ini. Kesalahan klien di satu waktu dapat berubah menjadi keberhasilan di lain waktu seiring penyebaran kondisi.

Terlepas dari risiko dan tantangan ini, percobaan ulang adalah mekanisme yang andal untuk memberikan ketersediaan tinggi saat menghadapi kesalahan sementara dan acak. Penilaian dibutuhkan untuk menemukan pertukaran yang tepat untuk setiap layanan. Dalam pengalaman kami, titik yang baik untuk memulai adalah mengingat bahwa percobaan ulang bersifat egois. Percobaan ulang adalah cara klien menegaskan pentingnya permintaan dan meminta agar kayanan menghabiskan lebih banyak sumber dayanya untuk menangani permintaan tersebut. Dapat timbul berbagai masalah jika klien terlalu egois.

Gangguan

Saat kegagalan disebabkan oleh kelebihan muatan atau ketidaksesuaian, kemunduran sering tidak membantu seperti yang diharapkan. Ini disebabkan oleh korelasi. Jika semua panggilan yang gagal mundur di saat yang bersamaan, mereka akan menyebabkan ketidaksesuaian atau kelebihan muatan lagi saat dicoba kembali. Solusi kami adalah gangguan. Gangguan menambahkan sejumlah keacakan ke kemunduran untuk menyebar percobaan kembali dalam satu waktu. Untuk informasi lebih lanjut mengenai berapa banyak gangguan yang harus ditambahkan dan cara terbaik untuk menambahkannya, lihat Kemunduran dan Gangguan Ekponensial.

Gangguan tidak hanya untuk percobaan ulang. Pengalaman operasional telah mengajarkan kita bahwa lalu lintas ke layanan kita, termasuk bidang kendali dan bidang data, cenderung akan mengalami lonjakan besar. Lonjakan lalu lintas ini dapat berlangsung singkat, dan sering tersembunyi oleh metrik agregat. Saat membangun sistem, kami mempertimbangkan menambahkan sejumlah gangguan ke pekerjaan sepanjang waktu, berkala, dan pekerjaan tertunda lainnya. Ini membantu menyebar lonjakan pekerjaan, dan memudahkan layanan downstream diskalakan untuk beban kerja.

Saat menambahkan gangguan ke pekerjaan terjadwal, kami tidak memilih gangguan pada setiap host secara acak. Sebagai gantinya, kami menggunakan metode konsisten yang menghasilkan angka yang sama setiap kali pada host yang sama. Dengan cara ini, jika ada layanan yang mengalami kelebihan muatan, atau kondisi kompetisi, ini akan terjadi dengan cara yang sama dalam pola. Kita manusia dapat mengidentifikasi pola dengan baik, dan lebih cenderung menentukan akar masalahnya. Menggunakan metode acak akan memastikan jika sumber daya mengalami kewalahan, ini hanya terjadi, ya, secara acak. Ini membuat penyelesaian masalah menjadi jauh lebih sulit.

Pada sistem yang saya gunakan untuk bekerja, seperti Amazon Elastic Block Store (Amazon EBS) dan AWS Lambda, kami menemukan bahwa klien sering mengirim permintaan dengan interval reguler, seperti satu menit sekali. Meski demikian, saat klien memiliki beberapa server yang berperilaku sama, mereka dapat berbaris dan memicu permintaan mereka di saat yang bersamaan. Ini bisa jadi beberapa detik pertama dalam satu menit, atau beberapa detik pertama setelah tengah malam untuk pekerjaan harian. Dengan memerhatikan muatan per detik, dan bekerja sama dengan klien untuk mengganggu beban kerja berkala mereka, kami menyelesaikan jumlah pekerjaan yang sama dengan kapasitas server lebih sedikit.

Kami memiliki kontrol yang lebih sedikit terhadap lalu lintas pelanggan. Meski demikian, bahkan untuk tugas yang dipicu oleh pelanggan, adalah ide yang bagus untuk menambahkan gangguan yang tidak memengaruhi pengalaman pelanggan.

Penutup

Dalam sistem terdistribusi, kegagalan atau latensi sementara dalam interaksi jarak jauh tidak terhindarkan. Batas waktu memastikan sistem tidak menggantung terlalu lama tanpa alasan, percobaan ulang dapat menutup kegagalan tersebut, dan kemunduran serta gangguan dapat meningkatkan utilisasi dan mengurangi kemacetan dalam sistem.

Di Amazon, kami telah belajar bahwa penting untuk bersikap hati-hati dengan percobaan ulang. Percobaan dapat memperkuat beban kerja pada sistem dependen. Jika panggilan ke sistem melebihi batas waktu, dan jika sistem mengalami kelebihan muatan, percobaan ulang dapat membuat kelebihan menjadi semakin buruk, bukan semakin baik. Kami menghindari penguatan ini dengan hanya melakukan percobaan ulang saat kami melihat bahwa dependensi sehat. Kami menghentikan percobaan ulang saat percobaan ulang tidak membantu meningkatkan ketersediaan.


Tentang penulis

Marc Brooker adalah Senior Principal Engineer di Amazon Web Services. Dia telah bekerja di AWS sejak 2008 di berbagai layanan termasuk EC2, EBS, dan IoT. Saat ini, dia berfokus pada AWS Lambda, termasuk pekerjaan penskalaan dan virtualisasi. Marc sangat menyukai membaca COE dan pascamortem. Dia bergelar PhD dalam bidang teknik listrik.

Tantangan dengan sistem terdistribusi Menggunakan pelepasan muatan untuk mencegah kelebihan muatan Menghindari fallback pada sistem terdistribusi