Test di carico Go prima della prima assunzione backend
Usa i test di carico Go per modellare traffico realistico, misurare latenza p95, pressione sul database, memoria ed errori, quindi decidere se assumere.

Diecimila utenti attivi mensili non sono un requisito di capacità. Sono un dato di fatturazione o analisi. Un servizio Go può supportarne molti di più quando le richieste sono leggere e distribuite nel mese, oppure andare in crisi con poche centinaia di utenti se ogni sessione genera query lente, upload e chiamate a servizi terzi.
La domanda utile è se il backend generato soddisfa un obiettivo di servizio dichiarato con il picco di traffico previsto, lasciando margine per la crescita e per un guasto ordinario. Puoi rispondere prima di assumere un backend engineer, ma solo se il test di carico assomiglia al tuo prodotto e registra contemporaneamente cosa fanno l'API, il runtime Go e PostgreSQL. Un grafico verde della latenza media dimostra quasi nulla.
Questo è il test che richiederei prima di dire a un founder che un backend Go generato è pronto per 10.000 utenti attivi mensili. Produce un risultato ripetibile di superamento o fallimento, individua il primo collo di bottiglia e separa un problema di capacità da uno di correttezza.
Gli utenti mensili devono diventare richieste di picco
Prima di scegliere qualsiasi livello di carico, trasforma la previsione degli utenti in richieste al secondo. Gli utenti attivi mensili nascondono le due variabili che determinano il comportamento di un backend: quante sessioni arrivano nella finestra più affollata e quanto lavoro crea ogni sessione.
Inizia dai dati osservati se esiste una beta privata. Conta le sessioni nei 15 minuti più affollati, le richieste per sessione e la distribuzione delle route. Se non c'è ancora traffico, annota le ipotesi in modo che tutti possano metterle in discussione. Per esempio, supponi che 10.000 utenti attivi creino otto sessioni al mese, 15 richieste API per sessione e che il 20% del traffico giornaliero cada nell'ora più affollata. Questo produce circa 8 richieste al secondo in una giornata mediamente intensa. Lanci, notifiche, scadenze per il pagamento degli stipendi o un fuso orario condiviso possono rendere il picco reale parecchie volte più alto.
Non trasformare questi calcoli in una precisione fittizia. Usali per definire tre livelli di test:
- Picco previsto: il carico più alto che prevedi ora.
- Picco di crescita: il doppio del picco previsto, salvo che l'azienda disponga di una previsione migliore.
- Livello di stress: aumenta il traffico finché un obiettivo di servizio fallisce o una risorsa si satura.
I test del picco previsto e di crescita rispondono alla domanda se il lancio pianificato ha margine. Il test di stress dice cosa si rompe per primo e come il guasto appare agli utenti. Quest'ultima risposta conta, perché un servizio che rifiuta rapidamente il lavoro in eccesso è più semplice da gestire di uno che consuma ogni connessione al database e blocca route non correlate.
Per il test di capacità usa un modello di carico aperto. Un modello aperto avvia le richieste a un tasso di arrivo fisso anche quando le richieste precedenti rallentano. Un modello chiuso con un numero fisso di utenti virtuali spesso nasconde il collasso: risposte più lente fanno sì che quegli utenti inviino meno nuove richieste, quindi il carico offerto diminuisce proprio quando il servizio è in difficoltà. La documentazione di Grafana k6 illustra questa differenza tramite gli executor a tasso di arrivo e segnala dropped_iterations quando il generatore non riesce ad avviare il lavoro pianificato. Considera le iterazioni scartate un errore del generatore di test, non un successo del server.
Dopo il warm-up, esegui ogni livello costante per almeno 30 minuti. I test di cinque minuti non rilevano il ricambio delle connessioni, i cicli di garbage collection, l'espulsione dalla cache, i job in background e la crescita graduale della memoria. Quando il test più breve passa, aggiungi uno soak test separato di due ore al picco previsto.
Scrivi la conversione in un piccolo foglio di calcolo e lascia visibile ogni unità. Utenti mensili moltiplicati per sessioni per utente e richieste per sessione danno le richieste mensili. Dividi solo dopo aver assegnato il traffico ai giorni operativi e all'ora più affollata. Poi aggiungi il traffico di retry, i job in background, i webhook e il polling che le analisi degli utenti potrebbero non contare. Un frontend che fa polling ogni dieci secondi può creare più lavoro API dei clic che hanno aperto la pagina.
Modella i picchi improvvisi separatamente dal picco costante. Il login dopo una notifica, il completamento di un'importazione o i client che ritentano dopo una breve interruzione possono concentrare il lavoro in un minuto. Aggiungi una fase di burst che raggiunga rapidamente il tasso previsto, lo mantenga abbastanza da riempire le code e torni alla normalità. Il servizio dovrebbe riprendersi senza una coda crescente né un riavvio manuale. Registra il tempo di recupero come risultato. Un sistema può superare il test costante e restare comunque rischioso se un breve burst lascia bloccati il pool o i worker.
Non moltiplicare il tasso finale per un fattore di sicurezza arbitrario chiamando realistico il risultato. Collega il margine a un'incertezza di business: errore di previsione, campagna pianificata, una replica non disponibile o tempo necessario per aggiungere capacità. Testa ogni ipotesi su cui intendi fare affidamento. Conserva il foglio accanto al risultato, perché un'esecuzione superata perde significato quando nessuno ricorda quali previsioni e ipotesi di burst hanno prodotto l'obiettivo.
La distribuzione del traffico deve assomigliare a una sessione reale
Un test di carico realistico conserva frequenza delle route, dimensione dei payload, autenticazione, distribuzione dei dati, tempi di riflessione e contesa sulle scritture. Colpire /health 100 volte al secondo misura l'handler di salute, non l'applicazione.
Quando possibile, costruisci la distribuzione dai log di accesso. Raggruppa le route per azione di business invece che per URL grezzo, perché /projects/123 e /projects/456 hanno la stessa forma. Una distribuzione plausibile per un SaaS iniziale potrebbe assegnare il 45% alle letture di elenco e dettaglio, il 20% alla ricerca, il 15% a creazioni o aggiornamenti, il 10% a login e rinnovo dei token e il 10% a esportazioni o altro lavoro pesante. I tuoi numeri devono derivare dal flusso del prodotto, non da questo esempio.
Usa molti account e record di test. Riutilizzare un solo account può creare una cache irrealisticamente calda, serializzare gli aggiornamenti su una riga o attivare limiti di frequenza che il traffico reale distribuirebbe. Prepara tenant piccoli, medi e grandi. Includi record mancanti, input non validi e errori di autorizzazione, perché i percorsi di errore spesso interrogano il database o allocano corpi di risposta in modo diverso dai percorsi riusciti.
Mantieni upload grandi ed esportazioni lunghe in uno scenario separato se hanno un obiettivo di servizio diverso. Eseguili comunque insieme al traffico normale. Altrimenti il test non coglie l'incidente esatto che gli utenti notano: una categoria di esportazioni occupa il pool mentre una semplice pagina delle impostazioni resta in attesa dietro di essa.
Nell'esecuzione finale di capacità, non simulare PostgreSQL, object storage, code o servizi in uscita. Un mock è utile per isolare il costo dell'handler, ma elimina le dipendenze che più probabilmente determinano la capacità. Punta il test a uno stack di staging con le stesse dimensioni delle istanze, impostazioni del database, indici, limiti di connessione e percorso di rete della produzione. Dati ripuliti ma simili a quelli di produzione sono migliori di mille righe seed identiche.
Evita di testare attraverso una cache di distribuzione dei contenuti se normalmente l'API non è in cache. Al contrario, mantieni la cache reale nel percorso quando la produzione la usa. Lo scopo non è far sembrare occupato il backend. Lo scopo è riprodurre il lavoro che una richiesta utente causa davvero.
Un test k6 eseguibile deve codificare il contratto
Metti soglie e fasi di traffico sotto controllo di versione, così un'esecuzione non può diventare uno screenshot interpretato a posteriori. k6 considera le soglie criteri di superamento o fallimento ed esce con un codice diverso da zero quando falliscono, quindi il risultato è adatto come controllo di release.
Lo scheletro seguente guida una sessione mista a un tasso di arrivo, controlla la semantica delle risposte e impone limiti di latenza distinti per letture normali ed esportazioni pesanti. Sostituisci route, payload e obiettivi con valori concordati per il tuo prodotto. Il codice nel test è volutamente semplice, così un controllo fallito corrisponde a un'azione dell'utente.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const businessErrors = new Rate('business_errors');
export const options = {
scenarios: {
expected_peak: {
executor: 'ramping-arrival-rate',
startRate: 5,
timeUnit: '1s',
preAllocatedVUs: 40,
maxVUs: 200,
stages: [
{ target: 10, duration: '5m' },
{ target: 10, duration: '30m' },
{ target: 20, duration: '10m' },
{ target: 20, duration: '30m' },
],
},
},
thresholds: {
'http_req_duration{name:project_list}': ['p(95)<300'],
'http_req_duration{name:project_create}': ['p(95)<500'],
'http_req_duration{name:export}': ['p(95)<2000'],
http_req_failed: ['rate<0.01'],
business_errors: ['rate<0.005'],
dropped_iterations: ['count==0'],
},
};
export function setup() {
const response = http.post(`${__ENV.BASE_URL}/api/login`, JSON.stringify({
email: __ENV.TEST_EMAIL,
password: __ENV.TEST_PASSWORD,
}), { headers: { 'Content-Type': 'application/json' } });
check(response, { 'login succeeds': r => r.status === 200 });
return { token: response.json('token') };
}
export default function (data) {
const headers = { Authorization: `Bearer ${data.token}`, 'Content-Type': 'application/json' };
const list = http.get(`${__ENV.BASE_URL}/api/projects?limit=25`, {
headers,
tags: { name: 'project_list' },
});
businessErrors.add(!check(list, {
'list status is 200': r => r.status === 200,
'list has items': r => Array.isArray(r.json('items')),
}));
if (Math.random() < 0.25) {
const create = http.post(`${__ENV.BASE_URL}/api/projects`, JSON.stringify({
name: `load-${__VU}-${__ITER}`,
}), { headers, tags: { name: 'project_create' } });
businessErrors.add(!check(create, { 'create status is 201': r => r.status === 201 }));
}
if (Math.random() < 0.03) {
const runExport = http.post(`${__ENV.BASE_URL}/api/exports`, '{}', {
headers,
tags: { name: 'export' },
});
businessErrors.add(!check(runExport, { 'export accepted': r => r.status === 202 }));
}
sleep(Math.random() * 2 + 1);
}
Gli obiettivi dell'esempio sono punti di partenza, non promesse universali. Definisci il p95 per classe di route in base al ritardo che un utente può tollerare e ai requisiti del prodotto. Non usare un'unica soglia globale di 300 ms per giudicare un'esportazione asincrona e una richiesta di completamento automatico.
Esegui lo script da una macchina che non ospita l'applicazione. Verifica che il generatore abbia CPU libera e nessuna iterazione scartata. Salva commit esatto, configurazione dell'ambiente, identificatore dello snapshot dei dati, comando e output grezzo del test. Senza questi elementi, un confronto successivo è soprattutto memoria e ottimismo.
Calibra il generatore prima di fidarti di un'esecuzione lunga. Puntalo a un handler minimo senza lavoro sul database, aumenta il tasso di arrivo richiesto oltre il test pianificato e verifica che il generatore lo sostenga senza esaurire CPU, socket o rete propri. Quando k6 aggiunge utenti virtuali o segnala iterazioni scartate, la macchina di carico può essere il limite. Distribuisci la generazione su più macchine solo quando una non può offrire il lavoro richiesto e mantieni sincronizzati i loro orologi affinché i grafici di server e client coincidano.
Dai a ogni esecuzione una base tranquilla. Ferma migrazioni, importazioni di dati e job di staging non correlati, a meno che tali job non girino anche durante il picco reale. Poi pianifica un secondo test con il lavoro in background reale attivo. La coppia indica sia la capacità pulita dell'API sia la capacità operativa che gli utenti riceveranno. Se passa solo l'esecuzione tranquilla, il piano di lancio dipende da una finzione di staging.
Crea un secondo script per un singolo percorso utente ed eseguilo con un utente virtuale prima di aggiungere carico. Ispeziona ogni risposta, record creato e azione di pulizia. Questo rileva token errati, controlli che restituiscono sempre true o dati di test che collidono dopo la prima iterazione. Un risultato di capacità non significa nulla se lo script esercita una pagina di errore o rilegge ripetutamente lo stesso oggetto in cache.
Il p95 richiede il contesto della route
Usa la latenza p95 perché le medie nascondono una minoranza lenta, ma non leggere mai il p95 da solo. Con un p95 di 800 ms, una richiesta su venti impiega almeno quel tempo, e una pagina con più richieste può sembrare costantemente lenta. Il percentile diventa anche rumoroso sulle route con pochissimi campioni, quindi riporta accanto il numero di richieste.
Registra p50, p95, p99, massimo, throughput e tasso di errore per ogni azione di business nominata. Il p50 mostra il comportamento normale, il p95 è un pratico limite di servizio e il p99 espone la coda senza lasciare che un singolo massimo domini la discussione. Suddividi i risultati per codice di stato. Una risposta 500 veloce non deve migliorare il quadro della latenza.
Misura la durata dell'handler lato server oltre alla durata osservata dal client. La differenza include apertura della connessione, proxy, tempo di rete e trasferimento della risposta. Se il p95 client cresce mentre quello dell'handler resta piatto, guarda fuori dall'handler. Se crescono entrambi e aumenta il tempo di attesa del database, la richiesta probabilmente è in coda per una connessione o una query.
I risultati di warm-up e di stato stazionario devono restare separati. In un binario Go distribuito non avviene compilazione, ma cache fredde, nuove connessioni al database, inizializzazione ritardata e autoscaling possono distorcere i minuti iniziali. Gli utenti sperimentano comunque il comportamento a freddo, quindi mantienilo come risultato distinto invece di eliminarlo.
Le medie restano utili per contabilizzare le risorse. Il tempo totale del database diviso per le chiamate può identificare una query moderatamente lenta ma estremamente frequente. Non può però sostituire un obiettivo di servizio percentile. La distinzione che spesso si confonde è quella tra latenza e capacità: la latenza descrive quanto tempo ha richiesto il lavoro completato, mentre la capacità descrive quanto lavoro offerto il servizio può sostenere senza far crescere code o errori. Una buona latenza con un basso tasso di richieste effettivamente raggiunto non dimostra la capacità.
Definisci il fallimento prima dell'esecuzione. Considererei fallito un test di lancio quando una route critica non raggiunge il suo obiettivo p95, gli errori HTTP inattesi superano il tasso concordato, falliscono i controlli di business, le iterazioni pianificate vengono scartate o una risorsa rimane satura. Superare quattro controlli su cinque significa avere un'esecuzione fallita con dati diagnostici utili.
Le attese delle connessioni rivelano code nascoste nel database
Strumenta database/sql prima del test, perché la latenza dell'applicazione non può dirti se PostgreSQL è lento o se l'applicazione sta aspettando di raggiungerlo. DB.Stats() di Go riporta OpenConnections, InUse, Idle, WaitCount e WaitDuration, oltre ai contatori di chiusura. Esportali nel tuo sistema di metriche ogni pochi secondi.
func recordDBStats(ctx context.Context, db *sql.DB, g GaugeSet) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s := db.Stats()
g.Set("db_open_connections", float64(s.OpenConnections))
g.Set("db_in_use_connections", float64(s.InUse))
g.Set("db_idle_connections", float64(s.Idle))
g.Set("db_wait_count_total", float64(s.WaitCount))
g.Set("db_wait_seconds_total", s.WaitDuration.Seconds())
}
}
}
Calcola la variazione dei contatori cumulativi nella finestra stazionaria. L'aumento di WaitCount significa che le richieste hanno dovuto aspettare una connessione libera. La variazione di WaitDuration divisa per la variazione di WaitCount fornisce l'attesa media nel pool per quell'intervallo. Rappresenta InUse rispetto a MaxOpenConnections: una linea piatta al limite insieme ad attese in aumento indica saturazione del pool.
Non rispondere aumentando SetMaxOpenConns finché il grafico non sembra migliore. Questa soluzione popolare sposta la coda in PostgreSQL e può aumentare contesa, uso della memoria e latenza delle query. Prima individua perché le connessioni restano occupate: query lente, transazioni mantenute durante chiamate di rete, righe lette una alla volta o chiamate Rows.Close() dimenticate. Poi dimensiona il pool entro il budget di connessioni del database considerando ogni replica dell'applicazione e ogni worker.
La documentazione Go afferma che un valore SetMaxOpenConns non positivo lascia il pool senza limiti. Senza limiti è un'impostazione predefinita pericolosa in produzione quando più repliche possono aprire connessioni contemporaneamente. Imposta un limite esplicito, definisci consapevolmente il comportamento delle connessioni inattive e della loro durata e riserva capacità del database per migrazioni, amministrazione e lavoro in background.
Tieni traccia separatamente della durata delle transazioni. Un handler può rispondere in 200 ms mentre una pulizia differita o una transazione persa trattiene una connessione molto più a lungo. Le metriche del pool rivelano la pressione, ma tracce o tempi delle transazioni rivelano il responsabile.
Le prove delle query lente devono arrivare da PostgreSQL
Abilita pg_stat_statements nell'ambiente di test e acquisisci snapshot prima e dopo ogni esecuzione. La documentazione PostgreSQL lo descrive come uno strumento che rileva statistiche di pianificazione ed esecuzione per istruzioni normalizzate. La sua vista include chiamate, righe, tempo di esecuzione totale e medio, attività dei blocchi e attività dei blocchi temporanei. Questa prova è molto migliore che indovinare dalla query comparsa in una singola traccia.
Usa una differenza tra snapshot perché la vista è cumulativa. Reimpostala solo in un database di test isolato, perché il reset distrugge le prove di altro lavoro. Questa query trova le istruzioni che hanno consumato più tempo di esecuzione durante una finestra di test pulita:
SELECT
queryid,
calls,
round(total_exec_time::numeric, 1) AS total_ms,
round(mean_exec_time::numeric, 2) AS mean_ms,
rows,
shared_blks_read,
temp_blks_written
FROM pg_stat_statements
WHERE calls > 20
ORDER BY total_exec_time DESC
LIMIT 20;
Il tempo totale individua query frequenti che dominano il lavoro del database. Il tempo medio individua istruzioni lente singolarmente. Nessuno dei due fornisce il p95 della query perché pg_stat_statements aggrega le chiamate: usa tracing o istogrammi di durata quando conta la latenza di coda di una singola istruzione. Anche qui c'è una distinzione spesso confusa: una query lenta può avere un'esecuzione media alta, una coda alta oppure un enorme costo totale dovuto alla frequenza. Ognuna richiede una correzione diversa.
Per le istruzioni principali, esegui EXPLAIN (ANALYZE, BUFFERS) con parametri sicuri e rappresentativi al di fuori del test di carico cronometrato. ANALYZE esegue l'istruzione, quindi racchiudi le istruzioni che modificano dati in una transazione di cui fai rollback, oppure ispezionale su una copia eliminabile. Cerca stime molto diverse dalle righe effettive, cicli ripetuti, scansioni sequenziali su tabelle grandi ma selettive, ordinamenti che finiscono su disco e molti blocchi condivisi letti.
Il codice generato spesso crea uno schema N+1 che sembra innocuo con dati seed: recupera 25 progetti, poi emette una query del proprietario e una query di conteggio per ogni progetto. Con dieci richieste al secondo, quell'unico endpoint può creare più di 500 istruzioni del database al secondo prima che venga eseguita qualsiasi altra route. La correzione può essere un join, un WHERE id = ANY($1) in batch o un conteggio precalcolato. Aumentare il limite del pool lascia intatto lo spreco.
Acquisisci anche le attese sui lock. Una query rapida isolatamente può bloccarsi con aggiornamenti concorrenti allo stesso tenant, account o sequenza. Quando la latenza aumenta solo nello scenario di scrittura, ispeziona attese attive e confini delle transazioni invece di aggiungere automaticamente un indice.
La memoria deve stabilizzarsi sotto carico costante
Valuta la memoria dalla sua forma nel tempo, non da un singolo picco. Un processo Go che cresce durante il warm-up e poi oscilla attorno a un livello stabile si comporta diversamente da uno il cui valore di base dopo il garbage collector aumenta durante tutto uno soak test di due ore.
Registra memoria residente del processo, allocazione heap Go, oggetti heap, numero di goroutine, frequenza del garbage collector, tempo di pausa e tasso di allocazione. La memoria del container conta perché il sistema operativo termina il processo in base al suo limite, non al solo heap Go. Confronta la memoria dopo le raccolte del garbage collector a carico simile. Così elimini gran parte del normale andamento a dente di sega e rendi visibile la ritenzione.
Esponi le metriche standard del runtime Go o un endpoint di profiling protetto in staging. Acquisisci un profilo heap vicino all'inizio e alla fine dello soak test, poi confronta i punti di allocazione trattenuti con go tool pprof. Acquisisci anche un profilo delle goroutine. Un numero di goroutine in crescita può rivelare richieste bloccate sui canali, corpi di risposta mai chiusi o lavoro in background avviato senza cancellazione.
Non impostare GOMEMLIMIT esattamente al limite del container. Il processo necessita di memoria anche per stack delle goroutine, mapping dell'eseguibile, buffer del driver del database e altre allocazioni fuori dall'heap. Lascia margine e dimostralo con i payload più grandi. Un test con piccoli corpi JSON dice poco su un endpoint che legge in memoria un upload di 20 MB.
Forza i casi scomodi: corpi di richiesta massimi accettati, risultati di query grandi, client cancellati, timeout ed esportazioni ripetute. Verifica che la memoria torni indietro dopo il completamento del lavoro. Osserva anche la CPU, perché un garbage collector pesante può mantenere la memoria sotto il limite distruggendo però la latenza.
Una condizione pratica di superamento combina un tetto e una tendenza. Richiedi che la memoria residente resti ampiamente sotto il limite di deployment al picco di crescita, poi che il valore di base dopo la raccolta e il numero di goroutine smettano di crescere durante lo soak test. Non esiste una percentuale sicura universale. Scegli il margine in base al comportamento di riavvio della piattaforma, alla variabilità del traffico e al fatto che un'altra replica possa assorbire un riavvio.
I fallimenti devono includere risposte sbagliate e comportamento in sovraccarico
Conta separatamente fallimenti di trasporto, fallimenti di stato HTTP, timeout, panic e risposte errate. http_req_failed intercetta le richieste HTTP fallite in base al callback di risposta di k6, ma una risposta 200 con un elenco vuoto, un addebito duplicato o un record mancante resta un fallimento. Per questo l'esempio emette business_errors dai controlli del contenuto.
Etichetta i rifiuti previsti, come input non valido o un limite di frequenza intenzionale, affinché non inquinino il tasso di errore inatteso. Poi verifica il loro contratto: lo stato è corretto, il corpo ha dimensioni limitate e il rifiuto arriva rapidamente. Un sistema in sovraccarico non dovrebbe spendere 30 secondi prima di restituire 503.
Osserva i log del server per recupero da panic, errori di scadenza del contesto, ritardi nell'acquisizione delle connessioni, errori di serializzazione PostgreSQL e query annullate. Raggruppa gli errori per causa stabile, non per messaggio completo, così gli identificatori non creano migliaia di categorie. Salva tracce rappresentative della prima occorrenza e della coda ad alta latenza.
Esegui un test di degrado dopo il test pulito di capacità. Riduci le connessioni disponibili al database, aggiungi latenza controllata a una dipendenza in uscita o riavvia una replica dell'applicazione mentre il traffico continua. Fallo solo nell'ambiente di test isolato. L'obiettivo è confermare che timeout, cancellazione e health check contengano il guasto invece di permettere alle code di consumare ogni risorsa.
Configura esplicitamente il server HTTP. La documentazione Go di net/http afferma che valori zero o negativi di ReadTimeout, WriteTimeout e IdleTimeout possono significare assenza di timeout, a seconda del campo. I servizi generati spesso chiamano http.ListenAndServe con i valori predefiniti senza mai prendere questa decisione. Un http.Server personalizzato, deadline per richiesta e dimensioni limitate dei corpi impediscono a client lenti e dipendenze bloccate di trattenere risorse per sempre.
Dopo l'esecuzione, rivedi la correttezza. Conta i record creati, verifica l'idempotenza dove i client hanno ritentato, controlla che i job in background siano terminati una volta sola e conferma che nessuno stato parziale sia sopravvissuto a richieste fallite. Nei miei progetti, i test di carico hanno scoperto più bug di lavoro duplicato di qualsiasi sofisticata revisione del codice.
La decisione di assumere dipende dal primo limite
Puoi lanciare senza un backend engineer quando il sistema supera ripetutamente i test di picco previsto e di crescita, lo soak test raggiunge una base di memoria stabile, le code del database restano controllate e il team sa spiegare il primo fallimento sotto stress. Un'unica esecuzione verde fortunata non è una prova. Esegui lo stesso commit almeno tre volte e indaga sulle grandi variazioni.
Conserva un riepilogo compatto per ogni esecuzione:
- Commit e ambiente, incluse dimensioni delle repliche e configurazione del database.
- Scala del dataset, distribuzione del traffico, tassi di arrivo e durata del test.
- p95 e p99 per route, throughput raggiunto, fallimenti di business e iterazioni scartate.
- Picco di CPU e memoria, andamento della memoria dopo la raccolta e andamento delle goroutine.
- Attese del pool, SQL principali per tempo totale, attese sui lock e punto di rottura osservato.
Assumi o ingaggia aiuto backend prima del lancio quando nessuno sa spiegare attese crescenti nel pool, memoria trattenuta, contesa sui lock o scritture incoerenti. Assumi quando l'unica persona capace di eseguire il test non può modificare in sicurezza il codice generato. È un vuoto di responsabilità, non una soglia di richieste al secondo.
Un test fallito non giustifica automaticamente un'assunzione a tempo pieno. Un indice mancante, una query N+1 o un'esportazione senza limiti possono essere una riparazione circoscritta. Fallimenti ripetuti nella progettazione delle transazioni, nell'osservabilità, nella cancellazione e nel comportamento di deployment indicano lavoro ingegneristico continuativo. La differenza è se hai trovato un difetto o hai scoperto che nessuno è responsabile del comportamento del sistema.
Koder.ai può generare ed esportare un backend Go, distribuirlo e conservare snapshot per il rollback, ma la generazione non elimina la pianificazione della capacità. Mantieni lo script di carico e le modifiche all'osservabilità insieme al codice sorgente, così ogni modifica rilevante al backend deve superare lo stesso contratto.
Non promettere al business che 10.000 utenti mensili sono sicuri. Prometti un tasso di arrivo misurato, una distribuzione delle route, un obiettivo di latenza, un budget di errore e un perimetro di risorse. Quando il prodotto cambia, modifica questi dati ed esegui di nuovo il test.
Domande frequenti
Un backend Go può gestire 10.000 utenti attivi mensili?
Spesso sì, ma gli utenti attivi mensili non descrivono il carico del backend. Trasforma la previsione in richieste al secondo nei picchi e in una distribuzione delle route, poi prova quel carico rispetto a limiti espliciti di latenza, errori, database e memoria.
Quante richieste al secondo corrispondono a 10.000 utenti mensili?
Non esiste una conversione fissa. Servono sessioni per utente, richieste per sessione, quota di traffico nella finestra più affollata e qualsiasi evento che faccia arrivare gli utenti insieme.
Quale latenza p95 è accettabile per un'API Go?
Definisci gli obiettivi in base all'azione dell'utente, non al linguaggio o al framework. Le letture interattive possono richiedere alcune centinaia di millisecondi, mentre i lavori in background accettati possono avere un obiettivo diverso, ma il team deve scegliere i valori prima di vedere i risultati.
Quanto dovrebbe durare un test di carico del backend?
Mantieni ogni livello di carico costante per almeno 30 minuti dopo il warm-up, poi esegui uno soak test più lungo al picco previsto. I test brevi possono non rilevare ricambio delle connessioni, memoria trattenuta, lavoro in background e crescita graduale delle code.
Per i test di carico devo usare utenti virtuali o un tasso di arrivo?
Per dichiarare la capacità, usa un modello basato sul tasso di arrivo, perché continua a offrire lavoro quando il servizio rallenta. Un modello con un numero fisso di utenti virtuali può ridurre la frequenza delle richieste durante un rallentamento e nascondere il punto in cui le code iniziano a crescere.
Come rilevo l'esaurimento del pool di connessioni al database in Go?
Esporta DB.Stats() e osserva InUse, OpenConnections, WaitCount e WaitDuration. Contatori di attesa in crescita mentre le connessioni in uso restano al massimo configurato mostrano che le richieste sono in coda per il pool.
Devo aumentare il pool di connessioni SQL di Go quando le richieste attendono?
Non prima di sapere perché le connessioni restano occupate e quanta capacità ha PostgreSQL. Un pool più grande può spostare la coda nel database e peggiorare la contesa.
Come trovo query PostgreSQL lente durante un test di carico?
Acquisisci snapshot di pg_stat_statements prima e dopo il test, poi ordina le differenze delle query per tempo totale e medio di esecuzione. Per la latenza di coda usa tracce o istogrammi rappresentativi, perché la vista aggregata non fornisce il p95 per query.
Come capisco se un servizio Go ha una perdita di memoria?
Esegui uno soak test costante e confronta la memoria dopo le raccolte del garbage collector a carico simile. Un valore di base che continua a salire, soprattutto con oggetti nell'heap o goroutine in aumento, richiede il confronto dei profili heap e goroutine.
Quando una startup dovrebbe assumere un backend engineer?
Assumi quando i problemi di prestazioni rivelano un bisogno continuativo di responsabilità su progettazione del database, osservabilità, concorrenza, correttezza o operatività. Una singola correzione circoscritta a un indice o a una query può non richiedere un ruolo a tempo pieno, ma un comportamento inspiegato sotto carico sì.