Bagikan melalui


Anti pola Instansiasi yang tidak tepat

Terkadang, instans baru dari suatu kelas terus menerus dibuat, saat dimaksudkan untuk dibuat sekali lalu dibagikan. Perilaku ini dapat merusak performa, dan disebut sebagai antipola instantiasi yang tidak tepat. Sebuah anti pola adalah respons umum untuk masalah berulang yang biasanya tidak efektif dan bahkan mungkin kontra-produktif.

Deskripsi masalah

Banyak pustaka menyediakan abstraksi sumber daya eksternal. Secara internal, kelas ini biasanya mengelola koneksinya sendiri ke sumber daya, yang bertindak sebagai broker yang dapat digunakan klien untuk mengakses sumber daya. Berikut adalah beberapa contoh kelas broker yang relevan dengan aplikasi Azure:

  • System.Net.Http.HttpClient. Berkomunikasi dengan layanan web menggunakan HTTP.
  • Microsoft.ServiceBus.Messaging.QueueClient. Mengirim dan menerima pesan ke antrean Bus Layanan.
  • Microsoft.Azure.Documents.Client.DocumentClient. Menyambungkan ke instans Azure Cosmos DB.
  • StackExchange.Redis.ConnectionMultiplexer. Terhubung ke Redis, termasuk Azure Cache for Redis.

Kelas ini dimaksudkan untuk dipakai sekali dan digunakan kembali sepanjang masa aplikasi. Namun, itu adalah kesalahpahaman umum bahwa kelas ini harus diperoleh hanya jika diperlukan dan dirilis dengan cepat. (Yang tercantum di sini kebetulan adalah pustaka .NET, tetapi polanya tidak unik untuk .NET.) Contoh ASP.NET berikut membuat instans HttpClient untuk berkomunikasi dengan layanan jarak jauh. Anda dapat menemukan sampel lengkap di sini.

public class NewHttpClientInstancePerRequestController : ApiController
{
    // This method creates a new instance of HttpClient and disposes it for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        using (var httpClient = new HttpClient())
        {
            var hostName = HttpContext.Current.Request.Url.Host;
            var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
            return new Product { Name = result };
        }
    }
}

Dalam aplikasi web, teknik ini tidak skalabel. Objek HttpClient baru dibuat untuk setiap permintaan pengguna. Di bawah beban berat, server web mungkin menghabiskan jumlah soket yang tersedia, yang mengakibatkan kesalahan SocketException.

Masalah ini tidak terbatas pada kelas HttpClient. Kelas lain yang membungkus sumber daya atau mahal untuk dibuat dapat menyebabkan masalah serupa. Contoh berikut membuat instans dari kelas ExpensiveToCreateService. Di sini masalahnya belum tentu gangguan soket, tetapi berapa lama waktu yang dibutuhkan untuk membuat setiap instans. Membuat dan menghancurkan instans kelas ini secara terus-menerus dapat berdampak buruk pada skalabilitas sistem.

public class NewServiceInstancePerRequestController : ApiController
{
    public async Task<Product> GetProductAsync(string id)
    {
        var expensiveToCreateService = new ExpensiveToCreateService();
        return await expensiveToCreateService.GetProductByIdAsync(id);
    }
}

public class ExpensiveToCreateService
{
    public ExpensiveToCreateService()
    {
        // Simulate delay due to setup and configuration of ExpensiveToCreateService
        Thread.SpinWait(Int32.MaxValue / 100);
    }
    ...
}

Cara memperbaiki anti pola instantiasi yang tidak tepat

Jika kelas yang membungkus sumber daya eksternal dapat dibagikan dan aman untuk utas, buat instans database tunggal bersama atau kumpulan instans kelas yang dapat digunakan kembali.

Contoh berikut menggunakan instans HttpClient statis, sehingga berbagi koneksi di semua permintaan.

public class SingleHttpClientInstanceController : ApiController
{
    private static readonly HttpClient httpClient;

    static SingleHttpClientInstanceController()
    {
        httpClient = new HttpClient();
    }

    // This method uses the shared instance of HttpClient for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        var hostName = HttpContext.Current.Request.Url.Host;
        var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
        return new Product { Name = result };
    }
}

Pertimbangan

  • Elemen utama dari anti pola ini adalah berulang kali membuat dan menghancurkan instans objek yang dapat dibagikan. Jika suatu kelas tidak dapat dibagikan (tidak aman untuk utas), anti pola ini tidak berlaku.

  • Jenis sumber daya bersama mungkin menentukan apakah Anda harus menggunakan database tunggal atau membuat kumpulan. Kelas HttpClient dirancang untuk dibagikan, bukan dikumpulkan. Objek lain mungkin mendukung pengumpulan, yang memungkinkan sistem menyebarkan beban kerja ke beberapa instans.

  • Objek yang Anda bagikan di beberapa permintaan harus aman dari utas. Kelas HttpClient dirancang untuk digunakan dengan cara ini, tetapi kelas lain mungkin tidak mendukung permintaan bersamaan, jadi periksa dokumentasi yang tersedia.

  • Berhati-hatilah dalam mengatur properti pada objek bersama, karena hal ini dapat menyebabkan kondisi perlombaan. Misalnya, mengatur DefaultRequestHeaders pada kelas HttpClient sebelum setiap permintaan dapat membuat kondisi perlombaan. Atur properti tersebut satu kali (misalnya, selama startup), dan buat instans terpisah jika Anda perlu mengonfigurasi pengaturan yang berbeda.

  • Beberapa jenis sumber daya langka dan tidak boleh digunakan. Koneksi database adalah contohnya. Memegang koneksi database terbuka yang tidak diperlukan dapat mencegah pengguna konkuren lainnya mendapatkan akses ke database.

  • Dalam .NET Framework, banyak objek yang membuat koneksi ke sumber daya eksternal dibuat dengan menggunakan metode pabrik statis dari kelas lain yang mengelola koneksi ini. Objek ini dimaksudkan untuk disimpan dan digunakan kembali, bukan dibuang dan dibuat kembali. Misalnya, di Azure Service Bus, objek QueueClient dibuat melalui objek MessagingFactory. Secara internal, MessagingFactory mengelola koneksi. Untuk informasi selengkapnya, lihat Praktik Terbaik untuk peningkatan performa menggunakan Pesan Bus Layanan.

Cara mendeteksi anti pola instantiasi yang tidak tepat

Gejala masalah ini termasuk penurunan throughput atau tingkat kesalahan yang meningkat, beserta satu atau beberapa hal berikut:

  • Peningkatan pengecualian yang menunjukkan kelelahan sumber daya seperti soket, koneksi database, pegangan file, dan sebagainya.
  • Peningkatan penggunaan memori dan pengumpulan sampah.
  • Peningkatan aktivitas jaringan, disk, atau database.

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

  1. Melakukan pemantauan proses sistem produksi, untuk mengidentifikasi titik-titik ketika waktu respons melambat atau sistem gagal karena kekurangan sumber daya.
  2. Periksa data telemetri yang ditangkap pada titik-titik ini untuk menentukan operasi mana yang mungkin menciptakan dan menghancurkan objek yang menghabiskan sumber daya.
  3. Uji beban setiap operasi yang dicurigai, dalam lingkungan pengujian yang terkontrol daripada sistem produksi.
  4. Tinjau kode sumber dan periksa bagaimana objek broker dikelola.

Lihat pelacakan tumpukan untuk operasi yang berjalan lambat atau yang menghasilkan pengecualian saat sistem sedang dimuat. Informasi ini dapat membantu mengidentifikasi bagaimana operasi ini menggunakan sumber daya. Pengecualian dapat membantu menentukan apakah kesalahan disebabkan oleh habisnya sumber daya bersama.

Contoh diagnosis

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

Identifikasi titik pelambatan atau kegagalan

Gambar berikut menunjukkan hasil yang dihasilkan menggunakan New Relic APM, yang menunjukkan operasi yang memiliki waktu respons yang buruk. Dalam hal ini, metode GetProductAsync di pengontrol NewHttpClientInstancePerRequest perlu diselidiki selengkapnya. Perhatikan bahwa tingkat kesalahan juga meningkat saat operasi ini berjalan.

Dasbor monitor New Relic menampilkan instans aplikasi yang membuat instans baru dari objek HttpClient untuk setiap permintaan

Memeriksa data telemetri dan menemukan korelasinya

Gambar berikutnya menunjukkan data yang diambil menggunakan pembuatan profil utas, selama periode yang sama sesuai dengan gambar sebelumnya. Sistem menghabiskan banyak waktu untuk membuka koneksi soket, dan bahkan lebih banyak waktu untuk menutupnya dan menangani pengecualian soket.

Profiler utas New Relic menampilkan instans aplikasi yang membuat instans baru dari objek HttpClient untuk setiap permintaan

Melakukan pengujian beban

Gunakan pengujian beban untuk menyimulasikan operasi umum yang mungkin dilakukan pengguna. Tindakan ini dapat membantu mengidentifikasi bagian mana dari sistem yang mengalami gangguan sumber daya di bawah beban yang bervariasi. Lakukan pengujian ini di lingkungan yang terkontrol, bukan di sistem produksi. Grafik berikut menunjukkan throughput permintaan yang ditangani oleh pengontrol NewHttpClientInstancePerRequest saat beban pengguna meningkat menjadi 100 pengguna bersamaan.

Throughput aplikasi sampel membuat instans baru objek HttpClient untuk setiap permintaan

Pada awalnya, volume permintaan yang ditangani per detik meningkat seiring dengan meningkatnya beban kerja. Namun, pada sekitar 30 pengguna, volume permintaan yang berhasil mencapai batas, dan sistem mulai menghasilkan pengecualian. Sejak saat itu, volume pengecualian secara bertahap meningkat seiring dengan beban pengguna.

Pengujian beban melaporkan kegagalan ini sebagai kesalahan HTTP 500 (Server Internal). Meninjau telemetri menunjukkan bahwa kesalahan ini disebabkan oleh sistem kehabisan sumber daya soket, karena semakin banyak HttpClient objek yang dibuat.

Grafik berikutnya menunjukkan pengujian serupa untuk pengontrol yang membuat objek ExpensiveToCreateService kustom.

Throughput aplikasi instans membuat instans baru dari ExpensiveToCreateService untuk setiap permintaan

Kali ini, pengontrol tidak menghasilkan pengecualian apa pun, tetapi throughput masih mencapai dataran tinggi, sementara waktu respons rata-rata meningkat dengan faktor 20. (Grafik menggunakan skala logaritma untuk waktu respons dan throughput.) Telemetri menunjukkan bahwa membuat instans baru adalah ExpensiveToCreateService penyebab utama masalah.

Menerapkan solusi dan memverifikasi hasilnya

Setelah mengganti metode GetProductAsync untuk berbagi satu instans HttpClient, pengujian beban kedua menunjukkan peningkatan performa. Tidak ada kesalahan yang dilaporkan, dan sistem mampu menangani peningkatan beban hingga 500 permintaan per detik. Waktu respons rata-rata dipotong setengahnya, dibandingkan dengan pengujian sebelumnya.

Throughput aplikasi sampel menggunakan kembali instans objek HttpClient yang sama untuk setiap permintaan

Sebagai perbandingan, gambar berikut menunjukkan telemetri jejak tumpukan. Kali ini, sistem menghabiskan sebagian besar waktunya untuk melakukan pekerjaan nyata, daripada membuka dan menutup soket.

Profiler utas New Relic menampilkan aplikasi instans yang membuat satu instans objek HttpClient untuk semua permintaan

Grafik berikutnya menunjukkan pengujian beban serupa menggunakan instans bersama dari objek ExpensiveToCreateService. Sekali lagi, volume permintaan yang ditangani meningkat sejalan dengan beban pengguna, sementara waktu respons rata-rata tetap rendah.

Grafik yang menunjukkan pengujian beban serupa menggunakan instans bersama dari objek ExpensiveToCreateService.