Best practice per le prestazioni dei microservizi

ID regione

REGION_ID è un codice abbreviato assegnato da Google in base all'area geografica selezionata al momento della creazione dell'app. Il codice non corrisponde a un paese o a una provincia, anche se alcuni ID regione possono sembrare simili ai codici paese e provincia di uso comune. Per le app create dopo febbraio 2020, REGION_ID.r è incluso negli URL di App Engine. Per le app esistenti create prima di questa data, l'ID regione è facoltativo nell'URL.

Scopri di più sugli ID regione.

Lo sviluppo di software riguarda i compromessi e i microservizi non fanno eccezione. Quello che guadagni in termini di deployment del codice e indipendenza operativa, paghi per l'overhead delle prestazioni. In questa sezione vengono forniti alcuni consigli sui passaggi da seguire per ridurre al minimo questo impatto.

Trasforma le operazioni CRUD in microservizi

I microservizi sono particolarmente adatti alle entità a cui si accede con il pattern CRUD (creazione, recupero, aggiornamento, eliminazione). Quando lavori con queste entità, in genere utilizzi una sola entità alla volta, ad esempio un utente, e in genere esegui solo una delle azioni CRUD alla volta. Di conseguenza, basta una singola chiamata di microservizi per l'operazione. Cerca le entità che hanno operazioni CRUD e un insieme di metodi aziendali che possono essere utilizzati in molte parti dell'applicazione. Queste entità sono ottimi candidati per i microservizi.

Fornisci API batch

Oltre alle API in stile CRUD, puoi comunque fornire buone prestazioni di microservizi per gruppi di entità fornendo API batch. Ad esempio, anziché mostrare solo un metodo dell'API GET che recupera un singolo utente, fornisci un'API che prenda un insieme di ID utente e restituisca un dizionario degli utenti corrispondenti:

Richiesta:

/user-service/v1/?userId=ABC123&userId=DEF456&userId=GHI789

Risposta:

{
  "ABC123": {
    "userId": "ABC123",
    "firstName": "Jake",
    … },
  "DEF456": {
    "userId": "DEF456",
    "firstName": "Sue",
    … },
  "GHI789": {
    "userId": "GHI789",
    "firstName": "Ted",
    … }
}

L'SDK di App Engine supporta molte API batch, ad esempio la possibilità di recuperare molte entità da Cloud Datastore tramite una singola RPC, quindi gestire questi tipi di API batch può essere molto efficiente.

Utilizza richieste asincrone

Spesso dovrai interagire con molti microservizi per scrivere una risposta. Ad esempio, potrebbe essere necessario recuperare le preferenze dell'utente che ha eseguito l'accesso e i dettagli della sua azienda. Spesso queste informazioni non dipendono l'una dall'altra e puoi recuperarle in parallelo. La libreria Urlfetch nell'SDK di App Engine supporta le richieste asincrone, consentendoti di chiamare i microservizi in parallelo.

Il seguente codice di esempio Python utilizza direttamente le RPC per impiegare le richieste asincrone:

from google.appengine.api import urlfetch

preferences_rpc = urlfetch.create_rpc()
urlfetch.make_fetch_call(preferences_rpc,
                         'https://preferences-service-dot-my-app.uc.r.appspot.com/preferences-service/v1/?userId=ABC123')

company_rpc = urlfetch.create_rpc()
urlfetch.make_fetch_call(company_rpc,
                         'https://company-service-dot-my-app.uc.r.appspot.com/company-service/v3/?companyId=ACME')

 ### microservice requests are now occurring in parallel

try:
  preferences_response = preferences_rpc.get_result()  # blocks until response
  if preferences_response.status_code == 200:
    # deserialize JSON, or whatever is appropriate
  else:
    # handle error
except urlfetch.DownloadError:
  # timeout, or other transient error

try:
  company_response = company_rpc.get_result()  # blocks until response
  if company_response.status_code == 200:
    # deserialize JSON, or whatever is appropriate
  else:
    # handle error
except urlfetch.DownloadError:
  # timeout, or other transient error

Lavorare in parallelo spesso è in contrasto con una buona struttura del codice perché, in uno scenario reale, spesso si utilizza una classe per incapsulare i metodi delle preferenze e un'altra per incapsulare i metodi aziendali. È difficile sfruttare le chiamate Urlfetch asincrone senza interrompere questo incapsulamento. Una soluzione valida è disponibile nel pacchetto NDB dell'SDK Python di App Engine: Tasklets. Le Tasklet ti consentono di mantenere un buon incapsulamento nel codice, offrendo al contempo un meccanismo per ottenere chiamate di microservizi in parallelo. Tieni presente che le attività utilizzano i futures anziché le RPC, ma l'idea è simile.

Usa il percorso più breve

A seconda di come richiami Urlfetch, puoi utilizzare un'infrastruttura e route diverse. Per utilizzare la rotta con il rendimento migliore, prendi in considerazione i seguenti suggerimenti:

Utilizza REGION_ID.r.appspot.com, non un dominio personalizzato
Un dominio personalizzato comporta l'utilizzo di una route diversa durante il routing tramite l'infrastruttura di Google. Poiché le chiamate ai microservizi sono interne, è facile da eseguire e ha un rendimento migliore se utilizzi https://PROJECT_ID.REGION_ID.r.appspot.com.
Imposta follow_redirects su False
Imposta esplicitamente follow_redirects=False durante la chiamata a Urlfetch, in quanto evita un servizio più pesante progettato per seguire i reindirizzamenti. Gli endpoint API non devono reindirizzare i client poiché sono microservizi di tua proprietà e dovrebbero restituire solo risposte HTTP 200, 400 e 500.
Preferisci i servizi all'interno di un progetto rispetto a più progetti
Ci sono buoni motivi per utilizzare più progetti quando crei un'applicazione basata su microservizi, ma se il tuo obiettivo principale sono le prestazioni, utilizza i servizi all'interno di un singolo progetto. I servizi di un progetto sono ospitati nello stesso data center e, anche se la velocità effettiva sulla rete tra i data center di Google è eccellente, le chiamate locali sono più veloci.

Evita avvisi durante l'applicazione forzata della sicurezza

L'utilizzo di meccanismi di sicurezza che comportano molte comunicazioni per l'autenticazione dell'API chiamante è negativo per le prestazioni. Ad esempio, se il microservizio deve convalidare un ticket dall'applicazione richiamando l'applicazione, sono stati effettuati diversi round trip per ottenere i dati.

Un'implementazione OAuth2 può ammortizzare questo costo nel tempo utilizzando token di aggiornamento e memorizzando nella cache un token di accesso tra le chiamate di Urlfetch. Tuttavia, se il token di accesso memorizzato nella cache è archiviato in memcache, per recuperarlo dovrai sottrarre l'overhead memcache. Per evitare questo overhead, potresti memorizzare nella cache il token di accesso nella memoria dell'istanza, ma continuerai a riscontrare spesso l'attività OAuth2, poiché ogni nuova istanza negozia un token di accesso. Tieni presente che le istanze di App Engine si avviano e si arrestano di frequente. Un approccio ibrido tra memcache e cache delle istanze contribuirà a mitigare questo problema, ma la soluzione diventerà più complessa.

Un altro approccio che funziona bene consiste nel condividere un token segreto tra i microservizi, ad esempio trasmesso come intestazione HTTP personalizzata. In questo approccio, ogni microservizio potrebbe avere un token univoco per ciascun chiamante. In genere, i secret condivisi sono una scelta discutibile per le implementazioni di sicurezza, ma poiché tutti i microservizi si trovano nella stessa applicazione, diventa meno problematico, considerati i miglioramenti nelle prestazioni. Con un secret condiviso, il microservizio deve eseguire solo un confronto tra stringhe del secret in entrata rispetto a un dizionario presumibilmente in memoria e l'applicazione delle misure di sicurezza è estremamente minima.

Se tutti i tuoi microservizi sono su App Engine, puoi anche esaminare l'intestazione X-Appengine-Inbound-Appid in arrivo. Questa intestazione viene aggiunta dall'infrastruttura Urlfetch quando si effettua una richiesta a un altro progetto App Engine e non può essere impostata da una parte esterna. A seconda dei requisiti di sicurezza, i microservizi possono esaminare questa intestazione in entrata per applicare i criteri di sicurezza.

Richieste di microservizi Trace

Man mano che crei la tua applicazione basata su microservizi, inizi ad accumulare overhead per le chiamate Urlfetch successive. In questi casi, puoi utilizzare Cloud Trace per capire quali chiamate vengono effettuate e dove si trova il sovraccarico. Inoltre, Cloud Trace può aiutarti a identificare dove vengono richiamati in serie microservizi indipendenti, in modo da poter refactoring del codice per eseguire questi recuperi in parallelo.

Una funzionalità utile di Cloud Trace entra in gioco quando utilizzi più servizi all'interno di un singolo progetto. Quando le chiamate vengono effettuate tra i servizi di microservizi nel progetto, Cloud Trace comprime tutte le chiamate in un unico grafico delle chiamate per consentirti di visualizzare l'intera richiesta end-to-end come un'unica traccia.

Screenshot di Google Cloud Trace

Tieni presente che, nell'esempio precedente, le chiamate a pref-service e user-service vengono eseguite in parallelo utilizzando un Urlfetch asincrono, quindi le RPC appaiono crittografate nella visualizzazione. Tuttavia, si tratta comunque di uno strumento prezioso per diagnosticare la latenza.

Passaggi successivi