Yanlış Örnek Oluşturma Kötü Modeli

Bazen bir sınıfın yeni örnekleri, bir kez oluşturulup sonra paylaşılması amaçlandığında sürekli oluşturulur. Bu davranış performansa zarar verebilir ve hatalı örnek oluşturma kötü modeli olarak adlandırılır. Kötü model, genellikle etkisiz olan ve hatta üretken olmayan yinelenen bir soruna verilen yaygın bir yanıttır.

Sorun açıklaması

Birçok kitaplık dış kaynakların özetlerini sağlar. Dahili olarak, bu sınıflar normalde kaynakla aralarındaki bağlantıları yönetir, istemcilerin kaynağa erişmek için kullanabilecekleri aracı işlevini görürler. Burada, Azure uygulamalarına uygun bazı aracı sınıfı örnekleri bulabilirsiniz:

  • System.Net.Http.HttpClient. HTTP kullanarak bir web hizmetiyle iletişim kurar.
  • Microsoft.ServiceBus.Messaging.QueueClient. Service Bus kuyruğuna iletiler gönderir ve alır.
  • Microsoft.Azure.Documents.Client.DocumentClient. Azure Cosmos DB örneğine Bağlan.
  • StackExchange.Redis.ConnectionMultiplexer. Redis için Azure Cache de dahil olmak üzere Redis'e bağlanır.

Bu sınıfların tek bir kez örneğinin oluşturulması ve uygulamanın kullanım ömrü boyunca bu örneğin yeniden kullanılması hedeflenmiştir. Öte yandan, bu sınıfların ancak ihtiyaç duyulduğunda alınması ve hızla serbest bırakılması gerektiği yaygın bir yanlış anlamadır. (Burada listelenenler .NET kitaplıkları olabilir, ancak desen .NET için benzersiz değildir.) Aşağıdaki ASP.NET örnek, uzak bir hizmetle iletişim kurmak için bir örneği HttpClient oluşturur. Örneğin tamamını burada bulabilirsiniz.

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 };
        }
    }
}

Web uygulamasında, bu teknik ölçeklenebilir değildir. Her kullanıcı isteği için yeni bir HttpClient nesnesi oluşturulur. Ağır yük koşullarında, web sunucusu kullanılabilir yuva sayısını tüketebilir ve bunun sonucunda SocketException hataları alınır.

Bu sorun, HttpClient sınıfıyla sınırlı değildir. Kaynakları sarmalayan veya oluşturması pahalıya gelen sınıflar benzer sorunlara neden olabilir. Aşağıdaki örnek, ExpensiveToCreateService sınıfının bir örneğini oluşturur. Burada sorun yuva tükenmesi olmayabilir, ama her kaynağı oluşturmanın ne kadar süreceğiyle ilgilidir. Sürekli olarak bu sınıfın örnekleri oluşturmak ve yok etmek, sistemin ölçeklenebilirliğini olumsuz etkileyebilir.

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);
    }
    ...
}

Yanlış örnek oluşturma kötü modeli nasıl düzeltilir

Dış kaynağı sarmalayan sınıf paylaşılabilir ve iş parçacığı güvenli bir sınıfsa, bu sınıfın paylaşılan tekil bir örneğini veya yeniden kullanılabilir örnekler havuzunu oluşturun.

Aşağıdaki örnek, statik bir HttpClient örneğini kullanır ve böylelikle bağlantıyı tüm istekler arasında paylaşır.

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 };
    }
}

Dikkat edilmesi gereken noktalar

  • Bu kötü modelin anahtar öğesi, paylaşılabilir bir nesnenin örneklerini sürekli oluşturmak ve yok etmektir. Sınıf paylaşılabilir değilse (iş parçacığı güvenli değilse), bu kötü model geçerli olmaz.

  • Tekil örnek mi kullanacağınızı yoksa havuz mu oluşturacağınızı, paylaşılan kaynağın türü belirleyebilir. HttpClient sınıfı, havuz oluşturmak yerine paylaşmak için tasarlanmıştır. Diğer nesneler havuz oluşturmayı destekleyerek, sistemin iş yükünü birden çok örneğe dağıtmasına olanak tanıyabilir.

  • Birden çok istek arasında paylaştığınız nesneler iş parçacığı güvenli olmalıdır. HttpClient sınıfı bu şekilde kullanılmak üzere tasarlanmıştır, ama diğer sınıflar eşzamanlı istekleri desteklemeyebilir; bu nedenle, sağlanan belgeleri gözden geçirin.

  • Paylaşılan nesnelerde özellikleri ayarlama konusunda dikkatli olun çünkü bu yarış durumlarına yol açabilir. Öneğin, her istekten önce HttpClient sınıfında DefaultRequestHeaders ayarı yapmak yarış durumu oluşturabilir. Bu tür özellikleri bir kez ayarlayın (örneğin başlatma sırasında) ve farklı ayarlar yapılandırmanız gerekirse ayrı örnekler oluşturun.

  • Bazı kaynak türleri azdır ve elde tutulmamalıdır. Veritabanı bağlantıları buna örnek verilebilir. Gerekli olmayan bir veritabanı bağlantısını açık tutmak, diğer eşzamanlı kullanıcıların veritabanına erişmesini engelleyebilir.

  • .NET Framework'te dış kaynaklara bağlantı oluşturan birçok nesne, bu bağlantıları yöneten diğer sınıfların statik fabrika yöntemleri kullanılarak oluşturulur. Bu nesnelerin atılıp yeniden oluşturulması değil, kaydedilmesi ve yeniden kullanılması hedeflenmiştir. Örneğin, Azure Service Bus'ta QueueClient nesnesi bir MessagingFactory nesnesi aracılığıyla oluşturulur. Dahili olarak, MessagingFactory bağlantıları yönetir. Daha fazla bilgi için bkz. Service Bus Mesajlaşması kullanarak en iyi performans geliştirme en deneyimleri.

Yanlış örnek oluşturma kötü modeli algılama

Bu sorunun belirtileri, aktarım hızında düşüş ve hata oranında artışla birlikte aşağıdakilerden birini veya birden çoğunu da içerir:

  • Yuvalar, veritabanı bağlantıları ve dosya tanıtıcıları gibi kaynakların tükendiğini gösteren özel durumlarda artış.
  • Bellek kullanımı ve atık toplamada artış.
  • Ağ, disk ve veritabanı etkinliğinde artış.

Bu sorunun belirlenmesine yardımcı olacak aşağıdaki adımları gerçekleştirebilirsiniz:

  1. Üretim sistemindeki süreçleri izleyerek uzun yanıt sürelerinin yaşandığı veya kaynak eksikliği nedeniyle sistemin başarısız olduğu noktaları belirleyin.
  2. Bu noktalarda yakalanan telemetri verilerini inceleyerek kaynağı tüketen nesneleri oluşturuyor veya yok ediyor olabilecek işlemleri saptayın.
  3. Kuşkulanılan her işlem için üretim ortamı yerine denetimli bir test ortamında yük testi yapın.
  4. Kaynak kodu gözden geçirin ve aracı nesnelerin nasıl yönetildiğini inceleyin.

Yavaş çalışan veya sistem yüklü olduğunda özel durumlar oluşturan işlemler için yığın izlemelerine bakın. Bu bilgiler, bu işlemlerin kaynakları nasıl kullandığını belirlemeye yardımcı olabilir. Özel durumlar hataların, paylaşılan kaynakların tükenmesinden kaynaklanıp kaynaklanmadığını saptamaya yardımcı olabilir.

Örnek tanılama

Aşağıdaki bölümlerde, bu adımlar yukarıda açıklanan örnek uygulamaya uygulanmaktadır.

Yavaşlama veya hata noktalarını belirleme

Aşağıdaki resimde, New Relic APM, kullanılarak oluşturulan ve yanıt süresi uzun olan uygulamaları gösteren sonuçlar görüntülenir. Bu durumda, NewHttpClientInstancePerRequest denetleyicisindeki GetProductAsync yöntemini daha fazla incelemek yararlı olacaktır. Bu işlemler çalışırken hata oranının da arttığına dikkat edin.

The New Relic monitor dashboard showing the sample application creating a new instance of an HttpClient object for each request

Telemetri verilerini inceleme ve bağıntılar bulma

Sonraki resimde, önceki resimle aynı dönemde iş parçacığı profili oluşturma kullanılarak yakalanan veriler gösterilir. Sistem, yuva bağlantılarını açmak için önemli bir zaman harcar ve hatta bunları kapatmak ve yuva özel durumlarını işlemek için daha fazla zaman harcar.

The New Relic thread profiler showing the sample application creating a new instance of an HttpClient object for each request

Yük testi yapma

Kullanıcıların gerçekleştirebileceği tipik işlemlerin benzetimini yapmak için yük testi kullanın. Bu, çeşitli yükler alında sistemin hangi bölümlerinde kaynak tükenmesi yaşandığını belirlemeye yardımcı olabilir. Bu testleri üretim sisteminde değil denetimli bir ortamda gerçekleştirin. Aşağıdaki grafikte, kullanıcı yükü 100 eşzamanlı kullanıcıya çıktığında NewHttpClientInstancePerRequest denetleyicisi tarafından işlenen isteklerin aktarım hızı gösterilir.

Throughput of the sample application creating a new instance of an HttpClient object for each request

Başlangıçta, iş yükü arttıkça saniyede işlenen istek hacmi de artar. Ancak, yaklaşık 30 kullanıcıda başarılı istek hacmi bir sınıra ulaşır ve sistem özel durumlar oluşturmaya başlar. Ondan sonra, kullanıcı yüküyle birlikte özel durumların hacmi de aşamalı olarak artar.

Yük testi bu hataları HTTP 500 (İç Sunucu) hataları olarak bildirir. Telemetri gözden geçirildiğinde, giderek daha çok HttpClient nesnesi oluşturuldukça sistemin yuva kaynaklarının tükenmesinin bu hatalara neden olduğunu görülür.

Sonraki grafikte, özel ExpensiveToCreateService nesnesini oluşturan denetleyici için benzer bir test gösterilir.

Throughput of the sample application creating a new instance of the ExpensiveToCreateService for each request

Bu kez, denetleyici özel durum oluşturmaz ancak yine ortalama yanıt süresi 20'nin katlarıyla artarken aktarım hızı yatay bir düzeye ulaşır. (Grafik, yanıt süresi ve aktarım hızı için logaritmik ölçek kullanır.) Telemetri, yeni örneklerinin oluşturulmasının ExpensiveToCreateService sorunun ana nedeni olduğunu gösterdi.

Çözümü uygulama ve sonucu doğrulama

Tek bir HttpClient örneğini paylaşmak için GetProductAsync yöntemine geçtikten sonra, ikinci bir yük testi performans artışını göstermiştir. Hiçbir hata bildirilmemiş ve sistem saniyede 500 isteğe kadar çıkan bir yükü işleyebilmiştir. Önceki testle karşılaştırıldığında, ortalama yanıt süresi yarıya inmiştir.

Throughput of the sample application reusing the same instance of an HttpClient object for each request

Karşılaştırma için, aşağıdaki resimde yığın izleme telemetrisi gösterilir. Bu kez, sistem zamanının çoğunu yuvaları açıp kapamak yerine gerçek çalışmayı yapmaya harcar.

The New Relic thread profiler showing the sample application creating single instance of an HttpClient object for all requests

Sonraki grafikte ExpensiveToCreateService nesnesinin paylaşılan örneğinin kullanıldığı benzer bir yük testi gösterilir. Yinelemek gerekirse, işlenen isteklerin hacmi kullanıcı yüküyle birlikte artarken, ortalama yanıt süresi kısa kalmayan devam eder.

Graph showing a similar load test using a shared instance of the ExpensiveToCreateService object.