Condividi tramite


Antipattern di persistenza monolitica

L'inserimento di tutti i dati di un'applicazione in un singolo archivio dati può indebolire le prestazioni, perché comporta conflitti di risorse o perché l'archivio dati non è adatto per alcuni dati.

Contesto e problema

In passato, le applicazioni usavano un singolo archivio dati, indipendentemente dai diversi tipi di dati che l'applicazione potrebbe dover archiviare. Le organizzazioni hanno usato questo metodo per semplificare la progettazione dell'applicazione o per trovare la corrispondenza con il set di competenze esistente del team di sviluppo.

I sistemi moderni basati sul cloud spesso hanno requisiti funzionali e non funzionali aggiuntivi. Questi sistemi devono archiviare molti tipi eterogenei di dati, ad esempio documenti, immagini, dati memorizzati nella cache, messaggi in coda, log applicazioni e dati di telemetria. L'approccio tradizionale e l'inserimento di tutte queste informazioni nello stesso archivio dati possono indebolire le prestazioni per due motivi principali:

  • L'archiviazione e il recupero di grandi quantità di dati non correlati nello stesso archivio dati possono causare conflitti, causando tempi di risposta lenti e errori di connessione.
  • Indipendentemente dall'archivio dati scelto, potrebbe non essere la scelta migliore per tutti i tipi di dati. In alternativa, potrebbe non essere ottimizzato per le operazioni eseguite dall'applicazione.

Nell'esempio seguente viene illustrato un controller API Web ASP.NET che aggiunge un nuovo record a un database e registra anche il risultato in un log. Il log viene archiviato nello stesso database dei dati aziendali. Per altre informazioni, vedere l'esempio completo.

public class MonoController : ApiController
{
    private static readonly string ProductionDb = ...;

    public async Task<IHttpActionResult> PostAsync([FromBody]string value)
    {
        await DataAccess.InsertPurchaseOrderHeaderAsync(ProductionDb);
        await DataAccess.LogAsync(ProductionDb, LogTableName);
        return Ok();
    }
}

La frequenza con cui vengono generati i record di log può influire sulle prestazioni delle operazioni aziendali. E se un altro componente, ad esempio un monitoraggio del processo dell'applicazione, legge e elabora regolarmente i dati di log, che può influire anche sulle operazioni aziendali.

Soluzione

Separare i dati in base al relativo utilizzo. Per ogni set di dati, selezionare un archivio dati che corrisponda al modo in cui si usa tale set di dati. Nell'esempio precedente, l'applicazione deve accedere a un archivio separato dal database che contiene dati aziendali:

public class PolyController : ApiController
{
    private static readonly string ProductionDb = ...;
    private static readonly string LogDb = ...;

    public async Task<IHttpActionResult> PostAsync([FromBody]string value)
    {
        await DataAccess.InsertPurchaseOrderHeaderAsync(ProductionDb);
        // Log to a different data store.
        await DataAccess.LogAsync(LogDb, LogTableName);
        return Ok();
    }
}

Problemi e considerazioni

  • Separare i dati in base a come usarli e accedervi. Ad esempio, non archiviare le informazioni di log e i dati aziendali nello stesso archivio dati. Questi tipi di dati hanno requisiti e modelli di accesso diversi. I record di log sono intrinsecamente sequenziali, mentre è più probabile che i dati aziendali richiedano l'accesso casuale ed è spesso relazionale.

  • Prendere in considerazione il modello di accesso ai dati per ogni tipo di dati. Ad esempio, archiviare report e documenti formattati in un database di documenti, ad esempio Azure Cosmos DB. Usare Cache Redis di Azure per memorizzare nella cache i dati temporanei.

  • Aumentare le prestazioni del database se si seguono queste indicazioni, ma si raggiungono comunque i limiti del database. Valutare anche la possibilità di ridimensionare orizzontalmente e partizionare il carico tra server di database. Tuttavia, il partizionamento potrebbe richiedere la riprogettazione dell'applicazione. Per altre informazioni, vedere Partizionamento dei dati.

Rilevare il problema

Il sistema può rallentare notevolmente e infine non riuscire quando il sistema esaurisce le risorse, ad esempio le connessioni al database.

È possibile eseguire la procedura seguente per identificare la causa:

  1. Instrumentare il sistema per registrare le statistiche delle prestazioni chiave. Acquisire informazioni sulla tempistica per ogni operazione. Acquisire i punti in cui l'applicazione legge e scrive i dati.

  2. Monitorare il sistema per alcuni giorni in un ambiente di produzione per ottenere una visualizzazione reale del modo in cui viene usato il sistema. Se non è possibile eseguire questo processo, eseguire test di carico con script con un volume realistico di utenti virtuali che eseguono una tipica serie di operazioni.

  3. Usare i dati di telemetria per identificare i periodi di prestazioni scarse.

  4. Identificare gli archivi dati a cui è stato eseguito l'accesso durante tali periodi.

  5. Identificare le risorse di archiviazione dei dati che potrebbero subire contenzioni.

Diagnosi di esempio

Le sezioni seguenti applicano questa procedura all'applicazione di esempio descritta in precedenza.

Instrumentare e monitorare il sistema

Il grafico seguente mostra i risultati del test di carico dell'applicazione di esempio descritta in precedenza. Il test utilizza un carico incrementale fino a 1.000 utenti contemporanei.

Grafico che mostra i risultati delle prestazioni dei test di carico per il controller basato su SQL.

Man mano che il carico aumenta a 700 utenti, anche il throughput aumenta. Tuttavia, a quel punto, la velocità effettiva si stabilizza e il sistema sembra essere eseguito alla sua capacità massima. La risposta media aumenta gradualmente con il carico dell'utente, mostrando che il sistema non può tenere il passo con la domanda.

Identificare i periodi di prestazioni scarse

Se si monitora il sistema di produzione, è possibile notare i modelli. Ad esempio, i tempi di risposta potrebbero scendere significativamente contemporaneamente ogni giorno. Un normale carico di lavoro o un processo batch pianificato può causare questa fluttuazione. Oppure il sistema potrebbe avere più utenti in determinati momenti. È consigliabile concentrarsi sui dati di telemetria per questi eventi.

Cercare correlazioni tra tempi di risposta aumentati e maggiore attività del database o input/output (I/O) alle risorse condivise. Se sono presenti correlazioni, significa che il database potrebbe essere un collo di bottiglia.

Identificare gli archivi dati a cui si accede durante tali periodi

Il grafico successivo mostra l'utilizzo delle unità di throughput del database (DTUs) durante il test di carico. Una DTU è una misura della capacità disponibile. Si tratta di una combinazione di utilizzo della CPU, allocazione di memoria e velocità di I/O. L'utilizzo delle DTU raggiunge rapidamente 100%. Nel grafico precedente la velocità effettiva ha raggiunto il picco a questo punto. L'utilizzo del database rimane elevato fino al completamento del test. C'è un lieve calo verso la fine, che può derivare dalla limitazione, dalla concorrenza per le connessioni di database o da altri fattori.

Grafico che mostra il monitoraggio del database nel portale di Azure classico che mostra l'utilizzo delle risorse del database.

Esaminare i dati di telemetria per gli archivi di dati

Instrumentare gli archivi di dati per acquisire i dettagli di basso livello dell'attività. Nell'applicazione di esempio le statistiche di accesso ai dati mostrano un volume elevato di operazioni di inserimento eseguite sia sulla PurchaseOrderHeader tabella che sulla MonoLog tabella.

Grafico che mostra le statistiche di accesso ai dati per l'applicazione di esempio.

Identificare la contesa delle risorse

A questo punto, è possibile esaminare il codice sorgente, concentrandosi sui punti in cui l'applicazione accede alle risorse contese. Cercare situazioni come:

  • I dati logicamente separati sono scritti nello stesso archivio. I dati, ad esempio log, report e messaggi in coda, non devono essere mantenuti nello stesso database delle informazioni aziendali.
  • Mancata corrispondenza tra la scelta dell'archivio dati e il tipo di dati, ad esempio BLOB di grandi dimensioni o documenti XML in un database relazionale.
  • Dati con modelli di utilizzo diversi che condividono lo stesso archivio. Un esempio include dati con un'alta quantità di scrittura e una bassa quantità di lettura archiviati insieme a dati con una bassa quantità di scrittura e un'alta quantità di lettura.

Implementare la soluzione e verificare il risultato

In questo esempio l'applicazione viene aggiornata per scrivere i log in un archivio dati separato. Il grafico seguente mostra i risultati del test di carico.

Grafico che mostra i risultati delle prestazioni del test di carico usando il controller Polyglot.

Il modello di throughput è simile al grafico precedente, ma il punto in cui le prestazioni raggiungono il picco è superiore di circa 500 richieste al secondo. Il tempo medio di risposta è marginalmente inferiore. Tuttavia, queste statistiche non raccontano la storia completa. I dati di telemetria per il database aziendale indicano che l'utilizzo di DTU raggiunge un picco di circa 75%, anziché 100%.

Grafico che mostra il monitoraggio del database nel portale di Azure classico che mostra l'utilizzo delle risorse del database nello scenario poliglotta.

Analogamente, l'utilizzo massimo di DTU del database di log raggiunge solo circa 70%. I database non sono più il fattore di limitazione delle prestazioni del sistema.

Grafico che mostra il monitoraggio del database nel portale di Azure classico che mostra l'utilizzo delle risorse del database di log nello scenario poliglotta.