Ottimizzazione delle Prestazioni
MongoDB: Ottimizzazione Query e Indici
Indici Compound con tenantId
In un sistema multi-tenant, ogni query include implicitamente il tenantId (separazione per database t_{tenantId}). Tuttavia, all'interno di ciascun database tenant, gli indici devono essere progettati per le query piu frequenti.
// Indici raccomandati per le collection principali
// orders - query per stato e data
db.orders.createIndex({ "status": 1, "createdAt": -1 });
db.orders.createIndex({ "tableId": 1, "status": 1 });
db.orders.createIndex({ "channel": 1, "createdAt": -1 });
// products - ricerca per categoria e disponibilita
db.products.createIndex({ "categoryId": 1, "isActive": 1 });
db.products.createIndex({ "barcode": 1 }, { unique: true, sparse: true });
// cash_movements - journal query
db.cash_movements.createIndex({ "sessionId": 1, "createdAt": -1 });
db.cash_movements.createIndex({ "registerId": 1, "createdAt": -1 });
// sync - query per dispositivo e timestamp
db.sync_log.createIndex({ "deviceId": 1, "updatedAt": -1 });
Verifica Utilizzo Indici
// Verificare che una query usi l'indice atteso
db.orders.find({ status: "open" }).sort({ createdAt: -1 }).explain("executionStats");
// Campi da controllare nell'output:
// - "stage": "IXSCAN" (bene) vs "COLLSCAN" (male)
// - "totalDocsExamined" vs "nReturned" (rapporto vicino a 1:1 = ottimale)
// - "executionTimeMillis": idealmente < 10ms
Connection Pool Sizing
Il pool di connessioni MongoDB e condiviso tra tutti i servizi sullo stesso nodo:
// moleculer.config.js - configurazione pool
const mongoOptions = {
maxPoolSize: 50, // Connessioni max per nodo
minPoolSize: 5, // Connessioni minime mantenute
maxIdleTimeMS: 30000, // Chiudi connessioni idle dopo 30s
waitQueueTimeoutMS: 5000, // Timeout se il pool e pieno
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 10000,
socketTimeoutMS: 45000
};
Regola empirica: maxPoolSize = (numero medio di azioni concorrenti per pod) * 1.5. Per la maggior parte dei nodi, 50 e sufficiente. Per core e commerce sotto carico, considerare 100.
Query Optimization
// EVITARE: query senza filtro su collection grandi
const allOrders = await this.adapter.find({}); // NO
// PREFERIRE: proiezione e paginazione
const orders = await this.adapter.find({
query: { status: "open" },
fields: ["_id", "orderNumber", "total", "status", "createdAt"],
sort: "-createdAt",
limit: 50,
offset: 0
});
// EVITARE: $regex non ancorato a sinistra (non usa indice)
{ name: { $regex: "pizza" } } // COLLSCAN
// PREFERIRE: $regex ancorato o text index
{ name: { $regex: "^pizza", $options: "i" } } // Usa indice su name
Redis: Ottimizzazione Cache
Cache Hit Rate
L'obiettivo e mantenere un hit rate superiore al 70% per le action cached.
# Monitoraggio hit rate in Prometheus
sum(rate(pos_cache_hits_total[5m])) by (service)
/ (sum(rate(pos_cache_hits_total[5m])) by (service) + sum(rate(pos_cache_misses_total[5m])) by (service))
TTL Tuning per Tipo di Dato
// moleculer.config.js - configurazione cache
cacher: {
type: "Redis",
options: {
redis: process.env.REDIS_URI,
prefix: "MOL-",
ttl: 300, // Default: 5 minuti
// TTL specifici per servizio
keygen(name, params, meta) {
// Include tenantId nella chiave cache
return `${meta.tenantId}:${name}:${JSON.stringify(params)}`;
}
}
}
// TTL raccomandati per tipo di dato
// Dati statici (categorie, configurazioni): 3600s (1 ora)
// Dati semi-statici (prodotti, listini): 600s (10 min)
// Dati dinamici (ordini attivi, sessioni): 60s o cache: false
// Dati real-time (tavoli, code): NON cachare
Invalidazione Cache
// Invalidazione esplicita dopo modifica
async afterUpdate(ctx, res) {
// Invalida tutte le chiavi del servizio per il tenant
const tenantId = ctx.meta.tenantId;
await this.broker.cacher.clean(`${tenantId}:products.**`);
return res;
}
// ATTENZIONE: raw MongoDB writes bypassano la cache
// Dopo insertMany/updateMany diretti, invalidare manualmente:
await this.broker.cacher.clean("products.**");
Gestione Memoria Redis
# Controllare la memoria utilizzata
redis-cli INFO memory | grep used_memory_human
# Analizzare le chiavi piu grandi
redis-cli --bigkeys
# Verificare il numero di chiavi per pattern
redis-cli EVAL "return #redis.call('keys', ARGV[1])" 0 "MOL-*"
# Politica di eviction (raccomandata per cache)
# maxmemory-policy: allkeys-lru
NATS: Ottimizzazione Transporter
Serializzazione
Il serializzatore di default Moleculer e JSON. Per ambienti ad alto throughput:
// moleculer.config.js
{
serializer: "MsgPack", // ~30% piu veloce di JSON, payload piu piccoli
// oppure
serializer: "CBOR", // Alternativa efficiente
}
Cambiare serializzatore richiede il restart simultaneo di tutti i nodi. Non mischiare serializzatori diversi nello stesso cluster.
Configurazione NATS
// moleculer.config.js - transporter
transporter: {
type: "NATS",
options: {
url: process.env.NATS_URL,
maxReconnectAttempts: -1, // Riprova all'infinito
reconnectTimeWait: 2000, // 2s tra tentativi
pingInterval: 10000, // Ping ogni 10s
maxPingOut: 3, // 3 ping mancati = disconnect
token: process.env.NATS_TOKEN
}
}
Moleculer: Circuit Breaker e Bulkhead
Circuit Breaker
Previene cascading failure quando un servizio e in difficolta:
// moleculer.config.js
{
circuitBreaker: {
enabled: true,
threshold: 0.5, // Apri il circuito se >50% delle richieste falliscono
windowTime: 60, // Finestra di osservazione: 60 secondi
minRequestCount: 20, // Minimo richieste prima di valutare
halfOpenTime: 10000, // Dopo 10s, prova una richiesta
check: (err) => err && err.code >= 500 // Solo errori server
}
}
Bulkhead
Limita la concorrenza per evitare il sovraccarico di risorse:
// moleculer.config.js
{
bulkhead: {
enabled: true,
concurrency: 50, // Max 50 azioni concorrenti per nodo
maxQueueSize: 200 // Coda massima prima di rifiutare
}
}
// Override per azioni specifiche (es. report pesanti)
actions: {
generateReport: {
bulkhead: {
concurrency: 5, // Solo 5 report concorrenti
maxQueueSize: 10
},
async handler(ctx) { ... }
}
}
Request Timeout
// moleculer.config.js
{
requestTimeout: 30000, // 30s default per tutte le azioni
// Override per singole azioni
actions: {
quickAction: {
timeout: 5000, // 5s per azioni veloci
handler(ctx) { ... }
},
heavyReport: {
timeout: 120000, // 2 minuti per report
handler(ctx) { ... }
}
}
}
Node.js: Ottimizzazione Runtime
Heap Size
# Deployment K8s - variabili d'ambiente
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=1536" # 1.5GB heap (con 2GB memory limit)
# Regola: heap = 75% del memory limit del container
Garbage Collector
env:
- name: NODE_OPTIONS
value: >-
--max-old-space-size=1536
--optimize-for-size
--gc-interval=100
Monitoraggio Memory Leak
# Trend della memoria heap nel tempo (dovrebbe essere stabile)
process_heap_bytes{namespace="pos-enterprise"}
# Se cresce linearmente = probabile memory leak
# Verificare con:
deriv(process_heap_bytes{namespace="pos-enterprise"}[1h]) > 0
Checklist di Performance Review
Periodicamente (mensile), verificare:
- Cache hit rate > 70% per tutti i servizi cached
- P95 latenza < 500ms per azioni CRUD standard
- P99 latenza < 2s per tutte le azioni
- Error rate < 1% per tutti i servizi
- MongoDB: nessun COLLSCAN nelle slow query
- Redis: memoria utilizzata < 75% del limite
- CPU usage medio < 60% sui pod (headroom per spike)
- Memory usage < 80% del limit (headroom per GC)
- Nessun pod in restart loop
Riferimenti Incrociati
- Dashboard Grafana - Metriche per monitorare le ottimizzazioni
- Manutenzione Database - Gestione indici e compaction
- Guida allo Scaling - Quando il tuning non basta e serve scalare
- Sistema di Alerting - Alert per degradazione prestazioni