Bagikan melalui


Antipola Pengambilan asing

Antipattern adalah kelemahan desain umum yang dapat merusak perangkat lunak atau aplikasi Anda dalam situasi stres dan tidak boleh diabaikan. Dalam antipattern pengambilan asing, lebih dari data yang dibutuhkan diambil untuk operasi bisnis, sering kali mengakibatkan overhead I/O yang tidak perlu dan respons yang berkurang.

Contoh antipattern pengambilan asing

Antipattern ini dapat terjadi jika aplikasi mencoba meminimalkan permintaan I/O dengan mengambil semua data yang mungkin diperlukan. Ini sering kali merupakan akibat dari kompensasi yang berlebihan untuk antipattern Chatty I/O. Misalnya, aplikasi mungkin mengambil detail untuk setiap produk dalam database. Namun pengguna mungkin hanya memerlukan sebagian detail (beberapa mungkin tidak relevan dengan pelanggan), dan mungkin tidak perlu melihat semua produk sekaligus. Bahkan jika pengguna menjelajahi seluruh katalog, akan masuk akal untuk memberi nomor halaman hasil—menampilkan 20 sekaligus, misalnya.

Sumber lain dari masalah ini adalah mengikuti praktik pemrograman atau desain yang buruk. Misalnya, kode berikut menggunakan Kerangka Kerja Entitas untuk mengambil detail lengkap untuk setiap produk. Kemudian memfilter hasil untuk mengembalikan hanya sebagian dari bidang, membuang sisanya. Anda dapat menemukan sampel lengkap di sini.

public async Task<IHttpActionResult> GetAllFieldsAsync()
{
    using (var context = new AdventureWorksContext())
    {
        // Execute the query. This happens at the database.
        var products = await context.Products.ToListAsync();

        // Project fields from the query results. This happens in application memory.
        var result = products.Select(p => new ProductInfo { Id = p.ProductId, Name = p.Name });
        return Ok(result);
    }
}

Pada contoh berikutnya, aplikasi mengambil data untuk melakukan agregasi yang dapat dilakukan oleh database sebagai gantinya. Aplikasi menghitung total penjualan dengan mendapatkan setiap rekaman untuk semua pesanan yang terjual, lalu menghitung jumlah dari rekaman tersebut. Anda dapat menemukan sampel lengkap di sini.

public async Task<IHttpActionResult> AggregateOnClientAsync()
{
    using (var context = new AdventureWorksContext())
    {
        // Fetch all order totals from the database.
        var orderAmounts = await context.SalesOrderHeaders.Select(soh => soh.TotalDue).ToListAsync();

        // Sum the order totals in memory.
        var total = orderAmounts.Sum();
        return Ok(total);
    }
}

Contoh berikutnya menunjukkan masalah halus yang disebabkan oleh cara Kerangka Kerja Entitas menggunakan LINQ to Entities.

var query = from p in context.Products.AsEnumerable()
            where p.SellStartDate < DateTime.Now.AddDays(-7) // AddDays cannot be mapped by LINQ to Entities
            select ...;

List<Product> products = query.ToList();

Aplikasi ini mencoba mencari produk dengan SellStartDate lebih dari seminggu. Dalam kebanyakan kasus, LINQ to Entities akan menerjemahkan klausa where ke pernyataan SQL yang dijalankan oleh database. Namun, dalam kasus ini, LINQ to Entities tidak dapat memetakan metode AddDays untuk SQL. Sebagai gantinya, setiap baris dari tabel Product dikembalikan, dan hasilnya difilter dalam memori.

Panggilan ke AsEnumerable adalah petunjuk bahwa ada masalah. Metode ini mengubah hasilnya menjadi antarmuka IEnumerable. Meskipun IEnumerable mendukung pemfilteran, pemfilteran dilakukan di sisi klien, bukan database. Secara default, LINQ to Entities menggunakan IQueryable, yang meneruskan tanggung jawab untuk memfilter ke sumber data.

Cara memperbaiki antipattern pengambilan asing

Hindari pengambilan data dalam jumlah besar yang dapat dengan cepat menjadi kedaluwarsa atau mungkin dibuang, dan hanya ambil data yang diperlukan untuk operasi yang sedang dilakukan.

Sebagai ganti dari mendapatkan setiap kolom dari tabel lalu memfilternya, pilih kolom yang Anda butuhkan dari database.

public async Task<IHttpActionResult> GetRequiredFieldsAsync()
{
    using (var context = new AdventureWorksContext())
    {
        // Project fields as part of the query itself
        var result = await context.Products
            .Select(p => new ProductInfo {Id = p.ProductId, Name = p.Name})
            .ToListAsync();
        return Ok(result);
    }
}

Demikian pula, lakukan agregasi di database dan bukan di memori aplikasi.

public async Task<IHttpActionResult> AggregateOnDatabaseAsync()
{
    using (var context = new AdventureWorksContext())
    {
        // Sum the order totals as part of the database query.
        var total = await context.SalesOrderHeaders.SumAsync(soh => soh.TotalDue);
        return Ok(total);
    }
}

Saat menggunakan Kerangka Kerja Entitas, pastikan bahwa kueri LINQ diselesaikan menggunakan antarmuka IQueryable dan bukan IEnumerable. Anda mungkin perlu menyesuaikan kueri untuk menggunakan hanya fungsi yang bisa dipetakan ke sumber data. Contoh sebelumnya dapat difaktorkan ulang untuk menghapus metode AddDays dari kueri, memungkinkan pemfilteran dilakukan oleh database.

DateTime dateSince = DateTime.Now.AddDays(-7); // AddDays has been factored out.
var query = from p in context.Products
            where p.SellStartDate < dateSince // This criterion can be passed to the database by LINQ to Entities
            select ...;

List<Product> products = query.ToList();

Pertimbangan

  • Dalam beberapa kasus, Anda dapat meningkatkan performa dengan mempartisi data secara horizontal. Jika operasi yang berbeda mengakses atribut data yang berbeda, partisi horizontal dapat mengurangi pertentangan. Sering kali, sebagian besar operasi dijalankan terhadap sebagian kecil data, sehingga menyebarkan beban ini dapat meningkatkan performa. Lihat Pemartisian data.

  • Untuk operasi yang harus mendukung kueri tak terbatas, terapkan penomoran halaman dan ambil entitas dalam jumlah terbatas pada satu waktu. Misalnya, jika pelanggan menelusuri katalog produk, Anda dapat menampilkan satu halaman hasil dalam satu waktu.

  • Jika memungkinkan, manfaatkan fitur yang ada di dalam penyimpanan data. Misalnya, database SQL biasanya menyediakan fungsi agregat.

  • Jika Anda menggunakan penyimpanan data yang tidak mendukung fungsi tertentu, seperti agregasi, Anda dapat menyimpan hasil terhitung di tempat lain, memperbarui nilai saat rekaman ditambahkan atau diperbarui, sehingga aplikasi tidak perlu menghitung ulang nilai setiap kali dibutuhkan.

  • Jika Anda melihat bahwa permintaan mengambil sejumlah besar bidang, periksa kode sumber untuk menentukan apakah semua bidang ini diperlukan. Terkadang permintaan ini merupakan hasil dari kueri SELECT * yang didesain dengan buruk.

  • Demikian pula, permintaan yang mengambil entitas dalam jumlah besar mungkin merupakan tanda bahwa aplikasi tidak memfilter data dengan benar. Verifikasi bahwa semua entitas ini diperlukan. Gunakan pemfilteran sisi database jika memungkinkan, misalnya, dengan menggunakan klausa WHERE dalam SQL.

  • Memindahkan pemrosesan ke database tidak selalu merupakan pilihan terbaik. Hanya gunakan strategi ini saat database didesain atau dioptimalkan untuk melakukannya. Kebanyakan sistem database sangat dioptimalkan untuk fungsi-fungsi tertentu, tetapi tidak didesain untuk bertindak sebagai mesin aplikasi tujuan umum. Untuk informasi selengkapnya, lihat antipattern Database Sibuk.

Cara mendeteksi antipattern pengambilan asing

Gejala pengambilan asing termasuk latensi tinggi dan throughput rendah. Jika data diambil dari penyimpanan data, peningkatan pertentangan juga mungkin terjadi. Pengguna akhir cenderung melaporkan waktu respons yang diperpanjang atau kegagalan yang disebabkan oleh waktu layanan habis. Kegagalan ini dapat mengembalikan kesalahan HTTP 500 (Server Internal) atau kesalahan HTTP 503 (Layanan Tidak Tersedia). Periksa log peristiwa untuk server web, yang kemungkinan berisi informasi lebih rinci tentang penyebab dan keadaan kesalahan.

Gejala antipattern ini dan beberapa telemetri yang diperoleh mungkin sangat mirip dengan antipattern Persistensi Monolitik.

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

  1. Identifikasi beban kerja atau transaksi yang lambat dengan melakukan pengujian beban, pemantauan proses, atau metode lain untuk mengambil data instrumentasi.
  2. Amati setiap pola perilaku yang ditunjukkan oleh sistem. Apakah ada batasan tertentu dalam hal transaksi per detik atau volume pengguna?
  3. Hubungkan instans beban kerja yang lambat dengan pola perilaku.
  4. Identifikasi penyimpanan data yang digunakan. Untuk setiap sumber data, jalankan telemetri tingkat rendah untuk mengamati perilaku operasi.
  5. Identifikasi kueri yang berjalan lambat yang mereferensikan sumber data ini.
  6. Lakukan analisis khusus sumber daya dari kueri yang berjalan lambat dan pastikan bagaimana data digunakan dan dikonsumsi.

Cari salah satu dari gejala ini:

  • Permintaan I/O besar yang sering dibuat ke sumber daya atau penyimpanan data yang sama.
  • Pertentangan dalam sumber daya bersama atau penyimpanan data.
  • Operasi yang sering menerima data dalam jumlah besar melalui jaringan.
  • Aplikasi dan layanan menghabiskan banyak waktu menunggu I/O selesai.

Contoh diagnosis

Bagian berikut menerapkan langkah-langkah ini ke contoh sebelumnya.

Mengidentifikasi beban kerja yang lambat

Grafik ini menunjukkan hasil performa dari pengujian beban yang menyimulasikan hingga 400 pengguna serentak yang menjalankan metode GetAllFieldsAsync yang ditunjukkan sebelumnya. Throughput berkurang secara perlahan seiring dengan meningkatnya beban. Waktu respons rata-rata meningkat seiring dengan meningkatnya beban kerja.

Hasil pengujian beban untuk metode GetAllFieldsAsync

Pengujian beban untuk operasi AggregateOnClientAsync menunjukkan pola yang serupa. Volume permintaan cukup stabil. Waktu respons rata-rata meningkat seiring dengan beban kerja, meskipun lebih lambat dari grafik sebelumnya.

Hasil pengujian beban untuk metode AggregateOnClientAsync

Menghubungkan beban kerja yang lambat dengan pola perilaku

Korelasi apa pun antara periode reguler penggunaan tinggi dan performa yang melambat dapat menunjukkan area yang menjadi perhatian. Periksa dengan cermat profil performa fungsionalitas yang diduga berjalan lambat, untuk menentukan apakah cocok dengan pengujian beban yang dilakukan sebelumnya.

Pengujian beban fungsi yang sama menggunakan beban pengguna berbasis langkah, untuk menemukan titik yang performanya turun secara signifikan atau gagal total. Jika titik itu berada dalam batas penggunaan dunia nyata yang Anda harapkan, periksa bagaimana fungsionalitas diterapkan.

Pengoperasian yang lambat tidak selalu menjadi masalah, jika tidak dilakukan saat sistem berada di bawah tekanan, tidak kritis terhadap waktu, dan tidak berdampak negatif terhadap performa operasi penting lainnya. Misalnya, menghasilkan statistik operasional bulanan mungkin merupakan operasi yang berjalan lama, tetapi mungkin dapat dilakukan sebagai proses batch dan dijalankan sebagai pekerjaan dengan prioritas rendah. Di sisi lain, pelanggan yang mengkueri katalog produk adalah operasi bisnis yang penting. Fokus pada telemetri yang dihasilkan oleh operasi kritis ini untuk melihat bagaimana performa bervariasi selama periode penggunaan tinggi.

Mengidentifikasi sumber data dalam beban kerja yang lambat

Jika Anda menduga bahwa suatu layanan berperforma buruk karena caranya mengambil data, selidiki bagaimana aplikasi berinteraksi dengan repositori yang digunakannya. Pantau sistem langsung untuk melihat sumber mana yang diakses selama periode performa buruk.

Untuk setiap sumber data, instrumen sistem untuk mengambil hal-hal berikut:

  • Frekuensi setiap penyimpanan data diakses.
  • Volume data yang masuk dan keluar dari penyimpanan data.
  • Waktu operasi ini, terutama latensi permintaan.
  • Sifat dan tingkat kesalahan yang terjadi saat mengakses setiap penyimpanan data di bawah beban biasa.

Bandingkan informasi ini dengan volume data yang dikembalikan oleh aplikasi ke klien. Lacak rasio volume data yang dikembalikan oleh penyimpanan data terhadap volume data yang dikembalikan ke klien. Jika ada perbedaan besar, selidiki untuk menentukan apakah aplikasi mengambil data yang tidak diperlukan.

Anda mungkin dapat mengambil data ini dengan mengamati sistem langsung dan menelusuri siklus hidup setiap permintaan pengguna, atau Anda dapat memodelkan serangkaian beban kerja sintetis dan menjalankannya terhadap sistem pengujian.

Grafik berikut menunjukkan telemetri yang diambil menggunakan New Relic APM selama pengujian beban metode GetAllFieldsAsync. Perhatikan perbedaan antara volume data yang diterima dari database dan respons HTTP yang sesuai.

Telemetri untuk metode GetAllFieldsAsync

Untuk setiap permintaan, database mengembalikan 80.503 byte, tetapi respons ke klien hanya berisi 19.855 byte, sekitar 25% dari ukuran respons database. Ukuran data yang dikembalikan ke klien dapat bervariasi tergantung pada formatnya. Untuk pengujian beban ini, klien meminta data JSON. Pengujian terpisah menggunakan XML (tidak ditampilkan) memiliki ukuran respons 35.655 byte, atau 44% dari ukuran respons database.

Pengujian beban untuk metode AggregateOnClientAsync menunjukkan hasil yang lebih ekstrim. Dalam hal ini, setiap pengujian melakukan kueri yang mengambil lebih dari 280 KB data dari database, tetapi respons JSON hanya 14 byte. Disparitas yang lebar karena metode menghitung hasil agregat dari volume data yang besar.

Telemetri untuk metode AggregateOnClientAsync

Mengidentifikasi dan menganalisis kueri lambat

Cari kueri database yang menghabiskan paling banyak sumber daya dan membutuhkan waktu paling lama untuk dijalankan. Anda dapat menambahkan instrumentasi untuk menemukan waktu mulai dan selesai untuk banyak operasi database. Banyak penyimpanan data juga memberikan informasi mendalam tentang bagaimana kueri dilakukan dan dioptimalkan. Misalnya, panel Performa Kueri di portal manajemen Azure SQL Database memungkinkan Anda memilih kueri dan menampilkan informasi performa runtime mendetail. Berikut adalah kueri yang dihasilkan oleh operasi GetAllFieldsAsync:

Panel Detail Kueri di portal manajemen Windows Azure SQL Database

Menerapkan solusi dan memverifikasi hasilnya

Setelah mengubah metode GetRequiredFieldsAsync untuk menggunakan pernyataan SELECT di sisi database, pengujian beban menunjukkan hasil berikut.

Hasil pengujian beban untuk metode GetRequiredFieldsAsync

Pengujian beban ini menggunakan penyebaran yang sama dan beban kerja simulasi yang sama dari 400 pengguna bersamaan seperti sebelumnya. Grafik menunjukkan latensi yang jauh lebih rendah. Waktu respons meningkat dengan beban menjadi sekitar 1,3 detik, dibandingkan dengan 4 detik dalam kasus sebelumnya. Throughput juga lebih tinggi pada 350 permintaan per detik dibandingkan dengan 100 sebelumnya. Volume data yang diambil dari database sekarang sangat cocok dengan ukuran pesan respons HTTP.

Telemetri untuk metode GetRequiredFieldsAsync

Pengujian beban menggunakan metode AggregateOnDatabaseAsync menghasilkan hasil berikut:

Hasil pengujian beban untuk metode AggregateOnDatabaseAsync

Waktu respons rata-rata sekarang minimal. Ini adalah urutan peningkatan performa yang besar, terutama disebabkan oleh pengurangan besar dalam I/O dari database.

Berikut adalah telemetri yang sesuai untuk metode AggregateOnDatabaseAsync. Jumlah data yang diambil dari database sangat berkurang, dari lebih dari 280 KB per transaksi menjadi 53 byte. Akibatnya, jumlah maksimum permintaan per menit ditingkatkan dari sekitar 2.000 menjadi lebih dari 25.000.

Telemetri untuk metode AggregateOnDatabaseAsync