Drei Möglichkeiten zur Verbesserung der Leistung IO-intensiver Dienste


  Die meisten Geschäftssysteme sind tatsächlich E/A-intensive Systeme. Beispielsweise stellen wir Kameradienste für die B-Seite bereit. Viele Schnittstellen aggregieren tatsächlich verschiedene Daten und zeigen sie den Benutzern an. Zu unseren Datenquellen gehören Redis, MySQL, Hbase und einige Daten Abhängige Serviceparteien erfordern keine allzu komplizierte Berechnungslogik. In den letzten sechs Monaten sind wir aufgrund des Wachstums unseres Datenvolumens und der Geschäftskomplexität tatsächlich auf einige offensichtliche Leistungsprobleme gestoßen. Der wesentliche Grund für die Analyse der meisten Probleme ist, dass IO zu langsam ist. Die langsamste Ausführung der komplexesten Berechnungslogik in unserem System liegt im Mikrosekundenbereich, und die schnellste Datenbankanpassung dauert 1–2 Millisekunden mit einer Lücke von 2–3 Größenordnungen.

  Allerdings ist IO ein Vorgang, der im Geschäftssystem nicht eliminiert werden kann, aber die häufige oder falsche Verwendung von IO führt zu sehr offensichtlichen Leistungsproblemen im System, die von einer Verlangsamung der Schnittstelle über eine Beeinträchtigung der Benutzererfahrung bis hin zu direkten Ausfallzeiten aufgrund von OOM reichen. Mit Blick auf Leistungsprobleme, die durch E/A-Probleme verursacht werden, fasse ich hier drei Methoden der Stapelverarbeitung, des Cachings und des Multithreadings zusammen . Obwohl es sich um einen sehr einfachen Vorgang zu handeln scheint, muss er an der richtigen Stelle richtig verwendet werden, um den Wert dieser Methoden hervorzuheben drei Methoden.

Stapelverarbeitung

  Das erste ist die Stapelverarbeitung. Lassen Sie mich hier über einen realen Fall sprechen. Bei der Migration von Diensten in die Cloud im Jahr 2021 stieg die Verzögerung nach dem Wechsel einer Schnittstelle in die Cloud von etwa 50 ms auf 150 ms. Um KMS praktisch aufzurufen Nachdem dieser Dienst mit der Cloud verbunden wurde und der KMS-Server einen computerraumübergreifenden Anruf tätigte, hat sich die Dauer eines einzelnen KMS-Anrufs um fast 0,5 ms erhöht. Nur diese 0,5 ms zu betrachten, ist wirklich nicht viel, aber es kann die Anhäufung von Dutzenden von seriellen Anrufen nicht ertragen, und schließlich kommt es zu einer Gesamtverzögerungserhöhung von 100 ms. Diese Art der Schnittstellenverzögerung erhöht sich auf das Dreifache des Originals, und Benutzer können es leicht spüren. Vielleicht haben sie das Gefühl, dass diese Anwendung wirklich feststeckt!

Das obige Problem ist sehr einfach zu reproduzieren. Tatsächlich handelt es sich um eine for-Schleife, die kms seriell aufruft, um das Datenvolumen zu entschlüsseln.

for (String str : strList) {
    
    
   decodedStr = kmsClient.decrypt(str);  // 单次调用需要0.5-1ms,串行100次需要50-100ms
}

  Der Hauptzeitaufwand des oben genannten Codes als Ganzes ist nicht der Prozess der km-Entschlüsselung der Daten (nur Mikrosekunden sind erforderlich), sondern die zeitaufwändige Datenübertragung im Netzwerk bei der Anforderung zum Senden und Empfangen der Ergebnisdaten Hängt von den Diensten beider Parteien ab. Die meisten unserer Dienste werden in Peking bereitgestellt, es wird jedoch weiterhin Anrufe zwischen Computerräumen geben. Zu diesem Zeitpunkt erhöht sich auch die Netzwerkverzögerung um 0,5 bis 1 ms. Das Prinzip der Stapelverarbeitung zur Verbesserung der IO-Leistung besteht darin, die ursprünglichen mehreren Netzwerk-IOs durch ein einziges Netzwerk-IO zu ersetzen. Je länger die IO-Dauer ist, desto bedeutender ist der Optimierungseffekt. Anhand eines Beispiels aus dem Leben wird es für jeden leichter zu verstehen sein. Angenommen, Sie möchten ein Abendessen für Ihre Familie zubereiten. Einer der wichtigsten Schritte besteht darin, auf den Gemüsemarkt zu gehen, um Gemüse zu kaufen. Kaufen Sie dasselbe? Oder alles auf einmal kaufen? Dies ist der Unterschied zwischen Einzelverarbeitung und Stapelverarbeitung.

  Dieses Leistungsproblem scheint einfach zu sein, wird aber tatsächlich häufig im eigentlichen Programmierprozess festgeschrieben, und eine große Anzahl serieller E/A-Aufrufe wird ohne Beachtung durchgeführt, z. B. beim Überprüfen der Bibliothek in der for-Schleife (haben Sie bereits darüber nachgedacht). Problemcode, den Sie in Ihrem Kopf geschrieben haben). Wie ich ähnliche Probleme in meiner täglichen Programmierung vermeiden kann, habe ich in einer Programmieranleitung zusammengefasst. Das heißt, versuchen Sie, in keiner Schleife E/A-Aufrufe zu generieren, es sei denn, Sie wissen, was Sie tun.

  Natürlich verursachen nicht alle E/As Probleme. Einige E/As sind sehr schnell und die Häufigkeit Ihrer Serialisierung ist nicht sehr hoch. Wenn Sie den Code auf Batch-Logik umstellen, erhöht sich die Komplexität des Codes erheblich und die Wartungskosten steigen. Das ist es Es wird empfohlen, zu bewerten, ob eine Stapelverarbeitung entsprechend dem spezifischen E/A-Typ und den spezifischen Anforderungen durchgeführt werden soll. Im Folgenden werde ich einige spezifische E/A-Typen und einen einzelnen zeitaufwändigen E/A-Referenzwert angeben, auf den Sie beim Schreiben von Code achten können.

E/A-Typ zeitaufwendig Anmerkung
Random-Zugriff auf SSD-Solid-State-Disk 0,1 ms Derzeit verwenden die meisten Server SSDs, und das zeitaufwändige Lesen und Schreiben kleiner Dateien kann vernachlässigt werden. Wenn die Dateien jedoch sehr groß sind, stellt die Bandbreite aller Parteien hier einen Engpass dar, und der Zeitaufwand ist einfach schnell zunehmen. Konzentrieren Sie sich auf große Dateien.
Redis-Zugriff 0,1 ms Einfache Redis-Abfragen finden hauptsächlich im Internet statt. Der Redis-Dienst selbst verarbeitet nur einige Dutzend Anfragen von uns. Solange kein großer Schlüssel gefunden wird, gibt es im Grunde kein Problem.
MySQL-Abfrage 1-10ms Einfache Abfragen können in weniger als 10 ms durchgeführt werden. Wenn jedoch komplexe Abfragen beteiligt sind oder große Datenmengen nicht indiziert sind, erhöht sich der Zeitverbrauch erheblich. Die abnormale Abfrage von MySQL ist die Hauptursache für Leistungsprobleme in vielen Geschäftssystemen.
Direkter Zugriff auf die mechanische Festplatte der Festplatte 10 ms Die Suchzeit der Hauptfestplatte hängt von der Festplattengeschwindigkeit ab. Wenn Sie zufällig eine Festplatte verwenden und Dateien lesen und schreiben möchten, unabhängig von der Größe der Datei, darf der zeitaufwändige Teil nicht ignoriert werden.
Rufen Sie Dienste von Drittanbietern an 1-100 ms Abhängig von der Schnittstellenleistung der vertrauenden Partei ist die Varianz der Verzögerung verschiedener Schnittstellen sehr groß. Beim Aufruf einer Schnittstelle eines Drittanbieters müssen Leistung und Kapazität sorgfältig bewertet werden.
RTT in Computerräumen in derselben Stadt 0,5 ms -
Alle 50-100 km erhöht sich die physische Distanz RTT +1ms Die Verzögerung ist hauptsächlich auf die zeitaufwändige Lichtausbreitung in der Glasfaser + die Verarbeitungszeit von Switches und Routern zurückzuführen. Beispielsweise dauert ein RTT von Guangzhou nach Peking 50 ms. Bei Verbindung mit einer externen Serviceschnittstelle, sofern die Leistung stimmt Bei Bedarf ist die räumliche Distanz zu berücksichtigen.

Zwischenspeicher

  Ein Merkmal von Anwendungen mit hoher E/A ist, dass tatsächlich große Datenmengen wiederholt geladen werden, was auch eine Manifestation der „Lokalität“ ist. Die Lokalität sagt uns, dass nur eine kleine Datenmenge in großen Mengen geladen wird. Mithilfe der Lokalität können wir viele E/A-Vorgänge reduzieren und die Leistung unseres Systems verbessern, solange wir wichtige kleine Datenteile zwischenspeichern. Wenn wir die durchschnittliche Latenz zur Bewertung der Leistung verwenden, können wir eine Berechnungsformel für die durchschnittliche Latenz verwenden, um die Leistung nach dem Hinzufügen von Cache zu beschreiben:

avgLatency = hitRate * cacheLatency +  (1 - hitRate) * originalLatency

  Unter diesen bezieht sich avgLatency auf die durchschnittliche Verzögerung nach dem Hinzufügen des Caches, hitRate gibt die Trefferrate des Caches an und CacheLatency bezieht sich auf die Zeit, die für den einmaligen Zugriff auf den Cache erforderlich ist. Wenn wir im tatsächlichen Gebrauch einen lokalen Cache verwenden, können wir dies einfach tun Stellen Sie sich vor, dass die Cache-Latenz 0 ist. Die obige Formel kann zu avgLatency = (1 - hitRate) * originalLatency vereinfacht werden. Aus der vereinfachten Formel ist ersichtlich, dass der Effekt des Hinzufügens von Cache nur mit der Cache-Trefferquote zusammenhängt. Wenn die Cache-Trefferquote 90 % beträgt, wird die Leistung um das Zehnfache verbessert, und wenn sie 99 % beträgt, Es wird eine Leistungsverbesserung von 100 Prozent geben. (Einfache Berechnung): Solange wir die Cache-Trefferrate unendlich erhöhen, scheint es, dass die Leistung unendlich verbessert werden kann. Was hat also die Trefferquote damit zu tun? Die Antwort ist, dass Datenverteilung, Cache-Größe und Dateneliminierungsstrategie miteinander zusammenhängen.
Fügen Sie hier eine Bildbeschreibung ein

Datenverteilung: In der realen Welt werden die meisten Datenzugriffe von der Lokalität beeinflusst. Im Klartext wird nur auf einen kleinen Teil der Daten häufig zugegriffen. Wenn die Datenzugriffshäufigkeitskurve gezeichnet wird, wie in der Abbildung oben dargestellt.
Cache-Größe: Dies ist leicht zu verstehen: Solange genügend zwischengespeicherte Daten vorhanden sind, ist die Cache-Trefferquote höher.
Eliminierungsstrategie: Die Eliminierungsstrategie bezieht sich darauf, wie die Daten mit dem niedrigsten Wert eliminiert werden, wenn die Cache-Kapazität nicht ausreicht. Zu den gängigen Eliminierungsstrategien gehören LRU, LFU und FIFO. In unserer tatsächlichen Situation wird LRU am häufigsten verwendet.

  Nachdem wir die oben genannten drei Punkte richtig berücksichtigt haben, können wir in den meisten Fällen eine kleine Menge häufig aufgerufener Daten zwischenspeichern und so die Systemleistung verbessern. Ein weiterer Punkt, auf den Sie bei der Verwendung von Cache achten sollten, ist die Datenkonsistenz. Bei der Verwendung von Cache sind die Cache-Trefferrate und die Datenkonsistenz nahezu widersprüchlich. Es ist schwierig, das Beste aus beiden Welten zu erreichen. Zum Beispiel in meinem vorherigen Artikel „ Aus der Perspektive der CPU: Warum Multithread-Code so schwer zu schreiben ist! Der in „geschriebene CPU-Cache“ ist eigentlich ein typischer Fall der Verwendung von Cache zur Optimierung der E/A-Leistung auf Hardwareebene, aber die CPU hinterlässt viel „Grube“ für zeitgenössische Programmierer, um die Datenkonsistenz sicherzustellen.

  In der tatsächlichen Arbeit haben wir viele Optionen für die Cache-Implementierung. Die am häufigsten verwendeten sind LoadingCache, Caffiene, Ehcache, Redis in Guava und Spring-Cache Advanced Packaging im Frühjahr. Wenn Sie diese nicht verwenden möchten, können Sie Map verwenden Roll up one... Hier ist zunächst eine Werbung, und später wird es einen ausführlichen Artikel über die Konfiguration, Verwendung und Vorsichtsmaßnahmen des Caches geben, daher werde ich hier nicht näher darauf eingehen.

Multithreading

  Der Kern der beiden oben genannten Methoden besteht darin, die Leistung durch Optimierung der Anzahl unnötiger E/As zu verbessern. In der Realität können jedoch nicht alle E/As optimiert werden. In diesem Fall gibt es nur eine Möglichkeit, Multithreading zu wählen. up. Diese Denkweise ist auch leicht zu verstehen: Im Klartext heißt es: Wenn zu viel Arbeit zu erledigen ist, stellen Sie zwei weitere Leute ein. In einem IO-intensiven System besteht der Vorteil von Multithreading darin, dass die Rechenleistung der CPU voll ausgenutzt werden kann. Wenn ein Thread auf den Abschluss von E/A-Vorgängen (z. B. Netzwerkanforderungen oder Lese- und Schreibvorgänge auf der Festplatte) wartet, kann die CPU zu anderen Threads wechseln, um andere Aufgaben auszuführen, anstatt im Leerlauf zu sein. Auf diese Weise können wir die CPU-Ressourcen voll ausnutzen und die Reaktionsgeschwindigkeit des Systems verbessern.

  Allerdings ist die Verwendung von Multithreading nicht kostenlos. Erstens muss man sich des Overheads des Thread-Wechsels bewusst sein. Wenn die Anzahl der Threads zu groß ist, kann der Overhead des Thread-Wechsels viele CPU-Ressourcen verbrauchen. Zweitens erhöht die Verwendung von Multithreading die Komplexität des Codes erheblich . Außerdem müssen viele Probleme im Zusammenhang mit der Parallelität berücksichtigt werden, z. B. Synchronisation zwischen Threads, Deadlocks, Ressourcenkonkurrenz usw., die alle sorgfältig behandelt werden müssen Während der Programmierung berücksichtigt und behandelt. Wenn Sie nicht aufpassen, werden Sie Fehler einführen, die schwer zu beheben sind.

  In Java können wir Threads erstellen und verwalten, indem wir Tools wie ExecutorService und CompletableFuture verwenden. Natürlich können wir die Thread-Klasse auch direkt zum Erstellen von Threads verwenden, aber Threads müssen selbst verwaltet werden, was nicht sehr zu empfehlen ist. Gleichzeitig bietet Java viele Synchronisierungs- und Parallelitätstools wie das synchronisierte Schlüsselwort, ReentrantLock, Semaphore usw., die uns bei der Bewältigung von Parallelitätsproblemen helfen.

  Bei der Multithread-Optimierung wird häufig ein Thread-Pool verwendet. Der Thread-Pool kann Threads effektiv verwalten und wiederverwenden und vermeidet so den Aufwand für das häufige Erstellen und Zerstören von Threads. In Java können wir ExecutorService verwenden, um einen Thread-Pool zu erstellen und dann Aufgaben zur Ausführung an den Thread-Pool zu senden. In Java8 und höheren Versionen können wir auch parallelStream() verwenden, um den Code einfach in Multithreading umzuwandeln. Es ist jedoch zu beachten, dass die unterste Ebene von parallelStream denselben ForkJoinPool verwendet, der sich gegenseitig stören kann, wenn er in großen Mengen verwendet wird .

  Eine weitere gängige Multithreading-Optimierung ist die Verwendung asynchroner Programmierung . Durch die asynchrone Programmierung kann das Programm zur Verarbeitung zu anderen Aufgaben wechseln, ohne den aktuellen Thread zu blockieren, während es auf den Abschluss des E/A-Vorgangs wartet. In Java können wir Future, CompletableFuture und andere Tools für die asynchrone Programmierung verwenden.
  Insgesamt kann Multithreading ein leistungsstarkes Werkzeug sein, das die Leistung IO-intensiver Systeme deutlich verbessern kann. Allerdings muss auch bei der Verwendung von Multithreading vorsichtig vorgegangen werden und das Problem der Parallelität gut gelöst werden, um die Korrektheit und Stabilität des Programms sicherzustellen.

Zusammenfassen

  Bei der Optimierung der E/A-intensiven Systemleistung können wir dies im Wesentlichen auf drei Arten tun: Stapelverarbeitung, Caching und Multithreading. Jede dieser drei Methoden hat ihre Vorteile und anwendbaren Szenarien.

  1. Durch die Stapelverarbeitung kann die Verzögerungszeit der Netzwerkübertragung erheblich verkürzt werden, indem die Anzahl der Netzwerk-E/As reduziert und so die Systemleistung verbessert wird. Es erfordert jedoch eine sorgfältige Analyse und Gestaltung unseres Datenverarbeitungsprozesses, um eine geeignete Stapelverarbeitungsstrategie zu finden.
  2. Caching verbessert die Leistung, indem es häufig aufgerufene Daten speichert und den Zugriff auf langsamen Speicher (z. B. Festplatte oder Netzwerk) reduziert. Bei der Verwendung von Cache müssen Sie jedoch die Datenkonsistenz berücksichtigen und eine geeignete Strategie zur Cache-Eliminierung auswählen.
  3. Beim Multithreading werden mehrere Aufgaben parallel verarbeitet und die Rechenleistung der CPU voll ausgenutzt, um die Leistung zu verbessern. Die Verwendung von Multithreading erfordert jedoch die Bewältigung von Parallelitätsproblemen sowie den Mehraufwand für die Thread-Verwaltung und -Planung.

  In praktischen Anwendungen werden diese drei Methoden häufig kombiniert verwendet, um sie an unterschiedliche Leistungsanforderungen und Systemumgebungen anzupassen. Welche Methode gewählt oder wie sie in Kombination verwendet werden soll, muss entsprechend den spezifischen Geschäftsanforderungen, der Systemumgebung und den Leistungszielen entschieden werden. Bei der Leistungsoptimierung müssen wir unser System tiefgreifend verstehen, Leistungsengpässe herausfinden und dann eine gezielte Optimierung durchführen. Gleichzeitig müssen wir unsere Optimierungseffekte durch Leistungstests und -überwachung überprüfen und neue Leistungsprobleme rechtzeitig erkennen und lösen. Nur so kann unser System weiterhin effiziente und stabile Dienste bereitstellen.

Acho que você gosta

Origin blog.csdn.net/xindoo/article/details/131753862
Recomendado
Clasificación