Apakah Anda ingin mendapat pemberitahuan tentang konten baru?
Algoritme meniru kehidupan
Sejak mata kuliah ilmu komputer pertama saya di kampus, saya merasa tertarik dengan bagaimana algoritme berperan dalam dunia nyata. Saat berpikir tentang beberapa hal yang terjadi di dunia nyata, kita dapat menghasilkan algoritme yang dapat meniru hal tersebut. Saya melakukan ini khususnya saat terjebak dalam antrean, seperti di toko, saat macet, atau di bandara. Saya menyadari bahwa merasa bosan saat berdiri dalam antrean memberikan peluang besar untuk merenungkan teori antrean.
Lebih dari satu dekade lalu, sepanjang hari saya bekerja di sebuah pusat pemenuhan Amazon. Saya dipandu oleh algoritme, mengambil item dari rak, memindahkan item dari satu kotak ke kotak lain, memindahkan tempat sampah. Bekerja secara paralel dengan begitu banyak orang, menurut saya sangat menyenangkan dapat menjadi bagian dari sesuatu yang pada dasarnya merupakan semacam penggabungan fisik yang diatur dengan luar biasa.
Dalam teori antrean, perilaku mengantre saat antrean pendek bukanlah hal yang cukup menarik. Namun, jika antreannya pendek, semua orang merasa bahagia. Hanya pada saat antrean di-backlog, ketika antrean ke sebuah acara mulai habis, barulah orang-orang mulai berpikir tentang throughput dan prioritas.
Dalam artikel ini, saya mendiskusikan strategi yang kami gunakan di Amazon untuk menghadapi skenario backlog antrean – pendekatan desain yang kami ambil untuk menguras antrean dengan cepat dan untuk menentukan prioritas beban kerja. Yang paling penting, saya menjelaskan cara mencegah backlog antrean terjadi. Pada setengah bagian pertama, saya menjelaskan deskripsikan skenario yang menyebabkan backlog, dan pada setengah bagian kedua, saya menjelaskan beragam pendekatan yang Amazon gunakan untuk menghindari backlog atau menanganinya dengan baik.
Sifat duplikat antrean
Sistem berbasis antrean
Untuk mengilustrasikan sistem berbasis antrean dalam artikel ini, saya akan membahas tentang bagaimana dua layanan AWS bekerja di balik layar: AWS Lambda, sebuah layanan yang menjalankan kode Anda sebagai respons dari kejadian dan Anda tidak perlu mengkhawatirkan infrastruktur yang menjalankannya; serta AWS IoT Core, sebuah layanan terkelola yang memungkinkan perangkat terhubung berinteraksi dengan aplikasi cloud serta perangkat lain secara mudah juga aman.
Dengan AWS Lambda, Anda mengunggah kode fungsi, lalu mengaktifkan fungsi Anda dengan satu dari dua cara:
• Secara sinkron: ketika output fungsi Anda dikembalikan dalam respons HTTP
• Secara asinkron: ketika respons HTTP dikembalikan segera, dan fungsi Anda dijalankan serta dicoba ulang di balik layar
Lambda memastikan fungsi Anda dijalankan, bahkan saat terjadi kegagalan server, sehingga memerlukan antrean yang tahan lama untuk menyimpan permintaan Anda. Dengan antrean yang tahan lama, permintaan Anda dapat didorong ulang jika fungsi gagal untuk pertama kali.
Dengan AWS IoT Core, perangkat dan aplikasi Anda saling terhubung serta dapat berlangganan ke topik pesan PubSub. Saat perangkat atau aplikasi memublikasikan sebuah pesan, aplikasi dengan langganan yang sesuai menerima salinan pesan mereka sendiri. Banyak perpesanan PubSub ini yang terjadi secara asinkron, karena perangkat IoT yang dibatasi tidak ingin menghabiskan sumber dayanya yang terbatas untuk menunggu demi meastikan seluruh perangkat, aplikasi, dan sistem langganan menerima salinan. Hal ini sangat penting karena perangkat berlangganan mungkin offline saat perangkat lain memublikasikan pesan yang menarik baginya. Saat perangkat offline terhubung kembali, perangkat diharapkan untuk meningkatkan kecepatan terlebih dahulu, lalu menerima pesannya nanti (untuk informasi tentang pengkodean sistem Anda guna mengelola pengiriman pesan setelah terhubung kembali, lihat Sesi Persisten MQTT di Panduan Pengembang AWS IoT). Terdapat beragam jenis pemrosesan persisten dan asinkron yang berjalan di balik layar untuk mewujudkan hal tersebut.
Sistem berbasis antrean seperti ini sering diterapkan dengan antrean yang tahan lama. SQS menawarkan semantik pengiriman sedikitnya satu pesan yang tahan lama dan dapat diskalakan, sehingga tim Amazon termasuk Lambda dan IoT secara rutin menggunakannya saat membangun sistem asinkron yang dapat diskalakan. Dalam sistem berbasis antrean, komponen memproduksi data dengan memasukkan pesan ke dalam antrean, dan komponen lain mengonsumsi data tersebut dengan secara berkali meminta pesan, pesan pemrosesan, dan akhirnya menghapusnya setelah selesai.
Kegagalan dalam sistem asinkron
Di AWS Lambda, jika permintaan fungsi Anda lebih lambat dari kondisi normal (contohnya, karena ketergantungan), atau jika mengalami kegagalan sementara, tidak akan ada data yang hilang dan Lambda akan mencoba kembali fungsi Anda. Lambda memasukkan panggilan pemintaan Anda ke dalam antrean, dan saat fungsi mulai berjalan kembali, Lambda mengerjakan backlog fungsi Anda. Tetapi, mari pertimbangkan waktu yang dibutuhkan untuk mengerjakan backlog dan kembali ke kondisi normal.
Bayangkan sebuah sistem yang mengalami pemadaman selama satu jam saat memproses pesan. Terlepas dari laju yang ditentukan dan kapasitas pemrosesan, pemulihan akibat pemadaman memerlukan dua kali lipat kapasitas sistem untuk satu jam berikutnya setelah pemadaman. Dalam praktiknya, sistem mungkin memiliki lebih dari dua kali lipat kapasitas yang tersedia, khususnya dengan layanan elastis seperti Lambda, dan pemulihan dapat berlangsung dengan cepat. Sebaliknya, sistem lain yang berinteraksi dengan fungsi Anda mungkin tidak siap untuk menangani peningkatan besar dalam pemrosesan saat Anda mengerjakan backlog. Memerlukan lebih banyak waktu untuk mengejar ketertinggalan ketika hal ini terjadi. Layanan asinkron menumpuk backlog selama pemadaman, menyebabkan lamanya waktu pemulihan, tidak seperti layanan sinkron, yang menjatuhkan permintaan selama pemadaman namun memiliki waktu pemulihan yang lebih cepat.
Selama bertahun-tahun, saat berpikir tentang antrean, terkadang kita tergoda untuk berpikir bahwa latensi tidaklah penting bagi sistem asinkron. Sistem asinkron sering diciptakan karena ketahanannya, atau untuk mengisolasi pemanggil langsung dari latensi. Namun, pada praktiknya kita sering melihat bahwa waktu pemrosesan ternyata penting, dan bahkan sistem asinkron diharapkan untuk memiliki subdetik atau latensi yang lebih baik. Saat antrean diperkenalkan untuk ketahanannya, mudah untuk melewatkan dampak yang menyebabkan pemrosesan latensi yang tinggi saat dihadapkan dengan backlog. Risiko tersembunyi dengan sistem asinkron adalah berhadapan dengan backlog besar.
Bagaimana kami mengukur ketersediaan dan latensi
Diskusi tentang menukar latensi dengan ketersediaan ini memunculkan pertanyaan menarik: bagaiman kami mengukur dan mengatur sasaran yang terkait dengan latensi dan ketersediaan untuk layanan asinkron? Mengukur tingkat kesalahan dari perspektif produsen memberikan kita bagian dari gambaran ketersediaan, tetapi tidak banyak. Ketersediaan produsen sebanding dengan ketersediaan antrean sistem yang kita gunakan. Jadi saat membangun di SQS, ketersediaan produsen kita sesuai dengan ketersediaan SQS.
Di pihak lain, jika mengukur ketersediaan dari sisi konsumen, yang dapat membuat ketersediaan sistem tampak lebih buruk dibanding kondisi aktual adalah, karena kegagalan dapat dicoba ulang lalu berhasil pada percobaan berikutnya.
Kami juga mengambil pengukuran ketersediaan dari dead-letter queue (DLQ). Jika sebuah pesan kehabisan percobaan ulang, maka akan diturunkan atau dimasukkan ke dalam DLQ. DLQ hanya antrean terpisah yang digunakan untuk menyimpan pesan yang tidak dapat diproses untuk investigasi dan intervensi kelak. Tingkat pesan DLQ atau yang dijatuhkan adalah pengukuran ketersediaan yang baik, namun terlambat dalam mendeteksi masalah. Meskipun mengaktifkan volume DLQ merupakan ide bagus, informasi DLQ akan tiba terlambat jika kita hanya mengandalkannya untuk mendeteksi masalah.
Bagaimana dengan latensi? Sekali lagi, latensi yang diamati oleh produsen mencerminkan latensi layanan antrean itu sendiri. Oleh karena itu, kita lebih berfokus pada pengukuran usia pesan yang ada dalam antrean. Tindakan ini dengan cepat menangkap kasus ketika sistem tertinggal, atau sering terjadi kesalahan dan menyebabkan percobaan ulang. Layanan seperti SQS menyediakan tanda waktu saat pesan diba di antrean. Dengan informasi tanda waktu, setiap kali kita mengeluarkan pesan dari antrean, kita dapat membuat log dan memproduksi metrik mengenai seberapa jauh sistem kita tertinggal.
Masalah latensi dapat menjadi lebih sedikit bernuansa. Lagipula, backlog memang diharapkan, dan nyatanya tidak masalah bagi beberapa pesan. Contohnya, di AWS IoT, ada saat ketika perangkat diharapkan untuk menjadi offline atau menjadi lambat untuk membaca pesannya. Ini karena banyak perangkat IoT berdaya rendah dan memiliki konektivitas internet yang tidak stabil. Sebagai operator AWS IoT Core, kita harus mampu membedakan antara backlog kecil yang diharapkan dan disebabkan oleh perangkat menjadi offline atau memilih untuk membaca pesan secara perlahan, dengan backlog di seluruh sistem yang tidak diharapkan.
Di AWS IoT, kita menginstrumentasikan layanan dengan metrik lain: AgeOfFirstAttempt. Pengukuran ini mencatat waktu penambahan pesan ke antrean dikurangi waktu saat ini, tapi hanya jika ini adalah kali pertama AWS IoT berusaha mengirim pesan ke perangkat. Dengan begitu, ketika perangkat dicadangkan, kami memiliki metrik bersih yang tidak tercemar oleh perangkat yang mecoba ulang pesan atau menambahkan ke antrean. Untuk membuat metrik menjadi jauh lebih bersih, kami mengeluarkan metrik kedua – AgeOfFirstSubscriberFirstAttempt. Dalam sistem PubSub seperti AWS IoT, tidak ada batas praktis bagi berapa banyak perangkat atau aplikasi yang dapat berlangganan topik tertentu, jadi letensi lebih tinggi saat mengirim pesan ke jutaan perangkat dibandingkan saat mengirim pesan ke satu perangkat. Untuk menghasilkan metrik yang stabil, kami mengeluarkan metrik pengatur waktu pada percobaan pertama untuk memublikasikan pesan ke pelanggan pertama topik tersebut. Kemudian kami memiliki metrik lain untuk mengukur kemajuan sistem dalam memublikasikan pesan yang tersisa.
Metrik AgeOfFirstAttempt berfungsi sebagai peringatan awal bagi masalah di penjuru sistem, dalam bagian besar karena metrik ini memfilter noise dari perangkat yang memilih untuk membaca pesan mereka secara lebih perlahan. Layak untuk disebutkan bahwa sistem seperti AWS IoT diinstrumenkan dengan lebih banyak metrik dibandingkan dengan ini. Namun dengan seluruh metrik terkait latensi yang tersedia, strategi untuk mengategorikan latensi upaya pertama agar terpisah dari latensi upaya pengulangan digunakan secara umum di seluruh Amazon.
Mengukur latensi dan ketersediaan sistem asinkron cukup menantang, dan melakukan debug juga tidak mudah, karena permintaan memantul di antara server dan dapat tertunda pada tempat di luar tiap sistem. Untuk membantu pelacakan terdistribusi, kami menyebarkan id permintaan di seluruh pesan yang berada dalam antrean sehingga kami dapat menyatukan semuanya. Kami umumnya menggunakan sistem seperti X-Ray untuk membantu dalam hal ini.
Backlog dalam sistem asinkron multipenyewa
Banyak sistem asinkron bersifat multipenyewa, menangani pekerjaan atas nama banyak pelanggan. Hal ini menambah kerumitan dimensi untuk mengelola latensi dan ketersediaan. Keuntungan multipenyewa adalah menghemat overhead pengoperasian karena tidak perlu mengoperasikan beberapa armada secara terpisah, dan memungkinkan kami menjalankan beban kerja yang digabungkan dengan penggunaan sumber daya yang jauh lebih tinggi. Meski demikian, pelanggan mengharapkannya untuk berperilaku seperti sistem penyewa tunggal milik mereka sendiri, dengan latensi yang dapat diprediksi dan ketersediaan yang tinggi, terlepas dari beban kerja pelanggan lain.
Layanan AWS tidak mengungkapkan antrean internal secara langsung bagi pemanggil untuk memasukkan pesan. Sebagai gantinya, mereka menerapkan API ringan untuk mengautentikasi pemanggil dan melampirkan informasi pemanggil ke tiap pesan sebelum ditambahkan ke antrean. Tindakan ini serupa dengan arsitektur Lambda yang dijelaskan sebelumnya: saat Anda memanggil fungsi secara asinkron, Lambda memasukkan pesan Anda ke antrean milik Lambda dan langsung kembali, bukan mengungkapkan antrean internal Lambda kepada Anda secara langsung.
API yang ringan ini juga memungkinkan kita menambahkan pembatasan kesetaraan. Kesetaraan pada sistem multipenyewa sangatlah penting agar tidak ada beban kerja pelanggan yang memengaruhi pelanggan lain. Cara umum yang AWS terapkan dalam kesetaraan adalah dengan mengatur batas berbasis tarif per pelanggan, dengan beberapa fleksibilitas untuk lonjakan. Di banyak sistem kami, contohnya di SQS, kami meningkatkan batas per pelanggan saat pelanggan tumbuh secara organik. Pembatasan ini berfungsi sebagai pagar pembatas untuk lonjakan tidak terduga, memberikan kami waktu untuk membuat penyesuaian pengadaan di balik layar.
Dalam beberapa cara, kesetaraan dalam sistem asinkron bekerja seperti pembatasan pada sistem sinkron. Meski demikian, kita berpikir bahwa jauh lebih penting untuk memikirkan sistem asinkron karena backlog besar yang dapat menumpuk dengan cepat.
Sebagai gambaran, pertimbangkan apa yang akan terjadi jika sistem asinkron secara internal tidak memiliki cukup perlindungan tetangga yang gaduh. Jika lalu lintas satu pelanggan sistem tiba-tiba memuncak tidak terbatas, dan menghasilkan backlog di seluruh sistem, mungkin perlu 30 menit bagi operator untuk dilibatkan, untuk mengetahui apa yang terjadi, dan untuk memitigasi masalahnya. Selama 30 menit tersebut, sisi produsen dari sistem mungkin telah diskalakan dengan baik dan memasukkan seluruh pesan ke dalam antrean. Tetapi, jika volume pesan yang diantrekan berkapasitas 10x lipat lebih besar dibandingkan sisi konsumen yang diskalakan, berarti diperlukan 300 menit bagi sistem untuk mengerjakan backlog dan memulihkan. Bahkan peningkatan muatan singkat dapat menghabiskan waktu pemulihan selama berjam-jam, sehingga menyebabkan pemadaman selama berjam-jam.
Pada praktiknya, sistem di AWS memiliki sejumlah faktor pelengkap untuk meminimalkan atau mencegah dampak negatif dari backlog antrean. Contohnya, penskalaan otomatis membantu memitigasi masalaah saat muatan meningkat. Namun melihat pengaruh dari antrean saja, tanpa mempertimbangkan faktor pelengkap akan berguna, karena hal ini membantu merancang sistem yang andal dalam beberapa lapisan. Berikut adalah beberapa pola desain yang kami temukan untuk membantu menghindari backlog antrean yang besar dan waktu pemulihan yang panjang:
• Perlindungan di setiap lapisan sangatlah penting dalam sistem asinkron. Karena sisten sinkron cenderung tidak menumpuk backlog, kami melindunginya dengan pembatasan pintu depan dan kontrol penerimaan. Pada sistem sinkron, tiap komponen sistem kita perlu melindungi dirinya dari kelebihan muatan, dan mencegah satu beban kerja agar tidak mengonsumsi bagian sumber daya secara tidak adil. Pasti akan selalu ada beberapa beban kerja di sekitar kontrol penerimaan pintu depan, jadi kami memerlukan sabuk, penahan, dan pelindung saku untuk menjaga layanan agar tidak kelebihan muatan.
• Menggunakan lebih dari satu antrean membantu membentuk lalu lintas. Pada beberapa cara, satu antrean dan multipenyewa tidak saling sesuai. Saat pekerjaan diantrekan ke antrean bersama, sulit untuk mengisolasi satu beban kerja dari beban kerja lainnya.
• Sistem real-time sering kali diterapkan dengan antrean FIFO, namun lebih memilih perilaku LIFO. Kami mendengar dari pelanggan bahwa ketika mereka berhadapan dengan backlog, mereka lebih memilih untuk melihat data baru diproses dengan segera. Data apa pun yang terakumulasi selama pemadaman atau lonjakan yang kemudian dapat diproses sebagai kapasitas akan tersedia.
Strategi Amazon untuk menciptakan sistem asinkron multipenyewa yang tangguh
Terdapat beberapa pola yang digunakan sistem di Amazon untuk membuat sistem asinkron multipenyewa mereka menjadi tangguh menghadapi perubahan pada beban kerja. Berikut adalah beragam teknik, namun terdapat juga banyak sistem yang digunakan di seluruh Amazon, masing-masing dengan kumpulan persyaratan masa pakai dan ketahanannya sendiri. Pada bagian berikut, saya menjelaskan beberapa pola yang kami gunakan, dan yang menurut cerita pelanggan AWS digunakan dalam sistem mereka.
Sebagai ganti membagikan satu antrean ke seluruh pelanggan, di beberapa sistem kami memberikan pelanggan antreannya masing-masing. Menambahkan antrean untuk tiap pelanggan atau beban kerja tidaklah selalu hemat biaya, karena layanan perlu menggunakan sumber daya untuk mengumpulkan seluruh antrean. Tetapi pada sistem dengan beberapa pelanggan atau sistem yang berdekatan, solusi sederhana ini dapat bermanfaat. Sebaliknya, jika sistem memeiliki puluhan atau ratusan pelanggan, antrean terpisah dapat mulai menjadi beban. Contohnya, AWS IoT tidak menggunakan antrean terpisah untuk setiap perangkat IoT di seluruh ruang. Biaya pengumpulan tidak dapat terukur dengan baik dalam tindakan tersebut.
AWS Lambda adalah contoh dari sebuah sistem yang membutuhkan banyak biaya jika mengumpulkan antrean terpisah bagi setiap pelanggan Lambda. Namun, memiliki satu antrean dapat menimbulkan beberapa masalah yang akan dijelaskan dalam artikel ini. Jadi dibanding menggunakan satu antrean, AWS Lambda menyediakan antrean dengan jumlah tetap, dan tiap pelanggan di-hash ke sejumlah kecil antrean. Sebelum menambahkan ke antrean, pesan memeriksa manakah antrean target yang berisi paling sedikit pesan, dan memasukkannya ke antrean tersebut. Saat beban kerja pelanggan meningkat, hal ini akan mendorong backlog di antrean yang dipetakan, tetapi beban kerja lain akan secara otomatis dirutekan menjauh dari antrean tersebut. Tidak memerlukan sejumlah besar antrean untuk membangun beberapa isolasi sumber daya yang ajaib. Ini adalah salah satu perlindungan internal Lambda, namun ini merupakan teknik yang juga digunakan di layanan lain di Amazon.
Dalam beberapa cara, ketika backlog menumpuk dalam antrean, sudah terlambat untuk memprioritaskan lalu lintas. Nakun, jika pemrosesan pesan cukup mahal atau memakan waktu, mungkin masih sepadan untuk dapat memindahkan pesan ke antrean limpahan yang terpisah. Di beberapa sistem di Amazon, layanan konsumen menerapkan pembatasan terdistribusi, dan saat mereka mengeluarkan pesan dari antrean untuk pelanggan yang telah melebihi tarif terkonfigurasi, mereka memasukkan pesan berlebih tersebut ke dalam antrean limpahan terpisah, dan menghapus pesan dari antrean utama. Sistem akan mengerjakan pesan di antrean limpahan segera setelah sumber daya tersedia. Esensinya, ini bisa juga disebut antrean prioritas. Logika serupa terkadang diterapkan di sisi produsen. Dengan cara ini, jika sistem menerima permintaan dalam volume besar dari satu beban kerja, beban kerja tersebut tidak mengurangi beban kerja lain di antrean jalur populer.
Serupa dengan menyisihkan lalu lintas berlebih, kita juga dapat menyisihkan lalu lintas lama. Saat mengeluarkan pesan dari antrean, kita juga dapat memeriksa usianya. Daripada hanya mencatat usia, kita dapat menggunakan informasi untuk memutuskan apakah pesan harus dipindahkan ke antrean backlog yang kami kerjakan hanya setelah kami terjebak pada antrian langsung. Jika muatan melonjak saat kita menyerap banyak data, dan menjadi tertinggal, kita dapat menyisihkan gelombang lalu lintas tersebut ke dalam antrean yang berbeda secepat kita mengeluarkan dan memasukkan lalu lintas ke antrean. Tindakan ini membebaskan sumber daya konsumen untuk mengerjakan pesan baru dengan lebih cepat dibanding jika kita hanya mengerjakan backlog secara berurutan. Ini merupakan salah satu cara untuk memperkirakan pengurutan LIFO.
Beberapa sistem dapat menoleransi penjatuhan pesan yang berusia sangat lama. Contohnya, beberapa sistem memproses delta ke sistem dengan cepat, namun juga melakukan sinkronisasi lengkap secara berkala. Kita sering menyebut sistem sinkronisasi berkala ini sebagai penyapu antientropi. Dalam hal ini, sebagai ganti menyisihkan lalu lintas lama yang menumpuk, dengan murah kita dapat menjatuhkannya jika datang sebelum penyapuan terbaru.
Seperti halnya dalam layanan sinkron, kami merancang sistem asinkron untuk mencegah satu beban kerja agar tidak menggunakan threads melebihi bagiannya. Satu aspek AWS IoT yang belum kami bicarakan adalah mesin peraturan. Pelanggan dapat mengonfigurasi AWS IoT untuk merutekan pesan dari perangkat ke klaster Amazon Elasticsearch yang dimiliki pelanggan, Kinesis Stream, dan lainnya. Jika latensi ke sumber daya milik pelanggan tersebut menjadi lamban, tetapi tingkat pesan masuk tetap sama, jumlah konkurensi di sistem akan meningkat. Karena jumlah konkurensi yang dapat ditangani sistem dibatasi, mesin peraturan mencegah satu beban kerja agar tidak mengonsumsi sumber daya terkait konkurensi melebihi bagiannya.
Dorongan saat bekerja dijelaskan oleh Little’s Law yang menyatakan bahwa konkurensi dalam sistem sama dengan tingkat kedatangan dikalikan latensi rata-rata tiap permintaan. Contohnya, jika server memproses 100 pesan/detik pada rata-rata 100 ms, ini akan mengonsumsi rata-rata 10 thread. Jika latensi melonjak secara tiba-tiba menjadi 10 detik, secara mendadak ini akan menggunakan 1000 thread (rata-rata, jadi bisa lebih banyak dalam praktiknya) yang dengan mudah dapat melemahkan kumpulan thread.
Mesin peraturan menggunakan beberapa teknik untuk mencegah agar hal tersebut tidak terjadi. Menggunakan I/O tanpa blok untuk menghindari pelemahan thread, meski masih terdapat batasan lain terkait berapa banyak pekerjaan yang dimiliki server yang ditentukan (contohnya, memori, dan deskriptor file saat klien berputar melalui koneksi dan waktu ketergantungan segera habis). Penjaga konkurensi kedua yang dapat digunakan adalah semafor yang mengukur dan membatasi jumlah konkurensi yang dapat digunakan untuk beban kerja tunggal setiap saat. Mesin peraturan juga menggunakan pembatasan kesetaraan berbasis tarif. Namun, karena wajar bagi beban kerja untuk berubah seiring waktu, mesin peraturan juga secara otomatis menskalakan batas dari waktu ke waktu untuk beradaptasi dengan perubahan pada beban kerja. Selain itu, karena berbasis antrean, mesin peraturan berfungsi sebagai buffer antara perangkat IoT dan penskalaan otomatis sumber daya serta penjaga batas di balik layar.
Pada seluruh sistem di Amazon, kami menggunakan kumpulan thread terpisah untuk tiap beban kerja agar satu beban kerja tidak mengonsumsi seluruh thread yang tersedia. Kami juga menggunakan AtomicInteger bagi tiap beban kerja untuk membatasi konkurensi yang diizinkan bagi tiap pendekatan pembatasan berbasis tarif dalam mengisolasi rumber daya berbasis tarif.
Jika beban kerja mendorong backlog yang tidak masuk akal sehingga konsumen tidak dapat menanganinya, banyak dari sistem kami yang secara otomatis mulai menolak pekerjaan secara lebih agresif di produsen. Membangun backlog sepanjang hari untuk sebuah beban kerja sangatlah mudah. Bahkan jika beban kerja tersebut terisolasi, beban kerja mungkin tidak disengaja, dan mahal untuk diputar. Penerapan pendekatan ini semudah sesekali mengukur kedalaman antrean beban kerja (dengan anggapan beban kerja berada di antreannya sendiri), dan menskalakan batas pembatasan masuk (berbanding terbalik) secara proporsional ke ukuran backlog.
Pada saat membagikan antrean SQS untuk beberapa beban kerja, pendekatan ini menjadi sedikit rumit. Ketika ada API SQS yang mengembalikan jumlah pesan dalam antrean, tidak ada API yang dapat mengembalikan jumlah pesan dalam antrean yang memiliki atribut khusus. Kita masih tetap dapat mengukur kedalaman antrean dan menerapkan backpressure, tapi ini akan menempatkan backpressure secara tidak adil ke beban kerja polos yang kebetulan berada dalam antrean yang sama. Sistem lain seperti Amazon MQ memiliki visibilitas backlog yang lebih halus.
Backpressure tidak sesuai untuk seluruh sistem di Amazon. Contohnya, pada sistem yang melakukan pemrosesan pesanan untuk amazon.com, kami cenderung memilih untuk menerima pesanan bahkan saat backlog menumpuk, daripada mencegah penerimaan pesanan baru. Tapi tentu saja tindakan ini disertai dengan sejumlah besar pemrioritasan di balik layar, sehingga pesanan yang paling mendesak ditangani terlebih dahulu.
Saat sistem merasa bahwa throughput beban kerja tertentu perlu dikurangi, kami mencoba menggunakan strategi mundur pada beban kerja tersebut. Untuk menerapkan ini, kami sering menggunakan fitur SQS yang menunda pengiriman pesan hingga beberapa waktu mendatang. Saat memproses pesan dan memutuskan untuk menyimpannya, kita terkadang memasukkan kembali pesan tersebut ke dalam antrean lonjakan terpisah, namun mengatur parameter penundaan sehingga pesan tetap tersembunyi dalam antrean tunda selama beberapa menit. Tindakan ini memberikan sistem kesempatan untuk mengerjakan data yang lebih baru.
Beberapa layanan antrean seperti SQS memiliki batas terkait berapa banyak pesan in-flight yang dapat dikirimkan ke konsumen antrean. Ini berbeda dari jumlah pesan yang dapat berada dalam antrean (tidak ada batas praktis), namun lebih kepada jumlah pesan yang armada konsumen kerjakan sekaligus. Angka ini dapat ditingkatkan jika sistem mengeluarkan pesan dari antrean, lalu gagal menghapusnya. Contohnya, kita pernah melihat bug berupa kegagalan kode menangkap pengecualian saat memproses pesan dan lupa untuk menghapusnya. Dalam kasus ini, pesan tetap in-flight dari perspektif SQS untuk VisibilityTimeout pesan. Saat merancang strategi penanganan kesalahan dan kelebihan muatan, kita mempertimbangkan batas tersebut, dan cenderung memilih untuk memindahkan pesan berlebih ke antrean yang berbeda, bukan membiarkannya tetap terlihat.
Antrean SQS FIFO memiliki batas yang serupa namun lebih samar. Dengan SQS FIFO, sistem mengonsumsi pesan Anda secara berurutan untuk grup pesan tertentu, tapi pesan dari grup yang berbeda diproses secara acak. Jadi jika kita mengembangkan backlog kecil di satu grup pesan, kita terus memproses pesan di grup lain. Namun, SQS FIFO hanya mengumpulkan 20.000 pesan terbaru yang tidak terproses. Jadi, jika ada lebih dari 20.000 pesan yang tidak terproses di subkumpulan grup pesan, grup pesan lain dengan pesan baru akan terabaikan.
Pesan yang tidak dapat diproses dapat menimbulkan kelebihan muatan sistem. Jika sistem memasukkan pesan yang tidak dapat diproses ke dalam antrean (mungkin karena memicu kasus edge validasi input), SQS dapat membantu dengan memindahkan pesan tersebut secara otomatis ke antrean terpisan dengan fitur antrean huruf mati (dead-letter queue/DLQ). Kita khawatir jika ada pesan dalam antrean ini, karena ini berarti bahwa kita memiliki bug yang harus diperbaiki. Manfaat DLQ adalah fitur ini memungkinkan kita memproses ulang pesan setelah bug diperbaiki.
Jika beban kerja mendorong cukup throughput ke titik di mana thread pengumpulan sibuk sepanjang waktu bahkan saat kondisi stabil, sistem mungkin telah mencapai titik di mana tidak ada buffer untuk menyerap lonjakan lalu lintas. Dalam kondisi ini, peningkatan kecil pada lalu lintas masuk akan menghasilkan jumlah tetap backlog yang belum diproses, menyebabkan latensi yang lebih tinggi. Kita merencanakan buffer tambahan dalam thread pengumpulan untuk menyerap lonjakan seperti itu. Salah satu langkah adalah dengan melacak percobaan pengumpulan yang menghasilkan respons kosong. Jika setiap percobaan pengumpulan menerima lebih dari satu pesan, artinya kita memiliki jumlah thread pengumpulan yang tepat, atau mungkin tidak cukup untuk menyamai lalu lintas masuk.
Saat sistem memproses pesan SQS, sejumlah waktu tertentu diberikan oleh SQS kepada sistem tersebut untuk menyelesaikan pemrosesan pesan sebelum SQS menganggap bahwa sistem crash, dan untuk mengirimkan pesan coba lagi ke konsumen lain. Jika kode terus berjalan dan lupa mengenai tenggat waktu ini, pesan yang sama dapat dikirimkan beberapa kali secara paralel. Ketika prosesor pertama masih memutar pesan setelah waktunya habis, prosesor kedua akan mengambilnya dan memutarnya juga melewati tenggat waktu, lalu prosesor ketiga, dan seterusnya. Hal yang berpotensi menimbulkan pengurangan bertingkat ini adalah alasan kita menerapkan logika pemrosesan pesan untuk berhenti bekerja ketika pesan kedaluwarsa, atau terus mendetakkan pesan tersebut untuk mengingatkan SQS bahwa kita masih mengerjakannya. Konsep ini mirip dengan sewa dalam pemilihan pemimpin.
Ini adalah masalah yang berat, karena kita melihat bahwa latensi sistem cenderung meningkat selama kelebihan muatan, mungkin dari antrean ke database memerlukan waktu yang lebih lama, atau dari server mengambil lebih banyak pekerjaan dari yang bisa dikerjakan. Latensi sistem yang melanggar ambang batas VisibilityTimeout dapat membuat layanan yang telah kelebihan muatan menghancurkan dirinya sendiri.
Cukup sulit memahami kegagalan pada sistem yang terdistribusi. Artikel terkait tentang instrumentasi menjelaskan beberapa pendekatan kami untuk menginstrumentasi sistem asinkron, dari mencatat kedalaman antrean secara berkala, hingga menyebarkan “id pelacakan” dan mengintegrasikan dengan X-Ray. Atau, saat sistem kita memiliki alur kerja asinkron yang rumit di balik antrean SQS biasa, kita sering menggunakan layanan alur kerja asinkron yang berbeda seperti Step Functions, yang memberikan visibilitas ke dalam alur kerja dan menyederhanakan debugging yang terdistribusi.
Penutup
Dalam sistem asinkron, sangat mudah melupakan pentingnya memikirkan tentang latensi. Bagaimanapun, sistem asinkron memang terkadang membutuhkan waktu yang lebih lama, karena mereka dihadapkan dengan antrean untuk melakukan percobaan ulang yang dapat diandalkan. Namun, skenario muatan berlebih dan kegagalan dapat menumpuk backlog yang sangat besar sehingga layanan tidak dapat pulih dalam jangka waktu yang wajar. Backlog tersebut dapat muncul dari satu beban kerja atau konsumen yang memasukkan ke dalam antrean pada tingkat yang cukup tinggi, dari beban kerja yang menjadi lebih mahal dari prediksi untuk diproses, atau dari latensi atau kesalahan dalam ketergantungan.
Saat membangun sistem asinkron, kita harus fokus pada dan mengantisipasi skenario backlog tersebut, serta meminimalkan mereka dengan menggunakan teknik seperti pemrioritasan, penyisihan, dan backpressure.
Baca lebih lanjut
• Teori antrean
• Little's law
• Amdahl's law
• Little A Proof for the Queuing Formula: L = λW, Case Western, 1961
• McKenney, Stochastic Fairness Queuing, IBM, 1990
• Nichols and Jacobson, Controlling Queue Delay, PARC, 2011
Tentang penulis
David Yanacek adalah Senior Principal Engineer yang bekerja di AWS Lambda. David telah menjadi pengembang perangkat lunak di Amazon sejak tahun 2006, sebelumnya bekerja di Amazon DynamoDB dan AWS IoT, juga kerangka kerja layanan web internal serta sistem automasi pengoperasian armada. Salah satu kegiatan favorit David di kantor adalah melakukan analisis log dan menyelidiki metrik pengoperasian guna menemukan cara untuk membuat sistem berjalan semakin mulus dari waktu ke waktu.