Danke an Pedro Boado und Abel Fernandez Alfonso vom Engineering-Team von Santander für ihre Mitarbeit an diesem Beitrag darüber, wie Santander UK Apache HBase als fast in Echtzeit arbeitende Engine verwendet, um seine innovative Spendlytics-App zu betreiben.
Die Spendlytics iOS-App wurde entwickelt, um den Kunden von Santander mit Debit- und Kreditkarten dabei zu helfen, den Überblick über ihre Ausgaben zu behalten, einschließlich Zahlungen über Apple Pay. Es verwendet Echtzeit-Transaktionsdaten, damit Kunden ihre Kartenausgaben über Zeiträume (wöchentlich, monatlich, jährlich), nach Kategorie (Reisen, Supermärkte, Bargeld usw.) und nach Einzelhändlern analysieren können.
In unserem vorherigen Beitrag haben wir beschrieben, wie Apache Flume und Apache Kafka verwendet werden, um Transaktionen in Apache HBase zu transformieren, anzureichern und zu streamen. In diesem Beitrag wird weiter beschrieben, wie Transaktionen in Apache HBase angeordnet werden, um die Leistung zu optimieren, und wie wir Coprozessoren verwenden, um pro Kunde Aggregationen von Kauftrends bereitzustellen. Santander und Cloudera gingen (und sind immer noch) eine HBase-Reise mit Spendlytics, die viele Iterationen und Optimierungen des Schemadesigns und der Coprozessor-Implementierungen erlebt hat. Wir hoffen, dass diese gewonnenen Erkenntnisse die wichtigsten Erkenntnisse aus diesem Beitrag sind.
Schema 1.0
Bei einem guten HBase-Schemadesign geht es darum, die beabsichtigten Zugriffsmuster zu verstehen. Machen Sie es richtig und HBase wird fliegen; Wenn Sie es falsch machen, könnten Sie aufgrund von Designkompromissen wie Regions-Hotspots oder der Notwendigkeit, große Scans über mehrere Regionen hinweg durchzuführen, mit einer suboptimalen Leistung enden. (Ein Hotspot In einer HBase-Tabelle kann eine ungleichmäßige Rowkey-Verteilung dazu führen, dass die Mehrheit der Anfragen an eine einzelne Region weitergeleitet wird, was den RegionServer überlastet und zu langsamen Antwortzeiten führt.)
Was wir über die beabsichtigten Zugriffsmuster von Spendlytics wussten und wie sie das anfängliche Schemadesign beeinflussten:
- Kunden analysieren nur Transaktionen auf ihren eigenen Konten:
- Für eine schnelle lineare Scanleistung sollten alle Kundentransaktionen nacheinander gespeichert werden.
- Kunden-IDs steigen monoton:
- Aufeinanderfolgende Kunden-IDs erhöhen die Wahrscheinlichkeit, dass sich neuere Kunden in derselben Region aufhalten, wodurch möglicherweise ein regionaler Hotspot entsteht. Um dieses Problem zu vermeiden, sollten Kunden-IDs gesalzen (vorangestellt) oder umgekehrt werden, um sie gleichmäßig über die Regionen zu verteilen, wenn sie am Anfang des Zeilenschlüssels verwendet werden.
- Kunden haben mehrere Karten
- Um Scans zu optimieren, sollten die Transaktionen eines Kunden weiter gruppiert und nach Kartenvertrag sortiert werden, d. h. die Vertrags-ID sollte Teil des Zeilenschlüssels sein.
- Auf Transaktionen wird vollständig zugegriffen, d.h. Attribute wie Händler, Händler, Standort, Währung und Betrag müssen nicht separat gelesen werden
- Das Speichern von Transaktionsattributen in separaten Zellen würde zu einer breiteren, spärlichen Tabelle führen, was die Suchzeiten verlängert. Da auf die Attribute zusammen zugegriffen wird, war es sinnvoll, sie zusammen in einem Apache Avro-Datensatz zu serialisieren. Avro ist kompakt und bietet uns eine effiziente Darstellung mit Schema-Entwicklungsfähigkeit.
- Auf Transaktionen wird einzeln, in Stapeln (nach Zeit, Kategorie und Einzelhändler) und nach Aggregat (nach Zeit, Kategorie und Einzelhändler) zugegriffen.
- Das Hinzufügen einer eindeutigen Transaktions-ID als Spaltenqualifizierer ermöglicht das Abrufen einzelner Transaktionen, ohne den Zeilenschlüssel komplexer zu machen.
- Um ein schnelles Scannen von Transaktionen über variable Zeiträume zu ermöglichen, sollte der Transaktionszeitstempel Teil des Zeilenschlüssels sein.
- Das Hinzufügen von Kategorie und Einzelhändler zum Zeilenschlüssel könnte zu granular sein und würde zu einer sehr großen und schmalen Tabelle mit einem komplexen Zeilenschlüssel führen. Groß und schmal ist in Ordnung, da Atomarität kein Problem ist, aber sie als Spaltenqualifizierer zu verwenden, würde die Tabelle erweitern und gleichzeitig sekundäre Aggregationen unterstützen.
- Trenddaten sollten so weit wie möglich vorberechnet werden, um die Leseleistung zu optimieren.
- Mehr dazu später, aber jetzt wissen Sie, dass wir eine zweite Spaltenfamilie hinzugefügt haben, um die Trends zu speichern.
Basierend auf dem oben Gesagten wird das anfängliche Schemadesign wie folgt dargestellt:
Computing-Trends
Der Aspekt des ursprünglichen Designs, aus dem wir am meisten gelernt haben, war die Berechnung von Trends. Die Anforderung war, den Kunden eine stundengenaue Auswertung ihrer Ausgaben nach Kategorie und Händler zu ermöglichen. Zu den Datenpunkten gehörten die kleinsten und größten Transaktionswerte, der Gesamttransaktionswert und die Anzahl der Transaktionen. Die Antwortzeiten mussten 200 ms oder weniger betragen.
Das Vorausberechnen von Trends würde uns die schnellsten Reaktionszeiten geben, also war dies unser erster Ansatz. Trends konnten den Transaktionen nicht hinterherhinken, also mussten sie auf dem Schreibpfad berechnet werden. Dies wäre großartig für die Leseleistung, stellte uns jedoch vor ein paar Herausforderungen:Wie lassen sich Trends in HBase am besten organisieren und wie können sie schnell und zuverlässig berechnet werden, ohne die Schreibleistung stark zu beeinträchtigen?
Wir haben mit verschiedenen Schema-Designs experimentiert und versucht, einige bekannte Designs nach Möglichkeit zu nutzen (z. B. das Schema von OpenTSDB). Nach mehreren Iterationen haben wir uns für das oben dargestellte Schemadesign entschieden. In der Transaktionstabelle in einer separaten Spaltenfamilie gespeicherte Trendwerte werden zusammen in einer einzigen Zeile organisiert, mit einer Trendzeile pro Kunde. Indem Sie dem Zeilenschlüssel das gleiche Präfix wie den Transaktionen eines Kunden zuweisen (z. B.
<reverse_customer_id>::<contract_id>
) wurde sichergestellt, dass die Trendzeile neben den Transaktionsdatensätzen des entsprechenden Kunden sortiert wird. Mit definierten Regionsgrenzen und einer benutzerdefinierten Regionsaufteilungsrichtlinie können wir auch garantieren, dass die Trendzeile immer mit den Transaktionsdatensätzen eines Kunden verbunden wird, sodass die Trendaggregation vollständig serverseitig im Coprozessor verbleibt.Um Trends vorauszuberechnen, haben wir einen benutzerdefinierten Beobachter-Coprozessor implementiert um sich in den Schreibpfad einzuklinken. (Beobachter-Coprozessoren ähneln Triggern in einem RDBMS darin, dass sie Benutzercode ausführen, bevor oder nachdem ein bestimmtes Ereignis eintritt. Zum Beispiel vor oder nach
Put
oderGet
.)Auf
postPut
der Koprozessor führt die folgenden Aktionen aus:- Überprüft das
Put
für ein Trendattribut (Flag). Das Attribut wird nur auf neue Transaktionsdatensätze gesetzt, um rekursive Aufrufe beim Aktualisieren des Trenddatensatzes zu vermeiden. Außerdem kann der Coprozessor fürPut
übersprungen werden s, die keine Trendaktualisierung erfordern (z. B. Abrechnungen ). - Erhalten Sie einen Trenddatensatz für den Kunden. Der Trenddatensatz eines Kunden wird mit seinen Transaktionen (basierend auf dem Rowkey-Präfix) zusammengelegt, sodass der Coprozessor ihn direkt aus der aktuellen Region abrufen kann. Die Trendzeile muss gesperrt werden, um zu verhindern, dass mehrere RegionServer-Handler-Threads versuchen, die Trends parallel zu aktualisieren.
- Datenpunkte aktualisieren:
- Trendzeile aktualisieren und entsperren.
Die Lösung erwies sich während des Tests als genau und wie erwartet übertraf die Leseleistung die Anforderungen. Allerdings gab es einige Bedenken mit diesem Ansatz. Die erste war, wie mit Fehlern umzugehen ist:Trends werden in einer separaten Zeile gespeichert, sodass die Unteilbarkeit nicht garantiert werden kann. Die zweite war, wie man die Genauigkeit von Trends im Laufe der Zeit validiert; Das heißt, wir müssten einen Mechanismus implementieren, um etwaige Trendungenauigkeiten zu identifizieren und zu beheben. Wenn wir auch die HA-Anforderungen und die Tatsache berücksichtigten, dass wir zwei Aktiv-Aktiv-Instanzen von HBase in verschiedenen Rechenzentren ausführen müssten, könnte dies ein größeres Problem darstellen. Nicht nur die Trendgenauigkeit könnte mit der Zeit abnehmen, sondern die beiden Cluster könnten auch driften und müssten je nach Methode, mit der wir sie synchronisiert haben, in Einklang gebracht werden. Schließlich wäre es schwierig, Fehler zu beheben oder neue Datenpunkte hinzuzufügen, da wir möglicherweise alle Trends zurückverfolgen und neu berechnen müssten.
Dann gab es Schreibleistung. Für jede neue Transaktion musste der Beobachter einen Trenddatensatz abrufen, 32 Datenpunkte aktualisieren und den Trenddatensatz zurücksetzen. Obwohl all dies innerhalb der Grenzen einer einzelnen Region geschah, stellten wir fest, dass der Durchsatz von über 20.000 Schreibvorgängen pro Sekunde auf 1.000 Schreibvorgänge pro Sekunde (pro RegionServer) reduziert wurde. Diese Leistung war kurzfristig akzeptabel, würde aber nicht skalieren, um die vorhergesagte langfristige Belastung zu unterstützen.
Wir wussten, dass die Schreibleistung ein Risiko darstellte, also hatten wir einen Backup-Plan, und das war ein Endpunkt-Coprozessor . Endpunkt-Coprozessoren ähneln gespeicherten Prozeduren in einem RDBMS insofern, als sie es Ihnen ermöglichen, serverseitige Berechnungen durchzuführen – auf dem RegionServer, auf dem sich die Daten befinden, und nicht auf dem Client. Endpunkte erweitern effektiv die HBase-API.
Anstatt Trends vorab zu berechnen, berechnet der Endpunkt sie im laufenden Betrieb serverseitig. Infolgedessen konnten wir die Trendspaltenfamilie aus dem Schema streichen, und damit ging das Risiko von Ungenauigkeiten und Abweichungen einher. Die Entfernung vom Betrachter führte zu einer guten Schreibleistung, aber wären Lesevorgänge schnell genug? Kurz gesagt, ja. Da die Transaktionen eines Kunden auf eine einzige Region beschränkt und nach Karte und Zeitstempel sortiert sind, kann der Endpunkt schnell scannen und aggregieren, weit innerhalb des 200-ms-Ziels von Spendlytics. Das bedeutet auch, dass eine Client-Anfrage (in diesem Fall von der Spendlytics-API) immer nur an eine einzelne Endpoint-Instanz (einzelner RegionServer) weitergeleitet wird und der Client eine einzige Antwort mit einem vollständigen Ergebnis zurückerhält – d. h. keine Client-Seite Die Verarbeitung ist erforderlich, um Teilergebnisse von mehreren Endpunkten zusammenzufassen, was der Fall wäre, wenn sich die Transaktionen eines Kunden über mehrere Regionen erstrecken würden.
Gelernte Lektionen
Spendlytics ist seit Juli 2015 live. Seitdem haben wir die Zugriffsmuster genau beobachtet und nach Möglichkeiten gesucht, die Leistung zu optimieren. Wir wollen das Nutzererlebnis kontinuierlich verbessern und den Kunden immer mehr Einblick in ihre Kartenausgaben geben. Der Rest dieses Beitrags beschreibt die Lehren, die wir aus der Ausführung von Spendlytics in der Produktion gezogen haben, und einige der Optimierungen, die vorgenommen wurden.
Nach der ersten Veröffentlichung haben wir eine Reihe von Schwachstellen identifiziert, auf deren Verbesserung wir uns konzentrieren wollten. Die erste war, wie man Ergebnisse nach Transaktionsattributen filtert. Wie bereits erwähnt, sind Transaktionsattribute in Avro-Datensätzen kodiert, aber wir haben festgestellt, dass immer mehr Zugriffsmuster nach Attributen filtern wollten und die Benutzer dazu gezwungen waren, dies clientseitig zu tun. Die ursprüngliche Lösung bestand darin, einen benutzerdefinierten HBase-
ValueFilter
zu implementieren die unsere eigenen komplexen Filterausdrücke akzeptierten, zum Beispiel:category='SUPERMARKETS' AND amount > 100 AND (brand LIKE 'foo%' OR brand = 'bar')
Der Ausdruck wird für jeden Avro-Datensatz ausgewertet, sodass wir die Ergebnisse serverseitig filtern und die Datenmenge reduzieren können, die an den Client zurückgegeben wird (was Netzwerkbandbreite und clientseitige Verarbeitung spart). Der Filter wirkt sich zwar auf die Scanleistung aus, aber die Antwortzeiten blieben deutlich innerhalb des Ziels von 200 ms.
Dies stellte sich als vorübergehende Lösung heraus, da weitere Änderungen zur Optimierung der Schreibvorgänge erforderlich waren. Aufgrund der Funktionsweise der Kreditkartenabrechnung erhalten wir zunächst eine autorisierte Transaktion ab dem Zeitpunkt des Verkaufs (nahezu in Echtzeit) und dann einige Zeit später abgerechnet Transaktion aus dem Kreditkartennetzwerk (im Batch). Diese Transaktionen müssen abgestimmt werden, im Wesentlichen durch Zusammenführen der abgerechneten Transaktionen mit dem Autorisierten Transaktionen bereits in HBase, beitreten auf Transaktions-ID. Als Teil dieses Prozesses können sich Transaktionsattribute ändern und neue Attribute können hinzugefügt werden. Dies erwies sich als schmerzhaft, da ganze Avro-Datensätze neu geschrieben werden mussten – selbst wenn einzelne Attribute aktualisiert wurden. Um die Attribute für Updates zugänglicher zu machen, haben wir sie in Spalten organisiert und die Avro-Serialisierung ersetzt.
Wir kümmern uns auch nur um die Atomarität auf Transaktionsebene, daher hat uns das Bündeln der Transaktionen nach Stunden keinen Vorteil gebracht. Außerdem siedelte Transaktionen, die jetzt im Batch eintreffen, haben nur eine Granularität auf Tagesebene, was es schwierig (kostspielig) machte, sie mit bestehenden autorisierten abzugleichen Transaktionen nach Stunde gespeichert. Um dieses Problem zu lösen, haben wir die Transaktions-ID in den Rowkey verschoben und den Zeitstempel auf Tage statt auf Stunden reduziert. Der Abstimmungsprozess ist jetzt viel einfacher, da wir die Änderungen einfach in HBase laden und die Abrechnung überlassen können Werte haben Vorrang.
Zusammengefasst:
- Observer-Coprozessoren können ein wertvolles Werkzeug sein, aber setzen Sie sie mit Bedacht ein.
- Für einige Anwendungsfälle ist die Erweiterung der HBase-API mithilfe von Endpunkten eine gute Alternative.
- Verwenden Sie benutzerdefinierte Filter, um die Leistung zu verbessern, indem Sie die Ergebnisse serverseitig kürzen.
- Serialisierte Werte sind für den richtigen Anwendungsfall sinnvoll, spielen aber die Stärken von HBase aus, indem sie native Unterstützung für Felder und Spalten bevorzugen.
- Die Verwaltung vorberechneter Ergebnisse ist schwierig; die zusätzliche Latenz durch spontanes Rechnen kann sich lohnen.
- Zugriffsmuster werden sich ändern, seien Sie also agil und offen für Änderungen am HBase-Schema, um sich anzupassen und dem Spiel einen Schritt voraus zu sein.
Fahrplan
Eine Optimierung, die wir derzeit evaluieren, sind Hybrid-Coprozessoren. Was wir damit meinen, ist die Kombination von sowohl Beobachter- als auch Endpunkt-Coprozessoren, um Trends vorauszuberechnen. Im Gegensatz zu früher würden wir dies jedoch nicht auf dem Schreibpfad tun, sondern im Hintergrund, indem wir uns in die Flush- und Komprimierungsoperationen von HBase einklinken. Ein Beobachter berechnet Trends während Spül- und Verdichtungsereignissen basierend auf dem abgesetzten zu diesem Zeitpunkt verfügbare Transaktionen. Wir würden dann einen Endpunkt verwenden, um die vorberechneten Trends mit spontanen Aggregationen des Transaktionsdeltas zu kombinieren. Indem wir Trends auf diese Weise vorausberechnen, hoffen wir, die Leseleistung zu steigern, ohne die Schreibleistung zu beeinträchtigen.
Ein weiterer Ansatz, den wir für die Trendaggregation und für den HBase-Zugriff im Allgemeinen evaluieren, ist Apache Phoenix. Phoenix ist eine SQL-Skin für HBase, die den Zugriff über standardmäßige JDBC-APIs ermöglicht. Wir hoffen, dass durch die Verwendung von SQL und JDBC der HBase-Zugriff vereinfacht und die Menge an Code, die wir schreiben müssen, reduziert wird. Wir können auch die intelligenten Ausführungsmuster von Phoenix und die integrierten Koprozessoren und Filter für schnelle Aggregationen nutzen. Phoenix galt zu Beginn von Spendlytics als zu unreif für den Produktionseinsatz, aber da ähnliche Anwendungsfälle von eBay und Salesforce gemeldet werden, ist es jetzt an der Zeit, eine Neubewertung vorzunehmen. (Über Cloudera Labs ist ein Phoenix-Paket für CDH zur Installation und Evaluierung verfügbar, jedoch ohne Support.)
Santander gab kürzlich bekannt, dass sie die erste Bank ist, die eine Voice-Banking-Technologie einführt, die es Kunden ermöglicht, mit ihrer SmartBank-App zu sprechen und nach ihren Kartenausgaben zu fragen. Die Plattform hinter dieser Technologie ist Cloudera, und die Architektur für Spendlytics – wie in diesen Posts beschrieben – diente als Blaupausendesign.
James Kinley ist Principal Solutions Architect bei Cloudera.
Ian Buss ist Senior Solutions Architect bei Cloudera.
Pedro Boado ist Hadoop-Ingenieur bei Santander (Isban) UK.
Abel Fernández Alfonso ist Hadoop-Ingenieur bei Santander (Isban) UK.