Antipola I/O sinkron

Memblokir utas panggilan saat I/O selesai dapat mengurangi performa dan memengaruhi skalabilitas vertikal.

Deskripsi masalah

Operasi I/O sinkron memblokir utas panggilan saat I/O selesai. Utas panggilan memasuki status menunggu dan tidak dapat melakukan pekerjaan yang berguna selama interval ini, yang membuang sumber daya pemrosesan.

Contoh umum I/O meliputi:

  • Mengambil atau menyimpan data ke database atau semua jenis penyimpanan persisten.
  • Mengirim permintaan ke layanan web.
  • Memposting pesan atau mengambil pesan dari antrean.
  • Menulis ke atau membaca dari file lokal.

Anti pola ini biasanya terjadi karena:

  • Tampaknya menjadi cara yang paling intuitif untuk melakukan operasi.
  • Aplikasi membutuhkan respons dari permintaan.
  • Aplikasi menggunakan pustaka yang hanya menyediakan metode sinkron untuk I/O.
  • Pustaka eksternal melakukan operasi I/O sinkron secara internal. Satu panggilan I/O sinkron dapat memblokir seluruh rantai panggilan.

Kode berikut mengunggah file ke penyimpanan blob Azure. Ada dua tempat dengan kode memblokir menunggu I/O sinkron, metode CreateIfNotExists dan metode UploadFromStream.

var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");

container.CreateIfNotExists();
var blockBlob = container.GetBlockBlobReference("myblob");

// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
    blockBlob.UploadFromStream(fileStream);
}

Berikut ini contoh menunggu respons dari layanan eksternal. Metode GetUserProfile memanggil layanan jarak jauh yang mengembalikan UserProfile.

public interface IUserProfileService
{
    UserProfile GetUserProfile();
}

public class SyncController : ApiController
{
    private readonly IUserProfileService _userProfileService;

    public SyncController()
    {
        _userProfileService = new FakeUserProfileService();
    }

    // This is a synchronous method that calls the synchronous GetUserProfile method.
    public UserProfile GetUserProfile()
    {
        return _userProfileService.GetUserProfile();
    }
}

Anda dapat menemukan kode lengkap untuk kedua contoh ini di sini.

Cara memperbaiki masalah ini

Ganti operasi I/O sinkron dengan operasi asinkron. Tindakan ini membebaskan rangkaian saat ini untuk terus melakukan pekerjaan yang berarti daripada memblokir, dan membantu meningkatkan pemanfaatan sumber daya komputasi. Melakukan I/O secara asinkron sangat efisien untuk menangani lonjakan permintaan yang tidak terduga dari aplikasi klien.

Banyak pustaka menyediakan versi metode sinkron dan asinkron. Jika memungkinkan, gunakan versi asinkron. Berikut adalah versi asinkron dari contoh sebelumnya yang mengunggah file ke penyimpanan blob Azure.

var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");

await container.CreateIfNotExistsAsync();

var blockBlob = container.GetBlockBlobReference("myblob");

// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
    await blockBlob.UploadFromStreamAsync(fileStream);
}

Operator await mengembalikan kontrol ke lingkungan panggilan saat operasi asinkron dilakukan. Kode setelah pernyataan ini bertindak sebagai kelanjutan yang berjalan ketika operasi asinkron telah selesai.

Layanan yang dirancang dengan baik juga harus menyediakan operasi asinkron. Berikut adalah versi asinkron dari layanan web yang mengembalikan profil pengguna. Metode GetUserProfileAsync bergantung pada versi layanan Profil Pengguna yang tidak sinkron.

public interface IUserProfileService
{
    Task<UserProfile> GetUserProfileAsync();
}

public class AsyncController : ApiController
{
    private readonly IUserProfileService _userProfileService;

    public AsyncController()
    {
        _userProfileService = new FakeUserProfileService();
    }

    // This is a synchronous method that calls the Task based GetUserProfileAsync method.
    public Task<UserProfile> GetUserProfileAsync()
    {
        return _userProfileService.GetUserProfileAsync();
    }
}

Untuk pustaka yang tidak menyediakan versi operasi asinkron, dimungkinkan untuk membuat wrapper asinkron di sekitar metode sinkron yang dipilih. Ikuti pendekatan ini dengan hati-hati. Meskipun mungkin meningkatkan daya tanggap pada rangkaian yang memanggil wrapper asinkron, proses ini sebenarnya menghabiskan lebih banyak sumber daya. Rangkaian tambahan dapat dibuat, dan ada overhead yang terkait dengan sinkronisasi pekerjaan yang dilakukan oleh rangkaian ini. Beberapa pertukaran dibahas dalam entri blog ini: Haruskah saya mengekspos wrapper asinkron untuk metode sinkron?

Berikut adalah contoh wrapper asinkron di sekitar metode sinkron.

// Asynchronous wrapper around synchronous library method
private async Task<int> LibraryIOOperationAsync()
{
    return await Task.Run(() => LibraryIOOperation());
}

Sekarang kode panggilan dapat menunggu di wrapper:

// Invoke the asynchronous wrapper using a task
await LibraryIOOperationAsync();

Pertimbangan

  • Operasi I/O yang diharapkan berumur sangat pendek dan tidak mungkin menyebabkan pertentangan mungkin lebih berperforma sebagai operasi sinkron. Contohnya mungkin membaca file kecil di drive SSD. Overhead pengiriman tugas ke rangkaian lain, dan menyinkronkan dengan rangkaian tersebut saat tugas selesai, mungkin lebih besar daripada manfaat I/O asinkron. Namun, kasus ini relatif jarang, dan sebagian besar operasi I/O harus dilakukan secara asinkron.

  • Meningkatkan performa I/O dapat menyebabkan bagian lain dari sistem mengalami penyempitan. Misalnya, membuka blokir rangkaian dapat menghasilkan volume permintaan serentak yang lebih tinggi ke sumber daya bersama, yang pada gilirannya menyebabkan kelaparan atau pembatasan sumber daya. Jika itu menjadi masalah, Anda mungkin perlu mengurangi jumlah server web atau penyimpanan data partisi untuk mengurangi pertengkaran.

Cara mendeteksi masalah

Bagi pengguna, aplikasi mungkin tampak tidak responsif secara berkala. Aplikasi mungkin gagal dengan pengecualian waktu habis. Kegagalan ini juga dapat mengembalikan kesalahan HTTP 500 (Server Internal). Di server, permintaan klien yang masuk mungkin diblokir hingga rangkaian tersedia, yang mengakibatkan panjang antrean permintaan yang berlebihan, yang dimanifestasikan sebagai kesalahan HTTP 503 (Layanan Tidak Tersedia).

Anda dapat melakukan langkah-langkah berikut untuk membantu mengidentifikasi masalah:

  1. Pantau sistem produksi dan tentukan apakah rangkaian pekerja yang diblokir membatasi throughput.

  2. Jika permintaan diblokir karena kurangnya rangkaian, tinjau aplikasi untuk menentukan operasi mana yang mungkin melakukan I/O secara sinkron.

  3. Lakukan pengujian beban terkontrol dari setiap operasi yang melakukan I/O sinkron, untuk mengetahui apakah operasi tersebut memengaruhi performa sistem.

Contoh diagnosis

Bagian berikut menerapkan langkah-langkah ini ke aplikasi contoh yang dijelaskan sebelumnya.

Pantau performa server web

Untuk aplikasi web dan peran web Azure, ada baiknya memantau performa server web IIS. Secara khusus, perhatikan panjang antrean permintaan untuk menetapkan apakah permintaan diblokir menunggu utas yang tersedia selama periode aktivitas tinggi. Anda dapat mengumpulkan informasi ini dengan mengaktifkan diagnostik Azure. Untuk informasi selengkapnya, lihat:

Instrumen aplikasi untuk melihat bagaimana permintaan ditangani setelah diterima. Menelusuri alur permintaan dapat membantu mengidentifikasi apakah permintaan tersebut menjalankan panggilan yang berjalan lambat dan memblokir permintaan saat ini. Pembuatan profil rangkaian juga dapat menyorot permintaan yang diblokir.

Menguji beban aplikasi

Grafik berikut menunjukkan performa metode GetUserProfile sinkron yang ditampilkan sebelumnya, di bawah beban yang bervariasi hingga 4000 pengguna bersamaan. Aplikasi ini adalah aplikasi ASP.NET yang berjalan dalam peran web Layanan Azure Cloud.

Performance chart for the sample application performing synchronous I/O operations

Operasi sinkron dikodekan keras untuk tidur selama 2 detik, untuk menyimulasikan I/O sinkron, sehingga waktu respons minimum sedikit lebih dari 2 detik. Ketika beban mencapai sekitar 2500 pengguna secara bersamaan, waktu respons rata-rata mencapai puncaknya, meskipun volume permintaan per detik terus meningkat. Perhatikan bahwa skala untuk kedua ukuran ini adalah logaritma. Jumlah permintaan per detik berlipat ganda antara titik ini dan akhir tes.

Secara terpisah, dari pengujian ini belum tentu jelas apakah I/O sinkron merupakan masalah. Di bawah beban yang lebih berat, aplikasi dapat mencapai titik kritis di mana server web tidak dapat lagi memproses permintaan secara tepat waktu, menyebabkan aplikasi klien menerima pengecualian waktu habis.

Permintaan masuk diantrekan oleh server web IIS dan diserahkan ke rangkaian yang berjalan di kumpulan rangkaian ASP.NET. Karena setiap operasi melakukan I/O secara sinkron, rangkaian diblokir hingga operasi selesai. Saat beban kerja meningkat, akhirnya semua utas ASP.NET di kumpulan rangkaian dialokasikan dan diblokir. Pada saat itu, permintaan masuk selengkapnya harus menunggu dalam antrean untuk rangkaian yang tersedia. Seiring bertambahnya panjang antrean, permintaan mulai kehabisan waktu.

Menerapkan solusi dan memverifikasi hasilnya

Grafik berikutnya menunjukkan hasil dari pengujian beban versi kode asinkron.

Performance chart for the sample application performing asynchronous I/O operations

Throughput-nya jauh lebih tinggi. Selama durasi yang sama dengan pengujian sebelumnya, sistem berhasil menangani peningkatan throughput yang hampir sepuluh kali lipat, yang diukur dalam permintaan per detik. Selain itu, waktu respons rata-rata relatif konstan dan tetap sekitar 25 kali lebih kecil dari pengujian sebelumnya.