Antipattern Front End yang Sibuk
Melakukan pekerjaan asinkron pada sejumlah besar thread latar belakang dapat kekurangan tugas latar depan bersamaan sumber daya lainnya, mengurangi waktu respon ke tingkat yang tidak dapat diterima.
Deskripsi masalah
Tugas intensif sumber daya dapat meningkatkan waktu respons untuk permintaan pengguna dan menyebabkan latensi tinggi. Salah satu cara untuk meningkatkan waktu respons adalah dengan membongkar tugas intensif sumber daya ke thread terpisah. Pendekatan ini memungkinkan aplikasi tetap responsif saat pemrosesan terjadi di latar belakang. Namun, tugas yang berjalan pada thread latar belakang masih menggunakan sumber daya. Jika ada terlalu banyak, tugas dapat kekurangan thread yang menangani permintaan.
Catatan
Istilah sumber daya dapat mencakup banyak hal, seperti pemanfaatan CPU, hunian memori, dan jaringan atau disk I/O.
Masalah ini biasanya terjadi saat aplikasi dikembangkan sebagai bagian kode monolitik, dengan semua logika bisnis digabungkan menjadi satu tingkat yang dibagikan dengan lapisan presentasi.
Berikut adalah pseudocode yang menunjukkan masalah.
public class WorkInFrontEndController : ApiController
{
[HttpPost]
[Route("api/workinfrontend")]
public HttpResponseMessage Post()
{
new Thread(() =>
{
//Simulate processing
Thread.SpinWait(Int32.MaxValue / 100);
}).Start();
return Request.CreateResponse(HttpStatusCode.Accepted);
}
}
public class UserProfileController : ApiController
{
[HttpGet]
[Route("api/userprofile/{id}")]
public UserProfile Get(int id)
{
//Simulate processing
return new UserProfile() { FirstName = "Alton", LastName = "Hudgens" };
}
}
Metode
Post
dalam pengontrolWorkInFrontEnd
mengimplementasikan operasi HTTP POST. Operasi ini menyimulasikan tugas yang berjalan lama dan intensif CPU. Pekerjaan dilakukan pada thread terpisah, dalam upaya untuk memungkinkan operasi POST menyelesaikan dengan cepat.Metode
Get
dalam pengontrolUserProfile
menerapkan operasi HTTP GET. Metode ini jauh lebih tidak intensif CPU.
Perhatian utama adalah persyaratan sumber daya dari metode Post
. Meskipun menempatkan pekerjaan ke thread latar belakang, pekerjaan masih dapat menggunakan sumber daya CPU yang cukup besar. Sumber daya ini dibagi dengan operasi lain yang dilakukan oleh pengguna bersamaan lainnya. Jika sejumlah pengguna moderat mengirim permintaan ini pada saat yang sama, performa keseluruhan kemungkinan akan kesulitan, memperlambat semua operasi. Pengguna mungkin mengalami latensi yang signifikan dalam metode Get
, misalnya.
Cara memperbaiki masalah ini
Pindahkan proses yang menggunakan sumber daya yang signifikan ke backend yang terpisah.
Dengan pendekatan ini, frontend menempatkan tugas yang intensif sumber daya ke antrean pesan. Backend mengambil tugas untuk pemrosesan asinkron. Antrean juga bertindak sebagai load leveler, melakukan buffer pada permintaan untuk backend. Jika panjang antrean menjadi terlalu panjang, Anda dapat mengonfigurasi penskalaan otomatis untuk menskalakan backend.
Berikut adalah versi revisi dari kode sebelumnya. Dalam versi ini, metode Post
menempatkan pesan pada antrean Bus Layanan.
public class WorkInBackgroundController : ApiController
{
private static readonly QueueClient QueueClient;
private static readonly string QueueName;
private static readonly ServiceBusQueueHandler ServiceBusQueueHandler;
public WorkInBackgroundController()
{
string serviceBusNamespace = ...;
QueueName = ...;
ServiceBusQueueHandler = new ServiceBusQueueHandler(serviceBusNamespace);
QueueClient = ServiceBusQueueHandler.GetQueueClientAsync(QueueName).Result;
}
[HttpPost]
[Route("api/workinbackground")]
public async Task<long> Post()
{
return await ServiceBusQueueHandler.AddWorkLoadToQueueAsync(QueueClient, QueueName, 0);
}
}
Backend menarik pesan dari antrean Bus Layanan dan melakukan pemrosesan.
public async Task RunAsync(CancellationToken cancellationToken)
{
this._queueClient.OnMessageAsync(
// This lambda is invoked for each message received.
async (receivedMessage) =>
{
try
{
// Simulate processing of message
Thread.SpinWait(Int32.MaxValue / 1000);
await receivedMessage.CompleteAsync();
}
catch
{
receivedMessage.Abandon();
}
});
}
Pertimbangan
- Pendekatan ini menambahkan beberapa kompleksitas tambahan pada aplikasi. Anda harus menangani antrean dan pembatalan antrean dengan aman untuk menghindari kehilangan permintaan jika terjadi kegagalan.
- Aplikasi mengambil dependensi pada layanan tambahan untuk antrean pesan.
- Lingkungan pemrosesan harus cukup terukur untuk menangani beban kerja yang diharapkan dan memenuhi target throughput yang diperlukan.
- Meskipun pendekatan ini harus meningkatkan responsivitas secara keseluruhan, tugas yang dipindahkan ke backend mungkin membutuhkan waktu lebih lama untuk diselesaikan.
Cara mendeteksi masalah
Gejala frontend yang sibuk termasuk latensi tinggi saat tugas intensif sumber daya sedang dilakukan. Pengguna akhir cenderung melaporkan waktu respons yang diperpanjang atau kegagalan yang disebabkan oleh waktu layanan. Kegagalan ini juga dapat menampilkan kesalahan HTTP 500 (Internal Server) atau kesalahan HTTP 503 (Service Unavailable). Periksa log peristiwa untuk server web, yang cenderung berisi informasi terperinci tentang penyebab dan keadaan kesalahan.
Anda dapat melakukan langkah-langkah berikut untuk membantu mengidentifikasi masalah ini:
- Lakukan pemantauan proses sistem produksi, untuk mengidentifikasi titik saat waktu respons melambat.
- Periksa data telemetri yang ditangkap pada titik ini untuk menentukan campuran operasi yang dilakukan dan sumber daya yang digunakan.
- Temukan korelasi antara waktu respons yang buruk dan volume serta kombinasi operasi yang terjadi pada saat itu.
- Uji beban setiap operasi yang dicurigai untuk mengidentifikasi operasi mana yang menggunakan sumber daya dan membuat operasi lainnya kekurangan.
- Tinjau kode sumber untuk operasi tersebut untuk menentukan alasannya dapat menyebabkan konsumsi sumber daya yang berlebihan.
Contoh diagnosis
Bagian berikut menerapkan langkah-langkah ini ke aplikasi contoh yang dijelaskan sebelumnya.
Mengidentifikasi titik perlambatan
Instrumentasikan setiap metode untuk melacak durasi dan sumber daya yang digunakan oleh setiap permintaan. Kemudian, pantau aplikasi dalam produksi. Ini dapat memberikan pandangan keseluruhan tentang cara permintaan bersaing satu sama lain. Selama periode stres, permintaan yang memerlukan sumber daya yang berjalan lambat kemungkinan akan memengaruhi operasi lain, dan perilaku ini dapat diamati dengan memantau sistem dan mencatat penurunan performa.
Gambar berikut menunjukkan dasbor pemantauan. (Kami menggunakan AppDynamics untuk pengujian kami.) Awalnya, sistem memiliki beban ringan. Kemudian, pengguna mulai meminta metode GET UserProfile
. Performa cukup baik sampai pengguna lain mulai mengeluarkan permintaan ke metode POST WorkInFrontEnd
. Pada saat itu, waktu respons meningkat secara dramatis (panah pertama). Waktu respons hanya meningkat setelah volume permintaan ke pengontrol WorkInFrontEnd
berkurang (panah kedua).
Memeriksa data telemetri dan menemukan korelasinya
Gambar berikutnya menunjukkan beberapa metrik yang dikumpulkan untuk memantau pemanfaatan sumber daya selama interval yang sama. Pada awalnya, hanya sedikit pengguna yang mengakses sistem. Karena semakin banyak pengguna yang terhubung, pemanfaatan CPU menjadi sangat tinggi (100%). Perhatikan juga bahwa tingkat I/O jaringan awalnya naik karena penggunaan CPU meningkat. Tetapi begitu penggunaan CPU memuncak, I/O jaringan benar-benar turun. Itu karena sistem hanya dapat menangani sejumlah kecil permintaan setelah CPU pada kapasitas. Saat pengguna terputus, tail beban CPU nonaktif.
Pada titik ini, tampaknya metode Post
dalam pengontrol WorkInFrontEnd
adalah kandidat utama untuk pemeriksaan lebih dekat. Pekerjaan lebih lanjut dalam lingkungan yang terkontrol diperlukan untuk mengonfirmasi hipotesis.
Melakukan pengujian beban
Langkah selanjutnya adalah melakukan pengujian di lingkungan yang terkontrol. Misalnya, jalankan serangkaian pengujian beban yang mencakup dan kemudian menghilangkan setiap permintaan secara bergantian untuk melihat efeknya.
Grafik di bawah ini menunjukkan hasil pengujian beban yang dilakukan terhadap penyebaran layanan cloud yang identik yang digunakan dalam pengujian sebelumnya. Pengujian ini menggunakan beban konstan 500 pengguna yang melakukan operasi Get
di pengontrol UserProfile
, bersama dengan beban langkah pengguna yang melakukan operasi Post
di pengontrol WorkInFrontEnd
.
Awalnya, beban langkah adalah 0, jadi satu-satunya pengguna aktif yang melakukan permintaan UserProfile
. Sistem ini mampu menanggapi sekitar 500 permintaan per detik. Setelah 60 detik, beban 100 pengguna tambahan mulai mengirim permintaan POST ke pengontrol WorkInFrontEnd
. Segera, beban kerja yang dikirim ke pengontrol UserProfile
turun menjadi sekitar 150 permintaan per detik. Hal ini disebabkan oleh cara runner pengujian beban berfungsi. Ini menunggu respons sebelum mengirim permintaan berikutnya, jadi semakin lama waktu yang dibutuhkan untuk menerima respons, semakin rendah tingkat permintaan.
Karena semakin banyak pengguna mengirim permintaan POST ke pengontrol WorkInFrontEnd
, tingkat respons pengontrol UserProfile
terus menurun. Tetapi perhatikan bahwa volume permintaan yang ditangani oleh pengontrol WorkInFrontEnd
tetap relatif konstan. Saturasi sistem menjadi jelas karena tingkat keseluruhan kedua permintaan cenderung menuju batas yang stabil namun rendah.
Meninjau kode sumber
Langkah terakhir adalah melihat kode sumber. Tim pengembangan menyadari bahwa metode Post
bisa memakan banyak waktu, itulah sebabnya implementasi asli menggunakan thread terpisah. Hal tersebut memecahkan masalah langsung, karena metode Post
tidak menghalangi menunggu tugas yang sudah berjalan lama untuk diselesaikan.
Namun, pekerjaan yang dilakukan dengan metode ini masih menggunakan CPU, memori, dan sumber daya lainnya. Mengaktifkan proses ini untuk berjalan secara asinkron sebenarnya dapat merusak performa, karena pengguna dapat memicu sejumlah besar operasi ini secara bersamaan, dengan cara yang tidak terkendali. Ada batasan jumlah thread yang dapat dijalankan server. Melewati batas ini, aplikasi kemungkinan akan mendapatkan pengecualian saat mencoba memulai thread baru.
Catatan
Ini tidak berarti Anda harus menghindari operasi asinkron. Melakukan menunggu asinkron pada panggilan jaringan adalah praktik yang disarankan. (Lihat Antipattern I/O sinkron.) Masalahnya di sini adalah bahwa pekerjaan intensif CPU diluapkan di utas lain.
Menerapkan solusi dan memverifikasi hasilnya
Gambar berikut menunjukkan pemantauan performa setelah solusi diterapkan. Bebannya mirip dengan yang ditunjukkan sebelumnya, tetapi waktu respons untuk pengontrol UserProfile
sekarang jauh lebih cepat. Volume permintaan meningkat selama durasi yang sama, dari 2.759 menjadi 23.565.
Perhatikan bahwa pengontrol WorkInBackground
juga menangani volume permintaan yang jauh lebih besar. Namun, Anda tidak dapat membuat perbandingan langsung dalam hal ini, karena pekerjaan yang dilakukan di pengontrol ini sangat berbeda dari kode aslinya. Versi baru hanya mengantrekan permintaan, daripada melakukan perhitungan yang memakan waktu. Poin utamanya adalah bahwa metode ini tidak lagi menyeret seluruh sistem di bawah beban.
Pemanfaatan CPU dan jaringan juga menunjukkan peningkatan performa. Penggunaan CPU tidak pernah mencapai 100%, dan volume permintaan jaringan yang ditangani jauh lebih besar dari sebelumnya, dan tidak bertahan sampai beban kerja turun.
Grafik berikut menunjukkan hasil pengujian beban. Volume keseluruhan permintaan yang dilayani sangat ditingkatkan dibandingkan dengan pengujian sebelumnya.