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.
Ein Blockieren des aufrufenden Threads, während ein E/A-Vorgang abgeschlossen wird, kann die Leistung senken und sich auf die vertikale Skalierbarkeit auswirken.
Problembeschreibung
Ein synchroner E/A-Vorgang blockiert den aufrufenden Thread, bis der E/A abgeschlossen ist. Der aufrufende Thread wird in einen Wartezustand versetzt und kann in diesem Intervall keine sinnvollen Vorgänge ausführen, wodurch Verarbeitungsressourcen verschwendet werden.
Häufige Beispiele für E/A-Vorgänge sind etwa folgende:
- Abrufen oder dauerhaftes Speichern von Daten in einer Datenbank oder einer anderen Art persistentem Speicher
- Senden einer Anforderung an einen Webdienst
- Übermitteln einer Nachricht oder Abrufen einer Nachricht aus einer Warteschlange
- Schreiben in eine lokale Datei oder Lesen aus einer lokalen Datei
Dieses Antimuster tritt üblicherweise aus folgenden Gründen auf:
- Es ist anscheinend die intuitivste Art, einen Vorgang auszuführen.
- Die Anwendung benötigt eine Antwort auf eine Anfrage.
- Die Anwendung verwendet eine Bibliothek, die nur synchrone Methoden für E/A-Vorgänge bereitstellt.
- Eine externe Bibliothek führt intern synchrone E/A-Vorgänge aus. Ein einziger synchroner E/A-Aufruf kann eine vollständige Aufrufkette blockieren.
Der folgende Code lädt eine Datei in Azure Blob Storage hoch. Es gibt zwei Stellen, an denen der Code blockiert wird, weil er auf die synchrone E/A-Verarbeitung wartet: die CreateIfNotExists-Methode und die UploadFromStream-Methode.
var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");
container.CreateIfNotExists();
var blockBlob = container.GetBlockBlobReference("myblob");
// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
blockBlob.UploadFromStream(fileStream);
}
Hier sehen Sie ein Beispiel für das Warten auf die Antwort eines externen Diensts. Die GetUserProfile-Methode ruft einen Remotedienst auf, der ein UserProfile zurückgibt.
public interface IUserProfileService
{
UserProfile GetUserProfile();
}
public class SyncController : ApiController
{
private readonly IUserProfileService _userProfileService;
public SyncController()
{
_userProfileService = new FakeUserProfileService();
}
// This is a synchronous method that calls the synchronous GetUserProfile method.
public UserProfile GetUserProfile()
{
return _userProfileService.GetUserProfile();
}
}
Wie man das Problem behebt
Ersetzen Sie synchrone E/A-Vorgänge durch asynchrone Vorgänge. Dadurch wird der aktuelle Thread wieder frei und kann weiterhin sinnvolle Tasks ausführen, anstatt zu blockieren. So lässt sich die Auslastung der Computeressourcen verbessern. Die asynchrone Ausführung von E/A-Vorgängen ist insbesondere dann effizient, wenn ein unerwarteter Anstieg der Anforderungen von Clientanwendungen verarbeitet werden muss.
Viele Bibliotheken bieten sowohl synchrone als auch asynchrone Versionen von Methoden. Verwenden Sie nach Möglichkeit die asynchronen Versionen. Hier ist die asynchrone Version des vorherigen Beispiels, das eine Datei in Azure Blob Storage hochlädt.
var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");
await container.CreateIfNotExistsAsync();
var blockBlob = container.GetBlockBlobReference("myblob");
// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
await blockBlob.UploadFromStreamAsync(fileStream);
}
Der await-Operator gibt die Steuerung an die aufrufende Umgebung zurück, während der asynchrone Vorgang ausgeführt wird. Der Code nach dieser Anweisung fungiert als Fortsetzung, die ausgeführt wird, wenn der asynchrone Vorgang abgeschlossen ist.
Ein sorgfältig entworfener Dienst sollte auch asynchrone Vorgänge bereitstellen. Hier sehen Sie eine asynchrone Version des Webdiensts, der Benutzerprofile zurückgibt. Die GetUserProfileAsync-Methode benötigt eine asynchrone Version des Benutzerprofildiensts.
public interface IUserProfileService
{
Task<UserProfile> GetUserProfileAsync();
}
public class AsyncController : ApiController
{
private readonly IUserProfileService _userProfileService;
public AsyncController()
{
_userProfileService = new FakeUserProfileService();
}
// This is a synchronous method that calls the Task based GetUserProfileAsync method.
public Task<UserProfile> GetUserProfileAsync()
{
return _userProfileService.GetUserProfileAsync();
}
}
Für Bibliotheken, die keine asynchronen Versionen von Vorgängen bereitstellen, kann es möglich sein, asynchrone Wrapper um ausgewählte synchrone Methoden zu erstellen. Gehen Sie dabei sehr vorsichtig vor. Obwohl die Reaktionsfähigkeit des Threads, der den asynchronen Wrapper aufruft, verbessert werden kann, verbraucht er tatsächlich mehr Ressourcen. Sie kann einen zusätzlichen Thread und mehr Aufwand für die Synchronisierung der Arbeit erstellen, die der Thread ausführt. Einige Kompromisse werden in diesem Blogbeitrag erläutert: Sollte ich asynchrone Wrapper für synchrone Methoden verfügbar machen?
Hier ist ein Beispiel für einen asynchronen Wrapper, der eine synchrone Methode umschließt.
// Asynchronous wrapper around synchronous library method
private async Task<int> LibraryIOOperationAsync()
{
return await Task.Run(() => LibraryIOOperation());
}
Jetzt kann der aufrufende Code im Wrapper warten:
// Invoke the asynchronous wrapper using a task
await LibraryIOOperationAsync();
Überlegungen
E/A-Vorgänge, die voraussichtlich kurzlebig sind und wahrscheinlich keine Konkurrenz verursachen, könnten als synchrone Vorgänge leistungsfähiger sein. Ein Beispiel könnte das Lesen kleiner Dateien auf einem SSD-Laufwerk (Solid-State-Laufwerk) sein. Der Overhead durch das Umleiten eines Tasks in einen anderen Thread und das Synchronisieren mit diesem Thread nach Abschluss des Tasks kann die Vorteile der asynchronen E/A-Verarbeitung überwiegen. Diese Fälle sind jedoch relativ selten, und die meisten E/A-Vorgänge lassen sich asynchron ausführen.
Die Verbesserung der E/A-Leistung kann dazu führen, dass andere Teile des Systems zu Engpässen werden. Dadurch, dass die Blockierung von Threads aufgehoben wird, kann eine höhere Anzahl gleichzeitiger Anforderungen an gemeinsam genutzte Ressourcen entstehen, wodurch wiederum möglicherweise nicht genügend Ressourcen verfügbar sind oder Ressourcen gedrosselt werden müssen. Wenn dies zu einem Problem wird, müssen Sie möglicherweise Ihre Webserver aufskalieren oder Datenspeicher partitionieren, um Konflikte zu reduzieren.
Erkennen des Problems
Für Benutzer scheint die Anwendung gelegentlich nicht zu reagieren. Die Anwendung kann mit Timeout-Exceptions fehlschlagen. In diesen Fällen können auch HTTP 500-Fehler (interner Server) zurückgegeben werden. Auf dem Server werden eingehende Clientanforderungen blockiert, bis ein Thread verfügbar wird, sodass die Anforderungswarteschlangen zu lang werden, was sich in HTTP 503-Fehlern (Dienst nicht verfügbar) manifestiert.
Sie können die folgenden Schritte ausführen, um das Problem zu identifizieren:
Überwachen Sie das Produktionssystem, und ermitteln Sie, ob blockierte Arbeiter-Threads den Durchsatz einschränken.
Wenn Anforderungen aufgrund fehlender Threads blockiert werden, überprüfen Sie die Anwendung, um zu ermitteln, welche Vorgänge synchron E/A ausführen.
Führen Sie kontrollierte Auslastungstests jedes Vorgangs durch, der synchrone E/A-Vorgänge ausführt, um herauszufinden, ob diese Vorgänge die Systemleistung beeinträchtigen.
Beispieldiagnose
In den folgenden Abschnitten werden diese Schritte auf die zuvor beschriebene Beispielanwendung angewendet.
Überwachen der Webserverleistung
Für Azure-Webanwendungen lohnt es sich, die Leistung des IIS-Webservers (Internetinformationsdienste) zu überwachen. Achten Sie besonders auf die Länge der Anforderungswarteschlange, um herauszufinden, ob Anforderungen während Zeiten hoher Aktivität dadurch blockiert werden, dass sie auf verfügbare Threads warten. Sie können diese Informationen erfassen, indem Sie Azure-Diagnose aktivieren. Weitere Informationen finden Sie unter
- Überwachen von Apps in Azure App Service
- Erstellen und Verwenden von Leistungsindikatoren in einer Azure-Anwendung
Instrumentieren Sie die Anwendung, um festzustellen, wie Anforderungen verarbeitet werden, sobald sie akzeptiert wurden. Durch Nachverfolgen eines Anforderungsflows kann ermittelt werden, ob die Anforderung langsame Aufrufe ausführt und den aktuellen Thread blockiert. Mithilfe eines Threadprofils lassen sich Anforderungen markieren, die blockiert werden.
Auslastungstest der Anwendung
Die folgende Abbildung zeigt die Leistung der zuvor gezeigten synchronen GetUserProfile Methode unter unterschiedlichen Lasten von bis zu 4.000 gleichzeitigen Benutzern. Die Anwendung ist eine ASP.NET Anwendung, die in Azure App Service ausgeführt wird.
Der synchrone Vorgang ist so programmiert, dass er für zwei Sekunden pausiert, um synchrone E/A zu simulieren, sodass die minimale Reaktionszeit etwas über zwei Sekunden beträgt. Wenn die Last etwa 2.500 gleichzeitige Benutzer erreicht, erreicht die durchschnittliche Reaktionszeit ein Plateau, obwohl die Anzahl der Anforderungen pro Sekunde weiterhin zunimmt. Die Skala für diese beiden Maße ist logarithmisch. Die Anzahl von Anforderungen pro Sekunde verdoppelt sich zwischen diesem Punkt und dem Ende des Tests.
In einer isolierten Umgebung ergibt sich aus diesem Test nicht eindeutig, ob die synchrone E/A-Verarbeitung ein Problem darstellt. Unter schwererer Auslastung erreicht die Anwendung möglicherweise einen kritischen Punkt, an dem der Webserver Anforderungen nicht mehr rechtzeitig verarbeiten kann, wodurch Clientanwendungen Timeout-Fehlermeldungen erhalten.
Eingehende Anforderungen werden vom IIS-Webserver in die Warteschlange eingereiht und an einen Thread weitergeleitet, der im ASP.NET-Threadpool ausgeführt wird. Da jeder Vorgang die E/A-Verarbeitung synchron ausführt, wird der Thread blockiert, bis der Vorgang abgeschlossen ist. Wenn die Workload steigt, werden schlussendlich alle ASP.NET-Threads im Threadpool zugewiesen und blockiert. Zu diesem Zeitpunkt müssen alle weiteren eingehenden Anfragen in der Warteschlange auf einen verfügbaren Thread warten. Wenn die Warteschlange länger wird, führen Anfragen zu Timeouts.
Implementieren der Lösung und Überprüfen des Ergebnisses
Das nächste Diagramm zeigt die Ergebnisse des Auslastungstests für die asynchrone Version des Codes.
Der Durchsatz ist wesentlich höher. Im gleichen Zeitraum wie beim vorherigen Test verarbeitet das System erfolgreich nahezu das 10-Fache des Durchsatzes (gemessen in Anforderungen pro Sekunde). Darüber hinaus ist die durchschnittliche Antwortzeit relativ konstant und bleibt etwa 25-mal kürzer als im vorherigen Test.