Best practice per le prestazioni dei microservizi

ID regione

REGION_ID è un codice abbreviato che Google assegna in base alla regione 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 si basa sui compromessi e i microservizi non fanno eccezione. Ciò che ottieni nell'indipendenza dal punto di vista del deployment del codice e delle operazioni, paghi in termini di overhead in termini di prestazioni. Questa sezione fornisce alcuni consigli su quali azioni intraprendere per ridurre al minimo l'impatto.

Trasforma le operazioni CRUD in microservizi

I microservizi sono particolarmente adatti alle entità a cui si accede con il pattern di creazione, recupero, aggiornamento ed eliminazione (CRUD). Quando si lavora con queste entità, in genere si utilizza una sola entità alla volta, ad esempio un utente, ed è possibile eseguire solo una delle azioni CRUD alla volta. Di conseguenza, è necessaria una sola chiamata a microservizi per l'operazione. Cerca le entità con operazioni CRUD più un insieme di metodi aziendali che potrebbero essere utilizzati in molte parti dell'applicazione. Queste entità sono buoni candidati per i microservizi.

Fornisci API batch

Oltre alle API di tipo CRUD, puoi comunque fornire buone prestazioni dei microservizi per gruppi di entità fornendo API batch. Ad esempio, anziché esporre solo un metodo API GET che recupera un singolo utente, fornisci un'API che prende un insieme di ID utente e restituisce un dizionario di 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 la gestione di questi tipi di API batch può essere molto efficiente.

Utilizza richieste asincrone

Spesso dovrai interagire con molti microservizi per scrivere una risposta. Ad esempio, potresti dover recuperare le preferenze dell'utente che ha eseguito l'accesso e i dettagli dell'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 utilizzare 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 è contro una buona struttura del codice perché, in uno scenario reale, spesso si utilizza una classe per incapsulare i metodi di preferenze e un'altra per incapsulare i metodi aziendali. È difficile sfruttare le chiamate Urlfetch asincrone senza interrompere questa incapsulamento. Nel pacchetto NDB dell'SDK Python di App Engine è presente una soluzione valida: Tasklets. I tasklet consentono di mantenere una buona incapsulamento nel codice, offrendo al contempo un meccanismo per ottenere chiamate di microservizi parallele. Tieni presente che i tasklet utilizzano i futures anziché le RPC, ma l'idea è simile.

Usa il percorso più breve

A seconda di come richiami Urlfetch, puoi causare l'utilizzo di infrastrutture e route diverse. Per utilizzare il percorso con il rendimento migliore, tieni in considerazione i seguenti consigli:

Utilizza REGION_ID.r.appspot.com, non un dominio personalizzato
Un dominio personalizzato comporta l'utilizzo di una route diversa durante l'instradamento tramite l'infrastruttura di Google. Poiché le chiamate ai microservizi sono interne, è facile e offre prestazioni migliori utilizzando https://PROJECT_ID.REGION_ID.r.appspot.com.
Imposta follow_redirects su False
Imposta in modo esplicito follow_redirects=False durante la chiamata a Urlfetch, per evitare di dover usare un servizio di peso maggiore progettato per seguire i reindirizzamenti. Gli endpoint API non dovrebbero dover reindirizzare i client, perché sono i tuoi microservizi, e gli endpoint dovrebbero restituire solo risposte HTTP delle serie 200, 400 e 500.
Preferisci i servizi all'interno di un progetto rispetto a più progetti
Esistono buoni motivi per utilizzare più progetti quando si crea un'applicazione basata su microservizi, ma se l'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 data center di Google è eccellente, le chiamate locali sono più veloci.

Evitare le conversazioni durante l'applicazione della sicurezza

L'utilizzo di meccanismi di sicurezza che richiedono 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, ti sono addebitati i costi per una serie di round trip per recuperare 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 viene archiviato in memcache, dovrai sostenere l'overhead di memcache per recuperarlo. Per evitare questo sovraccarico, potresti memorizzare nella cache il token di accesso nella memoria dell'istanza, ma continuerai a sperimentare l'attività OAuth2 di frequente, poiché ogni nuova istanza negozia un token di accesso. Tieni presente che le istanze di App Engine si avviano e si arrestano frequentemente. Alcuni ibridi tra memcache e cache dell'istanza consentono di mitigare il problema, ma la soluzione inizia a diventare più complessa.

Un altro approccio efficace consiste nella condivisione di un token secret tra microservizi, ad esempio trasmesso come intestazione HTTP personalizzata. In questo approccio, ciascun microservizio potrebbe avere un token univoco per ogni 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, non rappresenta un problema, considerati i miglioramenti delle prestazioni. Con un secret condiviso, il microservizio deve solo eseguire un confronto di stringhe del secret in entrata con un dizionario presumibilmente in memoria e l'applicazione della sicurezza è molto leggera.

Se tutti i microservizi si trovano su App Engine, puoi anche controllare l'intestazione X-Appengine-Inbound-Appid in arrivo. Questa intestazione viene aggiunta dall'infrastruttura Urlfetch quando viene effettuata 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 potrebbero esaminare l'intestazione in entrata per applicare i criteri di sicurezza.

Trace le richieste dei microservizi

Man mano che crei la tua applicazione basata su microservizi, inizi ad accumulare overhead dalle chiamate Urlfetch successive. In questo caso, puoi utilizzare Cloud Trace per capire quali chiamate vengono effettuate e dove si trova l'overhead. È importante anche identificare dove vengono richiamati in serie i microservizi indipendenti, in modo da poter eseguire il refactoring del codice in modo da eseguire questi recuperi in parallelo.

Una funzionalità utile di Cloud Trace viene attivata quando si utilizzano più servizi all'interno di un singolo progetto. Man mano che vengono effettuate chiamate 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 vengono visualizzate in modo criptato nella visualizzazione. Tuttavia, questo è comunque uno strumento prezioso per diagnosticare la latenza.

Passaggi successivi