Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Der kumulierte Effekt einer großen Anzahl von E/A-Anforderungen kann sich erheblich auf die Leistung und Reaktionsfähigkeit auswirken.
Problembeschreibung
Netzwerkaufrufe und andere E/A-Vorgänge sind im Vergleich zu Computeaufgaben inhärent langsam. Jede E/A-Anforderung hat in der Regel einen erheblichen Mehraufwand, und die kumulierte Auswirkung zahlreicher E/A-Vorgänge kann das System verlangsamen. Im Folgenden finden Sie einige häufige Gründe für eine zu hohe Anzahl von E/A-Vorgängen.
Lesen und Schreiben einzelner Datensätze in eine Datenbank als unterschiedliche Anfragen
Das folgende Beispiel liest Daten aus einer Produktdatenbank. Es gibt drei Tabellen, Product
, ProductSubcategory
und ProductPriceListHistory
. Der Code ruft alle Produkte in einer Unterkategorie zusammen mit den Preisinformationen ab, indem eine Reihe von Abfragen ausgeführt wird:
- Die Unterkategorie wird aus der Tabelle
ProductSubcategory
abgefragt. - Suchen Sie alle Produkte in dieser Unterkategorie, indem Sie die
Product
Tabelle abfragen. - Fragen Sie für jedes Produkt die Preisdaten aus der
ProductPriceListHistory
Tabelle ab.
Die Anwendung verwendet Entity Framework , um die Datenbank abzufragen.
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);
}
}
Dieses Beispiel veranschaulicht das Problem explizit, aber manchmal maskiert eine objektrelationale Abbildung (Object Relational Mapping, O/RM) das Problem, wenn untergeordnete Datensätze implizit nacheinander abgerufen werden. Dies wird als "N+1-Problem" bezeichnet.
Implementieren eines einzelnen logischen Vorgangs als Eine Reihe von HTTP-Anforderungen
Dies geschieht häufig, wenn Entwickler versuchen, einem objektorientierten Paradigma zu folgen und Remoteobjekte so zu behandeln, als wären sie lokale Objekte im Speicher. Dies kann zu vielen Netzwerk-Roundtrips führen. Die folgende Web-API macht beispielsweise die einzelnen Eigenschaften von User
Objekten über einzelne HTTP GET-Methoden verfügbar.
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)
{
...
}
}
Obwohl bei diesem Ansatz nichts technisch falsch ist, müssen die meisten Clients wahrscheinlich mehrere Eigenschaften für jeden User
abrufen, was zu Clientcode wie dem folgenden führt.
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();
Lesen und Schreiben in eine Datei auf dem Datenträger
Datei-E/A-Vorgänge umfassen das Öffnen einer Datei und das Springen an den geeigneten Punkt, bevor Daten gelesen oder geschrieben werden. Wenn der Vorgang abgeschlossen ist, wird die Datei möglicherweise geschlossen, um Betriebssystemressourcen zu speichern. Eine Anwendung, die ständig kleine Mengen von Informationen in eine Datei liest und schreibt, generiert erheblichen E/A-Aufwand. Kleine Schreibanforderungen können auch zu Dateifragmentierung führen und nachfolgende E/A-Vorgänge weiter verlangsamen.
Das folgende Beispiel verwendet ein FileStream
-Objekt, um ein Customer
-Objekt in eine Datei zu schreiben. Durch Erstellen des FileStream
-Objekts wird die Datei geöffnet, durch Löschen des Objekts wird sie wieder geschlossen. (Die using
Anweisung entfernt das FileStream
Objekt automatisch.) Wenn die Anwendung diese Methode wiederholt aufruft, wenn neue Kunden hinzugefügt werden, kann sich der E/A-Aufwand schnell ansammeln.
private async Task SaveCustomerToFileAsync(Customer customer)
{
using (Stream fileStream = new FileStream(CustomersFileName, FileMode.Append))
{
BinaryFormatter formatter = new BinaryFormatter();
byte [] data = null;
using (MemoryStream memStream = new MemoryStream())
{
formatter.Serialize(memStream, customer);
data = memStream.ToArray();
}
await fileStream.WriteAsync(data, 0, data.Length);
}
}
So lösen Sie das Problem
Verringern Sie die Anzahl der E/A-Anforderungen, indem Sie die Daten zu größeren und weniger Anforderungen zusammenfassen.
Abrufen von Daten aus einer Datenbank als einzelne Abfrage anstelle mehrerer kleinerer Abfragen. Hier ist eine überarbeitete Version des Programmcodes, die Produktdaten abfragt.
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);
}
}
Befolgen Sie REST-Entwurfsprinzipien für Web-APIs. Hier ist eine überarbeitete Version der Web-API aus dem vorherigen Beispiel. Anstelle der separaten GET-Methoden für jede Eigenschaft gibt es eine einzelne GET-Methode, die User
zurückgibt. Dies führt zu einem größeren Antworttext pro Anforderung, aber jeder Client führt wahrscheinlich weniger API-Aufrufe.
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();
Erwägen Sie bei Datei-E/A das Puffern von Daten im Arbeitsspeicher und das anschließende Schreiben der gepufferten Daten in eine Datei als einzelner Vorgang. Durch diesen Ansatz wird der Mehraufwand beim wiederholten Öffnen und Schließen der Datei verringert und die Fragmentierung der Datei auf dem Datenträger reduziert.
// 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 customer in customers)
{
byte[] data = null;
using (MemoryStream memStream = new MemoryStream())
{
formatter.Serialize(memStream, customer);
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 customer = new Customer(...);
customers.Add(customer);
// 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);
Überlegungen
Die ersten beiden Beispiele führen weniger E/A-Aufrufe, aber jeder ruft weitere Informationen ab. Sie müssen den Kompromiss zwischen diesen beiden Faktoren berücksichtigen. Die richtige Antwort hängt von den tatsächlichen Nutzungsmustern ab. Beispielsweise kann es im Web-API-Beispiel herausstellen, dass Clients häufig nur den Benutzernamen benötigen. In diesem Fall kann es sinnvoll sein, sie als separater API-Aufruf verfügbar zu machen. Weitere Informationen finden Sie im Antimuster Irrelevante Abrufe.
Achten Sie beim Lesen von Daten darauf, dass Ihre E/A-Anforderungen nicht zu groß sind. Eine Anwendung sollte nur die Informationen abrufen, die wahrscheinlich verwendet werden.
Manchmal hilft es, die Informationen zu einem Objekt in zwei Blöcke zu unterteilen: häufig zugegriffene Daten, die für die meisten Anfragen verantwortlich sind, und seltener verwendete Daten, die kaum gebraucht werden. Häufig handelt es sich bei den am häufigsten verwendeten Daten um einen relativ kleinen Teil der Gesamtdaten für ein Objekt, sodass die Rückgabe nur dieses Teils erheblichen E/A-Aufwand sparen kann.
Vermeiden Sie beim Schreiben von Daten, Ressourcen länger als nötig zu sperren, um die Wahrscheinlichkeit von Konflikten während eines langwierigen Vorgangs zu verringern. Wenn ein Schreibvorgang mehrere Datenspeicher, Dateien oder Dienste umfasst, übernehmen Sie schließlich einen konsistenten Ansatz. Siehe Leitfaden zur Datenkonsistenz.
Wenn Sie Daten vor dem Schreiben im Arbeitsspeicher puffern, sind die Daten anfällig, wenn der Prozess abstürzt. Wenn die Datenrate in der Regel verhältnismäßig gering ist oder Lastspitzen aufweist, ist es möglicherweise sicherer, die Daten in einer externen dauerhaften Warteschlange wie z.B. Event Hubs zu puffern.
Erwägen Sie das Zwischenspeichern von Daten, die Sie aus einem Dienst oder einer Datenbank abrufen. Dies kann dazu beitragen, das E/A-Volumen zu reduzieren, indem wiederholte Anforderungen für dieselben Daten vermieden werden. Weitere Informationen finden Sie unter Bewährte Methoden zum Zwischenspeichern.
So erkennen Sie das Problem
Zu den Symptomen einer zu hohen Anzahl von E/A-Vorgängen gehören eine hohe Latenz und ein geringer Durchsatz. Endbenutzer werden wahrscheinlich verzögerte Reaktionszeiten oder Fehler melden, die durch Zeitüberschreitungen der Dienste verursacht werden, aufgrund des erhöhten Konflikts um E/A-Ressourcen.
Sie können die folgenden Schritte ausführen, um die Ursachen von Problemen zu identifizieren:
- Führen Sie die Prozessüberwachung des Produktionssystems durch, um Vorgänge mit schlechten Reaktionszeiten zu identifizieren.
- Führen Sie Auslastungstests für jeden vorgang aus, der im vorherigen Schritt identifiziert wurde.
- Sammeln Sie während der Auslastungstests Telemetriedaten zu den Datenzugriffsanforderungen, die von jedem Vorgang vorgenommen werden.
- Sammeln Sie detaillierte Statistiken für jede Anforderung, die an einen Datenspeicher gesendet wird.
- Profilieren Sie die Anwendung in der Testumgebung, um mögliche E/A-Engpässe zu ermitteln.
Suchen Sie nach einem der folgenden Symptome:
- Eine große Anzahl von kleinen E/A-Anforderungen werden an dieselbe Datei gestellt.
- Eine große Anzahl kleiner Netzwerkanforderungen, die von einer Anwendungsinstanz an denselben Dienst vorgenommen wurden.
- Eine große Anzahl kleiner Anforderungen, die von einer Anwendungsinstanz an denselben Datenspeicher vorgenommen wurden.
- Anwendungen und Dienste, die zunehmend E/A-gebunden sind.
Beispieldiagnose
In den folgenden Abschnitten werden diese Schritte auf das beispiel angewendet, das weiter oben gezeigt wird, in dem eine Datenbank abfragt wird.
Auslastungstest der Anwendung
Dieses Diagramm zeigt die Ergebnisse von Auslastungstests. Die Medianantwortzeit wird in zehn Sekunden pro Anforderung gemessen. Das Diagramm zeigt sehr hohe Latenz. Bei einer Auslastung von 1000 Benutzern muss ein Benutzer möglicherweise fast eine Minute warten, um die Ergebnisse einer Abfrage anzuzeigen.
Hinweis
Die Anwendung wurde als Azure App Service-Web-App mit Azure SQL-Datenbank bereitgestellt. Der Auslastungstest verwendet eine simulierte Schrittarbeitsauslastung von bis zu 1000 gleichzeitigen Benutzern. Die Datenbank wurde mit einem Verbindungspool konfiguriert, der bis zu 1000 Verbindungen gleichzeitig unterstützt, um die Wahrscheinlichkeit zu verringern, dass Konkurrenz um Verbindungen die Ergebnisse beeinflusst.
Die Anwendung überwachen
Sie können ein Application Performance Management (APM)-Paket verwenden, um die wichtigsten Metriken zu erfassen und zu analysieren, mit denen chattige I/O identifiziert werden können. Welche Metriken wichtig sind, hängt von der I/O-Last ab. In diesem Beispiel waren die interessanten E/A-Anforderungen die Datenbankabfragen.
Die folgende Abbildung zeigt Ergebnisse, die mit New Relic APM generiert wurden. Die durchschnittliche Zeit für die Datenbankantwort lag bei ca. 5,6 Sekunden pro Anforderung während der maximalen Arbeitsauslastung. Das System konnte durchschnittlich 410 Anforderungen pro Minute während des Tests unterstützen.
Sammeln detaillierter Datenzugriffsinformationen
Wenn Sie tiefer in die Überwachungsdaten eintauchen, wird gezeigt, dass die Anwendung drei verschiedene SQL SELECT-Anweisungen ausführt. Diese entsprechen den Anforderungen, die von Entity Framework generiert werden, um Daten aus den ProductListPriceHistory
, Product
und ProductSubcategory
Tabellen abzurufen. Darüber hinaus ist die Abfrage, die Daten aus der Tabelle ProductListPriceHistory
abruft, die bei weitem am häufigsten ausgeführte SELECT-Anweisung.
Es stellt sich heraus, dass die GetProductsInSubCategoryAsync
zuvor gezeigte Methode 45 SELECT-Abfragen ausführt. Jede Abfrage bewirkt, dass die Anwendung eine neue SQL-Verbindung öffnet.
Hinweis
Diese Abbildung zeigt Ablaufverfolgungsinformationen für die langsamste Instanz des GetProductsInSubCategoryAsync
Vorgangs im Ladetest. In einer Produktionsumgebung ist es hilfreich, die Spuren der langsamsten Instanzen zu untersuchen, um festzustellen, ob ein Muster vorliegt, das auf ein Problem hindeutet. Wenn Sie nur die Durchschnittswerte betrachten, können Sie Probleme übersehen, die sich unter Last erheblich verschlimmern.
Die nächste Abbildung zeigt die tatsächlichen SQL-Anweisungen, die ausgegeben wurden. Die Abfrage, die Preisinformationen abruft, wird für jedes einzelne Produkt in der Produktunterkategorie ausgeführt. Die Verwendung eines Joins würde die Anzahl der Datenbankaufrufe erheblich reduzieren.
Wenn Sie ein O/RM verwenden, z. B. Entity Framework, kann die Ablaufverfolgung der SQL-Abfragen einblicke, wie die O/RM programmgesteuerte Aufrufe in SQL-Anweisungen übersetzt und Bereiche angibt, in denen der Datenzugriff optimiert werden kann.
Implementieren der Lösung und Überprüfen des Ergebnisses
Das Umschreiben des Aufrufs von Entity Framework hat die folgenden Ergebnisse erzeugt.
Dieser Auslastungstest wurde für dieselbe Bereitstellung mit demselben Ladeprofil durchgeführt. Dieses Mal zeigt das Diagramm deutlich niedrigere Latenz. Die durchschnittliche Anforderungszeit bei 1000 Benutzern liegt zwischen 5 und 6 Sekunden, von fast einer Minute nach unten.
Dieses Mal unterstützte das System durchschnittlich 3.970 Anforderungen pro Minute im Vergleich zu 410 für den früheren Test.
Die Ablaufverfolgung der SQL-Anweisung zeigt, dass alle Daten in einer einzelnen SELECT-Anweisung abgerufen werden. Obwohl diese Abfrage wesentlich komplexer ist, wird sie nur einmal pro Vorgang ausgeführt. Und während komplexe Verknüpfungen teuer werden können, sind relationale Datenbanksysteme für diesen Abfragetyp optimiert.