Geveze G/Ç kötü modeliChatty I/O antipattern

Çok sayıda G/Ç isteğinin toplu etkisi, performansta ve yanıtlama hızında büyük bir değişikliğe neden olabilir.The cumulative effect of a large number of I/O requests can have a significant impact on performance and responsiveness.

Sorun açıklamasıProblem description

Ağ çağrıları ve diğer G/Ç işlemleri, yapıları itibarıyla işlem görevlerine göre yavaştır.Network calls and other I/O operations are inherently slow compared to compute tasks. Her G/Ç isteğinin normal olarak ciddi miktarda ek yükü vardır ve çok sayıda G/Ç işleminin toplu etkisi sistemi yavaşlatabilir.Each I/O request typically has significant overhead, and the cumulative effect of numerous I/O operations can slow down the system. Geveze G/Ç’nin bazı yaygın nedenleri aşağıdadır.Here are some common causes of chatty I/O.

Bir veritabanında tek tek kayıtları ayrı istekler olarak okuma ve yazmaReading and writing individual records to a database as distinct requests

Aşağıdaki örnek bir ürün veritabanından okumaktadır.The following example reads from a database of products. Product, ProductSubcategory ve ProductPriceListHistory olarak üç tablo vardır.There are three tables, Product, ProductSubcategory, and ProductPriceListHistory. Kod, bir dizi sorgu yürüterek bir alt kategorideki tüm ürünleri fiyatlandırma bilgileriyle birlikte alır:The code retrieves all of the products in a subcategory, along with the pricing information, by executing a series of queries:

  1. ProductSubcategory tablosundan alt kategoriyi sorgulayın.Query the subcategory from the ProductSubcategory table.
  2. Product tablosunu sorgulayarak söz konusu alt kategorideki tüm ürünleri bulun.Find all products in that subcategory by querying the Product table.
  3. Her bir ürün için ProductPriceListHistory tablosundan fiyatlandırma verilerini sorgulayın.For each product, query the pricing data from the ProductPriceListHistory table.

Uygulama, veritabanını sorgulamak için Entity Framework’ü kullanır.The application uses Entity Framework to query the database. Örneğin tamamını burada bulabilirsiniz.You can find the complete sample here.

public async Task<IHttpActionResult> GetProductsInSubCategoryAsync(int subcategoryId)
{
    using (var context = GetContext())
    {
        // Get product subcategory.
        var productSubcategory = await context.ProductSubcategories
                .Where(psc => psc.ProductSubcategoryId == subcategoryId)
                .FirstOrDefaultAsync();

        // Find products in that category.
        productSubcategory.Product = await context.Products
            .Where(p => subcategoryId == p.ProductSubcategoryId)
            .ToListAsync();

        // Find price history for each product.
        foreach (var prod in productSubcategory.Product)
        {
            int productId = prod.ProductId;
            var productListPriceHistory = await context.ProductListPriceHistory
                .Where(pl => pl.ProductId == productId)
                .ToListAsync();
            prod.ProductListPriceHistory = productListPriceHistory;
        }
        return Ok(productSubcategory);
    }
}

Bu örnekte sorun açıkça gösterilmektedir, ancak bazen bir O/RM alt kayıtları örtük olarak teker teker getiriyorsa sorunu maskeleyebilir.This example shows the problem explicitly, but sometimes an O/RM can mask the problem, if it implicitly fetches child records one at a time. Bu duruma "N+1 sorunu" adı verilir.This is known as the "N+1 problem".

Tek bir mantıksal işlemi bir dizi HTTP isteği olarak uygulamaImplementing a single logical operation as a series of HTTP requests

Bu durum, genellikle geliştiriciler nesneye dayalı bir yaklaşım izleyip uzak nesnelere bellekteki yerel nesnelermiş gibi davrandıklarında ortaya çıkar.This often happens when developers try to follow an object-oriented paradigm, and treat remote objects as if they were local objects in memory. Bunun sonucunda ağda çok fazla gidiş dönüş yaşanabilir.This can result in too many network round trips. Örneğin, aşağıdaki web API'si ayrı HTTP GET yöntemleri aracılığıyla User nesnelerinin özelliklerini bireysel olarak kullanıma sunar.For example, the following web API exposes the individual properties of User objects through individual HTTP GET methods.

public class UserController : ApiController
{
    [HttpGet]
    [Route("users/{id:int}/username")]
    public HttpResponseMessage GetUserName(int id)
    {
        ...
    }

    [HttpGet]
    [Route("users/{id:int}/gender")]
    public HttpResponseMessage GetGender(int id)
    {
        ...
    }

    [HttpGet]
    [Route("users/{id:int}/dateofbirth")]
    public HttpResponseMessage GetDateOfBirth(int id)
    {
        ...
    }
}

Bu yaklaşımda teknik yönden yanlış bir şey yoktur, ancak birçok istemcinin büyük olasılıkla her bir User için çeşitli özellikler alması gerekecektir. Bu durum da aşağıdaki gibi istemci kodlarına yol açar.While there's nothing technically wrong with this approach, most clients will probably need to get several properties for each User, resulting in client code like the following.

HttpResponseMessage response = await client.GetAsync("users/1/username");
response.EnsureSuccessStatusCode();
var userName = await response.Content.ReadAsStringAsync();

response = await client.GetAsync("users/1/gender");
response.EnsureSuccessStatusCode();
var gender = await response.Content.ReadAsStringAsync();

response = await client.GetAsync("users/1/dateofbirth");
response.EnsureSuccessStatusCode();
var dob = await response.Content.ReadAsStringAsync();

Disk üzerindeki bir dosyada okuma ve yazmaReading and writing to a file on disk

Dosya G/Ç’si bir dosyanın açılmasını ve veri okumadan veya yazmadan önce uygun noktaya gidilmesini içerir.File I/O involves opening a file and moving to the appropriate point before reading or writing data. İşlem tamamlandığında işletim sistemi kaynaklarından tasarruf etmek için dosya kapanabilir.When the operation is complete, the file might be closed to save operating system resources. Bir dosyada sürekli olarak küçük miktarlarda bilgi okuyan ve yazan bir uygulama ciddi oranda G/Ç ek yükü oluşturur.An application that continually reads and writes small amounts of information to a file will generate significant I/O overhead. Küçük yazma istekleri ayrıca dosya parçalanmasına yol açarak daha sonraki G/Ç işlemlerinin daha da yavaşlamasına neden olabilir.Small write requests can also lead to file fragmentation, slowing subsequent I/O operations still further.

Aşağıdaki örnekte, FileStream kullanılarak bir dosyaya bir Customer nesnesi yazılmaktadır.The following example uses a FileStream to write a Customer object to a file. FileStream oluşturulması dosyayı açar ve bunun atılması dosyayı kapatır.Creating the FileStream opens the file, and disposing it closes the file. (using deyimi, FileStream nesnesini otomatik olarak atar.) Uygulama yeni müşteriler eklendikçe bu yöntemi tekrar tekrar çağırırsa G/Ç ek yükü hızla birikebilir.(The using statement automatically disposes the FileStream object.) If the application calls this method repeatedly as new customers are added, the I/O overhead can accumulate quickly.

private async Task SaveCustomerToFileAsync(Customer cust)
{
    using (Stream fileStream = new FileStream(CustomersFileName, FileMode.Append))
    {
        BinaryFormatter formatter = new BinaryFormatter();
        byte [] data = null;
        using (MemoryStream memStream = new MemoryStream())
        {
            formatter.Serialize(memStream, cust);
            data = memStream.ToArray();
        }
        await fileStream.WriteAsync(data, 0, data.Length);
    }
}

Sorunun çözümüHow to fix the problem

Verileri daha az sayıda, daha büyük istekler şeklinde paketleyerek G/Ç isteklerinin sayısını azaltın.Reduce the number of I/O requests by packaging the data into larger, fewer requests.

Verileri veritabanından çeşitli küçük sorgular yerine tek bir sorgu olarak getirin.Fetch data from a database as a single query, instead of several smaller queries. Ürün bilgilerini alan kodun düzeltilmiş bir hali aşağıdadır.Here's a revised version of the code that retrieves product information.

public async Task<IHttpActionResult> GetProductCategoryDetailsAsync(int subCategoryId)
{
    using (var context = GetContext())
    {
        var subCategory = await context.ProductSubcategories
                .Where(psc => psc.ProductSubcategoryId == subCategoryId)
                .Include("Product.ProductListPriceHistory")
                .FirstOrDefaultAsync();

        if (subCategory == null)
            return NotFound();

        return Ok(subCategory);
    }
}

Web API’leri için REST tasarım ilkelerini uygulayın.Follow REST design principles for web APIs. Yukarıdaki örnekteki web API’sinin düzeltilmiş bir hali aşağıdadır.Here's a revised version of the web API from the earlier example. Her bir özellik için ayrı GET yöntemleri yerine tek bir GET yöntemi ile User döndürülmektedir.Instead of separate GET methods for each property, there is a single GET method that returns the User. Bunun sonucunda istek başına yanıt hacmi artar, ancak her istemcinin yapacağı API çağrılarının sayısı büyük olasılıkla azalacaktır.This results in a larger response body per request, but each client is likely to make fewer API calls.

public class UserController : ApiController
{
    [HttpGet]
    [Route("users/{id:int}")]
    public HttpResponseMessage GetUser(int id)
    {
        ...
    }
}

// Client code
HttpResponseMessage response = await client.GetAsync("users/1");
response.EnsureSuccessStatusCode();
var user = await response.Content.ReadAsStringAsync();

Dosya G/Ç’si için bellekteki verileri arabelleğe alabilir ve arabelleğe alınmış verileri tek bir işlemde dosyaya yazabilirsiniz.For file I/O, consider buffering data in memory and then writing the buffered data to a file as a single operation. Bu yaklaşım sayesinde dosyanın tekrar tekrar açılıp kapanmasından doğan ek yük düşürülür ve diskteki dosyanın parçalanması azaltılır.This approach reduces the overhead from repeatedly opening and closing the file, and helps to reduce fragmentation of the file on disk.

// Save a list of customer objects to a file
private async Task SaveCustomerListToFileAsync(List<Customer> customers)
{
    using (Stream fileStream = new FileStream(CustomersFileName, FileMode.Append))
    {
        BinaryFormatter formatter = new BinaryFormatter();
        foreach (var cust in customers)
        {
            byte[] data = null;
            using (MemoryStream memStream = new MemoryStream())
            {
                formatter.Serialize(memStream, cust);
                data = memStream.ToArray();
            }
            await fileStream.WriteAsync(data, 0, data.Length);
        }
    }
}

// In-memory buffer for customers.
List<Customer> customers = new List<Customers>();

// Create a new customer and add it to the buffer
var cust = new Customer(...);
customers.Add(cust);

// Add more customers to the list as they are created
...

// Save the contents of the list, writing all customers in a single operation
await SaveCustomerListToFileAsync(customers);

Dikkat edilmesi gerekenlerConsiderations

  • İlk iki örnekte daha az G/Ç çağrısı yapılmakta ve her çağrıda daha fazla bilgi alınmaktadır.The first two examples make fewer I/O calls, but each one retrieves more information. Bu iki faktörün karşılıklı avantaj ve dezavantajlarını dikkate almanız gerekir.You must consider the tradeoff between these two factors. Doğru yanıt, gerçek kullanım kalıplarınıza bağlıdır.The right answer will depend on the actual usage patterns. Örneğin, web API’si örneğinde istemciler çoğu zaman yalnızca kullanıcı adına ihtiyaç duyuyor olabilir.For example, in the web API example, it might turn out that clients often need just the user name. Böyle bir durumda, bu bilginin ayrı bir API çağrısı olarak kullanıma sunulması mantıklı olabilir.In that case, it might make sense to expose it as a separate API call. Daha fazla bilgi için Fazlalık Getirme kötü modeline bakın.For more information, see the Extraneous Fetching antipattern.

  • Veri okurken G/Ç isteklerinizi çok büyük tutmayın.When reading data, do not make your I/O requests too large. Uygulamalar, yalnızca kullanma olasılıkları olan bilgileri almalıdır.An application should only retrieve the information that it is likely to use.

  • Bazen bir nesneye ait bilgilerin iki parça halinde bölümlenmesi faydalı olabilir: isteklerin çoğuna karşılık veren sık erişilen veriler ve nadiren kullanılan daha seyrek erişilen veriler.Sometimes it helps to partition the information for an object into two chunks, frequently accessed data that accounts for most requests, and less frequently accessed data that is used rarely. En sık erişilen veriler, çoğu zaman nesneye ait tüm verilerin nispeten küçük bir kısmıdır. Bu nedenle, sadece bu kısmın döndürülmesi G/Ç ek yükünü önemli ölçüde azaltabilir.Often the most frequently accessed data is a relatively small portion of the total data for an object, so returning just that portion can save significant I/O overhead.

  • Veri yazarken kaynakları gereğinden uzun süre kilitlemekten kaçının. Böylece uzun işlemler sırasında çekişme olasılığı azaltılır.When writing data, avoid locking resources for longer than necessary, to reduce the chances of contention during a lengthy operation. Bir yazma işlemi birden çok veri deposu, dosya veya hizmeti kapsıyorsa nihai tutarlılık yaklaşımını benimseyin.If a write operation spans multiple data stores, files, or services, then adopt an eventually consistent approach. Bkz. Veri Tutarlılığı kılavuzu.See Data Consistency guidance.

  • Verileri yazmadan önce bellekte arabelleğe alıyorsanız işlem kilitlenirse veriler savunmasızdır.If you buffer data in memory before writing it, the data is vulnerable if the process crashes. Veri oranı genellikle ani artışlar içeriyorsa veya nispeten seyrekse verilerin Event Hubs gibi dayanıklı bir dış kuyrukta arabelleğe alınması daha güvenli olabilir.If the data rate typically has bursts or is relatively sparse, it may be safer to buffer the data in an external durable queue such as Event Hubs.

  • Bir hizmet veya veritabanından alınan verileri önbelleğe almanız iyi bir fikir olabilir.Consider caching data that you retrieve from a service or a database. Bu sayede, aynı verilere yönelik yinelenen isteklerden kaçınılarak G/Ç hacmi azaltılabilir.This can help to reduce the volume of I/O by avoiding repeated requests for the same data. Daha fazla bilgi için bkz. Önbelleğe alma ile ilgili en iyi uygulamalar.For more information, see Caching best practices.

Sorunu algılamaHow to detect the problem

Geveze G/Ç durumunun belirtileri, gecikme sürelerinin yüksek ve aktarım hızının düşük olmasını içerir.Symptoms of chatty I/O include high latency and low throughput. G/Ç kaynakları için artan çekişme nedeniyle son kullanıcılar, uzayan yanıt süreleri veya hizmetlerin zaman aşımına uğramasından doğan hatalar bildirebilir.End users are likely to report extended response times or failures caused by services timing out, due to increased contention for I/O resources.

Herhangi bir sorunun nedenini belirlemenize yardımcı olması için aşağıdaki adımları gerçekleştirebilirsiniz:You can perform the following steps to help identify the causes of any problems:

  1. Üretim sistemindeki süreçleri izleyerek uzun yanıt sürelerine sahip işlemleri belirleyin.Perform process monitoring of the production system to identify operations with poor response times.
  2. Önceki adımda belirlenen her işlem için yük testi gerçekleştirin.Perform load testing of each operation identified in the previous step.
  3. Yük testleri sırasında, her bir işlem tarafından yapılan veri erişim istekleriyle ilgili telemetri verileri toplayın.During the load tests, gather telemetry data about the data access requests made by each operation.
  4. Veri depolarına gönderilen her istek için ayrıntılı istatistikler toplayın.Gather detailed statistics for each request sent to a data store.
  5. G/Ç performans sorunları ortaya çıkabilecek noktaları saptamak için test ortamında uygulamanın profilini oluşturun.Profile the application in the test environment to establish where possible I/O bottlenecks might be occurring.

Aşağıdaki belirtilerin varlığını denetleyin:Look for any of these symptoms:

  • Aynı dosyaya yönelik çok sayıda küçük G/Ç isteği.A large number of small I/O requests made to the same file.
  • Bir uygulama örneği tarafından aynı hizmete yönelik olarak yapılan çok sayıda küçük ağ isteği.A large number of small network requests made by an application instance to the same service.
  • Bir uygulama örneği tarafından aynı veri deposuna yönelik olarak yapılan çok sayıda küçük istek.A large number of small requests made by an application instance to the same data store.
  • Uygulamalar ve hizmetlerin G/Ç’ye bağlı bir hal alması.Applications and services becoming I/O bound.

Örnek tanılamaExample diagnosis

Aşağıdaki bölümlerde, bu adımlar yukarıdaki bir veritabanının sorgulandığı örneğe uygulanmaktadır.The following sections apply these steps to the example shown earlier that queries a database.

Uygulamaya yük testi uygulamaLoad test the application

Aşağıdaki grafikte yük testinin sonuçları gösterilmektedir.This graph shows the results of load testing. Ortanca yanıt süresi, istek başına saniyenin onda biri cinsinden ölçülmektedir.Median response time is measured in tens of seconds per request. Grafikte gecikme süresinin çok yüksek olduğu görülmektedir.The graph shows very high latency. Yük 1000 kullanıcı olduğunda kullanıcıların yaptıkları sorgunun sonuçlarını görmek için yaklaşık bir dakika beklemesi gerekebilir.With a load of 1000 users, a user might have to wait for nearly a minute to see the results of a query.

Geveze G/Ç örnek uygulaması için ana göstergelerin yük testi sonuçları

Not

Uygulama, Azure SQL Veritabanı kullanılıp bir Azure App Service web uygulaması olarak dağıtıldı.The application was deployed as an Azure App Service web app, using Azure SQL Database. Yük testi, 1000 adede kadar eş zamanlı kullanıcı içeren, benzetimi yapılmış adımlı bir iş yükü kullandı.The load test used a simulated step workload of up to 1000 concurrent users. Bağlantı için çekişmenin sonuçları etkilemesi olasılığını azaltmak için veritabanı, 1000 adede kadar eş zamanlı bağlantıyı destekleyen bir bağlantı havuzu ile yapılandırıldı.The database was configured with a connection pool supporting up to 1000 concurrent connections, to reduce the chance that contention for connections would affect the results.

Uygulamayı izlemeMonitor the application

Geveze G/Ç’yi tespit edebilecek ana ölçümleri yakalamak ve analiz etmek için bir uygulama performansı izleme (APM) paketi kullanabilirsiniz.You can use an application performance monitoring (APM) package to capture and analyze the key metrics that might identify chatty I/O. Hangi ölçümlerin önemli olduğu G/Ç iş yüküne bağlıdır.Which metrics are important will depend on the I/O workload. Bu örnekte ilginç G/Ç istekleri veritabanı sorgularıydı.For this example, the interesting I/O requests were the database queries.

Aşağıdaki resimde, New Relic APM kullanılarak üretilen sonuçlar gösterilmektedir.The following image shows results generated using New Relic APM. Ortalama veritabanı yanıt süresi, en fazla iş yükü sırasında istek başına yaklaşık 5,6 saniye ile en yüksek değerine ulaştı.The average database response time peaked at approximately 5.6 seconds per request during the maximum workload. Sistem, test boyunca dakika başına ortalama 410 isteği destekleyebildi.The system was able to support an average of 410 requests per minute throughout the test.

AdventureWorks2012 veritabanına gelen trafiğe genel bakış

Ayrıntılı veri erişim bilgileri toplamaGather detailed data access information

İzleme verilerine daha ayrıntılı olarak bakıldığında uygulamanın üç farklı SQL SELECT deyimi yürüttüğü görülmektedir.Digging deeper into the monitoring data shows the application executes three different SQL SELECT statements. Bu deyimler ProductListPriceHistory, Product ve ProductSubcategory tablolarından veri getirmek için Entity Framework tarafından oluşturulan isteklere karşılık gelmektedir.These correspond to the requests generated by Entity Framework to fetch data from the ProductListPriceHistory, Product, and ProductSubcategory tables. Ayrıca, ProductListPriceHistory tablosundan veri alan sorgu, açık arayla en sık yürütülen SELECT deyimidir.Furthermore, the query that retrieves data from the ProductListPriceHistory table is by far the most frequently executed SELECT statement, by an order of magnitude.

Test edilen örnek uygulama tarafından gerçekleştirilen sorgular

Daha önce gösterilmiş olan GetProductsInSubCategoryAsync yöntemi, 45 adet SELECT sorgusu gerçekleştirir.It turns out that the GetProductsInSubCategoryAsync method, shown earlier, performs 45 SELECT queries. Her bir sorgu, uygulamanın yeni bir SQL bağlantısı açmasına neden olur.Each query causes the application to open a new SQL connection.

Test edilen örnek uygulamanın sorgu istatistikleri

Not

Bu resimde, yük testinde GetProductsInSubCategoryAsync işleminin en yavaş örneği için izleme bilgileri gösterilmektedir.This image shows trace information for the slowest instance of the GetProductsInSubCategoryAsync operation in the load test. Üretim ortamında, bir sorun olduğuna işaret eden bir kalıp olup olmadığını görmek için en yavaş örneklerin izlemelerinin incelenmesi kullanışlıdır.In a production environment, it's useful to examine traces of the slowest instances, to see if there is a pattern that suggests a problem. Yalnızca ortalama değerlere bakarsanız yük altında çok daha kötü bir hal alacak olan sorunları atlayabilirsiniz.If you just look at the average values, you might overlook problems that will get dramatically worse under load.

Sıradaki resimde, gönderilen gerçek SQL deyimleri gösterilmektedir.The next image shows the actual SQL statements that were issued. Fiyat bilgilerini getiren sorgu, ürün alt kategorisindeki her bir ürün için çalıştırılır.The query that fetches price information is run for each individual product in the product subcategory. Bir birleşim kullanılması veritabanı çağrısı sayısını ciddi ölçüde azaltır.Using a join would considerably reduce the number of database calls.

Test edilen örnek uygulamanın sorgu ayrıntıları

Entity Framework gibi bir O/RM kullanıyorsanız SQL sorgularının izlenmesi, O/RM’nin programlı çağrıları SQL deyimlerine nasıl çevirdiğinin daha iyi anlaşılmasını sağlayabilir ve veri erişiminin iyileştirilebileceği alanları belirtebilir.If you are using an O/RM, such as Entity Framework, tracing the SQL queries can provide insight into how the O/RM translates programmatic calls into SQL statements, and indicate areas where data access might be optimized.

Çözümü uygulama ve sonucu doğrulamaImplement the solution and verify the result

Entity Framework çağrısının yeniden yazılması aşağıdaki sonuçları üretti.Rewriting the call to Entity Framework produced the following results.

Geveze G/Ç örnek uygulamasındaki öbeklenmiş API için ana göstergelerin yük testi sonuçları

Bu yük testi, aynı yük profili kullanılarak aynı dağıtım üzerinde gerçekleştirildi.This load test was performed on the same deployment, using the same load profile. Grafik, bu kez çok daha düşük gecikme süresi göstermektedir.This time the graph shows much lower latency. 1000 kullanıcı olduğunda ortalama istek zamanı, yaklaşık 1 dakikalık değerinden 5-6 saniyeye inmiştir.The average request time at 1000 users is between 5 and 6 seconds, down from nearly a minute.

Sistem, önceki testteki dakikada 410 istek yerine bu kez dakikada ortalama 3.970 isteği desteklemektedir.This time the system supported an average of 3,970 requests per minute, compared to 410 for the earlier test.

Öbeklenmiş API için işleme genel bakış

SQL deyiminin izlemesi, tüm verilerin tek bir SELECT deyiminde getirildiğini göstermektedir.Tracing the SQL statement shows that all the data is fetched in a single SELECT statement. Bu sorgu çok daha karmaşık olsa da işlem başına yalnızca bir kez gerçekleştirilir.Although this query is considerably more complex, it is performed only once per operation. Karmaşık birleştirmeler pahalı bir hal alabilir, ancak ilişkisel veritabanı sistemleri bu tür bir sorgu için iyileştirilmiştir.And while complex joins can become expensive, relational database systems are optimized for this type of query.

Öbeklenmiş API için sorgu ayrıntıları