Grazie a Pedro Boado e Abel Fernandez Alfonso del team di ingegneri di Santander per la loro collaborazione a questo post su come Santander UK sta utilizzando Apache HBase come motore di servizio quasi in tempo reale per alimentare la sua innovativa app Spendlytics.
L'app Spendlytics per iOS è progettata per aiutare i clienti con carta di credito e debito personale di Santander a tenere sotto controllo le proprie spese, compresi i pagamenti effettuati tramite Apple Pay. Utilizza i dati delle transazioni in tempo reale per consentire ai clienti di analizzare la spesa della carta in periodi di tempo (settimanale, mensile, annuale), per categoria (viaggi, supermercati, contanti, ecc.) e per rivenditore.
Nel nostro post precedente, abbiamo descritto come vengono utilizzati Apache Flume e Apache Kafka per trasformare, arricchire e trasmettere le transazioni in Apache HBase. Questo post continua descrivendo come le transazioni sono organizzate in Apache HBase per ottimizzare le prestazioni e come utilizziamo i coprocessori per fornire aggregazioni per cliente delle tendenze di acquisto. Santander e Cloudera hanno intrapreso (e sono ancora in corso) un viaggio HBase con Spendlytics, che ha visto molte iterazioni e ottimizzazioni della progettazione di schemi e delle implementazioni di coprocessori. Ci auguriamo che queste lezioni apprese siano i punti chiave da asporto di questo post.
Schema 1.0
Una buona progettazione dello schema HBase riguarda la comprensione dei modelli di accesso previsti. Fallo bene e HBase volerà; sbagliare e potresti finire con prestazioni non ottimali a causa di compromessi di progettazione come hotspot regionali o dover eseguire scansioni di grandi dimensioni su più regioni. (Un hotspot in una tabella HBase è dove una distribuzione di rowkey non uniforme può far sì che la maggior parte delle richieste venga instradata a una singola regione, sovraccaricando il RegionServer e provocando tempi di risposta lenti.)
Quello che sapevamo sui modelli di accesso previsti da Spendlytics e su come ha influenzato la progettazione dello schema iniziale:
- I clienti analizzano solo le transazioni sui propri conti:
- Per prestazioni di scansione lineare rapida, tutte le transazioni dei clienti devono essere archiviate in sequenza.
- Gli ID cliente stanno aumentando in modo monotono:
- Gli ID cliente sequenziali aumentano la probabilità che i nuovi clienti si trovino insieme all'interno della stessa regione, creando potenzialmente un hot spot regionale. Per evitare questo problema, gli ID cliente devono essere salati (prefissati) o invertiti per una distribuzione uniforme tra le regioni se utilizzati all'inizio della riga.
- I clienti hanno più carte
- Per ottimizzare le scansioni, le transazioni di un cliente dovrebbero essere ulteriormente raggruppate e ordinate in base al contratto della carta, ovvero l'ID del contratto dovrebbe far parte della rowkey.
- Le transazioni saranno accessibili nella loro interezza, ovvero attributi come rivenditore, commerciante, posizione, valuta e importo non devono essere letti separatamente
- La memorizzazione degli attributi di transazione in celle separate risulterebbe in una tabella più ampia e sparsa, che aumenterà i tempi di ricerca. Poiché gli attributi saranno accessibili insieme, aveva senso serializzarli insieme in un record Apache Avro. Avro è compatto e ci fornisce una rappresentazione efficiente con capacità di evoluzione dello schema.
- Le transazioni sono accessibili individualmente, in batch (per ora, categoria e rivenditore) e per aggregato (per ora, categoria e rivenditore).
- L'aggiunta di un ID transazione univoco come qualificatore di colonna consentirà il recupero di singole transazioni senza aggiungere ulteriore complessità alla chiave di riga.
- Per consentire la scansione rapida delle transazioni in periodi di tempo variabili, il timestamp della transazione dovrebbe far parte della rowkey.
- L'aggiunta di categoria e rivenditore al codice riga potrebbe essere troppo granulare e produrrebbe una tabella molto alta e stretta con una chiave riga complessa. Alto e stretto va bene dato che l'atomicità non è un problema, ma averli come qualificatori di colonna allargherebbe la tabella pur supportando le aggregazioni secondarie.
- I dati di trend dovrebbero essere precalcolati il più possibile per ottimizzare le prestazioni di lettura.
- Ne parleremo più avanti, ma per ora sappi che abbiamo aggiunto una seconda famiglia di colonne per memorizzare le tendenze.
Sulla base di quanto sopra, la progettazione dello schema iniziale è illustrata come segue:
Tendenze informatiche
L'aspetto del progetto iniziale da cui abbiamo imparato di più sono state le tendenze informatiche. Il requisito era consentire ai clienti di analizzare la spesa per categoria e rivenditore fino all'ora. I punti dati includevano i valori di transazione più piccoli e più grandi, il valore totale della transazione e il numero di transazioni. I tempi di risposta dovevano essere di 200 ms o meno.
Le tendenze del precalcolo ci darebbero i tempi di risposta più rapidi, quindi questo è stato il nostro primo approccio. Le tendenze non potevano ritardare le transazioni, quindi dovevano essere calcolate sul percorso di scrittura. Questo sarebbe ottimo per le prestazioni di lettura, ma ci ha presentato un paio di sfide:come organizzare al meglio le tendenze in HBase e come calcolarle in modo rapido e affidabile senza influire gravemente sulle prestazioni di scrittura.
Abbiamo sperimentato diversi progetti di schemi e cercato di sfruttare alcuni progetti ben noti ove possibile (come lo schema di OpenTSDB). Dopo diverse iterazioni abbiamo optato per il design dello schema illustrato sopra. Archiviati nella tabella delle transazioni, in una famiglia di colonne separata, i valori di tendenza sono organizzati insieme in un'unica riga, con una riga di tendenza per cliente. Assegnando alla chiave di riga lo stesso prefisso delle transazioni di un cliente (ad esempio,
<reverse_customer_id>::<contract_id>
) ha assicurato che la riga dell'andamento fosse ordinata insieme ai record delle transazioni del cliente corrispondente. Con i confini della regione definiti e una politica di suddivisione della regione personalizzata in atto, possiamo anche garantire che la riga di tendenza sarà sempre collocata con i record delle transazioni di un cliente, consentendo all'aggregazione di tendenze di rimanere interamente lato server nel coprocessore.Per calcolare in anticipo le tendenze, abbiamo implementato un coprocessore osservatore personalizzato per agganciarsi al percorso di scrittura. (I coprocessori Observer sono simili ai trigger in un RDBMS in quanto eseguono il codice utente prima o dopo che si verifica un evento specifico. Ad esempio, pre o post
Put
oGet
.)Su
postPut
il coprocessore esegue le seguenti azioni:- Controlla il
Put
per un attributo di tendenza (flag). L'attributo viene impostato sui nuovi record di transazione solo per evitare chiamate ricorsive durante l'aggiornamento del record di trend. Consente inoltre di saltare il coprocessore perPut
s che non richiedono l'aggiornamento delle tendenze (ad es. transazioni ). - Ottieni record di tendenza per il cliente. Il record di tendenza di un cliente viene collocato insieme alle sue transazioni (in base al prefisso della chiave di riga) in modo che il coprocessore possa recuperarlo direttamente dalla regione corrente. La riga delle tendenze deve essere bloccata per impedire a più thread del gestore RegionServer di tentare di aggiornare le tendenze in parallelo.
- Aggiorna punti dati:
- Aggiorna e sblocca la riga delle tendenze.
La soluzione si è rivelata accurata durante i test e, come previsto, le prestazioni di lettura hanno superato i requisiti. Tuttavia, c'erano alcune preoccupazioni con questo approccio. Il primo era come gestire l'errore:le tendenze sono archiviate in una riga separata, quindi l'atomicità non può essere garantita. Il secondo era come validare l'accuratezza delle tendenze nel tempo; ovvero, avremmo bisogno di implementare un meccanismo per identificare e correggere eventuali imprecisioni di tendenza. Quando abbiamo anche considerato i requisiti HA e il fatto che avremmo dovuto eseguire due istanze attivo-attivo di HBase in diversi data center, questo potrebbe essere un problema più grande. Non solo la precisione del trend potrebbe diminuire nel tempo, ma i due cluster potrebbero anche andare alla deriva e dover essere riconciliati a seconda del metodo utilizzato per sincronizzarli. Infine, correggere i bug o aggiungere nuovi punti dati sarebbe difficile perché probabilmente dovremmo tornare indietro e ricalcolare tutte le tendenze.
Poi c'era la performance di scrittura. Per ogni nuova transazione l'osservatore doveva recuperare un record di tendenza, aggiornare 32 punti dati e reinserire il record di tendenza. Nonostante tutto ciò avvenga entro i limiti di una singola regione, abbiamo riscontrato che il throughput è stato ridotto da oltre 20.000 scritture al secondo a 1.000 scritture al secondo (per RegionServer). Questa performance era accettabile a breve termine, ma non sarebbe stata scalabile per supportare il carico previsto a lungo termine.
Sapevamo che le prestazioni in scrittura erano un rischio, quindi avevamo un piano di backup e quello era un coprocessore endpoint . I coprocessori degli endpoint sono simili alle stored procedure in un RDBMS in quanto consentono di eseguire calcoli lato server, sul RegionServer in cui si trovano i dati, anziché sul client. Gli endpoint estendono efficacemente l'API HBase.
Invece di precalcolare le tendenze, l'endpoint le calcola al volo, lato server. Di conseguenza, potremmo eliminare la famiglia di colonne delle tendenze dallo schema e il rischio di imprecisioni e divergenze è andato di conseguenza. Allontanandosi dall'osservatore si ottengono buone prestazioni di scrittura, ma le letture sarebbero abbastanza veloci? In breve, sì. Con le transazioni di un cliente limitate a una singola regione e ordinate per carta e timestamp, l'endpoint può eseguire la scansione e l'aggregazione rapidamente, ben entro l'obiettivo di 200 ms di Spendlytics. Ciò significa anche che una richiesta client (in questo caso dall'API Spendlytics) viene instradata solo a una singola istanza di Endpoint (single RegionServer) e il client riceverà una risposta singola con un risultato completo, ovvero nessun lato client l'elaborazione è necessaria per aggregare risultati parziali da più endpoint, come sarebbe il caso se le transazioni di un cliente si estendessero su più regioni.
Lezioni apprese
Spendlytics è attivo da luglio 2015. Da allora abbiamo monitorato da vicino i modelli di accesso e cercato modi per ottimizzare le prestazioni. Vogliamo migliorare continuamente l'esperienza dell'utente e fornire ai clienti informazioni sempre più dettagliate sulla spesa con la carta. Il resto di questo post descrive le lezioni che abbiamo imparato dall'esecuzione di Spendlytics in produzione e alcune delle ottimizzazioni che sono state messe in atto.
Dopo il rilascio iniziale, abbiamo identificato una serie di punti deboli che volevamo concentrarci sul miglioramento. Il primo era come filtrare i risultati in base all'attributo della transazione. Come accennato in precedenza, gli attributi delle transazioni sono codificati nei record Avro, ma abbiamo riscontrato che un numero crescente di modelli di accesso voleva filtrare per attributo e gli utenti erano costretti a farlo lato client. La soluzione iniziale era implementare un
ValueFilter
HBase personalizzato che ha accettato le nostre complesse espressioni di filtro, ad esempio:category='SUPERMARKETS' AND amount > 100 AND (brand LIKE 'foo%' OR brand = 'bar')
L'espressione viene valutata per ogni record Avro, consentendoci di filtrare i risultati lato server e ridurre la quantità di dati restituiti al client (risparmiando larghezza di banda di rete ed elaborazione lato client). Il filtro influisce sulle prestazioni di scansione, ma i tempi di risposta sono rimasti ben entro l'obiettivo di 200 ms.
Questa si è rivelata una soluzione temporanea a causa di ulteriori modifiche necessarie per ottimizzare le scritture. A causa del modo in cui funziona la procedura di pagamento della carta di credito, riceviamo prima un autorizzato transazione dal momento della vendita (quasi in tempo reale) e poi qualche tempo dopo un regolato transazione dalla rete della carta di credito (in batch). Queste transazioni devono essere riconciliate, essenzialmente unendo i regolati transazioni con gli autorizzati transazioni già in HBase, unendo l'ID transazione. Come parte di questo processo, gli attributi della transazione possono cambiare e possono essere aggiunti nuovi attributi. Ciò si è rivelato doloroso a causa del sovraccarico di dover riscrivere interi record Avro, anche durante l'aggiornamento di singoli attributi. Quindi, per rendere gli attributi più accessibili per gli aggiornamenti, li abbiamo organizzati in colonne, sostituendo la serializzazione di Avro.
Ci preoccupiamo anche solo dell'atomicità a livello di transazione, quindi il bucket delle transazioni per ora non ci ha dato alcun vantaggio. Inoltre, il accontentato le transazioni che ora arrivano in batch hanno solo una granularità a livello di giorno, il che ha reso difficile (costoso) riconciliarle con le autorizzate esistenti transazioni memorizzate per ora. Per risolvere questo problema, abbiamo spostato l'ID transazione nel rowkey e ridotto la grana del timestamp a giorni, anziché a ore. Il processo di riconciliazione ora è molto più semplice perché possiamo semplicemente caricare in blocco le modifiche in HBase e lasciare che la transazione i valori hanno la precedenza.
In sintesi:
- I coprocessori Observer possono essere uno strumento prezioso, ma utilizzali con saggezza.
- Per alcuni casi d'uso, l'estensione dell'API HBase utilizzando gli endpoint è una buona alternativa.
- Utilizza filtri personalizzati per migliorare le prestazioni tagliando i risultati lato server.
- I valori serializzati hanno senso per il caso d'uso corretto, ma sfruttano i punti di forza di HBase favorendo il supporto nativo per campi e colonne.
- La gestione dei risultati precalcolati è difficile; la latenza aggiuntiva derivante dall'elaborazione al volo può essere utile.
- I modelli di accesso cambieranno, quindi sii agile e aperto ad apportare modifiche allo schema HBase per adattarti e stare al passo con il gioco.
Tabella di marcia
Un'ottimizzazione che stiamo attualmente valutando è quella dei coprocessori ibridi. Ciò che intendiamo con questo è la combinazione di coprocessori osservatore e endpoint per precalcolare le tendenze. Tuttavia, a differenza di prima, non lo faremmo sul percorso di scrittura ma in background agganciandoci alle operazioni di svuotamento e compattazione di HBase. Un osservatore calcolerà le tendenze durante gli eventi di svuotamento e compattazione in base al regolato transazioni disponibili in quel momento. Utilizzeremmo quindi un endpoint per combinare le tendenze precalcolate con aggregazioni al volo del delta delle transazioni. Precalcolando le tendenze in questo modo speriamo di aumentare le prestazioni delle letture, senza influire sulle prestazioni di scrittura.
Un altro approccio che stiamo valutando per l'aggregazione delle tendenze e per l'accesso HBase in generale è Apache Phoenix. Phoenix è una skin SQL per HBase che consente l'accesso tramite API JDBC standard. Ci auguriamo che l'utilizzo di SQL e JDBC semplificherà l'accesso a HBase e ridurrà la quantità di codice da scrivere. Possiamo anche sfruttare i modelli di esecuzione intelligenti di Phoenix e i coprocessori e i filtri integrati per aggregazioni veloci. Phoenix era considerata troppo immatura per l'uso in produzione all'inizio di Spendlytics, ma con casi d'uso simili segnalati da artisti del calibro di eBay e Salesforce, ora è il momento di rivalutare. (Un pacchetto Phoenix per CDH è disponibile per l'installazione e la valutazione, ma senza supporto, tramite Cloudera Labs.)
Santander ha recentemente annunciato di essere la prima banca a lanciare una tecnologia di voice banking che consente ai clienti di parlare con la sua app SmartBank e chiedere informazioni sulla spesa con la carta. La piattaforma alla base di questa tecnologia è Cloudera e l'architettura per Spendlytics, come descritto in questa serie di post, è servita da progetto.
James Kinley è Principal Solutions Architect presso Cloudera.
Ian Buss è Senior Solutions Architect presso Cloudera.
Pedro Boado è un ingegnere Hadoop a Santander (Isban) nel Regno Unito.
Abel Fernandez Alfonso è un ingegnere Hadoop a Santander (Isban) nel Regno Unito.